Peano
Loading...
Searching...
No Matches
PointProbe.py
Go to the documentation of this file.
1# This file is part of the Peano project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3from .Visualiser import Visualiser
4from ..input.PatchFileParser import unknown_names_from_description
5
6import csv
7import itertools
8import math
9import multiprocessing
10from concurrent.futures import ProcessPoolExecutor
11
12import numpy as np
13
14
16 """!
17 Patch filter (@see PatchFileParser.patch_filter) which accepts only
18 patches whose closed bounding box contains a given point
19
20 It is a class rather than a lambda, as it has to be picklable when
21 we farm snapshots out to worker processes.
22 """
23
24 def __init__(self, point, tolerance):
25 self.point = point
26 self.tolerance = tolerance
27
28 def __call__(self, offset, size):
29 return all(
30 offset[d] - self.tolerance * size[d]
31 <= self.point[d]
32 <= offset[d] + size[d] + self.tolerance * size[d]
33 for d in range(len(offset))
34 )
35
36
38 """!
39 Probe the data within a Peano patch file at one point over time
40
41 This is not a visualiser but a postprocessing tool, but it reuses the
42 whole meta file and snapshot logic of the Visualiser. Per snapshot,
43 we parse only those patches which contain the probe point (@see
44 PatchContainsPoint), pick the finest one of them, and take the value
45 of the cell (for cell data) or of the nearest dof (for vertex data)
46 which contains the point. There is no interpolation.
47
48 The result is a csv file with one row per snapshot. The first column
49 is the snapshot's time stamp, followed by all unknowns of all data
50 sets found in the file. If the point is not covered by any patch in a
51 snapshot, we write NaNs.
52
53 If the point sits exactly on the face between two patches of the same
54 size, we take the patch to the right/top/front, i.e. we use half-open
55 intervals [offset,offset+size). Only if no such patch exists (domain
56 boundary), we fall back to the patch to the left.
57 """
58
59 Tolerance = 1e-8
60
61 def __init__(self, file_name, point, output_file, start, end, verbose=False):
62 super(PointProbe, self).__init__(file_name, start, end, verbose)
63 self._point = tuple(float(x) for x in point)
64 self._output_file = output_file
65
66 def _probe_patch_data(self, patch_file_data, point):
67 """!
68 Return list of unknowns at point or None if point is not covered
69 """
70 dimensions = patch_file_data._dimensions
71 point = point[:dimensions]
72 dof = patch_file_data._dof
73 unknowns = patch_file_data._unknowns
74
75 candidates = [
76 patch
77 for patch in patch_file_data._cell_data
78 if all(
79 patch.offset[d] - self.Tolerance * patch.size[d]
80 <= point[d]
81 <= patch.offset[d] + patch.size[d] * (1.0 + self.Tolerance)
82 for d in range(dimensions)
83 )
84 ]
85 if not candidates:
86 return None
87
88 finest_size = min(patch.size[0] for patch in candidates)
89 candidates = [
90 patch
91 for patch in candidates
92 if math.isclose(patch.size[0], finest_size, rel_tol=1e-6)
93 ]
94
95 # Prefer patch where point is in half-open interval [offset,offset+size)
96 def distance_to_upper_faces(patch):
97 return sum(
98 1
99 for d in range(dimensions)
100 if point[d] >= patch.offset[d] + patch.size[d] * (1.0 - self.Tolerance)
101 )
102
103 patch = min(candidates, key=distance_to_upper_faces)
104
105 relative_position = [
106 min(max((point[d] - patch.offset[d]) / patch.size[d], 0.0), 1.0)
107 for d in range(dimensions)
108 ]
109
110 if patch_file_data._is_data_associated_to_cell:
111 self._assert_default_cell_mapping(patch_file_data)
112 index = 0
113 for d in reversed(range(dimensions)):
114 index_along_axis = min(int(relative_position[d] * dof), dof - 1)
115 index = index * dof + index_along_axis
116 else:
117 dof_positions = np.asarray(patch_file_data._mapping, dtype=float).reshape(
118 -1, dimensions
119 )
120 index = int(
121 np.argmin(
122 np.sum((dof_positions - np.asarray(relative_position)) ** 2, axis=1)
123 )
124 )
125
126 return [
127 float(x) for x in patch.values[index * unknowns : (index + 1) * unknowns]
128 ]
129
130 def _assert_default_cell_mapping(self, patch_file_data):
131 """!
132 Cell data with a user-defined (distorted) mapping is not supported
133 """
134 dimensions = patch_file_data._dimensions
135 vertices_per_axis = patch_file_data._dof + 1
136 axis = np.linspace(0.0, 1.0, vertices_per_axis)
137 # x runs fastest, i.e. reverse the itertools order
138 default_mapping = np.asarray(
139 [
140 coordinate
141 for vertex in itertools.product(axis, repeat=dimensions)
142 for coordinate in reversed(vertex)
143 ]
144 )
145 mapping = np.asarray(patch_file_data._mapping, dtype=float)
146 if mapping.size != default_mapping.size or not np.allclose(
147 mapping, default_mapping
148 ):
149 raise Exception(
150 "Probing cell data with a non-default mapping is not supported yet ("
151 + patch_file_data._unknown_name
152 + ")"
153 )
154
155 def process_snapshot(self, snapshot, i):
156 """!
157 Return (timestamp, dict from unknown name onto (number of unknowns,
158 description, values or None))
159 """
160 if self.verbose:
161 print(f"Probe snapshot {i} (t={snapshot.timestamp})")
162 data = self.parse_snapshot(snapshot)
163 result = {}
164 for unknown_name, patch_file_data in data.items():
165 result[unknown_name] = (
166 patch_file_data._unknowns,
167 patch_file_data._description,
168 self._probe_patch_data(patch_file_data, self._point),
169 )
170 return snapshot.timestamp, result
171
172 def _run(self, max_workers):
174 self.show_progressshow_progress = max_workers == 1
175
176 if max_workers == 1:
177 results = [
178 self.process_snapshotprocess_snapshot(snapshot, i)
179 for i, snapshot in enumerate(self.snapshotssnapshots)
180 ]
181 else:
182 # Use spawn for same reasons as VTUUnstructuredGrid.display()
183 with ProcessPoolExecutor(
184 max_workers=max_workers,
185 mp_context=multiprocessing.get_context("spawn"),
186 ) as executor:
187 futures = [
188 executor.submit(self.process_snapshotprocess_snapshot, snapshot, i)
189 for i, snapshot in enumerate(self.snapshotssnapshots)
190 ]
191 results = [future.result() for future in futures]
192
193 self._write_csv(sorted(results, key=lambda result: result[0]))
194
195 def _column_names(self, columns, descriptions):
196 """!
197
198 One column name per unknown of each data set
199
200 If a data set names its unknowns (@see unknown_names_from_description()),
201 we use these names, e.g. h, hu, hv, b. If two data sets use the same
202 name, we prefix the names with the data set, e.g. SWEQ_h. Data sets
203 without names yield SWEQ_0, SWEQ_1, ...
204
205 """
206 names = {
207 unknown_name: unknown_names_from_description(
208 descriptions.get(unknown_name, ""), unknowns
209 )
210 for unknown_name, unknowns in columns.items()
211 }
212 all_names = [name for n in names.values() if n is not None for name in n]
213 unique = len(all_names) == len(set(all_names))
214 header = []
215 for unknown_name, unknowns in columns.items():
216 if names[unknown_name] is None:
217 header += [f"{unknown_name}_{i}" for i in range(unknowns)]
218 elif unique:
219 header += names[unknown_name]
220 else:
221 header += [f"{unknown_name}_{name}" for name in names[unknown_name]]
222 return header
223
224 def _write_csv(self, results):
225 columns = {}
226 descriptions = {}
227 for _, result in results:
228 for unknown_name, (unknowns, description, _) in result.items():
229 columns[unknown_name] = max(unknowns, columns.get(unknown_name, 0))
230 descriptions.setdefault(unknown_name, description)
231
232 uncovered_snapshots = 0
233 with open(self._output_file, "w", newline="") as csv_file:
234 writer = csv.writer(csv_file, delimiter=",")
235 writer.writerow(["time"] + self._column_names(columns, descriptions))
236
237 for timestamp, result in results:
238 row = [timestamp]
239 for unknown_name, unknowns in columns.items():
240 values = result.get(unknown_name, (unknowns, "", None))[2]
241 if values is None:
242 values = [float("nan")] * unknowns
243 uncovered_snapshots += 1
244 row += values
245 writer.writerow(row)
246
247 if uncovered_snapshots > 0:
248 print(
249 f"Warning: point {self._point} not covered by data in {uncovered_snapshots} (snapshot,data set) pair(s), wrote NaN"
250 )
251 print(f"Wrote {len(results)} snapshots into {self._output_file}")
252
253 def display(self, max_workers=1):
254 super(PointProbe, self).display()
255 self._run(max_workers)
256
258 self.snapshotssnapshots = [super(PointProbe, self).display_single_file()]
259 self._run(1)
Probe the data within a Peano patch file at one point over time.
Definition PointProbe.py:37
_probe_patch_data(self, patch_file_data, point)
Return list of unknowns at point or None if point is not covered.
Definition PointProbe.py:66
__init__(self, file_name, point, output_file, start, end, verbose=False)
Abstract superclass of a visualiser.
Definition PointProbe.py:61
display(self, max_workers=1)
This is the one routine the visualisers typically do overwrite.
process_snapshot(self, snapshot, i)
Return (timestamp, dict from unknown name onto (number of unknowns, description, values or None))
display_single_file(self)
If we are here we've supplied a single file which is itself a snapshot.
_assert_default_cell_mapping(self, patch_file_data)
Cell data with a user-defined (distorted) mapping is not supported.
_column_names(self, columns, descriptions)
One column name per unknown of each data set.
The visualiser is first and foremost a persistency layer around datasets.
parse_snapshot(self, snapshot)
We rewrite "reload" from previous version of this file.