fdsreader 1.9.13__tar.gz → 1.10.0__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 (118) hide show
  1. {fdsreader-1.9.13 → fdsreader-1.10.0}/PKG-INFO +1 -1
  2. {fdsreader-1.9.13 → fdsreader-1.10.0}/RELEASE.md +5 -0
  3. fdsreader-1.10.0/examples/pl3d/pl3d_example.py +37 -0
  4. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/smoke3d/fds_steckler/input_steckler.fds +1 -1
  5. fdsreader-1.10.0/examples/smoke3d/smoke3d_example.py +43 -0
  6. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/_version.py +1 -1
  7. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/bndf/obstruction.py +4 -4
  8. fdsreader-1.10.0/fdsreader/pl3d/pl3d.py +295 -0
  9. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/pl3d/plot3D_collection.py +10 -11
  10. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/simulation.py +4 -9
  11. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/geomslice.py +5 -2
  12. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/slice.py +6 -5
  13. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/smoke3d/smoke3d.py +125 -6
  14. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/PKG-INFO +1 -1
  15. {fdsreader-1.9.13 → fdsreader-1.10.0}/requirements.txt +0 -0
  16. fdsreader-1.9.13/examples/pl3d/pl3d_example.py +0 -43
  17. fdsreader-1.9.13/examples/smoke3d/smoke3d_example.py +0 -51
  18. fdsreader-1.9.13/fdsreader/pl3d/pl3d.py +0 -170
  19. {fdsreader-1.9.13 → fdsreader-1.10.0}/.gitignore +0 -0
  20. {fdsreader-1.9.13 → fdsreader-1.10.0}/.travis.yml +0 -0
  21. {fdsreader-1.9.13 → fdsreader-1.10.0}/Dockerfile +0 -0
  22. {fdsreader-1.9.13 → fdsreader-1.10.0}/LICENSE +0 -0
  23. {fdsreader-1.9.13 → fdsreader-1.10.0}/MANIFEST.in +0 -0
  24. {fdsreader-1.9.13 → fdsreader-1.10.0}/README.md +0 -0
  25. {fdsreader-1.9.13 → fdsreader-1.10.0}/docker_deployment.sh +0 -0
  26. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/bndf.rst +0 -0
  27. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/conf.py +0 -0
  28. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/devc.rst +0 -0
  29. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/evac.rst +0 -0
  30. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/export.rst +0 -0
  31. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/fds_classes.rst +0 -0
  32. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/geom.rst +0 -0
  33. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/img/data-structure.drawio +0 -0
  34. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/img/data-structure.svg +0 -0
  35. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/index.rst +0 -0
  36. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/isof.rst +0 -0
  37. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/part.rst +0 -0
  38. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/pl3d.rst +0 -0
  39. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/quantity.rst +0 -0
  40. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/simulation.rst +0 -0
  41. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/slcf.rst +0 -0
  42. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/smoke3d.rst +0 -0
  43. {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/utils.rst +0 -0
  44. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/bndf/bndf_example_anim.py +0 -0
  45. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/bndf/fds_data/wall_005.fds +0 -0
  46. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/devc/devc_example.py +0 -0
  47. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/devc/fds_steckler/steckler.fds +0 -0
  48. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/evac/evac_example.py +0 -0
  49. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/evac/fds_fed/fed_verification_multi_mesh.fds +0 -0
  50. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/evac/fds_no_fed/stairs_evss_meshes.fds +0 -0
  51. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/export/export_all.py +0 -0
  52. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/export/obst_export.py +0 -0
  53. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/export/slcf_export.py +0 -0
  54. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/export/smoke3d_export.py +0 -0
  55. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/geom/fds_data/sphere_radiate.fds +0 -0
  56. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/geom/fds_geomslices/PWB01_GEOM.fds +0 -0
  57. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/geom/geom_example.py +0 -0
  58. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/geom/geom_slices.py +0 -0
  59. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/isof/fds_steckler/input_steckler.fds +0 -0
  60. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/isof/isof_example.py +0 -0
  61. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/part/fds_data/impact.fds +0 -0
  62. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/part/part_example.py +0 -0
  63. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/pl3d/fds_data/vort3d_80.fds +0 -0
  64. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/fds_data/vort2d_80.fds +0 -0
  65. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/fds_multimesh/multimesh.fds +0 -0
  66. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/fds_steckler/input_steckler.fds +0 -0
  67. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/slcf_example_global.py +0 -0
  68. {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/slcf_example_mask.py +0 -0
  69. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/__init__.py +0 -0
  70. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/bndf/__init__.py +0 -0
  71. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/bndf/obstruction_collection.py +0 -0
  72. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/bndf/utils.py +0 -0
  73. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/devc/__init__.py +0 -0
  74. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/devc/device.py +0 -0
  75. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/devc/device_collection.py +0 -0
  76. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/evac/__init__.py +0 -0
  77. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/evac/evac_collection.py +0 -0
  78. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/evac/evacuation.py +0 -0
  79. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/__init__.py +0 -0
  80. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/obst_exporter.py +0 -0
  81. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/sim_exporter.py +0 -0
  82. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/slcf_exporter.py +0 -0
  83. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/smoke3d_exporter.py +0 -0
  84. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/__init__.py +0 -0
  85. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/mesh.py +0 -0
  86. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/mesh_collection.py +0 -0
  87. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/surface.py +0 -0
  88. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/ventilation.py +0 -0
  89. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/geom/__init__.py +0 -0
  90. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/geom/geometry.py +0 -0
  91. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/geom/geometry_collection.py +0 -0
  92. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/isof/__init__.py +0 -0
  93. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/isof/isosurface.py +0 -0
  94. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/isof/isosurface_collection.py +0 -0
  95. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/part/__init__.py +0 -0
  96. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/part/particle.py +0 -0
  97. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/part/particle_collection.py +0 -0
  98. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/pl3d/__init__.py +0 -0
  99. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/settings.py +0 -0
  100. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/__init__.py +0 -0
  101. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/geomslice_collection.py +0 -0
  102. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/slice_collection.py +0 -0
  103. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/smoke3d/__init__.py +0 -0
  104. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/smoke3d/smoke3D_collection.py +0 -0
  105. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/__init__.py +0 -0
  106. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/data.py +0 -0
  107. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/dimension.py +0 -0
  108. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/extent.py +0 -0
  109. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/fortran_data.py +0 -0
  110. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/misc.py +0 -0
  111. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/SOURCES.txt +0 -0
  112. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/dependency_links.txt +0 -0
  113. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/requires.txt +0 -0
  114. {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/top_level.txt +0 -0
  115. {fdsreader-1.9.13 → fdsreader-1.10.0}/setup.cfg +0 -0
  116. {fdsreader-1.9.13 → fdsreader-1.10.0}/setup.py +0 -0
  117. {fdsreader-1.9.13 → fdsreader-1.10.0}/tests/test.py +0 -0
  118. {fdsreader-1.9.13 → fdsreader-1.10.0}/tests/test.smv +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fdsreader
3
- Version: 1.9.13
3
+ Version: 1.10.0
4
4
  Summary: Python reader for data generated by FDS.
5
5
  Home-page: https://github.com/FireDynamics/fdsreader
6
6
  Author: FZJ IAS-7 (Jan Vogelsang, Prof. Dr. Lukas Arnold, Tristan Hehnen)
@@ -1,4 +1,9 @@
1
1
  ## Release History
2
+ * 1.10.0
3
+ * Restructured Plot3D data to now be represented by a single Plot3D instance for all timesteps instead of one individual object per timestep
4
+ * Added a to_global function for Plot3D data
5
+ * Added a to_global function for Smoke3D data
6
+ * Fixed bugs and typos in documentation
2
7
  * 1.9.13
3
8
  * Fixed slice.to_global for 3D slices
4
9
  * 1.9.12
@@ -0,0 +1,37 @@
1
+ import pyvista as pv
2
+ import numpy as np
3
+ import fdsreader as fds
4
+
5
+
6
+ def main():
7
+ sim = fds.Simulation("./fds_data")
8
+
9
+ # Load temperature 3D data
10
+ quantity = "Temperature"
11
+ pl_t1 = sim.data_3d.get_by_quantity(quantity)
12
+ data, coordinates = pl_t1.to_global(masked=True, return_coordinates=True, fill=np.nan)
13
+
14
+ # Select the last available timestep
15
+ t = -1
16
+
17
+ # Create 3D grid
18
+ x_ = np.linspace(coordinates['x'][0], coordinates['x'][-1], len(coordinates['x']))
19
+ y_ = np.linspace(coordinates['y'][0], coordinates['y'][-1], len(coordinates['y'])) # y_ = np.array([29]) # when using a slice
20
+ z_ = np.linspace(coordinates['z'][0], coordinates['z'][-1], len(coordinates['z']))
21
+ x, y, z = np.meshgrid(x_, y_, z_, indexing='ij')
22
+ points = np.stack((x.flatten(), y.flatten(), z.flatten()), axis=1)
23
+
24
+ color_data = data[t, :, :, :]
25
+ # It is also possible to just plot a slice
26
+ # color_data = pl_t1[mesh].data[t, :, 29:30, :]
27
+
28
+ # Plot 3D data
29
+ plotter = pv.Plotter()
30
+ plotter.add_mesh(pv.PolyData(points), scalars=color_data.flatten(),
31
+ opacity=0.3, render_points_as_spheres=False, point_size=25)
32
+ plotter.add_scalar_bar(title=quantity)
33
+ plotter.show()
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -3,7 +3,7 @@
3
3
  &MESH IJK=36,28,22, XB=0.00,3.60,-1.40,1.40,0.00,2.13 /
4
4
 
5
5
  &TIME T_END=100.0, TIME_SHRINK_FACTOR=10. /
6
- &DUMP NFRAMES=100, DT_DEVC=10., DT_DEVC_LINE=10., DT_HRR=10., SIG_FIGS=4, SIG_FIGS_EXP=2 /
6
+ &DUMP NFRAMES=100 /
7
7
 
8
8
  &MISC TMPA=22. /
9
9
 
@@ -0,0 +1,43 @@
1
+ import pyvista as pv
2
+ import numpy as np
3
+
4
+ import fdsreader as fds
5
+
6
+
7
+ def main():
8
+ sim = fds.Simulation("./fds_steckler/")
9
+
10
+ # Get 3D smoke data for a specific quantity in one of the meshes
11
+ quantity = "Temperature" # "SOOT MASS FRACTION"
12
+ smoke = sim.smoke_3d.get_by_quantity(quantity)
13
+
14
+ data, coordinates = smoke.to_global(masked=True, return_coordinates=True, fill=np.nan)
15
+
16
+ # Create 3D grid
17
+ x_ = np.linspace(coordinates['x'][0], coordinates['x'][-1], len(coordinates['x']))
18
+ y_ = np.linspace(coordinates['y'][0], coordinates['y'][-1], len(coordinates['y']))
19
+ z_ = np.linspace(coordinates['z'][0], coordinates['z'][-1], len(coordinates['z']))
20
+ x, y, z = np.meshgrid(x_, y_, z_, indexing='ij')
21
+ points = np.stack((x.flatten(), y.flatten(), z.flatten()), axis=1)
22
+
23
+ # Initialize data grid
24
+ grid = pv.PolyData(points)
25
+
26
+ # Plot 3D data
27
+ plotter = pv.Plotter(notebook=False, off_screen=True)
28
+ plotter.add_mesh(grid, scalars=data[0].flatten(), opacity=0.3, render_points_as_spheres=False, point_size=25)
29
+ plotter.add_scalar_bar(title=quantity)
30
+
31
+ # Open a gif
32
+ plotter.open_gif("smoke.gif")
33
+
34
+ for d in data:
35
+ plotter.update_scalars(d.flatten(), render=False)
36
+ plotter.write_frame()
37
+
38
+ # Closes and finalizes movie
39
+ plotter.close()
40
+
41
+
42
+ if __name__ == "__main__":
43
+ main()
@@ -7,5 +7,5 @@ Provides fdsreader version information.
7
7
 
8
8
  from incremental import Version
9
9
 
10
- __version__ = Version("fdsreader", 1, 9, 13)
10
+ __version__ = Version("fdsreader", 1, 10, 0)
11
11
  __all__ = ["__version__"]
@@ -40,7 +40,7 @@ class Patch:
40
40
  self.mesh = mesh
41
41
  self._boundary_parent: 'Boundary' = None
42
42
 
43
- def n_t(self, count_duplicates=True):
43
+ def n_t(self, count_duplicates=True) -> int:
44
44
  """Get the number of timesteps for which data was output.
45
45
  :param count_duplicates: If true, return the total number of data points, even if there is
46
46
  duplicate data for a timestep. Duplicate data might be output when restarting the
@@ -146,7 +146,7 @@ class Boundary:
146
146
  self.upper_bounds = upper_bounds
147
147
 
148
148
 
149
- def n_t(self, count_duplicates=True):
149
+ def n_t(self, count_duplicates=True) -> int:
150
150
  """Get the number of timesteps for which data was output.
151
151
  :param count_duplicates: If true, return the total number of data points, even if there is
152
152
  duplicate data for a timestep. Duplicate data might be output when restarting the
@@ -305,7 +305,7 @@ class SubObstruction:
305
305
  self.show_times.append(time)
306
306
  self.show_times.sort()
307
307
 
308
- def n_t(self, count_duplicates=True):
308
+ def n_t(self, count_duplicates=True) -> int:
309
309
  """Returns the number of timesteps for which boundary data is available.
310
310
  :param count_duplicates: If true, return the total number of data points, even if there is
311
311
  duplicate data for a timestep. Duplicate data might be output when restarting the
@@ -417,7 +417,7 @@ class Obstruction:
417
417
  return sorted(list(orientations))
418
418
  return []
419
419
 
420
- def n_t(self, count_duplicates=True):
420
+ def n_t(self, count_duplicates=True) -> int:
421
421
  """Returns the number of timesteps for which boundary data is available.
422
422
  :param count_duplicates: If true, return the total number of data points, even if there is
423
423
  duplicate data for a timestep. Duplicate data might be output when restarting the
@@ -0,0 +1,295 @@
1
+ import logging
2
+ import os
3
+ from copy import deepcopy
4
+ from typing import Dict, Sequence, Tuple, Literal, Union, List
5
+ import numpy as np
6
+ import math
7
+ import bisect
8
+
9
+ from fdsreader.fds_classes import Mesh
10
+ from fdsreader.utils import Quantity
11
+ from fdsreader import settings
12
+ import fdsreader.utils.fortran_data as fdtype
13
+
14
+ _HANDLED_FUNCTIONS = {np.mean: (lambda pl: pl.mean)}
15
+
16
+
17
+ def implements(np_function):
18
+ """Decorator to register an __array_function__ implementation for Plot3Ds.
19
+ """
20
+
21
+ def decorator(func):
22
+ _HANDLED_FUNCTIONS[np_function] = func
23
+ return func
24
+
25
+ return decorator
26
+
27
+
28
+ class SubPlot3D:
29
+ """Subplot of a pl3d output for a single mesh.
30
+
31
+ :ivar mesh: The mesh containing the data.
32
+ """
33
+ # Offset of the binary file to the end of the file header.
34
+ _offset = fdtype.new((('i', 3),)).itemsize + fdtype.new((('i', 4),)).itemsize
35
+
36
+ def __init__(self, mesh: Mesh, quantity_idx: int):
37
+ self.file_paths: List[str] = list() # Path to the binary data file for each time step
38
+ self.mesh = mesh
39
+ self._quantity_idx = quantity_idx
40
+
41
+ def _add_timestep(self, time_idx: int, file_path: str):
42
+ self.file_paths.insert(time_idx, file_path)
43
+
44
+ @property
45
+ def data(self) -> np.ndarray:
46
+ """Method to lazy load the 3D data for each quantity of a single mesh.
47
+
48
+ :returns: 4D numpy array with (t,x,y,z) as dimensions.
49
+ """
50
+ if not hasattr(self, "_data"):
51
+ self._data = np.empty(shape=(len(self.file_paths),) + self.mesh.dimension.shape())
52
+ dtype_data = fdtype.new((('f', self.mesh.dimension.size() * 5),))
53
+ for t, file_path in enumerate(self.file_paths):
54
+ with open(file_path, 'rb') as infile:
55
+ infile.seek(self._offset)
56
+ self._data[t, :, :, :] = fdtype.read(infile, dtype_data, 1)[0][0].reshape(
57
+ self.mesh.dimension.shape() + (5,), order='F')[:, :, :, self._quantity_idx]
58
+ return self._data
59
+
60
+ def clear_cache(self):
61
+ """Remove all data from the internal cache that has been loaded so far to free memory.
62
+ """
63
+ if hasattr(self, "_data"):
64
+ del self._data
65
+
66
+
67
+ class Plot3D(np.lib.mixins.NDArrayOperatorsMixin):
68
+ """Plot3d file data container including metadata. Consists of multiple subplots, one for each
69
+ mesh.
70
+
71
+ :ivar times: All times for which data has been recorded.
72
+ :ivar quantities: List with quantity objects containing information about recorded quantities
73
+ calculated for this Plot3D with the corresponding short_name and unit.
74
+ """
75
+
76
+ def __init__(self, root_path: str):
77
+ self._root_path = root_path
78
+ self.times: List[float] = list()
79
+
80
+ # List of all subplots this Plot3D consists of (one per mesh).
81
+ self._subplots: Dict[str, SubPlot3D] = dict()
82
+
83
+ def _add_subplot(self, filename: str, time: float, quantity: Quantity, quantity_idx: int, mesh: Mesh):
84
+ self.quantity: Quantity = quantity
85
+ if mesh.id not in self._subplots.keys():
86
+ self._subplots[mesh.id] = SubPlot3D(mesh, quantity_idx)
87
+ if not any(np.isclose(time, t) for t in self.times):
88
+ time_idx = bisect.bisect(self.times, time)
89
+ self.times.insert(time_idx, time)
90
+ else:
91
+ time_idx = next(idx for idx, t in enumerate(self.times) if np.isclose(time, t))
92
+ self._subplots[mesh.id]._add_timestep(time_idx, os.path.join(self._root_path, filename))
93
+
94
+ # If lazy loading has been disabled by the user, load the data instantaneously instead
95
+ if not settings.LAZY_LOAD:
96
+ _ = self._subplots[mesh.id].data
97
+
98
+ def __getitem__(self, mesh: Mesh):
99
+ """Returns the :class:`SubPlot` that contains data for the given mesh.
100
+ """
101
+ return self._subplots[mesh.id]
102
+
103
+ @implements(np.mean)
104
+ def mean(self) -> float:
105
+ """Calculates the mean value of the whole Plot3D.
106
+
107
+ :returns: The calculated mean value.
108
+ """
109
+ mean_sum = 0
110
+ for subplot in self._subplots.values():
111
+ mean_sum += np.mean(subplot.data)
112
+ return mean_sum / len(self._subplots)
113
+
114
+ @implements(np.std)
115
+ def std(self) -> float:
116
+ """Calculates the standard deviation for each quantity individually of the whole Plot3D.
117
+
118
+ :returns: The calculated standard deviation.
119
+ """
120
+ mean = self.mean
121
+ sum = np.sum([np.sum(np.power(subplot.data - mean, 2)) for subplot in self._subplots.values()])
122
+ N = np.sum([subplot.data.size for subplot in self._subplots.values()])
123
+ return np.sqrt(sum / N)
124
+
125
+ def clear_cache(self):
126
+ """Remove all data from the internal cache that has been loaded so far to free memory.
127
+ """
128
+ for subplot in self._subplots.values():
129
+ subplot.clear_cache()
130
+
131
+ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: bool = False) -> \
132
+ Union[np.ndarray, Tuple[np.ndarray, Dict[Literal['x', 'y', 'z'], np.ndarray]]]:
133
+ """Creates a global numpy ndarray from all subplots.
134
+
135
+ :param masked: Whether to apply the obstruction mask to the data or not.
136
+ :param fill: The fill value to use for masked entries. Only used when masked=True.
137
+ :param return_coordinates: If true, return the matching coordinate for each value on the generated grid.
138
+ """
139
+ coord_min = {'x': math.inf, 'y': math.inf, 'z': math.inf}
140
+ coord_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf}
141
+ for dim in ('x', 'y', 'z'):
142
+ for subplot in self._subplots.values():
143
+ co = subplot.mesh.coordinates[dim]
144
+ coord_min[dim] = min(co[0], coord_min[dim])
145
+ coord_max[dim] = max(co[-1], coord_max[dim])
146
+
147
+ # The global grid will use the finest mesh as base and duplicate values of the coarser
148
+ # meshes. Therefore, we first find the finest mesh and calculate the step size in each
149
+ # dimension.
150
+ step_sizes_min = {'x': coord_max['x'] - coord_min['x'],
151
+ 'y': coord_max['y'] - coord_min['y'],
152
+ 'z': coord_max['z'] - coord_min['z']}
153
+ step_sizes_max = {'x': 0, 'y': 0, 'z': 0}
154
+ steps = dict()
155
+ global_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf}
156
+
157
+ for dim in ('x', 'y', 'z'):
158
+ for subplot in self._subplots.values():
159
+ step_size = subplot.mesh.coordinates[dim][1] - subplot.mesh.coordinates[dim][0]
160
+ step_sizes_min[dim] = min(step_size, step_sizes_min[dim])
161
+ step_sizes_max[dim] = max(step_size, step_sizes_max[dim])
162
+ global_max[dim] = max(subplot.mesh.coordinates[dim][-1], global_max[dim])
163
+
164
+ for dim in ('x', 'y', 'z'):
165
+ if step_sizes_min[dim] == 0:
166
+ step_sizes_min[dim] = math.inf
167
+ steps[dim] = 1
168
+ else:
169
+ steps[dim] = max(int(round((coord_max[dim] - coord_min[dim]) / step_sizes_min[dim])),
170
+ 1) + 1 # + step_sizes_max[dim] / step_sizes_min[dim]
171
+
172
+ grid = np.full((self.n_t, steps['x'], steps['y'], steps['z']), np.nan)
173
+
174
+ start_idx = dict()
175
+ end_idx = dict()
176
+ for subplot in self._subplots.values():
177
+ subplot_data = subplot.data.copy()
178
+ if masked:
179
+ mask = subplot.mesh.get_obstruction_mask(self.times)
180
+
181
+ for axis in range(3):
182
+ dim = ('x', 'y', 'z')[axis]
183
+ n_repeat = max(int(round(
184
+ (subplot.mesh.coordinates[dim][1] - subplot.mesh.coordinates[dim][0]) /
185
+ step_sizes_min[dim])), 1)
186
+
187
+ start_idx[dim] = int(round(
188
+ (subplot.mesh.coordinates[dim][0] - coord_min[dim]) / step_sizes_min[dim]))
189
+ end_idx[dim] = int(round(
190
+ (subplot.mesh.coordinates[dim][-1] - coord_min[dim]) / step_sizes_min[dim]))
191
+
192
+ # We ignore border points unless they are actually on the border of the simulation space as all
193
+ # other border points actually appear twice, as the subslices overlap. This only
194
+ # applies for face_centered slices, as cell_centered slices will not overlap.
195
+ reduced_shape_slices = (slice(subplot.data.shape[0]),) + tuple(slice(s - 1) for s in subplot.data.shape[1:])
196
+ subplot_data = subplot_data[reduced_shape_slices]
197
+ if masked:
198
+ mask = mask[reduced_shape_slices]
199
+
200
+ for axis in range(3):
201
+ dim = ('x', 'y', 'z')[axis]
202
+ # Temporarily save border points to add them back to the array again later
203
+ if subplot.mesh.coordinates[dim][-1] == global_max[dim]:
204
+ end_idx[dim] += 1
205
+ temp_data_slices = [slice(s) for s in subplot_data.shape]
206
+ temp_data_slices[axis + 1] = slice(subplot_data.shape[axis + 1] - 1, None)
207
+ temp_data = subplot_data[tuple(temp_data_slices)]
208
+ if masked:
209
+ temp_mask = mask[tuple(temp_data_slices)]
210
+
211
+ if n_repeat > 1:
212
+ subplot_data = np.repeat(subplot_data, n_repeat, axis=axis + 1)
213
+ if masked:
214
+ mask = np.repeat(mask, n_repeat, axis=axis + 1)
215
+
216
+ # Add border points back again if needed
217
+ if subplot.mesh.coordinates[dim][-1] == global_max[dim]:
218
+ subplot_data = np.concatenate((subplot_data, temp_data), axis=axis + 1)
219
+ if masked:
220
+ mask = np.concatenate((mask, temp_mask), axis=axis + 1)
221
+
222
+ # If the slice should be masked, we set all cells at which an obstruction is in the
223
+ # simulation space to the fill value set by the user
224
+ if masked:
225
+ subplot_data = np.where(mask, subplot_data, fill)
226
+
227
+ grid[:, start_idx['x']: end_idx['x'], start_idx['y']: end_idx['y'],
228
+ start_idx['z']: end_idx['z']] = subplot_data.reshape(
229
+ (self.n_t, end_idx['x'] - start_idx['x'], end_idx['y'] - start_idx['y'],
230
+ end_idx['z'] - start_idx['z']))
231
+
232
+ if return_coordinates:
233
+ coordinates = dict()
234
+ for dim_index, dim in enumerate(('x', 'y', 'z')):
235
+ coordinates[dim] = np.linspace(coord_min[dim], coord_max[dim], grid.shape[dim_index + 1])
236
+
237
+ if return_coordinates:
238
+ return grid, coordinates
239
+ else:
240
+ return grid
241
+
242
+ @property
243
+ def n_t(self) -> int:
244
+ """Get the number of timesteps for which data was output.
245
+ """
246
+ return len(self.times)
247
+
248
+ @property
249
+ def subplots(self):
250
+ """Returns a list with one SubPlot3D object per mesh.
251
+ """
252
+ return list(self._subplots.values())
253
+
254
+ def __array__(self):
255
+ """Method that will be called by numpy when trying to convert the object to a numpy ndarray.
256
+ """
257
+ raise UserWarning(
258
+ "Plot3Ds can not be converted to numpy arrays, but they support all typical numpy"
259
+ " operations such as np.multiply. If a 'global' array containing all subplots is"
260
+ " required, please use the 'to_global' method and use the returned numpy-array explicitly.")
261
+
262
+ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
263
+ """Method that will be called by numpy when using a ufunction with a Plot3D as input.
264
+
265
+ :returns: A new pl3d on which the ufunc has been applied.
266
+ """
267
+ if method != "__call__":
268
+ logging.warning(
269
+ "The %s method has been used which is not explicitly implemented. Correctness of"
270
+ " results is not guaranteed. If you require this feature to be implemented please"
271
+ " submit an issue on Github where you explain your use case.", method)
272
+ input_list = list(inputs)
273
+ for i, inp in enumerate(inputs):
274
+ if isinstance(inp, self.__class__):
275
+ del input_list[i]
276
+
277
+ new_pl3d = deepcopy(self)
278
+ for subplot in self._subplots.values():
279
+ subplot._data = ufunc(subplot.data, input_list[0], **kwargs)
280
+ return new_pl3d
281
+
282
+ def __array_function__(self, func, types, args, kwargs):
283
+ """Method that will be called by numpy when using an array function with a Slice as input.
284
+
285
+ :returns: The output of the array function.
286
+ """
287
+ if func not in _HANDLED_FUNCTIONS:
288
+ return NotImplemented
289
+ # Note: this allows subclasses that don't override __array_function__ to handle Plot3Ds.
290
+ if not all(issubclass(t, self.__class__) for t in types):
291
+ return NotImplemented
292
+ return _HANDLED_FUNCTIONS[func](*args, **kwargs)
293
+
294
+ # __array_function__ implementations
295
+ # ...
@@ -1,4 +1,5 @@
1
1
  from typing import Iterable, Union, List
2
+ import numpy as np
2
3
 
3
4
  from fdsreader.pl3d import Plot3D
4
5
  from fdsreader.utils.data import FDSDataCollection, Quantity
@@ -9,26 +10,24 @@ class Plot3DCollection(FDSDataCollection):
9
10
  using plot3Ds as well as its subclasses such as :class:`SubPlot3D`.
10
11
  """
11
12
 
12
- def __init__(self, times: Iterable[float], *plot3ds: Iterable[Plot3D]):
13
+ def __init__(self, *plot3ds: Iterable[Plot3D]):
13
14
  super().__init__(*plot3ds)
14
- self.times = list(times)
15
+
16
+ @property
17
+ def times(self) -> np.ndarray:
18
+ return np.array(self._elements[0].times)
15
19
 
16
20
  @property
17
21
  def quantities(self) -> List[Quantity]:
18
- qs = set()
19
- for pl3d in self._elements:
20
- for q in pl3d.quantities:
21
- qs.add(q)
22
- return list(qs)
22
+ return [pl.quantity for pl in self._elements]
23
23
 
24
- def filter_by_quantity(self, quantity: Union[str, Quantity]):
24
+ def get_by_quantity(self, quantity: Union[str, Quantity]):
25
25
  """Filters all plot3d data by a specific quantity.
26
26
  """
27
27
  if type(quantity) == Quantity:
28
28
  quantity = quantity.name
29
- return Plot3DCollection(x for x in self._elements if any(
30
- q.name.lower() == quantity.lower() or q.short_name.lower() == quantity.lower() for q in
31
- x.quantities))
29
+ return next(x for x in self._elements if
30
+ x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower())
32
31
 
33
32
  def __repr__(self):
34
33
  return "Plot3DCollection(" + super(Plot3DCollection, self).__repr__() + ")"
@@ -144,7 +144,7 @@ class Simulation:
144
144
  self._obstructions = list()
145
145
  self._slices = dict()
146
146
  self._geomslices = dict()
147
- self._data_3d = dict()
147
+ self.data_3d = Plot3DCollection([Plot3D(self.root_path) for _ in range(5)])
148
148
  self._smoke_3d = dict()
149
149
  self._isosurfaces = dict()
150
150
  self._particles = list()
@@ -179,7 +179,6 @@ class Simulation:
179
179
  self.geomslices = GeomSliceCollection(
180
180
  GeomSlice(self.root_path, slice_data[0]["id"],
181
181
  slice_data[0]["times"], slice_data[1:]) for slice_data in self._geomslices.values())
182
- self.data_3d = Plot3DCollection(self._data_3d.keys(), self._data_3d.values())
183
182
  self.smoke_3d = Smoke3DCollection(self._smoke_3d.values())
184
183
  self.isosurfaces = IsosurfaceCollection(self._isosurfaces.values())
185
184
  self.devices = DeviceCollection(self._devices.values())
@@ -194,7 +193,7 @@ class Simulation:
194
193
  if len(self._evacs) == 0:
195
194
  self.evacs = EvacCollection((), "", ())
196
195
  self.meshes = MeshCollection(self._meshes)
197
- del self._geom_data, self._geomslices, self._slices, self._obstructions, self._data_3d, self._smoke_3d, self._isosurfaces, self._devices, self._particles, self._evacs, self._meshes, self._subobstructions
196
+ del self._geom_data, self._geomslices, self._slices, self._obstructions, self._smoke_3d, self._isosurfaces, self._devices, self._particles, self._evacs, self._meshes, self._subobstructions
198
197
 
199
198
  if settings.ENABLE_CACHING:
200
199
  # Hash will be saved to simulation pickle file and compared to new hash when loading
@@ -748,16 +747,12 @@ class Simulation:
748
747
  mesh_index = int(line[2]) - 1
749
748
 
750
749
  filename = smv_file.readline().strip()
751
- quantities = list()
752
- for _ in range(5):
750
+ for i in range(5):
753
751
  quantity = smv_file.readline().strip()
754
752
  short_name = smv_file.readline().strip()
755
753
  unit = smv_file.readline().strip()
756
- quantities.append(Quantity(quantity, short_name, unit))
757
754
 
758
- if time not in self._data_3d:
759
- self._data_3d[time] = Plot3D(self.root_path, time, quantities)
760
- self._data_3d[time]._add_subplot(filename, self._meshes[mesh_index])
755
+ self.data_3d[i]._add_subplot(filename, time, Quantity(quantity, short_name, unit), i, self._meshes[mesh_index])
761
756
 
762
757
  @log_error("smoke3d")
763
758
  def _load_smoke_3d(self, smv_file: TextIO, line: str):
@@ -52,8 +52,11 @@ class SubGeomSlice:
52
52
  def times(self):
53
53
  return self._parent_slice.times
54
54
 
55
+
55
56
  @property
56
- def n_t(self):
57
+ def n_t(self) -> int:
58
+ """Get the number of timesteps for which data was output.
59
+ """
57
60
  return self._parent_slice.n_t
58
61
 
59
62
  def _load_geom_data(self):
@@ -409,7 +412,7 @@ class GeomSlice(np.lib.mixins.NDArrayOperatorsMixin):
409
412
  """
410
413
  raise UserWarning(
411
414
  "Slices can not be converted to numpy arrays, but they support all typical numpy"
412
- " operations such as np.multiply. If a 'global' array containg all subgeomslices is"
415
+ " operations such as np.multiply. If a 'global' array containing all subgeomslices is"
413
416
  " required, use the 'to_global' method and use the returned numpy-array explicitly.")
414
417
 
415
418
  def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
@@ -100,7 +100,9 @@ class SubSlice:
100
100
  return self._parent_slice.times
101
101
 
102
102
  @property
103
- def n_t(self):
103
+ def n_t(self) -> int:
104
+ """Get the number of timesteps for which data was output.
105
+ """
104
106
  return self._parent_slice.n_t
105
107
 
106
108
  def _load_data(self, file_path: str, data_out: np.ndarray):
@@ -507,8 +509,7 @@ class Slice(np.lib.mixins.NDArrayOperatorsMixin):
507
509
 
508
510
  :param masked: Whether to apply the obstruction mask to the slice or not.
509
511
  :param fill: The fill value to use for masked slice entries. Only used when masked=True.
510
- :param return_coordinates: If true, return the matching coordinate for each value on the
511
- generated grid.
512
+ :param return_coordinates: If true, return the matching coordinate for each value on the generated grid.
512
513
  """
513
514
  subslice_sets = [dict(), dict()]
514
515
 
@@ -567,7 +568,7 @@ class Slice(np.lib.mixins.NDArrayOperatorsMixin):
567
568
  start_idx = dict()
568
569
  end_idx = dict()
569
570
  for slc in subslices.values():
570
- slc_data = slc.data if slc.orientation == 0 else np.expand_dims(slc.data,
571
+ slc_data = slc.data.copy() if slc.orientation == 0 else np.expand_dims(slc.data.copy(),
571
572
  axis=slc.orientation)
572
573
  if masked:
573
574
  mask = slc.mesh.get_obstruction_mask_slice(slc)
@@ -694,7 +695,7 @@ class Slice(np.lib.mixins.NDArrayOperatorsMixin):
694
695
  """
695
696
  raise UserWarning(
696
697
  "Slices can not be converted to numpy arrays, but they support all typical numpy"
697
- " operations such as np.multiply. If a 'global' array containg all subslices is"
698
+ " operations such as np.multiply. If a 'global' array containing all subslices is"
698
699
  " required, use the 'to_global' method and use the returned numpy-array explicitly.")
699
700
 
700
701
  def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):