2424
2525@dataclass
2626class ProcessorData :
27- """Data from a single processor file."""
27+ """Data from a single processor file.
28+
29+ m, n, p follow the Fortran header convention: x_cb has m+2 elements,
30+ data arrays have (m+1) cells per dimension. The silo reader uses
31+ m = len(x_cb) - 1 (= number of cells) which differs by one, but
32+ assembly code only uses x_cb lengths and n > 0 / p > 0 for
33+ dimensionality, so both conventions work correctly.
34+ """
2835 m : int
2936 n : int
3037 p : int
@@ -44,23 +51,6 @@ class AssembledData:
4451 variables : Dict [str , np .ndarray ] = field (default_factory = dict )
4552
4653
47- def read_record (f ) -> bytes :
48- """Read one Fortran unformatted record, returning the payload bytes."""
49- raw = f .read (4 )
50- if len (raw ) < 4 :
51- raise EOFError ("Unexpected end of file reading record marker" )
52- rec_len = struct .unpack ('<i' , raw )[0 ]
53- if rec_len < 0 :
54- # Try big-endian
55- rec_len = struct .unpack ('>i' , raw )[0 ]
56- if rec_len < 0 :
57- raise ValueError (f"Invalid record length: { rec_len } " )
58- payload = f .read (rec_len )
59- if len (payload ) < rec_len :
60- raise EOFError ("Unexpected end of file reading record payload" )
61- f .read (4 ) # trailing marker
62- return payload
63-
6454
6555def _detect_endianness (path : str ) -> str :
6656 """Detect endianness from the first record marker (should be 16 for header)."""
@@ -121,12 +111,12 @@ def read_binary_file(path: str, var_filter: Optional[str] = None) -> ProcessorDa
121111 n_vals = m + 2
122112
123113 # Auto-detect grid precision from record size
124- bytes_per_val = grid_bytes / n_vals
125- if abs (bytes_per_val - 8.0 ) < 0.5 :
114+ if grid_bytes == n_vals * 8 :
126115 grid_dtype = np .dtype (f'{ endian } f8' )
127- elif abs ( bytes_per_val - 4.0 ) < 0.5 :
116+ elif grid_bytes == n_vals * 4 :
128117 grid_dtype = np .dtype (f'{ endian } f4' )
129118 else :
119+ bytes_per_val = grid_bytes / n_vals if n_vals else 0
130120 raise ValueError (
131121 f"Cannot determine grid precision: { grid_bytes } bytes for { n_vals } values "
132122 f"({ bytes_per_val :.1f} bytes/value)"
@@ -161,12 +151,12 @@ def read_binary_file(path: str, var_filter: Optional[str] = None) -> ProcessorDa
161151
162152 # Auto-detect variable data precision from record size
163153 data_bytes = len (var_raw ) - NAME_LEN
164- var_bpv = data_bytes / data_size
165- if abs (var_bpv - 8.0 ) < 0.5 :
154+ if data_bytes == data_size * 8 :
166155 var_dtype = np .dtype (f'{ endian } f8' )
167- elif abs ( var_bpv - 4.0 ) < 0.5 :
156+ elif data_bytes == data_size * 4 :
168157 var_dtype = np .dtype (f'{ endian } f4' )
169158 else :
159+ var_bpv = data_bytes / data_size if data_size else 0
170160 raise ValueError (
171161 f"Cannot determine variable precision for '{ varname } ': "
172162 f"{ data_bytes } bytes for { data_size } values ({ var_bpv :.1f} bytes/value)"
@@ -261,55 +251,34 @@ def _is_1d(case_dir: str) -> bool:
261251 return os .path .isdir (os .path .join (case_dir , 'binary' , 'root' ))
262252
263253
264- def assemble (case_dir : str , step : int , fmt : str = 'binary' , # pylint: disable=too-many-locals,too-many-statements
265- var : Optional [str ] = None ) -> AssembledData :
254+ def assemble_from_proc_data ( # pylint: disable=too-many-locals
255+ proc_data : List [Tuple [int , ProcessorData ]],
256+ ) -> AssembledData :
266257 """
267- Read and assemble multi-processor data for a given timestep .
258+ Assemble multi-processor data into a single global grid .
268259
269- For 1D, reads the root file directly.
270- For 2D/3D, reads all processor files and assembles into global arrays.
260+ Shared helper used by both binary and silo assembly paths.
261+ Handles ghost/buffer cell overlap between processors by using
262+ per-cell coordinate lookup (np.unique + np.searchsorted + np.ix_).
271263 """
272- if fmt != 'binary' :
273- raise ValueError (f"Format ' { fmt } ' not supported by binary reader. Use silo_reader. " )
264+ if not proc_data :
265+ raise ValueError ("No processor data to assemble " )
274266
275- # 1D case: read root file directly
276- if _is_1d ( case_dir ) :
277- root_path = os . path . join ( case_dir , 'binary' , 'root' , f' { step } .dat' )
278- if not os . path . isfile ( root_path ):
279- raise FileNotFoundError ( f"Root file not found: { root_path } " )
280- pdata = read_binary_file ( root_path , var_filter = var )
281- x_cc = ( pdata . x_cb [: - 1 ] + pdata . x_cb [ 1 :]) / 2.0
267+ # Single processor — fast path
268+ if len ( proc_data ) == 1 :
269+ _ , pd = proc_data [ 0 ]
270+ x_cc = ( pd . x_cb [: - 1 ] + pd . x_cb [ 1 :]) / 2.0
271+ y_cc = ( pd . y_cb [: - 1 ] + pd . y_cb [ 1 :]) / 2.0 if pd . n > 0 else np . array ([ 0.0 ] )
272+ z_cc = ( pd . z_cb [: - 1 ] + pd . z_cb [ 1 :]) / 2.0 if pd . p > 0 else np . array ([ 0.0 ] )
273+ ndim = 1 + ( pd . n > 0 ) + ( pd . p > 0 )
282274 return AssembledData (
283- ndim = 1 , x_cc = x_cc ,
284- y_cc = np .array ([0.0 ]), z_cc = np .array ([0.0 ]),
285- variables = pdata .variables ,
275+ ndim = ndim , x_cc = x_cc , y_cc = y_cc , z_cc = z_cc ,
276+ variables = pd .variables ,
286277 )
287278
288- # Multi-dimensional: read all processor files
289- ranks = _discover_processors (case_dir , fmt )
290- if not ranks :
291- raise FileNotFoundError (f"No processor directories found in { case_dir } /binary/" )
292-
293- # Read all processor data
294- proc_data : List [Tuple [int , ProcessorData ]] = []
295- for rank in ranks :
296- fpath = os .path .join (case_dir , 'binary' , f'p{ rank } ' , f'{ step } .dat' )
297- if not os .path .isfile (fpath ):
298- continue
299- pdata = read_binary_file (fpath , var_filter = var )
300- if pdata .m == 0 and pdata .n == 0 and pdata .p == 0 :
301- continue
302- proc_data .append ((rank , pdata ))
303-
304- if not proc_data :
305- raise FileNotFoundError (f"No valid processor data found for step { step } " )
306-
307- ndim = 1
279+ # Multi-processor assembly
308280 sample = proc_data [0 ][1 ]
309- if sample .n > 0 :
310- ndim = 2
311- if sample .p > 0 :
312- ndim = 3
281+ ndim = 1 + (sample .n > 0 ) + (sample .p > 0 )
313282
314283 # Compute cell centers for each processor
315284 proc_centers = []
@@ -346,7 +315,6 @@ def assemble(case_dir: str, step: int, fmt: str = 'binary', # pylint: disable=t
346315 global_vars [vn ] = np .zeros (nx )
347316
348317 # Place each processor's data using per-cell coordinate lookup
349- # (handles ghost/buffer cell overlap between processors)
350318 for _rank , pd , x_cc , y_cc , z_cc in proc_centers :
351319 xi = np .searchsorted (global_x , np .round (x_cc , 12 ))
352320 yi = np .searchsorted (global_y , np .round (y_cc , 12 )) if ndim >= 2 else np .array ([0 ])
@@ -366,3 +334,52 @@ def assemble(case_dir: str, step: int, fmt: str = 'binary', # pylint: disable=t
366334 ndim = ndim , x_cc = global_x , y_cc = global_y , z_cc = global_z ,
367335 variables = global_vars ,
368336 )
337+
338+
339+ def assemble (case_dir : str , step : int , fmt : str = 'binary' , # pylint: disable=too-many-locals
340+ var : Optional [str ] = None ) -> AssembledData :
341+ """
342+ Read and assemble multi-processor data for a given timestep.
343+
344+ For 1D, reads the root file directly.
345+ For 2D/3D, reads all processor files and assembles into global arrays.
346+ """
347+ if fmt != 'binary' :
348+ raise ValueError (f"Format '{ fmt } ' not supported by binary reader. Use silo_reader." )
349+
350+ # 1D case: read root file directly
351+ if _is_1d (case_dir ):
352+ root_path = os .path .join (case_dir , 'binary' , 'root' , f'{ step } .dat' )
353+ if not os .path .isfile (root_path ):
354+ raise FileNotFoundError (f"Root file not found: { root_path } " )
355+ pdata = read_binary_file (root_path , var_filter = var )
356+ x_cc = (pdata .x_cb [:- 1 ] + pdata .x_cb [1 :]) / 2.0
357+ return AssembledData (
358+ ndim = 1 , x_cc = x_cc ,
359+ y_cc = np .array ([0.0 ]), z_cc = np .array ([0.0 ]),
360+ variables = pdata .variables ,
361+ )
362+
363+ # Multi-dimensional: read all processor files
364+ ranks = _discover_processors (case_dir , fmt )
365+ if not ranks :
366+ raise FileNotFoundError (f"No processor directories found in { case_dir } /binary/" )
367+
368+ proc_data : List [Tuple [int , ProcessorData ]] = []
369+ for rank in ranks :
370+ fpath = os .path .join (case_dir , 'binary' , f'p{ rank } ' , f'{ step } .dat' )
371+ if not os .path .isfile (fpath ):
372+ import warnings # pylint: disable=import-outside-toplevel
373+ warnings .warn (f"Processor file not found, skipping: { fpath } " )
374+ continue
375+ pdata = read_binary_file (fpath , var_filter = var )
376+ if pdata .m == 0 and pdata .n == 0 and pdata .p == 0 :
377+ import warnings # pylint: disable=import-outside-toplevel
378+ warnings .warn (f"Processor p{ rank } has zero dimensions, skipping" )
379+ continue
380+ proc_data .append ((rank , pdata ))
381+
382+ if not proc_data :
383+ raise FileNotFoundError (f"No valid processor data found for step { step } " )
384+
385+ return assemble_from_proc_data (proc_data )
0 commit comments