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.
- {fdsreader-1.9.13 → fdsreader-1.10.0}/PKG-INFO +1 -1
- {fdsreader-1.9.13 → fdsreader-1.10.0}/RELEASE.md +5 -0
- fdsreader-1.10.0/examples/pl3d/pl3d_example.py +37 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/smoke3d/fds_steckler/input_steckler.fds +1 -1
- fdsreader-1.10.0/examples/smoke3d/smoke3d_example.py +43 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/_version.py +1 -1
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/bndf/obstruction.py +4 -4
- fdsreader-1.10.0/fdsreader/pl3d/pl3d.py +295 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/pl3d/plot3D_collection.py +10 -11
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/simulation.py +4 -9
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/geomslice.py +5 -2
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/slice.py +6 -5
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/smoke3d/smoke3d.py +125 -6
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/PKG-INFO +1 -1
- {fdsreader-1.9.13 → fdsreader-1.10.0}/requirements.txt +0 -0
- fdsreader-1.9.13/examples/pl3d/pl3d_example.py +0 -43
- fdsreader-1.9.13/examples/smoke3d/smoke3d_example.py +0 -51
- fdsreader-1.9.13/fdsreader/pl3d/pl3d.py +0 -170
- {fdsreader-1.9.13 → fdsreader-1.10.0}/.gitignore +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/.travis.yml +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/Dockerfile +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/LICENSE +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/MANIFEST.in +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/README.md +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docker_deployment.sh +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/bndf.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/conf.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/devc.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/evac.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/export.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/fds_classes.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/geom.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/img/data-structure.drawio +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/img/data-structure.svg +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/index.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/isof.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/part.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/pl3d.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/quantity.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/simulation.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/slcf.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/smoke3d.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/docs/utils.rst +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/bndf/bndf_example_anim.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/bndf/fds_data/wall_005.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/devc/devc_example.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/devc/fds_steckler/steckler.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/evac/evac_example.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/evac/fds_fed/fed_verification_multi_mesh.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/evac/fds_no_fed/stairs_evss_meshes.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/export/export_all.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/export/obst_export.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/export/slcf_export.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/export/smoke3d_export.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/geom/fds_data/sphere_radiate.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/geom/fds_geomslices/PWB01_GEOM.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/geom/geom_example.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/geom/geom_slices.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/isof/fds_steckler/input_steckler.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/isof/isof_example.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/part/fds_data/impact.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/part/part_example.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/pl3d/fds_data/vort3d_80.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/fds_data/vort2d_80.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/fds_multimesh/multimesh.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/fds_steckler/input_steckler.fds +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/slcf_example_global.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/examples/slcf/slcf_example_mask.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/bndf/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/bndf/obstruction_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/bndf/utils.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/devc/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/devc/device.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/devc/device_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/evac/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/evac/evac_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/evac/evacuation.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/obst_exporter.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/sim_exporter.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/slcf_exporter.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/export/smoke3d_exporter.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/mesh.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/mesh_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/surface.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/fds_classes/ventilation.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/geom/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/geom/geometry.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/geom/geometry_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/isof/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/isof/isosurface.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/isof/isosurface_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/part/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/part/particle.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/part/particle_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/pl3d/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/settings.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/geomslice_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/slcf/slice_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/smoke3d/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/smoke3d/smoke3D_collection.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/__init__.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/data.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/dimension.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/extent.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/fortran_data.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader/utils/misc.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/SOURCES.txt +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/dependency_links.txt +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/requires.txt +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/fdsreader.egg-info/top_level.txt +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/setup.cfg +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/setup.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/tests/test.py +0 -0
- {fdsreader-1.9.13 → fdsreader-1.10.0}/tests/test.smv +0 -0
|
@@ -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()
|
|
@@ -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()
|
|
@@ -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,
|
|
13
|
+
def __init__(self, *plot3ds: Iterable[Plot3D]):
|
|
13
14
|
super().__init__(*plot3ds)
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
30
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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):
|