4444 "CheckPriorDistribution" ,
4545 "CheckUndefinedExperiments" ,
4646 "CheckInitialChangeSymbols" ,
47+ "CheckMappingTable" ,
4748 "lint_problem" ,
4849 "default_validation_tasks" ,
4950]
@@ -551,7 +552,8 @@ def run(self, problem: Problem) -> ValidationIssue | None:
551552
552553class CheckAllParametersPresentInParameterTable (ValidationTask ):
553554 """Ensure all required parameters are contained in the parameter table
554- with no additional ones."""
555+ with no additional ones. This also ensures that the mapping table petab ids
556+ are used in the PEtab problem."""
555557
556558 def run (self , problem : Problem ) -> ValidationIssue | None :
557559 if problem .model is None :
@@ -825,17 +827,17 @@ def run(self, problem: Problem) -> ValidationIssue | None:
825827
826828 if parameter .prior_distribution not in PRIOR_DISTRIBUTIONS :
827829 messages .append (
828- f"Prior distribution `{ parameter .prior_distribution } ' "
829- f"for parameter `{ parameter .id } ' is not valid."
830+ f"Prior distribution `{ parameter .prior_distribution } ` "
831+ f"for parameter `{ parameter .id } ` is not valid."
830832 )
831833 continue
832834
833835 if (
834836 exp_num_par := self ._num_pars [parameter .prior_distribution ]
835837 ) != len (parameter .prior_parameters ):
836838 messages .append (
837- f"Prior distribution `{ parameter .prior_distribution } ' "
838- f"for parameter `{ parameter .id } ' requires "
839+ f"Prior distribution `{ parameter .prior_distribution } ` "
840+ f"for parameter `{ parameter .id } ` requires "
839841 f"{ exp_num_par } parameters, but got "
840842 f"{ len (parameter .prior_parameters )} "
841843 f"({ parameter .prior_parameters } )."
@@ -848,8 +850,8 @@ def run(self, problem: Problem) -> ValidationIssue | None:
848850 _ = parameter .prior_dist .sample (1 )
849851 except Exception as e :
850852 messages .append (
851- f"Prior parameters `{ parameter .prior_parameters } ' "
852- f"for parameter `{ parameter .id } ' are invalid "
853+ f"Prior parameters `{ parameter .prior_parameters } ` "
854+ f"for parameter `{ parameter .id } ` are invalid "
853855 f"(hint: { e } )."
854856 )
855857
@@ -874,16 +876,16 @@ def run(self, problem: Problem) -> ValidationIssue | None:
874876 continue
875877
876878 messages .append (
877- f"Measurement `{ measurement } ' does not have a model ID, "
879+ f"Measurement `{ measurement } ` does not have a model ID, "
878880 "but there are multiple models available. "
879881 "Please specify the model ID in the measurement table."
880882 )
881883 continue
882884
883885 if measurement .model_id not in available_models :
884886 messages .append (
885- f"Measurement `{ measurement } ' has model ID "
886- f"`{ measurement .model_id } ' which does not match "
887+ f"Measurement `{ measurement } ` has model ID "
888+ f"`{ measurement .model_id } ` which does not match "
887889 "any of the available models: "
888890 f"{ available_models } ."
889891 )
@@ -894,6 +896,102 @@ def run(self, problem: Problem) -> ValidationIssue | None:
894896 return None
895897
896898
899+ class CheckMappingTable (ValidationTask ):
900+ """Validate the mapping table."""
901+
902+ def run (self , problem : Problem ) -> ValidationIssue | None :
903+ messages = []
904+
905+ # Mapping table is optional
906+ if problem .mappings :
907+ # Check that each id only occurs once
908+ counter = Counter (
909+ [
910+ getattr (mapping , attr )
911+ for mapping in problem .mappings
912+ for attr in ["petab_id" , "model_id" ]
913+ if getattr (mapping , attr )
914+ ]
915+ )
916+ non_unique = {id_ for id_ , count in counter .items () if count > 1 }
917+ if non_unique :
918+ return ValidationError (
919+ f"Mapping table contains non-unique IDs: { non_unique } ."
920+ )
921+
922+ # petabEntityId is not defined elsewhere in the PEtab problem
923+ petab_ids_mapping = {m .petab_id for m in problem .mappings }
924+ defined_petab_ids = (
925+ {c .id for c in problem .conditions }
926+ | {e .id for e in problem .experiments }
927+ | {o .id for o in problem .observables }
928+ )
929+ if petab_ids_mapping & defined_petab_ids :
930+ messages .append (
931+ f"PEtab IDs `{ petab_ids_mapping & defined_petab_ids } ` are "
932+ "defined in the mapping table but also defined through "
933+ "other PEtab tables."
934+ )
935+ # Grab all symbols from condition table for later
936+ condition_target_ids_values = {
937+ sym
938+ for cond in problem .conditions
939+ for change in cond .changes
940+ for sym in (
941+ change .target_value .free_symbols
942+ | change .target_id .free_symbols
943+ )
944+ }
945+
946+ for mapping in problem .mappings :
947+ # petabEntityId is not referenced in any model
948+ for model in problem .models :
949+ if model .has_entity_with_id (mapping .petab_id ):
950+ messages .append (
951+ f"`{ mapping .petab_id } ` is used in the mapping "
952+ "table and referenced directly in the model "
953+ f"`{ model .model_id } `."
954+ )
955+
956+ # modelEntityId can be empty
957+ if mapping .model_id :
958+ # model_id is not in petab problem
959+ if mapping .model_id in defined_petab_ids :
960+ messages .append (
961+ f"PEtab ID `{ mapping .model_id } ` mirrors the "
962+ "model entity ID referenced in the mapping table."
963+ )
964+ if mapping .model_id in condition_target_ids_values :
965+ messages .append (
966+ f"Identifier `{ mapping .model_id } ` is mapped to a "
967+ "PEtab ID in the mapping table, but also directly "
968+ "used in the conditions table."
969+ )
970+ for observable_formula in [
971+ o .formula for o in problem .observables
972+ ]:
973+ if mapping .model_id in observable_formula .free_symbols :
974+ messages .append (
975+ f"Identifier `{ mapping .model_id } ` is mapped "
976+ "to a PEtab ID in the mapping table, but also "
977+ "directly used in an observable formula."
978+ )
979+ for obs_noise_formula in [
980+ o .noise_formula for o in problem .observables
981+ ]:
982+ if mapping .model_id in obs_noise_formula .free_symbols :
983+ messages .append (
984+ f"Identifier `{ mapping .model_id } ` is mapped "
985+ "to a PEtab ID in the mapping table, but also "
986+ "directly used in a noise formula."
987+ )
988+
989+ if messages :
990+ return ValidationError ("\n " .join (messages ))
991+
992+ return None
993+
994+
897995def get_valid_parameters_for_parameter_table (
898996 problem : Problem ,
899997) -> set [str ]:
@@ -933,9 +1031,13 @@ def get_valid_parameters_for_parameter_table(
9331031 if p not in invalid
9341032 )
9351033
1034+ # Add petab ids from mapping table if they are used for aliasing
9361035 for mapping in problem .mappings :
937- if mapping .model_id and mapping . model_id in parameter_ids . keys () :
1036+ if mapping .model_id :
9381037 parameter_ids [mapping .petab_id ] = None
1038+ # An aliased model id is not a valid parameter id
1039+ if mapping .model_id in parameter_ids :
1040+ del parameter_ids [mapping .model_id ]
9391041
9401042 # add output parameters from observable table
9411043 output_parameters = problem .get_output_parameters ()
@@ -977,20 +1079,13 @@ def get_required_parameters_for_parameter_table(
9771079 measurement table as well as all parametric condition table overrides
9781080 that are not defined in the model.
9791081 """
980- parameter_ids = set ()
981- condition_targets = {
982- change .target_id
983- for cond in problem .conditions
984- for change in cond .changes
985- }
1082+ # Start with mapping table petab ids
1083+ parameter_ids = {m .petab_id for m in problem .mappings }
9861084
9871085 # Add parameters from measurement table, unless they are fixed parameters
9881086 def append_overrides (overrides ):
9891087 parameter_ids .update (
990- str_p
991- for p in overrides
992- if isinstance (p , sp .Symbol )
993- and (str_p := str (p )) not in condition_targets
1088+ str (p ) for p in overrides if isinstance (p , sp .Symbol )
9941089 )
9951090
9961091 for m in problem .measurements :
@@ -1033,7 +1128,12 @@ def append_overrides(overrides):
10331128 if not problem .model .has_entity_with_id (str (p ))
10341129 )
10351130
1036- # parameters that are overridden via the condition table are not allowed
1131+ # Parameters that are overridden via the condition table are not allowed
1132+ condition_targets = {
1133+ change .target_id
1134+ for cond in problem .conditions
1135+ for change in cond .changes
1136+ }
10371137 parameter_ids -= condition_targets
10381138
10391139 return parameter_ids
@@ -1090,5 +1190,5 @@ def get_placeholders(
10901190 CheckUnusedConditions (),
10911191 CheckPriorDistribution (),
10921192 CheckInitialChangeSymbols (),
1093- # TODO validate mapping table
1193+ CheckMappingTable (),
10941194]
0 commit comments