plot3d 1.8.2__tar.gz → 1.9.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.
- {plot3d-1.8.2 → plot3d-1.9.0}/PKG-INFO +1 -1
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/__init__.py +4 -3
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/block.py +50 -1
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/blockfunctions.py +97 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/connectivity.py +110 -1
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/glennht/__init__.py +16 -2
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/glennht/class_definitions.py +46 -35
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/glennht/export_functions.py +44 -28
- plot3d-1.9.0/plot3d/glennht/validation.py +448 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/periodicity.py +126 -34
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/read.py +118 -5
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/verify.py +144 -82
- {plot3d-1.8.2 → plot3d-1.9.0}/pyproject.toml +1 -1
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/block_merging_mixed_facepairs.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/differencing.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/face.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/facefunctions.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/glennht/import_functions.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/graph.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/gridpro/__init__.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/gridpro/import_functions.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/listfunctions.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/normals.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/point_match.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/pointwise/__init__.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/pointwise/import_functions.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/split_block.py +0 -0
- {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/write.py +0 -0
|
@@ -3,9 +3,9 @@ from importlib import import_module
|
|
|
3
3
|
import os, warnings
|
|
4
4
|
|
|
5
5
|
from .block import Block
|
|
6
|
-
from .blockfunctions import rotate_block, get_outer_bounds, block_connection_matrix,split_blocks, plot_blocks, reduce_blocks, find_matching_faces, compute_min_gcd, scale_face_bounds, constant_axis
|
|
6
|
+
from .blockfunctions import rotate_block, get_outer_bounds, block_connection_matrix,split_blocks, plot_blocks, reduce_blocks, find_matching_faces, compute_min_gcd, scale_face_bounds, constant_axis, make_right_handed
|
|
7
7
|
from .block_merging_mixed_facepairs import combine_nxnxn_cubes_mixed_pairs
|
|
8
|
-
from .connectivity import find_matching_blocks, get_face_intersection, connectivity_fast, face_matches_to_dict, PERMUTATION_MATRICES
|
|
8
|
+
from .connectivity import find_matching_blocks, get_face_intersection, connectivity_fast, face_matches_to_dict, PERMUTATION_MATRICES, normalize_face_matches
|
|
9
9
|
from .face import Face
|
|
10
10
|
from .facefunctions import create_face_from_diagonals, get_outer_faces, find_bounding_faces,split_face,find_face_nearest_point,match_faces_dict_to_list,outer_face_dict_to_list,find_closest_block
|
|
11
11
|
from .read import read_plot3D, read_ap_nasa
|
|
@@ -13,7 +13,8 @@ from .write import write_plot3D
|
|
|
13
13
|
from .differencing import find_edges, find_face_edges
|
|
14
14
|
from .periodicity import periodicity, periodicity_fast, create_rotation_matrix, rotated_periodicity, translational_periodicity
|
|
15
15
|
from .verify import (verify_connectivity, verify_periodicity,
|
|
16
|
-
extract_canonical_grid,
|
|
16
|
+
extract_canonical_grid, extract_directed_grid,
|
|
17
|
+
apply_permutation, verify_match,
|
|
17
18
|
verify_partial_match, try_all_permutations, get_bounds,
|
|
18
19
|
determine_plane)
|
|
19
20
|
from .point_match import point_match
|
|
@@ -188,9 +188,58 @@ class Block:
|
|
|
188
188
|
'kmax': (self.X[:,:,-1], self.Y[:,:,-1], self.Z[:,:,-1]),
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
def check_handedness(self) -> bool:
|
|
192
|
+
"""Check if the block has right-handed (positive volume) cells.
|
|
193
|
+
|
|
194
|
+
Samples cells at the block center and corners using a scalar
|
|
195
|
+
triple product. Returns ``True`` if right-handed, ``False``
|
|
196
|
+
if left-handed (negative volume).
|
|
197
|
+
"""
|
|
198
|
+
X, Y, Z = self.X, self.Y, self.Z
|
|
199
|
+
ni, nj, nk = self.IMAX, self.JMAX, self.KMAX
|
|
200
|
+
if ni < 2 or nj < 2 or nk < 2:
|
|
201
|
+
return True # degenerate block, nothing to check
|
|
202
|
+
|
|
203
|
+
# Sample a few representative cells
|
|
204
|
+
samples = [
|
|
205
|
+
(ni // 2, nj // 2, nk // 2), # center
|
|
206
|
+
(0, 0, 0), # corner
|
|
207
|
+
(ni - 2, nj - 2, nk - 2), # opposite corner
|
|
208
|
+
]
|
|
209
|
+
total = 0.0
|
|
210
|
+
for i, j, k in samples:
|
|
211
|
+
if i >= ni - 1 or j >= nj - 1 or k >= nk - 1:
|
|
212
|
+
continue
|
|
213
|
+
p000 = np.array([X[i, j, k], Y[i, j, k], Z[i, j, k]])
|
|
214
|
+
d1 = np.array([X[i+1,j,k] - p000[0], Y[i+1,j,k] - p000[1], Z[i+1,j,k] - p000[2]])
|
|
215
|
+
d2 = np.array([X[i,j+1,k] - p000[0], Y[i,j+1,k] - p000[1], Z[i,j+1,k] - p000[2]])
|
|
216
|
+
d3 = np.array([X[i,j,k+1] - p000[0], Y[i,j,k+1] - p000[1], Z[i,j,k+1] - p000[2]])
|
|
217
|
+
total += np.dot(d1, np.cross(d2, d3))
|
|
218
|
+
return total >= 0
|
|
219
|
+
|
|
220
|
+
def fix_handedness(self) -> bool:
|
|
221
|
+
"""Fix left-handed blocks by reversing the k-axis.
|
|
222
|
+
|
|
223
|
+
If the block has negative cell volumes (left-handed), the
|
|
224
|
+
k-index is reversed so that the cell orientation becomes
|
|
225
|
+
right-handed. Reversing k (spanwise) preserves the i and j
|
|
226
|
+
conventions (i=chord, j=wall-normal) that connectivity and
|
|
227
|
+
boundary conditions depend on.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
bool: ``True`` if a fix was applied, ``False`` if already
|
|
231
|
+
right-handed.
|
|
232
|
+
"""
|
|
233
|
+
if self.check_handedness():
|
|
234
|
+
return False
|
|
235
|
+
self.X = self.X[:, :, ::-1].copy()
|
|
236
|
+
self.Y = self.Y[:, :, ::-1].copy()
|
|
237
|
+
self.Z = self.Z[:, :, ::-1].copy()
|
|
238
|
+
return True
|
|
239
|
+
|
|
191
240
|
@property
|
|
192
241
|
def size(self)->int:
|
|
193
|
-
"""returns the total number of nodes
|
|
242
|
+
"""returns the total number of nodes
|
|
194
243
|
|
|
195
244
|
Returns:
|
|
196
245
|
int: number of nodes
|
|
@@ -481,3 +481,100 @@ def build_connectivity_graph(connectivities: List[List[Dict]]) -> nx.Graph:
|
|
|
481
481
|
block2 = pair['block2']['block_index'] # type: ignore
|
|
482
482
|
G.add_edge(block1, block2)
|
|
483
483
|
return G
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def make_right_handed(
|
|
487
|
+
blocks: List[Block],
|
|
488
|
+
face_matches: Optional[List[dict]] = None,
|
|
489
|
+
periodic_matches: Optional[List[dict]] = None,
|
|
490
|
+
outer_faces: Optional[List[dict]] = None,
|
|
491
|
+
block_names: Optional[List[str]] = None,
|
|
492
|
+
flip_axis: int = 2,
|
|
493
|
+
) -> Tuple[List[Block], List[dict], List[dict], List[dict]]:
|
|
494
|
+
"""Make all blocks right-handed by reversing one axis on left-handed blocks.
|
|
495
|
+
|
|
496
|
+
A left-handed block has majority-negative cell volumes (computed via
|
|
497
|
+
the Davies & Salmond hexahedral formula). This commonly occurs when
|
|
498
|
+
converting from cylindrical to Cartesian coordinates. GlennHT and
|
|
499
|
+
other structured solvers require right-handed blocks.
|
|
500
|
+
|
|
501
|
+
For each left-handed block the function:
|
|
502
|
+
1. Reverses the chosen axis in the X, Y, Z arrays.
|
|
503
|
+
2. Remaps all connectivity indices for that block:
|
|
504
|
+
``idx -> n - 1 - idx`` along the flipped axis.
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
blocks : list of Block
|
|
509
|
+
Plot3D blocks (modified in place and returned).
|
|
510
|
+
face_matches : list of dict, optional
|
|
511
|
+
Interface connectivity records (modified in place).
|
|
512
|
+
periodic_matches : list of dict, optional
|
|
513
|
+
Periodic connectivity records (modified in place).
|
|
514
|
+
outer_faces : list of dict, optional
|
|
515
|
+
Boundary-condition face records (modified in place).
|
|
516
|
+
block_names : list of str, optional
|
|
517
|
+
Names for log output. If None, blocks are numbered.
|
|
518
|
+
flip_axis : int
|
|
519
|
+
Which axis to reverse on left-handed blocks: 0=i, 1=j, 2=k.
|
|
520
|
+
Default 2 (k) preserves the i/j blade-plane topology.
|
|
521
|
+
|
|
522
|
+
Returns
|
|
523
|
+
-------
|
|
524
|
+
(blocks, face_matches, periodic_matches, outer_faces)
|
|
525
|
+
The same objects, modified in place, for convenience.
|
|
526
|
+
"""
|
|
527
|
+
if face_matches is None:
|
|
528
|
+
face_matches = []
|
|
529
|
+
if periodic_matches is None:
|
|
530
|
+
periodic_matches = []
|
|
531
|
+
if outer_faces is None:
|
|
532
|
+
outer_faces = []
|
|
533
|
+
|
|
534
|
+
flipped: List[int] = []
|
|
535
|
+
|
|
536
|
+
for bi, b in enumerate(blocks):
|
|
537
|
+
vol = b.cell_volumes()
|
|
538
|
+
interior = vol[1:, 1:, 1:]
|
|
539
|
+
n_neg = int(np.sum(interior < 0))
|
|
540
|
+
if n_neg <= interior.size // 2:
|
|
541
|
+
continue # already right-handed (or mixed — leave alone)
|
|
542
|
+
|
|
543
|
+
# Flip the chosen axis
|
|
544
|
+
slices = [slice(None)] * 3
|
|
545
|
+
slices[flip_axis] = slice(None, None, -1)
|
|
546
|
+
s = tuple(slices)
|
|
547
|
+
b.X = np.ascontiguousarray(b.X[s])
|
|
548
|
+
b.Y = np.ascontiguousarray(b.Y[s])
|
|
549
|
+
b.Z = np.ascontiguousarray(b.Z[s])
|
|
550
|
+
|
|
551
|
+
name = block_names[bi] if block_names else str(bi)
|
|
552
|
+
dim_name = "ijk"[flip_axis]
|
|
553
|
+
n_dim = [b.IMAX, b.JMAX, b.KMAX][flip_axis]
|
|
554
|
+
print(f" make_right_handed: flipped {name} {dim_name}-axis ({n_dim} planes)")
|
|
555
|
+
flipped.append(bi)
|
|
556
|
+
|
|
557
|
+
if not flipped:
|
|
558
|
+
return blocks, face_matches, periodic_matches, outer_faces
|
|
559
|
+
|
|
560
|
+
# Remap connectivity indices for flipped blocks
|
|
561
|
+
def _remap_face(face: dict) -> None:
|
|
562
|
+
bi = face["block_index"]
|
|
563
|
+
if bi not in flipped:
|
|
564
|
+
return
|
|
565
|
+
b = blocks[bi]
|
|
566
|
+
n = [b.IMAX, b.JMAX, b.KMAX][flip_axis]
|
|
567
|
+
for key in ("lb", "ub"):
|
|
568
|
+
face[key] = list(face[key]) # ensure mutable
|
|
569
|
+
face[key][flip_axis] = n - 1 - face[key][flip_axis]
|
|
570
|
+
|
|
571
|
+
for m in face_matches:
|
|
572
|
+
_remap_face(m["block1"])
|
|
573
|
+
_remap_face(m["block2"])
|
|
574
|
+
for m in periodic_matches:
|
|
575
|
+
_remap_face(m["block1"])
|
|
576
|
+
_remap_face(m["block2"])
|
|
577
|
+
for of in outer_faces:
|
|
578
|
+
_remap_face(of)
|
|
579
|
+
|
|
580
|
+
return blocks, face_matches, periodic_matches, outer_faces
|
|
@@ -733,6 +733,19 @@ def get_face_intersection(face1:Face,face2:Face,block1:Block,block2:Block,tol:fl
|
|
|
733
733
|
df = __filter_block_increasing(df,'i2')
|
|
734
734
|
df = __filter_block_increasing(df,'j2')
|
|
735
735
|
|
|
736
|
+
# Reject matches where matched points don't cover the full
|
|
737
|
+
# matched sub-face area. Two blocks that share only edges
|
|
738
|
+
# (e.g. O-grid SS and PS sharing LE/TE lines) can pass the
|
|
739
|
+
# edge check above because the matched points span two
|
|
740
|
+
# separate edges, making the diagonal look like a face.
|
|
741
|
+
# Verify that matched point count == expected sub-face area.
|
|
742
|
+
if len(df) >= 4:
|
|
743
|
+
ilb1, jlb1, klb1 = int(df['i1'].min()), int(df['j1'].min()), int(df['k1'].min())
|
|
744
|
+
iub1, jub1, kub1 = int(df['i1'].max()), int(df['j1'].max()), int(df['k1'].max())
|
|
745
|
+
matched_area = _face_point_count([ilb1, jlb1, klb1], [iub1, jub1, kub1])
|
|
746
|
+
if matched_area > 0 and len(df) < matched_area:
|
|
747
|
+
df = pd.DataFrame() # Not a face — only partial (edge) coverage
|
|
748
|
+
|
|
736
749
|
# Do a final check after doing all these checks
|
|
737
750
|
if len(df)>=4: # Greater than 4 because match can occur with simply 4 corners but the interior doesn't match.
|
|
738
751
|
# Check for Split faces
|
|
@@ -896,7 +909,7 @@ def combinations_of_nearest_blocks(blocks:List[Block],nearest_nblocks:int=4):
|
|
|
896
909
|
new_combos.append((i,j))
|
|
897
910
|
return new_combos
|
|
898
911
|
|
|
899
|
-
def connectivity_fast(blocks:List[Block]):
|
|
912
|
+
def connectivity_fast(blocks:List[Block], use_minmax:bool=False):
|
|
900
913
|
"""Find connectivity by GCD-reducing blocks first for speed.
|
|
901
914
|
|
|
902
915
|
Computes the minimum GCD across all block dimensions, reduces all blocks
|
|
@@ -907,6 +920,10 @@ def connectivity_fast(blocks:List[Block]):
|
|
|
907
920
|
|
|
908
921
|
Args:
|
|
909
922
|
blocks (List[Block]): List of blocks to find connectivity for.
|
|
923
|
+
use_minmax (bool): If True, normalise lb/ub to strict min/max
|
|
924
|
+
order (IMIN,JMIN,KMIN → IMAX,JMAX,KMAX) and recompute the
|
|
925
|
+
permutation matrix accordingly. Default is False (traversal
|
|
926
|
+
order).
|
|
910
927
|
|
|
911
928
|
Returns:
|
|
912
929
|
(List[Dict]): Face matches with orientation info.
|
|
@@ -921,6 +938,8 @@ def connectivity_fast(blocks:List[Block]):
|
|
|
921
938
|
# scale it up
|
|
922
939
|
scale_face_bounds(face_matches, gcd_to_use)
|
|
923
940
|
scale_face_bounds(outer_faces_formatted, gcd_to_use)
|
|
941
|
+
if use_minmax:
|
|
942
|
+
face_matches = normalize_face_matches(face_matches)
|
|
924
943
|
return face_matches, outer_faces_formatted
|
|
925
944
|
|
|
926
945
|
def connectivity(blocks:List[Block]):
|
|
@@ -1253,3 +1272,93 @@ def face_matches_to_dict(face1:Face, face2:Face,block1:Block,block2:Block):
|
|
|
1253
1272
|
return match
|
|
1254
1273
|
|
|
1255
1274
|
|
|
1275
|
+
def normalize_face_matches(face_matches: list) -> list:
|
|
1276
|
+
"""Convert face match lb/ub to strict min/max order.
|
|
1277
|
+
|
|
1278
|
+
By default the library encodes traversal direction in lb/ub ordering
|
|
1279
|
+
(lb is not necessarily < ub). This function normalises every face
|
|
1280
|
+
match so that ``lb = [IMIN, JMIN, KMIN]`` and ``ub = [IMAX, JMAX,
|
|
1281
|
+
KMAX]`` on **both** sides, and recomputes the orientation /
|
|
1282
|
+
permutation matrix accordingly.
|
|
1283
|
+
|
|
1284
|
+
The resulting PM satisfies::
|
|
1285
|
+
|
|
1286
|
+
Face_A(IMIN,JMIN,KMIN -> IMAX,JMAX,KMAX) * PM
|
|
1287
|
+
= Face_B(IMIN,JMIN,KMIN -> IMAX,JMAX,KMAX)
|
|
1288
|
+
|
|
1289
|
+
Parameters
|
|
1290
|
+
----------
|
|
1291
|
+
face_matches : list of dict
|
|
1292
|
+
Face match dicts as returned by :func:`connectivity_fast` or
|
|
1293
|
+
:func:`connectivity`. Each dict must have ``block1`` and
|
|
1294
|
+
``block2`` sub-dicts with ``lb`` and ``ub`` keys. If a ``match``
|
|
1295
|
+
key (point-match DataFrame) is present it is used to recompute
|
|
1296
|
+
the orientation; otherwise the orientation is recomputed from the
|
|
1297
|
+
new min/max bounds.
|
|
1298
|
+
|
|
1299
|
+
Returns
|
|
1300
|
+
-------
|
|
1301
|
+
list of dict
|
|
1302
|
+
A **new** list with the same structure, but lb/ub in min/max
|
|
1303
|
+
order and orientation updated.
|
|
1304
|
+
"""
|
|
1305
|
+
out = []
|
|
1306
|
+
for fm in face_matches:
|
|
1307
|
+
fm = deepcopy(fm)
|
|
1308
|
+
lb1 = fm['block1']['lb']
|
|
1309
|
+
ub1 = fm['block1']['ub']
|
|
1310
|
+
lb2 = fm['block2']['lb']
|
|
1311
|
+
ub2 = fm['block2']['ub']
|
|
1312
|
+
|
|
1313
|
+
new_lb1 = [min(lb1[d], ub1[d]) for d in range(3)]
|
|
1314
|
+
new_ub1 = [max(lb1[d], ub1[d]) for d in range(3)]
|
|
1315
|
+
new_lb2 = [min(lb2[d], ub2[d]) for d in range(3)]
|
|
1316
|
+
new_ub2 = [max(lb2[d], ub2[d]) for d in range(3)]
|
|
1317
|
+
|
|
1318
|
+
fm['block1']['lb'] = new_lb1
|
|
1319
|
+
fm['block1']['ub'] = new_ub1
|
|
1320
|
+
fm['block2']['lb'] = new_lb2
|
|
1321
|
+
fm['block2']['ub'] = new_ub2
|
|
1322
|
+
|
|
1323
|
+
# Recompute orientation if we have the point-match DataFrame
|
|
1324
|
+
# with the expected columns (i1,j1,k1,i2,j2,k2)
|
|
1325
|
+
df = fm.get('match')
|
|
1326
|
+
has_point_match = (df is not None and isinstance(df, pd.DataFrame)
|
|
1327
|
+
and len(df) > 1 and 'i2' in df.columns)
|
|
1328
|
+
if has_point_match:
|
|
1329
|
+
orientation = _compute_orientation(df, new_lb1, new_ub1)
|
|
1330
|
+
perm_idx, plane = _orient_vec_to_permutation(
|
|
1331
|
+
orientation, new_lb1, new_ub1, new_lb2, new_ub2)
|
|
1332
|
+
export_perm = -1 if plane == 'in-plane' else perm_idx
|
|
1333
|
+
fm['orientation'] = {
|
|
1334
|
+
'permutation_index': export_perm,
|
|
1335
|
+
'plane': plane,
|
|
1336
|
+
'permutation_matrix': PERMUTATION_MATRICES[perm_idx].tolist(),
|
|
1337
|
+
}
|
|
1338
|
+
elif 'orientation' in fm:
|
|
1339
|
+
# No DataFrame — recompute from min/max bounds.
|
|
1340
|
+
# With min/max ordering, all axes go forward, so reversal
|
|
1341
|
+
# flags depend only on the axis mapping (no direction flip).
|
|
1342
|
+
ca1 = _constant_axis(new_lb1, new_ub1)
|
|
1343
|
+
ca2 = _constant_axis(new_lb2, new_ub2)
|
|
1344
|
+
plane = 'in-plane' if ca1 == ca2 else 'cross-plane'
|
|
1345
|
+
|
|
1346
|
+
# Build identity-like orientation: face1 axis d -> face2 axis d
|
|
1347
|
+
# unless cross-plane, in which case use the old orientation's
|
|
1348
|
+
# permutation matrix to infer the mapping.
|
|
1349
|
+
old_perm = fm['orientation'].get('permutation_matrix')
|
|
1350
|
+
if old_perm is not None and plane == 'cross-plane':
|
|
1351
|
+
# Preserve the cross-plane axis swap from original
|
|
1352
|
+
fm['orientation']['plane'] = plane
|
|
1353
|
+
else:
|
|
1354
|
+
# In-plane with min/max ordering: both sides go forward,
|
|
1355
|
+
# so the PM is identity (no reversal, no swap).
|
|
1356
|
+
perm_idx = 0
|
|
1357
|
+
fm['orientation'] = {
|
|
1358
|
+
'permutation_index': -1,
|
|
1359
|
+
'plane': plane,
|
|
1360
|
+
'permutation_matrix': PERMUTATION_MATRICES[perm_idx].tolist(),
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
out.append(fm)
|
|
1364
|
+
return out
|
|
@@ -5,5 +5,19 @@ from .class_definitions import BoundaryConditionType,InletBC_Subtype,InletBC_Dir
|
|
|
5
5
|
# Job related stuff
|
|
6
6
|
from .class_definitions import JobFiles, JobControl, TurbModelInput, Plot3DParameters, InitialCond, TimeStpControl, SPDSchemeControl, RKSchemeControl, MGSchemeControl, GasPropertiesInput, ReferenceCondFull, Job
|
|
7
7
|
|
|
8
|
-
# export functions
|
|
9
|
-
from .export_functions import export_to_boundary_condition, export_to_job_file
|
|
8
|
+
# export functions
|
|
9
|
+
from .export_functions import export_to_boundary_condition, export_to_job_file
|
|
10
|
+
|
|
11
|
+
# validation functions
|
|
12
|
+
from .validation import (
|
|
13
|
+
check_handedness,
|
|
14
|
+
check_corners,
|
|
15
|
+
check_verify_connectivity,
|
|
16
|
+
check_verify_periodicity,
|
|
17
|
+
compute_pm,
|
|
18
|
+
check_pm,
|
|
19
|
+
check_face_coverage,
|
|
20
|
+
check_negative_volumes,
|
|
21
|
+
check_face_normals,
|
|
22
|
+
run_all_checks,
|
|
23
|
+
)
|
|
@@ -4,6 +4,15 @@ from dataclasses import dataclass, field
|
|
|
4
4
|
from enum import IntEnum, Enum
|
|
5
5
|
from typing import Any, Dict, List, Optional
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
class FortranLiteral(str):
|
|
9
|
+
"""String subclass for Fortran namelist values that must NOT be quoted.
|
|
10
|
+
|
|
11
|
+
Use for repeat-count arrays (``4*T``, ``0.25,0.3333333,0.5,1.,6*0``)
|
|
12
|
+
and any other raw Fortran syntax that the namelist reader expects unquoted.
|
|
13
|
+
"""
|
|
14
|
+
pass
|
|
15
|
+
|
|
7
16
|
# ----------------------------
|
|
8
17
|
# Enums (mirror your C#)
|
|
9
18
|
# ----------------------------
|
|
@@ -89,6 +98,8 @@ class InletBC(BoundaryCondition):
|
|
|
89
98
|
bet1_const: Optional[float] = None
|
|
90
99
|
|
|
91
100
|
annular_inlet: bool = False
|
|
101
|
+
mult_for_full_ring: Optional[int] = None
|
|
102
|
+
angularPeriod: Optional[float] = None
|
|
92
103
|
deltah: Optional[float] = None
|
|
93
104
|
deltat: Optional[float] = None
|
|
94
105
|
twall_hub: Optional[float] = None
|
|
@@ -241,41 +252,36 @@ class TimeStpControl:
|
|
|
241
252
|
@dataclass
|
|
242
253
|
class SPDSchemeControl:
|
|
243
254
|
# GlennHT-style defaults (with shorthand preserved)
|
|
244
|
-
NS_Central:
|
|
245
|
-
TB2_Upwind1:
|
|
246
|
-
NS_Upwind2:
|
|
255
|
+
NS_Central: FortranLiteral = FortranLiteral("4*T")
|
|
256
|
+
TB2_Upwind1: FortranLiteral = FortranLiteral("4*T")
|
|
257
|
+
NS_Upwind2: FortranLiteral = FortranLiteral("4*F")
|
|
247
258
|
|
|
248
259
|
ScalrCoeff_ArtDiss: bool = True
|
|
249
260
|
useSecDiffArtDiss: bool = True
|
|
250
261
|
useFrthDiffArtDiss: bool = True
|
|
251
262
|
|
|
252
|
-
rk2:
|
|
253
|
-
rk4:
|
|
263
|
+
rk2: FortranLiteral = FortranLiteral("4*0.12500")
|
|
264
|
+
rk4: FortranLiteral = FortranLiteral("4*0.032")
|
|
254
265
|
|
|
255
|
-
# Keep other optional fields from your previous version
|
|
256
266
|
NS_Upwind1: bool = False
|
|
257
267
|
use_AUSM_Chima: bool = False
|
|
258
268
|
use_AUSM_Liou_hTot: bool = False
|
|
259
269
|
TB2_Central: bool = False
|
|
260
270
|
TBRSM_Central: bool = False
|
|
261
271
|
TBRSM_Upwind1: bool = True
|
|
262
|
-
|
|
263
|
-
scalarArtDiss: bool = True
|
|
272
|
+
ConstCoeff_ArtDiss: bool = False
|
|
264
273
|
MatrxCoeff_ArtDiss: bool = False
|
|
265
|
-
secDiffArtDiss: bool = True
|
|
266
|
-
matrixArtDiss: bool = False
|
|
267
|
-
frthDiffArtDiss: bool = True
|
|
268
274
|
MachCutOff: float = 0.1
|
|
269
275
|
ivanAlbada: int = 1
|
|
270
276
|
|
|
271
277
|
@dataclass
|
|
272
278
|
class RKSchemeControl:
|
|
273
279
|
nStages: int = 4
|
|
274
|
-
RKCoeff:
|
|
275
|
-
compute_pdiff_in_stage:
|
|
276
|
-
compute_adiss_in_stage:
|
|
277
|
-
export_import_after_stage:
|
|
278
|
-
use_implicit_residual_smoothing:
|
|
280
|
+
RKCoeff: FortranLiteral = FortranLiteral("0.25,0.3333333,0.5,1.,6*0")
|
|
281
|
+
compute_pdiff_in_stage: FortranLiteral = FortranLiteral("T,T,T,T,6*F")
|
|
282
|
+
compute_adiss_in_stage: FortranLiteral = FortranLiteral("T,T,T,T,6*F")
|
|
283
|
+
export_import_after_stage: FortranLiteral = FortranLiteral("T,T,T,T,6*F")
|
|
284
|
+
use_implicit_residual_smoothing: FortranLiteral = FortranLiteral(".T.")
|
|
279
285
|
irs_neqs: Optional[int] = 1
|
|
280
286
|
irs_use_GS: bool = True
|
|
281
287
|
n_GS_iterations: Optional[int] = 3
|
|
@@ -309,26 +315,31 @@ class GasPropertiesInput:
|
|
|
309
315
|
|
|
310
316
|
@dataclass
|
|
311
317
|
class ReferenceCond:
|
|
312
|
-
#
|
|
318
|
+
# Minimal set for GlennHT: only fields that are explicitly set get exported.
|
|
319
|
+
# When Re is NOT specified, GlennHT requires:
|
|
320
|
+
# Group 0: refLen, refVisc
|
|
321
|
+
# Group 1: 3 of {refP0, refT0, refRho0, Rgas}
|
|
322
|
+
# Group 2: gamma or refCp
|
|
323
|
+
# Group 3: Pr or refCond
|
|
313
324
|
useDimensionalVariables: bool = False
|
|
314
|
-
refLen: Optional[float] =
|
|
315
|
-
refP0: Optional[float] =
|
|
316
|
-
refT0: Optional[float] =
|
|
317
|
-
refRho0: Optional[float] =
|
|
318
|
-
refVel: Optional[float] =
|
|
319
|
-
refVisc: Optional[float] =
|
|
320
|
-
refCond: Optional[float] =
|
|
321
|
-
refCp: Optional[float] =
|
|
322
|
-
MolW: Optional[float] =
|
|
323
|
-
RgasUnv: Optional[float] =
|
|
324
|
-
Rgas: Optional[float] =
|
|
325
|
-
gamma: Optional[float] =
|
|
326
|
-
Re: Optional[float] =
|
|
327
|
-
Pr: Optional[float] =
|
|
328
|
-
ndVisc: Optional[float] =
|
|
329
|
-
ndCond: Optional[float] =
|
|
330
|
-
Omegab: Optional[float] =
|
|
331
|
-
ReScalingFactor: Optional[float] =
|
|
325
|
+
refLen: Optional[float] = None
|
|
326
|
+
refP0: Optional[float] = None
|
|
327
|
+
refT0: Optional[float] = None
|
|
328
|
+
refRho0: Optional[float] = None
|
|
329
|
+
refVel: Optional[float] = None
|
|
330
|
+
refVisc: Optional[float] = None
|
|
331
|
+
refCond: Optional[float] = None
|
|
332
|
+
refCp: Optional[float] = None
|
|
333
|
+
MolW: Optional[float] = None
|
|
334
|
+
RgasUnv: Optional[float] = None
|
|
335
|
+
Rgas: Optional[float] = None
|
|
336
|
+
gamma: Optional[float] = None
|
|
337
|
+
Re: Optional[float] = None
|
|
338
|
+
Pr: Optional[float] = None
|
|
339
|
+
ndVisc: Optional[float] = None
|
|
340
|
+
ndCond: Optional[float] = None
|
|
341
|
+
Omegab: Optional[float] = None
|
|
342
|
+
ReScalingFactor: Optional[float] = None
|
|
332
343
|
rho_solid: Optional[float] = None
|
|
333
344
|
cond_solid: Optional[float] = None
|
|
334
345
|
Csp_solid: Optional[float] = None
|
|
@@ -116,12 +116,17 @@ def _fmt_bool(v: bool) -> str:
|
|
|
116
116
|
|
|
117
117
|
def _fmt_value(v: Any) -> str:
|
|
118
118
|
from enum import Enum, IntEnum
|
|
119
|
+
from .class_definitions import FortranLiteral
|
|
119
120
|
if isinstance(v, bool):
|
|
120
121
|
return _fmt_bool(v)
|
|
121
122
|
if isinstance(v, (IntEnum, Enum)):
|
|
122
123
|
return str(int(v))
|
|
123
|
-
if isinstance(v,
|
|
124
|
+
if isinstance(v, int):
|
|
124
125
|
return repr(v)
|
|
126
|
+
if isinstance(v, float):
|
|
127
|
+
return f"{v:.4g}"
|
|
128
|
+
if isinstance(v, FortranLiteral):
|
|
129
|
+
return str(v)
|
|
125
130
|
if isinstance(v, str):
|
|
126
131
|
return f"'{v}'"
|
|
127
132
|
if isinstance(v, (list, tuple)):
|
|
@@ -195,7 +200,7 @@ def _write_gif_pair(w, pair: Any) -> None:
|
|
|
195
200
|
)
|
|
196
201
|
w.write(f" &GIF_Spec\nSurfID_1={sid1}, SurfID2={sid2}\n &END\n\n")
|
|
197
202
|
|
|
198
|
-
def _write_vzconditions(w, vz: Any) -> None:
|
|
203
|
+
def _write_vzconditions(w, vz: Any, ref_T0: float = 285.0) -> None:
|
|
199
204
|
"""
|
|
200
205
|
Convert inputs like:
|
|
201
206
|
{"block_index": 1, "zone_type": "fluid"|"solid", "contiguous_index": 1}
|
|
@@ -207,10 +212,11 @@ def _write_vzconditions(w, vz: Any) -> None:
|
|
|
207
212
|
|
|
208
213
|
if vztype == 1:
|
|
209
214
|
# Fluid default
|
|
215
|
+
t_ref = _get_field(vz, "Fluid_Tref", ref_T0)
|
|
210
216
|
w.write(
|
|
211
217
|
" &VZConditions\n"
|
|
212
|
-
f"VZid={vzid}, VZtype=1, OmegaVZ=0., VZMaterialName=Air,\n"
|
|
213
|
-
"Fluid_Tref_prop=0., Fluid_k_Tref=
|
|
218
|
+
f"VZid={vzid}, VZtype=1, OmegaVZ=0., VZMaterialName='Air',\n"
|
|
219
|
+
f"Fluid_Tref_prop=0., Fluid_k_Tref={t_ref}, Fluid_amu_Tref={t_ref}, Fluid_expnt=.7,\n"
|
|
214
220
|
"!Fluid_cp=1002., Fluid_Pr=.7, Fluid_MW=28.964\n"
|
|
215
221
|
" &END\n\n"
|
|
216
222
|
)
|
|
@@ -218,7 +224,7 @@ def _write_vzconditions(w, vz: Any) -> None:
|
|
|
218
224
|
# Solid default
|
|
219
225
|
w.write(
|
|
220
226
|
" &VZConditions\n"
|
|
221
|
-
f"VZid={vzid}, VZtype=2, OmegaVZ=0., VZMaterialName=CMC,\n"
|
|
227
|
+
f"VZid={vzid}, VZtype=2, OmegaVZ=0., VZMaterialName='CMC',\n"
|
|
222
228
|
"Solid_Tref_prop=285., Solid_rho_Tref=2707. , Solid_condN_Tref=6.5, Solid_condT_Tref=6.5, Solid_condA_Tref=6.5,\n"
|
|
223
229
|
"Solid_Csp_Tref=896.\n"
|
|
224
230
|
" &END\n\n"
|
|
@@ -299,28 +305,31 @@ def populate_reference_from_inputs(
|
|
|
299
305
|
rcfull.cond_solid = 20.0
|
|
300
306
|
rcfull.csp_solid = 896.0
|
|
301
307
|
|
|
302
|
-
#
|
|
308
|
+
# Fill GasPropertiesInput constants from computed values so the job
|
|
309
|
+
# file doesn't contain -1e+99 sentinel defaults.
|
|
310
|
+
gas = job.GasPropertiesInput
|
|
311
|
+
gas.const_cp = cp
|
|
312
|
+
gas.const_visc = mu
|
|
313
|
+
gas.const_kth = k_val
|
|
314
|
+
gas.SpecialGasMW = MolW
|
|
315
|
+
if gas.RefT_Properties is None or gas.RefT_Properties < -1e+90:
|
|
316
|
+
gas.RefT_Properties = T0
|
|
317
|
+
|
|
318
|
+
# compact RefCond — minimal set so GlennHT derives the rest:
|
|
319
|
+
# Group 0: refLen, refVisc
|
|
320
|
+
# Group 1: refP0, refT0, Rgas (3 of 4)
|
|
321
|
+
# Group 2: gamma
|
|
322
|
+
# Group 3: Pr
|
|
303
323
|
rc = ReferenceCond(
|
|
304
324
|
useDimensionalVariables=False,
|
|
305
325
|
refLen=rcfull.reflen,
|
|
306
326
|
refP0=rcfull.refP0,
|
|
307
327
|
refT0=rcfull.refT0,
|
|
308
|
-
refRho0=rcfull.refrho0,
|
|
309
|
-
refVel=rcfull.refVel,
|
|
310
|
-
refVisc=rcfull.refvisc,
|
|
311
|
-
refCond=rcfull.refcond,
|
|
312
|
-
refCp=rcfull.refCp,
|
|
313
|
-
MolW=rcfull.MolW,
|
|
314
|
-
RgasUnv=rcfull.RgasUnv,
|
|
315
328
|
Rgas=rcfull.Rgas,
|
|
316
329
|
gamma=rcfull.gamma,
|
|
317
|
-
|
|
330
|
+
refVisc=rcfull.refvisc,
|
|
318
331
|
Pr=rcfull.Pr,
|
|
319
|
-
ndVisc=1.0,
|
|
320
|
-
ndCond=1.0,
|
|
321
332
|
Omegab=rcfull.Omegab,
|
|
322
|
-
ReScalingFactor=1.0,
|
|
323
|
-
rho_solid=rcfull.rho_solid,
|
|
324
333
|
cond_solid=rcfull.cond_solid,
|
|
325
334
|
Csp_solid=rcfull.csp_solid,
|
|
326
335
|
)
|
|
@@ -413,8 +422,7 @@ def export_to_boundary_condition(
|
|
|
413
422
|
_set_field(inlet, "twall_hub", _get_field(inlet, "twall_hub") / ref.refT0)
|
|
414
423
|
if _get_field(inlet, "twall_case") is not None and ref.refT0 not in (None, 0):
|
|
415
424
|
_set_field(inlet, "twall_case", _get_field(inlet, "twall_case") / ref.refT0)
|
|
416
|
-
|
|
417
|
-
_set_field(inlet, "Ts_const", _get_field(inlet, "Ts_const") / ref.reflen)
|
|
425
|
+
# Ts_const is nondimensional (turbulence length scale) — no normalization needed
|
|
418
426
|
_write_bsurf_spec(w, inlet)
|
|
419
427
|
|
|
420
428
|
# OUTLETS (normalize back-pressure by refP0)
|
|
@@ -440,9 +448,10 @@ def export_to_boundary_condition(
|
|
|
440
448
|
for vz in volume_zones:
|
|
441
449
|
key = _first_field(vz, ("contiguous_index", "contiguous_id"))
|
|
442
450
|
volume_zone_unique[key] = vz
|
|
451
|
+
ref_T0 = getattr(getattr(job_settings, "ReferenceCondFull", None), "refT0", 285.0) or 285.0
|
|
443
452
|
for vz in volume_zone_unique.values():
|
|
444
453
|
if _get_field(vz, "zone_type") is not None:
|
|
445
|
-
_write_vzconditions(w, vz)
|
|
454
|
+
_write_vzconditions(w, vz, ref_T0=ref_T0)
|
|
446
455
|
else:
|
|
447
456
|
w.write(_export_namelist_block("VZConditions", vz)); w.write("\n")
|
|
448
457
|
|
|
@@ -451,8 +460,13 @@ def export_to_boundary_condition(
|
|
|
451
460
|
if sid is not None:
|
|
452
461
|
_set_field(obj, field_name, sid)
|
|
453
462
|
|
|
454
|
-
# Detailed BC blocks (skip meta + *_unit)
|
|
455
|
-
|
|
463
|
+
# Detailed BC blocks (skip meta + *_unit + base-class fields).
|
|
464
|
+
# IsPostProcessing, IsCalculateMassFlow, ToggleProcessSurface belong
|
|
465
|
+
# in &BSurf_Spec only — GlennHT's INLET_BC/OUTLET_BC/WALL_BC namelists
|
|
466
|
+
# do not include them and will reject unrecognised names.
|
|
467
|
+
exclude = {"Name", "SurfaceID", "BCType",
|
|
468
|
+
"IsPostProcessing", "IsCalculateMassFlow",
|
|
469
|
+
"ToggleProcessSurface"}
|
|
456
470
|
for inlet in bc_group.Inlets:
|
|
457
471
|
_sync_detail_surface_id(inlet, "surfID_inlet")
|
|
458
472
|
w.write(f"! {inlet.Name}\n")
|
|
@@ -613,13 +627,15 @@ def export_to_glennht_conn(matches:List[Dict[str, Dict[int, str]]],outer_faces:L
|
|
|
613
627
|
for k,v in summary['zone_types_by_id'].items():
|
|
614
628
|
lines.append(f"{k} ")
|
|
615
629
|
lines.append("\n")
|
|
616
|
-
# Print Zone Groups
|
|
630
|
+
# Print Zone Groups (all block contiguous indices on one line,
|
|
631
|
+
# wrapping after columns_to_print entries)
|
|
617
632
|
columns_to_print = 10
|
|
618
|
-
for i,v in enumerate(volume_zones):
|
|
619
|
-
|
|
620
|
-
|
|
633
|
+
for i, v in enumerate(volume_zones):
|
|
634
|
+
lines.append(f"{v['contiguous_index']}")
|
|
635
|
+
if (i + 1) % columns_to_print == 0 or i == len(volume_zones) - 1:
|
|
636
|
+
lines.append("\n")
|
|
621
637
|
else:
|
|
622
|
-
lines.append(
|
|
638
|
+
lines.append(" ")
|
|
623
639
|
|
|
624
640
|
filename = ensure_extension(filename,'.ght_conn')
|
|
625
641
|
with open(f'{filename}','w') as fp:
|