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.
Files changed (28) hide show
  1. {plot3d-1.8.2 → plot3d-1.9.0}/PKG-INFO +1 -1
  2. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/__init__.py +4 -3
  3. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/block.py +50 -1
  4. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/blockfunctions.py +97 -0
  5. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/connectivity.py +110 -1
  6. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/glennht/__init__.py +16 -2
  7. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/glennht/class_definitions.py +46 -35
  8. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/glennht/export_functions.py +44 -28
  9. plot3d-1.9.0/plot3d/glennht/validation.py +448 -0
  10. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/periodicity.py +126 -34
  11. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/read.py +118 -5
  12. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/verify.py +144 -82
  13. {plot3d-1.8.2 → plot3d-1.9.0}/pyproject.toml +1 -1
  14. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/block_merging_mixed_facepairs.py +0 -0
  15. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/differencing.py +0 -0
  16. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/face.py +0 -0
  17. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/facefunctions.py +0 -0
  18. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/glennht/import_functions.py +0 -0
  19. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/graph.py +0 -0
  20. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/gridpro/__init__.py +0 -0
  21. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/gridpro/import_functions.py +0 -0
  22. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/listfunctions.py +0 -0
  23. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/normals.py +0 -0
  24. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/point_match.py +0 -0
  25. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/pointwise/__init__.py +0 -0
  26. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/pointwise/import_functions.py +0 -0
  27. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/split_block.py +0 -0
  28. {plot3d-1.8.2 → plot3d-1.9.0}/plot3d/write.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plot3d
3
- Version: 1.8.2
3
+ Version: 1.9.0
4
4
  Summary: Plot3D python utilities for reading and writing and also finding connectivity between blocks
5
5
  Author: Paht Juangphanich
6
6
  Author-email: paht.juangphanich@nasa.gov
@@ -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, apply_permutation, verify_match,
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: str = "4*T"
245
- TB2_Upwind1: str = "4*T"
246
- NS_Upwind2: str = "4*F"
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: str = "4*0.12500"
253
- rk4: str = "4*0.032"
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
- constArtDiss: bool = False
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: str = "0.25,0.3333333,0.5,1.,6*0"
275
- compute_pdiff_in_stage: str = "T,T,T,T,6*F"
276
- compute_adiss_in_stage: str = "T,T,T,T,6*F"
277
- export_import_after_stage: str = "T,T,T,T,6*F"
278
- use_implicit_residual_smoothing: str = ".T."
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
- # Derived (not user input) kept for export
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] = 1.0
315
- refP0: Optional[float] = 101325.0 # Pa (total)
316
- refT0: Optional[float] = 300.0
317
- refRho0: Optional[float] = 1.1765823
318
- refVel: Optional[float] = 293.4588
319
- refVisc: Optional[float] = 1.84e-5
320
- refCond: Optional[float] = 0.02636
321
- refCp: Optional[float] = 1004.5784
322
- MolW: Optional[float] = 28.964
323
- RgasUnv: Optional[float] = 8314.4126
324
- Rgas: Optional[float] = 287.06023
325
- gamma: Optional[float] = 1.4
326
- Re: Optional[float] = 1.8765e7
327
- Pr: Optional[float] = 0.706
328
- ndVisc: Optional[float] = 1.0
329
- ndCond: Optional[float] = 1.0
330
- Omegab: Optional[float] = 0.0
331
- ReScalingFactor: Optional[float] = 1.0
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, (int, float)):
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=285., Fluid_amu_Tref=285., Fluid_expnt=.7,UseDryAir=.TRUE.,\n"
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
- # compact RefCond derived from Full (stored back on the job)
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
- Re=rcfull.Re,
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
- if _get_field(inlet, "Ts_const") is not None and ref.reflen not in (None, 0):
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
- exclude = {"Name", "SurfaceID", "BCType"}
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
- if i % columns_to_print==0:
620
- lines.append(f"{v["contiguous_index"]}\n")
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(f"{v["contiguous_index"]} ")
638
+ lines.append(" ")
623
639
 
624
640
  filename = ensure_extension(filename,'.ght_conn')
625
641
  with open(f'{filename}','w') as fp: