1010import numpy as np
1111
1212from plaid .storage import init_from_disk
13+ from plaid .storage .registry import available_backends
1314from plaid .storage .common .reader import (
1415 load_infos_from_disk ,
1516 load_metadata_from_disk ,
1617 load_problem_definitions_from_disk ,
1718)
18-
19+ from plaid .constants import (
20+ CGNS_ELEMENT_NAMES ,
21+ CGNS_FIELD_LOCATIONS ,
22+ )
1923
2024@dataclass
2125class CheckMessage :
@@ -214,14 +218,26 @@ def compute_checksum(sample):
214218def check_dataset (
215219 path : Path ,
216220 splits : Optional [list [str ]] = None ,
217- max_samples : Optional [int ] = None ,
218221) -> CheckReport :
219222 """Run integrity checks on a local PLAID dataset.
220223
224+ Algorithm overview:
225+ 1. Validate the required on-disk PLAID layout.
226+ 2. Load infos, metadata, and split-specific dataset/converter objects.
227+ 3. Validate top-level declarations from ``infos.yaml`` (backend, sample counts).
228+ 4. Resolve requested splits and report unknown ones.
229+ 5. For each checked split:
230+ - verify split-level schema/value consistency,
231+ - validate sample IDs,
232+ - convert each sample and validate values,
233+ - compute checksums for duplicate-data detection,
234+ - build scalar signatures to detect duplicated DOE-like inputs.
235+ 6. Validate optional problem definitions against available features/splits/indices.
236+ 7. Emit an ``OK`` info message when no issue is detected.
237+
221238 Args:
222239 path: Dataset directory.
223240 splits: Optional selected split names.
224- max_samples: Optional cap for per-sample checks.
225241
226242 Returns:
227243 A populated :class:`CheckReport`.
@@ -276,7 +292,7 @@ def check_dataset(
276292 target_splits = set (splits ) if splits else dataset_splits
277293 unknown_splits = target_splits - dataset_splits
278294 for split in sorted (unknown_splits ):
279- report .add ("error" , "UNKNOWN_SPLIT" , split , "Split not found in dataset" )
295+ report .add ("error" , "UNKNOWN_SPLIT" , split , f "Split not found in dataset, available are { ' and ' . join ( ' \" ' + x + ' \" ' for x in dataset_splits ) } " )
280296 target_splits = target_splits & dataset_splits
281297
282298 checksum_report = {}
@@ -302,6 +318,7 @@ def check_dataset(
302318 split ,
303319 "No constant schema for split" ,
304320 )
321+
305322 if split not in flat_cst :
306323 report .add (
307324 "error" ,
@@ -310,88 +327,11 @@ def check_dataset(
310327 "No constant values for split" ,
311328 )
312329
313- ids = getattr (dataset , "ids" , None )
314- if ids is not None and isinstance (expected_n , int ):
315- id_list = [int (i ) for i in ids ]
316- duplicates = len (id_list ) - len (set (id_list ))
317- if duplicates > 0 :
318- report .add (
319- "warning" ,
320- "DUPLICATED_SAMPLE_IDS" ,
321- split ,
322- f"Found { duplicates } duplicated sample id(s)" ,
323- )
324-
325- expected_ids = set (range (expected_n ))
326- missing_ids = sorted (expected_ids - set (id_list ))
327- extra_ids = sorted (set (id_list ) - expected_ids )
328- if missing_ids :
329- report .add (
330- "warning" ,
331- "MISSING_SAMPLE_IDS" ,
332- split ,
333- f"Missing sample ids (first 10): { missing_ids [:10 ]} " ,
334- )
335- if extra_ids :
336- report .add (
337- "warning" ,
338- "UNEXPECTED_SAMPLE_IDS" ,
339- split ,
340- f"Unexpected sample ids (first 10): { extra_ids [:10 ]} " ,
341- )
342-
343- # Deep-check a bounded number of samples to validate content and detect
344- # duplicated scalar DOE signatures without necessarily scanning all data.
345- n_to_check = actual_n if max_samples is None else min (max_samples , actual_n )
346- doe_signatures : dict [tuple [Any , ...], int ] = {}
347- for idx in range (n_to_check ):
348- # CGNS-backed datasets expose sample trees directly through
349- # `to_plaid`, so validate global scalar values from the tree API.
350- if converter .backend == "cgns" :
351- try :
352- sample = converter .to_plaid (dataset , idx )
353- except Exception as exc :
354- report .add (
355- "error" ,
356- "SAMPLE_CONVERSION_ERROR" ,
357- f"{ split } [{ idx } ]" ,
358- str (exc ),
359- )
360- continue
361330
362- # Track whole-sample checksums to detect duplicated data across
363- # all checked splits after the per-split loop completes.
364- sample_checksum = compute_checksum (sample )
365- checksum_report [(idx , split )] = sample_checksum
366-
367- scalar_signature : list [Any ] = []
368- for global_name in sample .get_global_names ():
369- value = sample .get_feature_by_path (global_name )
370- if _is_branch_without_data (sample , global_name ):
371- continue
372- issue = _check_numeric_content (value )
373- if issue is not None :
374- report .add (
375- "warning" ,
376- "INVALID_DATA_VALUE A" ,
377- f"{ split } [{ idx } ] global/{ global_name } " ,
378- issue ,
379- )
380- arr = np .asarray (value )
381- if arr .size == 1 and np .issubdtype (arr .dtype , np .number ):
382- scalar_signature .append (
383- ("global" , global_name , float (arr .ravel ()[0 ]))
384- )
385-
386- sig_key = tuple (sorted (scalar_signature ))
387- if sig_key :
388- doe_signatures [sig_key ] = doe_signatures .get (sig_key , 0 ) + 1
389- continue
390-
391- # Non-CGNS backends are normalized to dictionaries so validation can
392- # iterate over time keys and feature maps in a backend-neutral way.
331+ # Deep-check to validate content and detect non valide data in fields (nan inf)
332+ for idx in range (actual_n ):
393333 try :
394- sample_dict = converter .to_dict (dataset , idx )
334+ sample = converter .to_plaid (dataset , idx )
395335 except Exception as exc :
396336 report .add (
397337 "error" ,
@@ -401,55 +341,60 @@ def check_dataset(
401341 )
402342 continue
403343
404- # Track whole-sample checksums for duplicate data detection.
405- sample_checksum = compute_checksum (sample_dict )
344+ # Track whole-sample checksums to detect duplicated data across
345+ # all checked splits after the per-split loop completes.
346+ sample_checksum = compute_checksum (sample )
406347 checksum_report [(idx , split )] = sample_checksum
407348
408- # Validate each materialized feature value while ignoring branch
409- # entries that exist only to group child feature paths.
410- for time_key , feat_map in sample_dict .items ():
411- for feature_name , value in feat_map .items ():
412- if _is_branch_without_data_in_mapping (
413- feature_name , value , feat_map
414- ):
415- continue
416-
417- issue = _check_numeric_content (value )
418-
419- if issue is not None :
420- report .add (
421- "warning" ,
422- "INVALID_DATA_VALUE B" ,
423- f"{ split } [{ idx } ]/{ time_key } { feature_name } " ,
424- issue ,
425- )
426-
427- # Build a scalar signature for duplicate DOE input detection within
428- # this split. Only scalar numeric values are included.
429- scalar_signature : list [Any ] = []
430- for time_key , feat_map in sample_dict .items ():
431- for feature_name , value in feat_map .items ():
432- arr = np .asarray (value )
433- if arr .size == 1 and np .issubdtype (arr .dtype , np .number ):
434- scalar_signature .append (
435- (str (time_key ), feature_name , float (arr .ravel ()[0 ]))
436- )
437-
438- sig_key = tuple (sorted (scalar_signature ))
439- if sig_key :
440- doe_signatures [sig_key ] = doe_signatures .get (sig_key , 0 ) + 1
441-
442- repeated = sum (1 for count in doe_signatures .values () if count > 1 )
443- if repeated > 0 :
444- report .add (
445- "warning" ,
446- "DUPLICATED_DOE_INPUTS" ,
447- split ,
448- f"Detected { repeated } duplicated scalar signature(s) in checked samples" ,
449- )
349+ for global_name in sample .get_global_names ():
350+ global_path = "Global/" + global_name
351+ value = sample .get_feature_by_path (global_path )
352+
353+ if _is_branch_without_data (sample , global_path ):
354+ continue
355+
356+ issue = _check_numeric_content (value )
357+ if issue is not None :
358+ report .add (
359+ "warning" ,
360+ "INVALID_DATA_VALUE A" ,
361+ f"{ split } [{ idx } ] global/{ global_name } " ,
362+ issue ,
363+ )
364+
365+ for time in sample .get_all_time_values ():
366+ local_bases = sample .get_base_names (time = time )
367+ for base in local_bases :
368+ zone_names = sample .features .get_zone_names (
369+ base = base , time = time
370+ )
371+ for zone in zone_names :
372+ for location in CGNS_FIELD_LOCATIONS :
373+ field_names = sample .get_field_names (
374+ location = location ,
375+ zone = zone ,
376+ base = base ,
377+ time = time ,
378+ )
379+
380+ for f_name in field_names :
381+ field_value = sample .get_field (f_name ,
382+ location = location ,
383+ zone = zone ,
384+ base = base ,
385+ time = time )
386+ issue = _check_numeric_content (field_value )
387+ if issue is not None :
388+ report .add (
389+ "warning" ,
390+ "INVALID_DATA_VALUE A" ,
391+ f"{ split } [{ idx } ][{ time } ] { base } /{ zone } /{ location } /{ f_name } " ,
392+ issue ,
393+ )
450394
451395 # Compare checksums from every checked sample to flag identical sample data.
452- if len (checksum_report ) != len (np .unique (checksum_report .values ())):
396+ checksum_values = list (checksum_report .values ())
397+ if len (checksum_report ) != len (np .unique (checksum_values )):
453398 k = list (checksum_report .keys ())
454399 v = list (checksum_report .values ())
455400 uni , cou = np .unique (v , return_counts = True )
@@ -464,7 +409,6 @@ def check_dataset(
464409 str (duplicated ),
465410 "duplicated sample" ,
466411 )
467-
468412 # If problem definitions are present, verify that their feature references,
469413 # split names, and sample indices are compatible with the dataset.
470414 pb_def_dir = path / "problem_definitions"
@@ -478,7 +422,7 @@ def check_dataset(
478422 "problem_definitions" ,
479423 str (exc ),
480424 )
481- pb_defs = {}
425+ return report
482426
483427 all_features = set (variable_schema .keys ())
484428 for split_cst in flat_cst .values ():
@@ -493,13 +437,6 @@ def check_dataset(
493437 f"problem_definitions/{ pb_name } " ,
494438 f"Unknown input feature: { feat } " ,
495439 )
496- if "GlobalConvergenceHistory" not in feat and "Global" not in feat :
497- report .add (
498- "warning" ,
499- "DOE_INPUT_NOT_SCALAR" ,
500- f"problem_definitions/{ pb_name } " ,
501- f"Input feature may not be scalar/global for DOE: { feat } " ,
502- )
503440
504441 for feat in pb_def .output_features :
505442 if feat not in all_features :
@@ -514,6 +451,15 @@ def check_dataset(
514451 split_dict = getattr (pb_def , split_dict_name )
515452 if split_dict is None :
516453 continue
454+ #split_dict must have only one elements
455+ if len (split_dict ) > 1 :
456+ report .add (
457+ "error" ,
458+ "PB_DEF_SPLIT" ,
459+ f"problem_definitions/{ pb_name } " ,
460+ f"{ split_dict_name } has more than 1 split: { list (split_dict .keys ())} " ,
461+ )
462+ continue
517463 split_name = next (iter (split_dict .keys ()))
518464 split_ids = next (iter (split_dict .values ()))
519465 if split_name not in dataset_splits :
@@ -564,12 +510,6 @@ def _build_parser() -> argparse.ArgumentParser:
564510 default = None ,
565511 help = "Split to check (can be provided multiple times)" ,
566512 )
567- parser .add_argument (
568- "--max-samples" ,
569- type = int ,
570- default = None ,
571- help = "Maximum number of samples per split for deep checks" ,
572- )
573513 parser .add_argument (
574514 "--json" ,
575515 action = "store_true" ,
@@ -597,8 +537,7 @@ def main(argv: Optional[list[str]] = None) -> int:
597537
598538 report = check_dataset (
599539 path = args .path ,
600- splits = args .split ,
601- max_samples = args .max_samples ,
540+ splits = args .split
602541 )
603542
604543 if args .json :
0 commit comments