44import logging
55import numbers
66import re
7- from typing import Optional , Iterable , Union
7+ from typing import Optional , Iterable , Any
88from collections import Counter
99
1010import libsbml
@@ -526,68 +526,106 @@ def assert_parameter_estimate_is_boolean(parameter_df: pd.DataFrame) -> None:
526526 f"Expected 0 or 1 but got { estimate } in { ESTIMATE } column." )
527527
528528
529+ def is_scalar_float (x : Any ):
530+ """
531+ Checks whether input is a number or can be transformed into a number
532+ via float
533+ :param x:
534+ input
535+ :return:
536+ True if is or can be converted to number, False otherwise.
537+ """
538+ if isinstance (x , numbers .Number ):
539+ return True
540+ try :
541+ float (x )
542+ return True
543+ except (ValueError , TypeError ):
544+ return False
545+
546+
529547def measurement_table_has_timepoint_specific_mappings (
530- measurement_df : pd .DataFrame ) -> bool :
548+ measurement_df : pd .DataFrame ,
549+ allow_scalar_numeric_noise_parameters : bool = False ,
550+ allow_scalar_numeric_observable_parameters : bool = False ,
551+ ) -> bool :
531552 """
532553 Are there time-point or replicate specific parameter assignments in the
533554 measurement table.
534555
535556 Arguments:
536- measurement_df: PEtab measurement table
557+ measurement_df:
558+ PEtab measurement table
559+
560+ allow_scalar_numeric_noise_parameters:
561+ ignore scalar numeric assignments to noiseParamater placeholders
562+
563+ allow_scalar_numeric_observable_parameters:
564+ ignore scalar numeric assignments to observableParamater
565+ placeholders
537566
538567 Returns:
539- True if there are time-point or replicate specific parameter
540- assignments in the measurement table, False otherwise.
568+ True if there are time-point or replicate specific (non-numeric)
569+ parameter assignments in the measurement table, False otherwise.
541570 """
542571 # since we edit it, copy it first
543572 measurement_df = copy .deepcopy (measurement_df )
544573
545- def is_numeric (x : Union [str , numbers .Number ]) -> bool :
546- """
547- Checks whether x can be transformed into a (list of) float(s)
548- :param x:
549- number or string containing numbers seperated by ;
550- :return:
551- True if conversion is possible for all values
552- """
553- if isinstance (x , numbers .Number ):
554- return True
555- if not isinstance (x , str ):
556- return False
557- try :
558- [float (y ) for y in x .split (';' )]
559- return True
560- except (ValueError , TypeError ):
561- return False
562-
563574 # mask numeric values
564- for col in [OBSERVABLE_PARAMETERS , NOISE_PARAMETERS ]:
575+ for col , allow_scalar_numeric in [
576+ (OBSERVABLE_PARAMETERS , allow_scalar_numeric_observable_parameters ),
577+ (NOISE_PARAMETERS , allow_scalar_numeric_noise_parameters )
578+ ]:
565579 if col not in measurement_df :
566580 continue
567- measurement_df .loc [measurement_df [col ].apply (is_numeric ), col ] = np .nan
581+
582+ measurement_df [col ] = measurement_df [col ].apply (str )
583+
584+ if allow_scalar_numeric :
585+ measurement_df .loc [
586+ measurement_df [col ].apply (is_scalar_float ), col
587+ ] = np .nan
568588
569589 grouping_cols = core .get_notnull_columns (
570590 measurement_df ,
571591 [OBSERVABLE_ID ,
572592 SIMULATION_CONDITION_ID ,
573593 PREEQUILIBRATION_CONDITION_ID ,
574594 OBSERVABLE_PARAMETERS ,
575- NOISE_PARAMETERS ,
576- ])
577- grouped_df = measurement_df .groupby (grouping_cols ,
578- dropna = False ).size ().reset_index ()
595+ NOISE_PARAMETERS ])
596+ grouped_df = measurement_df .groupby (grouping_cols , dropna = False )
579597
580598 grouping_cols = core .get_notnull_columns (
581- grouped_df ,
599+ measurement_df ,
582600 [OBSERVABLE_ID ,
583601 SIMULATION_CONDITION_ID ,
584602 PREEQUILIBRATION_CONDITION_ID ])
585- grouped_df2 = grouped_df .groupby (grouping_cols ). size (). reset_index ( )
603+ grouped_df2 = measurement_df .groupby (grouping_cols )
586604
587605 # data frame has timepoint specific overrides if grouping by noise
588606 # parameters and observable parameters in addition to observable,
589607 # condition and preeq id yields more groups
590- return len (grouped_df .index ) != len (grouped_df2 .index )
608+ return len (grouped_df ) != len (grouped_df2 )
609+
610+
611+ def observable_table_has_nontrivial_noise_formula (
612+ observable_df : pd .DataFrame ) -> bool :
613+ """
614+ Does any observable have a noise formula that is not just a single
615+ parameter?
616+
617+ Arguments:
618+ observable_df: PEtab observable table
619+
620+ Returns:
621+ True if any noise formula does not consist of a single identifier,
622+ False otherwise.
623+ """
624+
625+ return not observable_df [NOISE_FORMULA ].apply (
626+ lambda x : is_scalar_float (x ) or
627+ re .match (r'^[\w]+$' , str (x )) is not None
628+ ).all ()
591629
592630
593631def measurement_table_has_observable_parameter_numeric_overrides (
@@ -598,7 +636,7 @@ def measurement_table_has_observable_parameter_numeric_overrides(
598636 measurement_df: PEtab measurement table
599637
600638 Returns:
601- True if there any numbers to override observable parameters,
639+ True if there are any numbers to override observable/noise parameters,
602640 False otherwise.
603641 """
604642 if OBSERVABLE_PARAMETERS not in measurement_df :
0 commit comments