Peano
Loading...
Searching...
No Matches
PatchFileParser.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
3import re
4
5import numpy as np
6
7from .Patch import Patch
8
9
10def unknown_names_from_description(description, unknowns):
11 """!
12
13 Names of the unknowns as stored in the description of a patch file
14
15 Plotters such as exahype2.api.fv.PlotAsVTKFile write the names of the
16 unknowns as comma-separated list into the description, e.g.
17 "h, hu, hv, b". A variable with multiplicity k shows up as k names
18 v[0], ..., v[k-1].
19
20 description: String
21 Description of an unknown as read from the patch file.
22
23 unknowns: Integer
24 Number of unknowns per degree of freedom.
25
26 Returns the list of names, or None if the description does not hold
27 exactly one name per unknown. Names are C++ identifiers, as they stem
28 from the Variables struct of a solver, optionally followed by an index
29 [i]. Any other description is free text, which we do not interpret.
30
31 """
32 names = [name.strip() for name in description.split(",")]
33 if len(names) == unknowns and all(
34 re.fullmatch(r"[A-Za-z_]\w*(\[\d+\])?", name) for name in names
35 ):
36 return names
37 return None
38
39
41 """!
42 Helper class to capture all of the attributes we need for each
43 unknown, but were originally fixed to the PatchFileParser class
44 itself.
45
46 For instance, at time of writing the vis scripts cannot handle
47 displaying separate vtu files for different data that is produced
48 in Peano patch files. An excerpt:
49
50 begin patch
51 offset 9.629630e-01 9.629630e-01
52 size 3.703704e-02 3.703704e-02
53 begin vertex-values "value"
54 0 0 0 0
55 end vertex-values
56 begin vertex-values "rhs"
57 0 0 0 0
58 end vertex-values
59 end patch
60
61 Previously, the only thing that would get parsed is the "value"
62 field, before moving on. We aim to get it to do the "rhs" part
63 as well.
64
65 However, we cannot assume that the number of dofs per axis or
66 unknowns per patch volume will be the same for both. So we
67 create this helper class to hold those.
68
69 We fix is_data_associated_to_cell to be the same as the
70 PatchFileParser object which, in turn, means they must
71 be the same for all of the unknowns
72
73 Attributes:
74 ----------
75
76 file_path: String
77 String of input file path (including filename and extension)
78
79 cell_data: list of patches
80 List of Patches with file data
81
82 dimensions: Integer
83 Dimension of domains
84
85 dof: Integer
86 Number of degrees of freedom per axis
87
88 unknowns: Integer
89 Number of unknowns per patch volume
90
91 mapping: series of d-dimensional tuples
92 Distorts the domain
93
94 set_identifier: String
95 Can be empty to parse everything.
96
97 """
98
99 def __init__(self, unknown_name, dimensions, is_data_associated_to_cell):
100 # this is the name of the unknown we want to capture
101 self.unknown_name = unknown_name
102
103 # this is the number of unknowns per patch volume
104 self.unknowns = 0
105 # number of dofs per axis
106 self.dof = 0
107 self.description = ""
108 self.cell_data = []
109 self.mapping = []
110 self.is_data_associated_to_cell = is_data_associated_to_cell
111 self.dimensions = dimensions
112
113 def set_unknowns(self, unknowns):
114 assert self.unknowns == 0, "attribute already set!"
115 self.unknowns = unknowns
116
117 def set_dof(self, dof):
118 assert self.dof == 0, "attribute already set!"
119 self.dof = dof
120
121 def set_description(self, description):
122 self.description = description
123
124 def unknown_names(self):
125 """! @see unknown_names_from_description()"""
126 return unknown_names_from_description(self.description, self.unknowns)
127
128 def set_celldata(self, celldata):
129 assert not self.cell_data, "attribute already set!"
130 self.cell_data = celldata
131
132 def set_mapping(self, mapping):
133 assert not self.mapping, "attribute already set!"
134 self.mapping = mapping
135
137 assert not self.mapping, "attribute already set!"
138 vertices_per_axis = self.dof
140 vertices_per_axis += 1
141 if self.mapping == [] and self.dimensions == 2:
142 for y in range(vertices_per_axis):
143 for x in range(vertices_per_axis):
144 self.mapping.append(x * 1.0 / (vertices_per_axis - 1))
145 self.mapping.append(y * 1.0 / (vertices_per_axis - 1))
146 if self.mapping == [] and self.dimensions == 3:
147 for z in range(vertices_per_axis):
148 for y in range(vertices_per_axis):
149 for x in range(vertices_per_axis):
150 self.mapping.append(x * 1.0 / (vertices_per_axis - 1))
151 self.mapping.append(y * 1.0 / (vertices_per_axis - 1))
152 self.mapping.append(z * 1.0 / (vertices_per_axis - 1))
153
154 def append_patch(self, patch):
155 self.cell_data.append(patch)
156
157 def __repr__(self):
158 return f"{self.unknown_name}: unknowns:{self.unknowns}, dof:{self.dof}"
159
160
162 """!
163
164 Binary-safe view onto a Peano patch file
165
166 A patch file is a text file, but the quantities of the patches of a
167 file with `format Binary` are raw doubles. We hence open every file in
168 binary mode, hand out the text lines as decoded strings, and let the
169 parser fetch or skip exactly as many bytes as the quantities of a patch
170 occupy (@ref tarch::plotter::griddata::blockstructured::PeanoBinaryPatchFileWriter).
171 For an ASCII file, the class behaves like a text file.
172
173 """
174
175 def __init__(self, file_path):
176 self._file = open(file_path, "rb")
177
178 def __enter__(self):
179 return self
180
181 def __exit__(self, *args):
182 self._file.close()
183
184 def readline(self):
185 return self._file.readline().decode("utf-8", errors="replace")
186
187 def read_doubles(self, count):
188 """!
189
190 Read count doubles in the byte order of this machine, plus the line
191 break behind them. Returns a writable numpy array.
192
193 @param count: int
194 Number of doubles.
195
196 """
197 raw = self._file.read(8 * count)
198 if len(raw) != 8 * count:
199 raise Exception("file ended within the binary quantities of a patch")
200 self._file.readline()
201 return np.frombuffer(raw, dtype=np.float64).copy()
202
203 def skip_doubles(self, count):
204 self._file.seek(8 * count, 1)
205 self._file.readline()
206
207
209 """!
210 Parser for Peano block file output.
211
212 This class should be instantiated from within the Visualiser class.
213 This class should be called once per patch file that contains
214 patch data that we want to render. The main method that is called
215 is parse_file().
216
217 This method doesn't return anything. Rather, it sets unknown_attributes
218 to capture all the patch data for each of the unknowns that we find.
219
220 Then, the Visualiser class will steal this data, and combine it into
221 its own helper class.
222
223 Attributes:
224 ----------
225
226 file_path: String
227 String of input file path (including filename and extension)
228
229 dimensions: Integer
230 Dimension of domains
231
232 mapping: series of d-dimensional tuples
233 Distorts the domain
234
235 set_identifier: String
236 Can be empty to parse everything.
237
238 unknown_attributes: Dict{ String, UnknownAttributes }
239 Dictionary that has the names of each unknown we wanna capture
240 as its keys, and the values are the helper class which captures
241 dimensions, cell data etc. Each of the unknowns should be
242 listed during the metadata part of the patch file, ie between
243 "begin cell-metadata" and "end cell-metadata".
244 """
245
247 self,
248 file_path,
249 set_identifier,
250 subdomain_number,
251 show_progress=False,
252 patch_filter=None,
253 ):
254 """!
255
256 show_progress: Boolean
257 Print a single-line, in-place progress bar (percentage plus
258 patch count) to the terminal while parse_file() works through
259 this file's patches - @see __count_patches()/__print_progress().
260 Off by default: it writes \\r-terminated lines, so it only
261 produces clean output when nothing else concurrently writes to
262 the same terminal - the caller (Visualiser.show_progress) is
263 responsible for only turning this on when that is guaranteed
264 (e.g. sequential, single-worker processing).
265
266 patch_filter: Callable (offset, size) -> Boolean, or None
267 If set, parse_file() calls this predicate on each patch as soon as
268 it has read the patch's offset and size, and skips the patch's
269 values without converting them if the predicate returns False.
270 This is way cheaper than parsing everything and throwing the data
271 away afterwards, if a postprocessing script needs only a few
272 patches (@see output.PointProbe). With multiple worker processes,
273 the predicate has to be picklable, i.e. no lambda.
274
275 """
276 self.file_path = file_path
277 self.patch_filter = patch_filter
279 self.dimensions = -1
280 # "ASCII" or "Binary", as the header of the file says
281 self.file_format = "ASCII"
282 # Number of doubles per patch of each dataset in the file, filled for
283 # binary files only. We need it to step over datasets we ignore.
285
286 self.set_identifier = set_identifier
287 self.subdomain_number = subdomain_number
288 self.parsed = False
290 self.show_progress = show_progress
291
292 """
293 Here we expected the keys to be a list of unknown names that we
294 have associated with each cell/vertex, and the values to be
295 instances of the helper class UnknownAttributes
296 """
298
299 def __repr__(self):
300 return f"Parser for {self.file_path}, subdomain {self.subdomain_number}, identifier {self.set_identifier}"
301
302 def __parse_meta_data_region(self, current_line, file_object, end_condition):
303 """!
304 Pass in file object, move it along and return it
305 """
306 # unknown_name is contained at end of "begin x-metadata" line
307 unknown_name = current_line.strip().split()[-1][1:-1]
308
309 # create new helper class for this
310 # set the bool for data associated with cell to be the same as self.
311 # we set self.is_data_associated_to_cell already, during parse_file(),
312 # just before we came into this function.
313 patch_unknown = UnknownAttributes(
314 unknown_name, self.dimensions, self.is_data_associated_to_cell
315 )
316
317 current_line = file_object.readline().strip()
318
319 use_default_mapping = True
320
321 # do everything that we did in old line reading function
322 while not end_condition in current_line:
323 if "number-of-unknowns" in current_line:
324 patch_unknown.set_unknowns(int(current_line.strip().split()[1]))
325 if "number-of-dofs-per-axis" in current_line:
326 patch_unknown.set_dof(int(current_line.strip().split()[1]))
327 if "description" in current_line:
328 patch_unknown.set_description(
329 current_line.strip().split("description")[1].strip()
330 )
331 if '"' in patch_unknown.description:
332 print(
333 "Warning: data description field "
334 + patch_unknown.description
335 + " holds \" which might lead to issues with VTK's XML export. Remove them"
336 )
337 patch_unknown.description = patch_unknown.description.replace(
338 '"', ""
339 )
340 if "mapping" in current_line:
341 use_default_mapping = False
342 values = current_line.strip().split("mapping")[1]
343 patch_unknown.set_mapping(np.fromstring(values, dtype=float, sep=" "))
344
345 current_line = file_object.readline().strip()
346
347 if use_default_mapping:
348 patch_unknown._initialise_default_mapping_if_no_mapping_specified()
349
350 # finally, add this unknown to our dictionary
351 self.unknown_attributes[unknown_name] = patch_unknown
352 self._doubles_per_patch[unknown_name] = (
353 patch_unknown.unknowns * patch_unknown.dof**self.dimensions
354 )
355
356 return file_object, current_line
357
358 def __read_values(self, unknown_name, file_object):
359 """!
360
361 Read the quantities of one patch of one dataset. The line
362 "begin cell-values" or "begin vertex-values" has just been read.
363
364 """
365 if self.file_format == "Binary":
366 return file_object.read_doubles(self._doubles_per_patch[unknown_name])
367 return np.fromstring(file_object.readline(), dtype=float, sep=" ")
368
369 def __skip_values_if_binary(self, current_line, file_object):
370 """!
371
372 If current_line opens the quantities of a dataset in a binary file,
373 step over them.
374
375 """
376 if self.file_format == "Binary" and (
377 current_line.startswith("begin cell-values")
378 or current_line.startswith("begin vertex-values")
379 ):
380 unknown_name = current_line.split()[-1][1:-1]
381 file_object.skip_doubles(self._doubles_per_patch[unknown_name])
382
383 def __record_doubles_per_patch(self, current_line, file_object, end_condition):
384 """!
385
386 Binary files only: Run through a metadata block of a dataset which
387 we do not parse and remember how many doubles each of its patches
388 holds, so we can step over them.
389
390 """
391 unknown_name = current_line.strip().split()[-1][1:-1]
392 unknowns = 0
393 dof = 0
394 current_line = file_object.readline().strip()
395 while end_condition not in current_line:
396 if "number-of-unknowns" in current_line:
397 unknowns = int(current_line.split()[1])
398 if "number-of-dofs-per-axis" in current_line:
399 dof = int(current_line.split()[1])
400 current_line = file_object.readline().strip()
401 self._doubles_per_patch[unknown_name] = unknowns * dof**self.dimensions
402 return file_object, current_line
403
405 self, current_line, file_object, end_condition="end patch"
406 ):
407 """!
408 Pass in file object, run it until we encounter the end condition,
409 and then return it
410
411 We assume that current_line == "begin patch file" at this stage
412
413 This is where we read the actual data
414 """
415 self.file_contains_patches = True
416 # Get patch offset
417 current_line = file_object.readline().strip()
418 line = current_line.strip().split()
419 if self.dimensions == 2:
420 offset = (float(line[1]), float(line[2]))
421 elif self.dimensions == 3:
422 offset = (float(line[1]), float(line[2]), float(line[3]))
423
424 # Get patch size
425 current_line = file_object.readline().strip()
426 line = current_line.strip().split()
427 if self.dimensions == 2:
428 size = (float(line[1]), float(line[2]))
429 elif self.dimensions == 3:
430 size = (float(line[1]), float(line[2]), float(line[3]))
431
432 if self.patch_filter is not None and not self.patch_filter(offset, size):
433 while end_condition not in current_line:
434 current_line = file_object.readline().strip()
435 self.__skip_values_if_binary(current_line, file_object)
436 return file_object, current_line
437
438 # run until we have found all of the fields in this
439 # end condition is "end patch" by default
440 while end_condition not in current_line:
441 current_line = file_object.readline().strip()
442 values = None # prevents us adding empty patch to list at end of loop
443 unknown_name = ""
444
445 # get cell data
446 if current_line.startswith("begin cell-values") and (
447 self.set_identifier == ""
448 or current_line.endswith('"' + self.set_identifier + '"')
449 ):
450 assert (
452 ), "is_data_associated_to_cell flag set incorrectly"
453 unknown_name = current_line.strip().split()[-1][1:-1]
454 values = self.__read_values(unknown_name, file_object)
455 current_line = ""
456
457 # do same for vertex data
458 if current_line.startswith("begin vertex-values") and (
459 self.set_identifier == ""
460 or current_line.endswith('"' + self.set_identifier + '"')
461 ):
462 assert (
464 ), "is_data_associated_to_cell flag set incorrectly"
465 unknown_name = current_line.strip().split()[-1][1:-1]
466 values = self.__read_values(unknown_name, file_object)
467 current_line = ""
468
469 # Datasets we are not interested in. Their binary quantities have
470 # to be stepped over, as they may hold any byte sequence.
471 if values is None:
472 self.__skip_values_if_binary(current_line, file_object)
473
474 # if we have read some data, let's add it to our list
475 if values is not None:
476 patch = Patch(offset, size, values, unknown_name, self.subdomain_number)
477
478 # by this point, all the unknown_names should be present
479 self.unknown_attributes[unknown_name].append_patch(patch)
480
481 return file_object, current_line
482
484 """!
485
486 Quick pre-scan (just a substring search per line, no numpy
487 parsing) to count "begin patch" occurrences, i.e. the number of
488 cells/patches this file holds, so parse_file() can show progress
489 against a known total instead of an estimate. Reads the file a
490 second time, but that is negligible next to the actual parse
491 below, which parses a numpy array per patch.
492
493 """
494 count = 0
495 with open(self.file_path, "rb") as f:
496 for line in f:
497 if line.startswith(b"begin patch"):
498 count += 1
499 return count
500
501 def __print_progress(self, current, total):
502 """!
503
504 Render current/total as an in-place, single-line percentage/bar
505 on the terminal (\\r-terminated, so nothing else may write to
506 stdout while this runs - @see show_progress).
507
508 """
509 fraction = current / total if total > 0 else 1.0
510 bar_length = 30
511 filled = int(bar_length * fraction)
512 bar = "#" * filled + "-" * (bar_length - filled)
513 print(
514 f"\rParsing {self.file_path}: [{bar}] {fraction * 100:5.1f}% ({current}/{total} cells)",
515 end="",
516 flush=True,
517 )
518
519 def parse_file(self):
520 """!
521 Read file and return cell data, dimensions, number of degrees of freedom and number of unknowns
522
523 Parameters:
524 ----------
525 set_identifier: String
526 Name of the set of unknowns we want to extract. If this is the empty string
527 then I parse all content.
528
529 @todo: clean up the loop below. we can separate into getting metadata, and then reading patches
530 """
531 # print("Reading " + self.file_path + " as subdomain " + str(self.subdomain_number))
532 current_line = ""
533 total_patches = self.__count_patches() if self.show_progress else 0
534 patches_parsed = 0
535 # Redraw only when the displayed (one-decimal) percentage actually
536 # changes, not on every single patch - flush()ing a print for
537 # every one of a file's potentially hundreds of thousands of
538 # patches measurably slows parsing down otherwise.
539 last_printed_permille = -1
540 try:
541 with _PatchFile(self.file_path) as data_file:
542 current_line = data_file.readline()
543 while current_line:
544 # when we reach EOF, this_line will eval to False
545 """
546 the reason we keep track of 2 variables current_line and
547 line is because we want current_line to eval to False when we reach
548 EOF. However, we need to strip the whitespaces and newline
549 characters off the line for the data processing, without
550 exiting the loop if we have empty lines.
551 """
552 current_line = data_file.readline()
553 stripped_line = current_line.strip()
554 if stripped_line.startswith("format"):
555 self.file_format = stripped_line.split()[1]
556 assert self.file_format in [
557 "ASCII",
558 "Binary",
559 ], "Unknown patch file format " + self.file_format
560
561 if stripped_line.startswith("dimensions"):
562 self.dimensions = int(stripped_line.strip().split()[1])
563 assert self.dimensions in [
564 2,
565 3,
566 ], "Only 2d and 3d patches are supported"
567
568 # Read out meta data
569 if stripped_line.startswith("begin cell-metadata") and (
570 self.set_identifier == ""
571 or stripped_line.endswith('"' + self.set_identifier + '"')
572 ):
574 # this function advances us along the data file
575 data_file, stripped_line = self.__parse_meta_data_region(
576 stripped_line, data_file, "end cell-metadata"
577 )
578
579 if stripped_line.startswith("begin vertex-metadata") and (
580 self.set_identifier == ""
581 or stripped_line.endswith('"' + self.set_identifier + '"')
582 ):
583 self.is_data_associated_to_cell = False
584 # this function advances us along the data file
585 data_file, stripped_line = self.__parse_meta_data_region(
586 stripped_line, data_file, "end vertex-metadata"
587 )
588
589 elif self.file_format == "Binary" and (
590 stripped_line.startswith("begin cell-metadata")
591 or stripped_line.startswith("begin vertex-metadata")
592 ):
593 # Dataset we do not read. We nevertheless need to know
594 # how many bytes its quantities occupy.
595 data_file, stripped_line = self.__record_doubles_per_patch(
596 stripped_line,
597 data_file,
598 "end " + stripped_line.split()[1],
599 )
600
601 elif stripped_line.startswith("begin patch"):
602 # pass data_file into self.__parse_patch_region, which will modify and return it
603 # ie we move further along the file inside this function
604 # appending new patches happens in this function
605 data_file, stripped_line = self.__parse_patch_region(
606 stripped_line, data_file
607 )
608 if self.show_progress:
609 patches_parsed += 1
610 permille = (patches_parsed * 1000) // total_patches
611 if (
612 permille != last_printed_permille
613 or patches_parsed == total_patches
614 ):
615 last_printed_permille = permille
616 self.__print_progress(patches_parsed, total_patches)
617
618 # self._initialise_default_mapping_if_no_mapping_specified() # handled in meta data reader
619 self.parsed = True
620 except Exception as e:
621 print(
622 "Error: was not able to read "
623 + self.file_path
624 + ": "
625 + str(e)
626 + " in line "
627 + current_line
628 )
629 self.parsed = False
630 finally:
631 if self.show_progress and total_patches > 0:
632 # Move past the \r-updated progress line so subsequent
633 # output (errors, the next file's own progress bar, ...)
634 # starts on a fresh line.
635 print()
636
637 # COMMENTING OUT DEBUG STATEMENTS
638 # print("finished parsing the file.")
639 # print(f"we now have {len(self.unknown_attributes.keys())} unknown names, and we have seen a total of {sum([len(v.cell_data) for _,v in self.unknown_attributes.items()])} patches...")
640 # print("we pipe the data into UnknownAttributes objects")
641
642 def render_or_validate_for_each_unknown(self, render_or_validate):
643 """!
644 We need to call the render function for each filter
645 on a variety of attributes, and then return them if they
646 are modified.
647
648 We do this in a functional style, ie the Visualiser class
649 contains all of the filters we wish to apply, and we pass
650 the render function that we intend to use back to this
651 layer. Ugly, but we need to do some looping over the
652 different unknowns we see in the patch files, and I thought
653 it best to hide that from the parse_snapshot region of
654 the vis code.
655
656 The same apply for the validate function from the Visualiser
657 file as well.
658 """
659
660 for unknown_name, unknown_data in self.unknown_attributes.items():
661 # this is horrible to read. but we need to pass all these
662 # attributes in, and capture their return values from the
663 # renderer function
664 (
665 unknown_data.cell_data,
666 unknown_data.dof,
667 unknown_data.dimensions,
668 unknown_data.unknowns,
669 unknown_data.is_data_associated_to_cell,
670 unknown_data.description,
671 unknown_data.mapping,
672 ) = render_or_validate(
673 unknown_data.cell_data,
674 unknown_data.dof,
675 unknown_data.dimensions,
676 unknown_data.unknowns,
677 unknown_data.is_data_associated_to_cell,
678 unknown_data.description,
679 unknown_data.mapping,
680 )
parse_file(self)
Read file and return cell data, dimensions, number of degrees of freedom and number of unknowns.
__record_doubles_per_patch(self, current_line, file_object, end_condition)
Binary files only: Run through a metadata block of a dataset which we do not parse and remember how m...
__parse_meta_data_region(self, current_line, file_object, end_condition)
Pass in file object, move it along and return it.
render_or_validate_for_each_unknown(self, render_or_validate)
We need to call the render function for each filter on a variety of attributes, and then return them ...
__print_progress(self, current, total)
Render current/total as an in-place, single-line percentage/bar on the terminal (\r-terminated,...
__init__(self, file_path, set_identifier, subdomain_number, show_progress=False, patch_filter=None)
show_progress: Boolean Print a single-line, in-place progress bar (percentage plus patch count) to th...
__parse_patch_region(self, current_line, file_object, end_condition="end patch")
Pass in file object, run it until we encounter the end condition, and then return it.
__count_patches(self)
Quick pre-scan (just a substring search per line, no numpy parsing) to count "begin patch" occurrence...
__skip_values_if_binary(self, current_line, file_object)
If current_line opens the quantities of a dataset in a binary file, step over them.
__read_values(self, unknown_name, file_object)
Read the quantities of one patch of one dataset.
Helper class to capture all of the attributes we need for each unknown, but were originally fixed to ...
__init__(self, unknown_name, dimensions, is_data_associated_to_cell)
Binary-safe view onto a Peano patch file.
read_doubles(self, count)
Read count doubles in the byte order of this machine, plus the line break behind them.
Patch class that contains offset, size and value of a Peano patch.
Definition Patch.py:5