PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
picgrid.py
Go to the documentation of this file.
1"""!
2@file picgrid.py
3@brief The canonical PICGRID text grid format: its coordinate precision, header reader and
4 coordinate reader, shared by the conductor and the generators.
5
6Not a command. `core.py`, `grid.gen`, `ic.gen`, `profile.gen`, `reduce.gen` and `spectra.gen`
7import it from `picurv_cli/`, as they import `petsc_binary.py`. A PICGRID file is the token
8`PICGRID`, a block count, one `IM JM KM` node-count row per block, then every block's node
9coordinates in block order, one `x y z` row per node with i fastest, then j, then k. Blank lines
10and lines beginning with `#` are not part of the content.
11"""
12
13import math
14import os
15
16#: Coordinate text precision; 17 significant digits round-trip any binary64 value.
17PICGRID_FLOAT_FORMAT = ".17e"
18
19
20def format_picgrid_coordinate(value: float) -> str:
21 """!
22 @brief Format a coordinate with round-trip-safe binary64 precision.
23 @param[in] value Coordinate value.
24 @return Formatted coordinate.
25 """
26 return format(value, PICGRID_FLOAT_FORMAT)
27
28
29def content_lines(stream):
30 """!
31 @brief Yield the content lines of a PICurv text payload, skipping blank and `#` lines.
32 @param[in] stream Open text stream.
33 @return Generator of `(line_number, stripped_line)` pairs, numbered from 1.
34 """
35 for lineno, raw in enumerate(stream, start=1):
36 line = raw.strip()
37 if line and not line.startswith("#"):
38 yield lineno, line
39
40
41def read_picgrid_header(lines, path: str, expected_nblk: int = None) -> list:
42 """!
43 @brief Consume a PICGRID header from a content-line iterator.
44 @details Leaves the iterator at the first coordinate row.
45 @param[in] lines Iterator from content_lines().
46 @param[in] path File path, for errors.
47 @param[in] expected_nblk Optional block count the caller requires.
48 @return List of `(IM, JM, KM)` node-count tuples, one per block.
49 @throws ValueError on a malformed header.
50 """
51 try:
52 _, first_token = next(lines)
53 except StopIteration:
54 raise ValueError(f"Grid file '{path}' is empty.")
55 if first_token != "PICGRID":
56 raise ValueError(f"Grid file '{path}' must begin with the canonical PICGRID header token.")
57
58 try:
59 _, nblk_line = next(lines)
60 nblk = int(nblk_line)
61 except StopIteration:
62 raise ValueError(f"Grid file '{path}' missing block count after PICGRID header.")
63 except ValueError:
64 raise ValueError(f"Invalid block count '{nblk_line}' in grid file '{path}'.")
65 if nblk <= 0:
66 raise ValueError(f"Grid file '{path}' has non-positive block count: {nblk}.")
67 if expected_nblk is not None and nblk != expected_nblk:
68 raise ValueError(f"Grid file block count mismatch: case expects {expected_nblk}, grid contains {nblk}.")
69
70 dims = []
71 for bi in range(nblk):
72 try:
73 lineno, dim_line = next(lines)
74 except StopIteration:
75 raise ValueError(f"Grid file '{path}' missing dimensions for block {bi}.")
76 parts = dim_line.split()
77 if len(parts) != 3:
78 raise ValueError(f"Invalid dimensions line at {path}:{lineno}. Expected 3 integers, got: '{dim_line}'.")
79 try:
80 im, jm, km = (int(parts[0]), int(parts[1]), int(parts[2]))
81 except ValueError:
82 raise ValueError(f"Invalid dimensions line at {path}:{lineno}. Non-integer values: '{dim_line}'.")
83 if im <= 0 or jm <= 0 or km <= 0:
84 raise ValueError(f"Invalid block dimensions at {path}:{lineno}: ({im}, {jm}, {km}). Must be > 0.")
85 dims.append((im, jm, km))
86 return dims
87
88
89def read_picgrid_dimensions(path: str, expected_nblk: int = None) -> list:
90 """!
91 @brief Read only the header of a PICGRID file.
92 @param[in] path PICGRID path.
93 @param[in] expected_nblk Optional block count the caller requires.
94 @return List of `(IM, JM, KM)` node-count tuples, one per block.
95 @throws ValueError on a missing file or malformed header.
96 """
97 if not os.path.isfile(path):
98 raise ValueError(f"Grid file not found: {path}")
99 with open(path, "r", encoding="utf-8") as stream:
100 return read_picgrid_header(content_lines(stream), path, expected_nblk)
101
102
103def parse_coordinate_row(lineno: int, line: str, path: str) -> tuple:
104 """!
105 @brief Parse one coordinate row into three finite floats.
106 @param[in] lineno Line number, for errors.
107 @param[in] line Stripped content line.
108 @param[in] path File path, for errors.
109 @return `(x, y, z)`.
110 @throws ValueError when the row is not three finite numbers.
111 """
112 parts = line.split()
113 if len(parts) != 3:
114 raise ValueError(f"Invalid coordinate row at {path}:{lineno}. Expected 3 floats, got: '{line}'.")
115 try:
116 point = (float(parts[0]), float(parts[1]), float(parts[2]))
117 except ValueError:
118 raise ValueError(f"Invalid coordinate row at {path}:{lineno}. Non-numeric values: '{line}'.")
119 if not all(math.isfinite(value) for value in point):
120 raise ValueError(f"Invalid coordinate row at {path}:{lineno}. Coordinates must be finite: '{line}'.")
121 return point
122
123
124def read_picgrid_blocks(path: str, expected_nblk: int = None) -> list:
125 """!
126 @brief Read every block of a PICGRID file as node-coordinate arrays.
127 @details For consumers that build cell geometry, so each axis needs at least two nodes.
128 @param[in] path PICGRID path.
129 @param[in] expected_nblk Optional block count the caller requires.
130 @return List of `{"dims": (IM, JM, KM), "coords": array shaped (KM, JM, IM, 3)}`.
131 @throws ValueError on a missing or malformed file.
132 """
133 from periodic_spectral import require_numpy
134
135 numpy = require_numpy()
136 if not os.path.isfile(path):
137 raise ValueError(f"Grid file not found: {path}")
138 with open(path, "r", encoding="utf-8") as stream:
139 lines = content_lines(stream)
140 dims = read_picgrid_header(lines, path, expected_nblk)
141 blocks = []
142 for block_idx, (im, jm, km) in enumerate(dims):
143 if min(im, jm, km) < 2:
144 raise ValueError(f"PICGRID block {block_idx} dimensions {(im, jm, km)} are too small.")
145 coords = []
146 for lineno, line in lines:
147 coords.append(parse_coordinate_row(lineno, line, path))
148 if len(coords) == im * jm * km:
149 break
150 if len(coords) != im * jm * km:
151 raise ValueError(f"Grid file '{path}' missing coordinates for block {block_idx}.")
152 blocks.append({"dims": (im, jm, km),
153 "coords": numpy.asarray(coords, dtype=float).reshape((km, jm, im, 3))})
154 if next(lines, None) is not None:
155 raise ValueError(f"Grid file '{path}' has trailing coordinate rows.")
156 return blocks
require_numpy()
Import NumPy only for commands that need numeric reductions.
Definition core.py:115
content_lines(stream)
Yield the content lines of a PICurv text payload, skipping blank and # lines.
Definition picgrid.py:29
list read_picgrid_blocks(str path, int expected_nblk=None)
Read every block of a PICGRID file as node-coordinate arrays.
Definition picgrid.py:124
list read_picgrid_header(lines, str path, int expected_nblk=None)
Consume a PICGRID header from a content-line iterator.
Definition picgrid.py:41
tuple parse_coordinate_row(int lineno, str line, str path)
Parse one coordinate row into three finite floats.
Definition picgrid.py:103
list read_picgrid_dimensions(str path, int expected_nblk=None)
Read only the header of a PICGRID file.
Definition picgrid.py:89
str format_picgrid_coordinate(float value)
Format a coordinate with round-trip-safe binary64 precision.
Definition picgrid.py:20