4444 "CheckPriorDistribution" ,
4545 "CheckUndefinedExperiments" ,
4646 "CheckInitialChangeSymbols" ,
47+ "CheckMappingTable" ,
4748 "lint_problem" ,
4849 "default_validation_tasks" ,
4950]
@@ -445,31 +446,31 @@ def run(self, problem: Problem) -> ValidationIssue | None:
445446
446447 # check for uniqueness of all primary keys
447448 counter = Counter (c .id for c in problem .conditions )
448- duplicates = { id_ for id_ , count in counter .items () if count > 1 }
449+ duplicates = sorted ( id_ for id_ , count in counter .items () if count > 1 )
449450
450451 if duplicates :
451452 return ValidationError (
452453 f"Condition table contains duplicate IDs: { duplicates } "
453454 )
454455
455456 counter = Counter (o .id for o in problem .observables )
456- duplicates = { id_ for id_ , count in counter .items () if count > 1 }
457+ duplicates = sorted ( id_ for id_ , count in counter .items () if count > 1 )
457458
458459 if duplicates :
459460 return ValidationError (
460461 f"Observable table contains duplicate IDs: { duplicates } "
461462 )
462463
463464 counter = Counter (e .id for e in problem .experiments )
464- duplicates = { id_ for id_ , count in counter .items () if count > 1 }
465+ duplicates = sorted ( id_ for id_ , count in counter .items () if count > 1 )
465466
466467 if duplicates :
467468 return ValidationError (
468469 f"Experiment table contains duplicate IDs: { duplicates } "
469470 )
470471
471472 counter = Counter (p .id for p in problem .parameters )
472- duplicates = { id_ for id_ , count in counter .items () if count > 1 }
473+ duplicates = sorted ( id_ for id_ , count in counter .items () if count > 1 )
473474
474475 if duplicates :
475476 return ValidationError (
@@ -508,7 +509,9 @@ def run(self, problem: Problem) -> ValidationIssue | None:
508509 for experiment in problem .experiments :
509510 # Check that there are no duplicate timepoints
510511 counter = Counter (period .time for period in experiment .periods )
511- duplicates = {time for time , count in counter .items () if count > 1 }
512+ duplicates = sorted (
513+ time for time , count in counter .items () if count > 1
514+ )
512515 if duplicates :
513516 messages .append (
514517 f"Experiment { experiment .id } contains duplicate "
@@ -551,7 +554,8 @@ def run(self, problem: Problem) -> ValidationIssue | None:
551554
552555class CheckAllParametersPresentInParameterTable (ValidationTask ):
553556 """Ensure all required parameters are contained in the parameter table
554- with no additional ones."""
557+ with no additional ones.
558+ """
555559
556560 def run (self , problem : Problem ) -> ValidationIssue | None :
557561 if problem .model is None :
@@ -825,17 +829,17 @@ def run(self, problem: Problem) -> ValidationIssue | None:
825829
826830 if parameter .prior_distribution not in PRIOR_DISTRIBUTIONS :
827831 messages .append (
828- f"Prior distribution `{ parameter .prior_distribution } ' "
829- f"for parameter `{ parameter .id } ' is not valid."
832+ f"Prior distribution `{ parameter .prior_distribution } ` "
833+ f"for parameter `{ parameter .id } ` is not valid."
830834 )
831835 continue
832836
833837 if (
834838 exp_num_par := self ._num_pars [parameter .prior_distribution ]
835839 ) != len (parameter .prior_parameters ):
836840 messages .append (
837- f"Prior distribution `{ parameter .prior_distribution } ' "
838- f"for parameter `{ parameter .id } ' requires "
841+ f"Prior distribution `{ parameter .prior_distribution } ` "
842+ f"for parameter `{ parameter .id } ` requires "
839843 f"{ exp_num_par } parameters, but got "
840844 f"{ len (parameter .prior_parameters )} "
841845 f"({ parameter .prior_parameters } )."
@@ -848,8 +852,8 @@ def run(self, problem: Problem) -> ValidationIssue | None:
848852 _ = parameter .prior_dist .sample (1 )
849853 except Exception as e :
850854 messages .append (
851- f"Prior parameters `{ parameter .prior_parameters } ' "
852- f"for parameter `{ parameter .id } ' are invalid "
855+ f"Prior parameters `{ parameter .prior_parameters } ` "
856+ f"for parameter `{ parameter .id } ` are invalid "
853857 f"(hint: { e } )."
854858 )
855859
@@ -874,16 +878,16 @@ def run(self, problem: Problem) -> ValidationIssue | None:
874878 continue
875879
876880 messages .append (
877- f"Measurement `{ measurement } ' does not have a model ID, "
881+ f"Measurement `{ measurement } ` does not have a model ID, "
878882 "but there are multiple models available. "
879883 "Please specify the model ID in the measurement table."
880884 )
881885 continue
882886
883887 if measurement .model_id not in available_models :
884888 messages .append (
885- f"Measurement `{ measurement } ' has model ID "
886- f"`{ measurement .model_id } ' which does not match "
889+ f"Measurement `{ measurement } ` has model ID "
890+ f"`{ measurement .model_id } ` which does not match "
887891 "any of the available models: "
888892 f"{ available_models } ."
889893 )
@@ -894,6 +898,79 @@ def run(self, problem: Problem) -> ValidationIssue | None:
894898 return None
895899
896900
901+ class CheckMappingTable (ValidationTask ):
902+ """Validate the mapping table."""
903+
904+ def run (self , problem : Problem ) -> ValidationIssue | None :
905+ # Mapping table is optional
906+ if not problem .mappings :
907+ return None
908+
909+ messages = []
910+
911+ # Check that each id, across both the petabEntityId and
912+ # modelEntityId columns, occurs only once
913+ must_be_unique_ids = []
914+ for mapping in problem .mappings :
915+ petab_id = mapping .petab_id
916+ model_id = mapping .model_id
917+
918+ if petab_id :
919+ must_be_unique_ids .append (petab_id )
920+ # Identity mappings are permitted for annotation
921+ if petab_id == model_id :
922+ continue
923+ if model_id :
924+ must_be_unique_ids .append (model_id )
925+
926+ non_unique_ids = sorted (
927+ id_
928+ for id_ , count in Counter (must_be_unique_ids ).items ()
929+ if count > 1
930+ )
931+ if non_unique_ids :
932+ return ValidationError (
933+ f"Mapping table contains non-unique IDs: { non_unique_ids } ."
934+ )
935+
936+ # petabEntityId is not defined elsewhere in the PEtab problem
937+ new_petab_ids = {
938+ m .petab_id
939+ for m in problem .mappings
940+ # Ignore identity mappings used for annotation
941+ if m .petab_id != m .model_id
942+ }
943+ old_petab_ids = (
944+ {c .id for c in problem .conditions }
945+ | {e .id for e in problem .experiments }
946+ | {o .id for o in problem .observables }
947+ )
948+ if overdefined_ids := sorted (new_petab_ids & old_petab_ids ):
949+ messages .append (
950+ f"PEtab IDs `{ overdefined_ids } ` are "
951+ "defined in the mapping table but also defined through "
952+ "other PEtab tables."
953+ )
954+
955+ for mapping in problem .mappings :
956+ # petabEntityId not referenced in any model, if alias
957+ for model in problem .models :
958+ if (
959+ mapping .petab_id != mapping .model_id
960+ and model .has_entity_with_id (mapping .petab_id )
961+ ):
962+ messages .append (
963+ f"`{ mapping .petab_id } ` is used in the mapping "
964+ "table and referenced directly in the model "
965+ f"`{ model .model_id } `."
966+ )
967+
968+ if messages :
969+ return ValidationError ("\n " .join (messages ))
970+
971+ return None
972+
973+
897974def get_valid_parameters_for_parameter_table (
898975 problem : Problem ,
899976) -> set [str ]:
@@ -984,13 +1061,9 @@ def get_required_parameters_for_parameter_table(
9841061 for change in cond .changes
9851062 }
9861063
987- # Add parameters from measurement table, unless they are fixed parameters
9881064 def append_overrides (overrides ):
9891065 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
1066+ str (p ) for p in overrides if isinstance (p , sp .Symbol )
9941067 )
9951068
9961069 for m in problem .measurements :
@@ -1033,7 +1106,7 @@ def append_overrides(overrides):
10331106 if not problem .model .has_entity_with_id (str (p ))
10341107 )
10351108
1036- # parameters that are overridden via the condition table are not allowed
1109+ # Parameters that are overridden via the condition table are not allowed
10371110 parameter_ids -= condition_targets
10381111
10391112 return parameter_ids
@@ -1090,5 +1163,5 @@ def get_placeholders(
10901163 CheckUnusedConditions (),
10911164 CheckPriorDistribution (),
10921165 CheckInitialChangeSymbols (),
1093- # TODO validate mapping table
1166+ CheckMappingTable (),
10941167]
0 commit comments