@@ -732,6 +732,135 @@ def _open_file(self, actionable, file_path, sep, mode):
732732 file_path , mode , sep
733733 )
734734
735+ def _validate_yaml_structure (self , yaml_content ):
736+ """Validate PEtab YAML structure before attempting to load files.
737+
738+ Parameters
739+ ----------
740+ yaml_content : dict
741+ The parsed YAML content.
742+
743+ Returns
744+ -------
745+ tuple
746+ (is_valid: bool, errors: list[str])
747+ """
748+ errors = []
749+
750+ # Check format version
751+ if "format_version" not in yaml_content :
752+ errors .append ("Missing 'format_version' field" )
753+
754+ # Check problems array
755+ if "problems" not in yaml_content :
756+ errors .append ("Missing 'problems' field" )
757+ return False , errors
758+
759+ if (
760+ not isinstance (yaml_content ["problems" ], list )
761+ or not yaml_content ["problems" ]
762+ ):
763+ errors .append ("'problems' must be a non-empty list" )
764+ return False , errors
765+
766+ problem = yaml_content ["problems" ][0 ]
767+
768+ # Optional but recommended fields
769+ if (
770+ "visualization_files" not in problem
771+ or not problem ["visualization_files" ]
772+ ):
773+ errors .append ("Warning: No visualization_files specified" )
774+
775+ # Required fields in problem
776+ for field in [
777+ "sbml_files" ,
778+ "measurement_files" ,
779+ "observable_files" ,
780+ "condition_files" ,
781+ ]:
782+ if field not in problem or not problem [field ]:
783+ errors .append ("Problem must contain at least one SBML file" )
784+
785+ # Check parameter_file (at root level)
786+ if "parameter_file" not in yaml_content :
787+ errors .append ("Missing 'parameter_file' at root level" )
788+
789+ return len ([e for e in errors if "Warning" not in e ]) == 0 , errors
790+
791+ def _validate_files_exist (self , yaml_dir , yaml_content ):
792+ """Validate that all files referenced in YAML exist.
793+
794+ Parameters
795+ ----------
796+ yaml_dir : Path
797+ The directory containing the YAML file.
798+ yaml_content : dict
799+ The parsed YAML content.
800+
801+ Returns
802+ -------
803+ tuple
804+ (all_exist: bool, missing_files: list[str])
805+ """
806+ missing_files = []
807+ problem = yaml_content ["problems" ][0 ]
808+
809+ # Check SBML files
810+ for sbml_file in problem .get ("sbml_files" , []):
811+ if not (yaml_dir / sbml_file ).exists ():
812+ missing_files .append (str (sbml_file ))
813+
814+ # Check measurement files
815+ for meas_file in problem .get ("measurement_files" , []):
816+ if not (yaml_dir / meas_file ).exists ():
817+ missing_files .append (str (meas_file ))
818+
819+ # Check observable files
820+ for obs_file in problem .get ("observable_files" , []):
821+ if not (yaml_dir / obs_file ).exists ():
822+ missing_files .append (str (obs_file ))
823+
824+ # Check condition files
825+ for cond_file in problem .get ("condition_files" , []):
826+ if not (yaml_dir / cond_file ).exists ():
827+ missing_files .append (str (cond_file ))
828+
829+ # Check parameter file
830+ if "parameter_file" in yaml_content :
831+ param_file = yaml_content ["parameter_file" ]
832+ if not (yaml_dir / param_file ).exists ():
833+ missing_files .append (str (param_file ))
834+
835+ # Check visualization files (optional)
836+ for vis_file in problem .get ("visualization_files" , []):
837+ if not (yaml_dir / vis_file ).exists ():
838+ missing_files .append (str (vis_file ))
839+
840+ return len (missing_files ) == 0 , missing_files
841+
842+ def _load_file_list (self , controller , file_list , file_type , yaml_dir ):
843+ """Load multiple files for a given controller.
844+
845+ Parameters
846+ ----------
847+ controller : object
848+ The controller to load files into (e.g., measurement_controller).
849+ file_list : list[str]
850+ List of file names to load.
851+ file_type : str
852+ Human-readable file type for logging (e.g., "measurement").
853+ yaml_dir : Path
854+ The directory containing the YAML and data files.
855+ """
856+ for i , file_name in enumerate (file_list ):
857+ file_mode = "overwrite" if i == 0 else "append"
858+ controller .open_table (yaml_dir / file_name , mode = file_mode )
859+ self .logger .log_message (
860+ f"Loaded { file_type } file ({ i + 1 } /{ len (file_list )} ): { file_name } " ,
861+ color = "blue" ,
862+ )
863+
735864 def open_yaml_and_load_files (self , yaml_path = None , mode = "overwrite" ):
736865 """Open files from a YAML configuration.
737866
@@ -749,62 +878,149 @@ def open_yaml_and_load_files(self, yaml_path=None, mode="overwrite"):
749878 if controller == self .sbml_controller :
750879 continue
751880 controller .release_completers ()
881+
752882 # Load the YAML content
753- with open (yaml_path ) as file :
883+ with open (yaml_path , encoding = "utf-8" ) as file :
754884 yaml_content = yaml .safe_load (file )
755885
886+ # Validate PEtab version
756887 if (major := get_major_version (yaml_content )) != 1 :
757888 raise ValueError (
758889 f"Only PEtab v1 problems are currently supported. "
759890 f"Detected version: { major } .x."
760891 )
761892
893+ # Validate YAML structure
894+ is_valid , errors = self ._validate_yaml_structure (yaml_content )
895+ if not is_valid :
896+ error_msg = "Invalid YAML structure:\n - " + "\n - " .join (
897+ [e for e in errors if "Warning" not in e ]
898+ )
899+ self .logger .log_message (error_msg , color = "red" )
900+ QMessageBox .critical (
901+ self .view , "Invalid PEtab YAML" , error_msg
902+ )
903+ return
904+
905+ # Log warnings but continue
906+ warnings = [e for e in errors if "Warning" in e ]
907+ for warning in warnings :
908+ self .logger .log_message (warning , color = "orange" )
909+
762910 # Resolve the directory of the YAML file to handle relative paths
763911 yaml_dir = Path (yaml_path ).parent
764912
765- # Upload SBML model
766- sbml_file_path = (
767- yaml_dir / yaml_content [ "problems" ][ 0 ][ "sbml_files" ][ 0 ]
913+ # Validate file existence
914+ all_exist , missing_files = self . _validate_files_exist (
915+ yaml_dir , yaml_content
768916 )
769- self .sbml_controller .overwrite_sbml (sbml_file_path )
770- self .measurement_controller .open_table (
771- yaml_dir / yaml_content ["problems" ][0 ]["measurement_files" ][0 ]
772- )
773- self .observable_controller .open_table (
774- yaml_dir / yaml_content ["problems" ][0 ]["observable_files" ][0 ]
775- )
776- self .parameter_controller .open_table (
777- yaml_dir / yaml_content ["parameter_file" ]
778- )
779- self .condition_controller .open_table (
780- yaml_dir / yaml_content ["problems" ][0 ]["condition_files" ][0 ]
781- )
782- # Visualization is optional
783- vis_path = yaml_content ["problems" ][0 ].get ("visualization_files" )
784- if vis_path :
785- self .visualization_controller .open_table (
786- yaml_dir / vis_path [0 ]
917+ if not all_exist :
918+ error_msg = (
919+ "The following files referenced in the YAML are missing:\n - "
920+ + "\n - " .join (missing_files )
921+ )
922+ self .logger .log_message (error_msg , color = "red" )
923+ QMessageBox .critical (self .view , "Missing Files" , error_msg )
924+ return
925+
926+ problem = yaml_content ["problems" ][0 ]
927+
928+ # Load SBML model (required, single file)
929+ sbml_files = problem .get ("sbml_files" , [])
930+ if sbml_files :
931+ sbml_file_path = yaml_dir / sbml_files [0 ]
932+ self .sbml_controller .overwrite_sbml (sbml_file_path )
933+ self .logger .log_message (
934+ f"Loaded SBML file: { sbml_files [0 ]} " , color = "blue"
935+ )
936+
937+ # Load measurement files (multiple allowed)
938+ measurement_files = problem .get ("measurement_files" , [])
939+ if measurement_files :
940+ self ._load_file_list (
941+ self .measurement_controller ,
942+ measurement_files ,
943+ "measurement" ,
944+ yaml_dir ,
945+ )
946+
947+ # Load observable files (multiple allowed)
948+ observable_files = problem .get ("observable_files" , [])
949+ if observable_files :
950+ self ._load_file_list (
951+ self .observable_controller ,
952+ observable_files ,
953+ "observable" ,
954+ yaml_dir ,
955+ )
956+
957+ # Load condition files (multiple allowed)
958+ condition_files = problem .get ("condition_files" , [])
959+ if condition_files :
960+ self ._load_file_list (
961+ self .condition_controller ,
962+ condition_files ,
963+ "condition" ,
964+ yaml_dir ,
965+ )
966+
967+ # Load parameter file (required, single file at root level)
968+ if "parameter_file" in yaml_content :
969+ param_file = yaml_content ["parameter_file" ]
970+ self .parameter_controller .open_table (yaml_dir / param_file )
971+ self .logger .log_message (
972+ f"Loaded parameter file: { param_file } " , color = "blue"
973+ )
974+
975+ # Load visualization files (optional, multiple allowed)
976+ visualization_files = problem .get ("visualization_files" , [])
977+ if visualization_files :
978+ self ._load_file_list (
979+ self .visualization_controller ,
980+ visualization_files ,
981+ "visualization" ,
982+ yaml_dir ,
787983 )
788984 else :
789985 self .visualization_controller .clear_table ()
986+
790987 # Simulation should be cleared
791988 self .simulation_controller .clear_table ()
989+
792990 self .logger .log_message (
793991 "All files opened successfully from the YAML configuration." ,
794992 color = "green" ,
795993 )
796994 self .check_model ()
797- # rerun the completers
995+
996+ # Rerun the completers
798997 for controller in self .controllers :
799998 if controller == self .sbml_controller :
800999 continue
8011000 controller .setup_completers ()
8021001 self .unsaved_changes_change (False )
8031002
1003+ except FileNotFoundError as e :
1004+ error_msg = f"File not found: { e .filename if hasattr (e , 'filename' ) else str (e )} "
1005+ self .logger .log_message (error_msg , color = "red" )
1006+ QMessageBox .warning (self .view , "File Not Found" , error_msg )
1007+ except KeyError as e :
1008+ error_msg = f"Missing required field in YAML: { str (e )} "
1009+ self .logger .log_message (error_msg , color = "red" )
1010+ QMessageBox .warning (self .view , "Invalid YAML" , error_msg )
1011+ except ValueError as e :
1012+ error_msg = f"Invalid YAML structure: { str (e )} "
1013+ self .logger .log_message (error_msg , color = "red" )
1014+ QMessageBox .warning (self .view , "Invalid YAML" , error_msg )
1015+ except yaml .YAMLError as e :
1016+ error_msg = f"YAML parsing error: { str (e )} "
1017+ self .logger .log_message (error_msg , color = "red" )
1018+ QMessageBox .warning (self .view , "YAML Parsing Error" , error_msg )
8041019 except Exception as e :
805- self .logger .log_message (
806- f"Failed to open files from YAML: { str (e )} " , color = "red"
807- )
1020+ error_msg = f"Unexpected error loading YAML: { str (e )} "
1021+ self .logger .log_message (error_msg , color = "red" )
1022+ logging .exception ("Full traceback for YAML loading error:" )
1023+ QMessageBox .critical (self .view , "Error" , error_msg )
8081024
8091025 def open_omex_and_load_files (self , omex_path = None ):
8101026 """Opens a petab problem from a COMBINE Archive."""
0 commit comments