ReactionDynamics – Reference

Source code




Class ExcessiveTimeStepHard



Class ExcessiveTimeStepSoft



Class ReactionDynamics

__init__
TO SET AND READ DATA:
set_conc
set_chem_conc
get_system_conc
get_chem_conc
get_conc_dict
slow_rxns
fast_rxns
are_all_slow_rxns
get_rxn_speed
set_rxn_speed
set_rxn_speed_all_fast
clear_reactions
TO VISUALIZE SYSTEM:
describe_state
TO PERFORM THE REACTIONS:
specify_steps
set_thresholds
set_step_factors
set_error_step_factor
single_compartment_react
reaction_step_common
_attempt_reaction_step
norm_A
norm_B
adjust_speed
display_thresholds
_reaction_elemental_step
_reaction_elemental_step_SINGLE_REACTION
criterion_fast_reaction
validate_increment
compute_all_reaction_deltas
compute_reaction_delta_rate
MACROMOLECULE DYNAMICS:
set_macromolecules
set_occupancy
get_occupancy
update_occupancy
sigmoid
logistic
FOR DIAGNOSTICS:
stoichiometry_checker
stoichiometry_checker_from_deltas
stoichiometry_checker_entire_run
_delta_names
_delta_conc_dict
set_diagnostics
unset_diagnostics
save_diagnostic_rxn_data
comment_diagnostic_rxn_data
get_diagnostic_rxn_data
save_diagnostic_conc_data
get_diagnostic_conc_data
save_diagnostic_decisions_data
get_diagnostic_decisions_data
get_diagnostic_decisions_data_ALT
explain_reactions
_explain_reactions_helper
explain_time_advance
_explain_time_advance_helper
GRAPHICS:
plot_curves
plot_step_sizes
HISTORY:
add_snapshot
get_history
get_historical_concentrations
RESULT ANALYSIS:
is_in_equilibrium
reaction_in_equilibrium
extract_delta_concentrations
curve_intersection




Class ExcessiveTimeStepHard

    Used to raise Exceptions arising from excessively large time steps
    (that lead to negative concentration values, i.e. "HARD" errors)
    



Class ExcessiveTimeStepSoft

    Used to raise Exceptions arising from excessively large time steps
    (that lead to norms regarded as excessive because of user-specified values, i.e. "SOFT" errors)
    



Class ReactionDynamics

    Used to simulate the dynamics of reactions (in a single compartment.)
    In the context of Life123, this may be thought of as a "zero-dimensional system"
    
nameargumentsreturns
__init__self, chem_data
        :param chem_data:   Object of type "ChemData" (with data about the chemicals and their reactions)
                                    It's acceptable to pass None,
                                    and take care of it later (though probably a bad idea!)
                                    TODO: maybe offer an option to let the constructor instantiate that object?
        

TO SET AND READ DATA

nameargumentsreturns
set_concself, conc: Union[list, tuple, dict], snapshot=TrueNone
        Set the concentrations of ALL the chemicals at once
        TODO: maybe a dict indicates a selection of chemicals, while a list, etc means "ALL"

        :param conc:        A list or tuple of concentration values for ALL the chemicals, in their index order;
                                alternatively, a dict indexed by the chemical names, again for ALL the chemicals
                                EXAMPLE of the latter:  {"A": 12.4, "B": 0.23, "C": 2.6}
                                                        (assuming that "A", "B", "C" are ALL the chemicals)

                                Note: any previous values will get over-written)
                                TODO: [maybe the dict version should be the responsibility of set_chem_conc() instead;] OR
                                      allow the dict to be a subset of all chemicals
                                TODO: also allow a Numpy array; make sure to do a copy() to it!
                                TODO: pytest for the dict option
        :param snapshot:    (OPTIONAL) boolean: if True, add to the history
                                a snapshot of this state being set.  Default: True
        :return:            None
        
nameargumentsreturns
set_chem_concself, conc, species_index=None, species_name=None, snapshot=TrueNone
        Set the concentrations of 1 chemical
        Note: if both species_index and species_name are provided, species_name is used     TODO: pytest this part
        
        :param conc:            A non-negative number with the desired concentration value for the above value.
                                    (Any previous value will get over-written)
        :param species_index:   (OPTIONAL) An integer that indexes the chemical of interest (numbering starts at 0)
        :param species_name:    (OPTIONAL) A name for the chemical of interest.
                                    If both species_index and species_name are provided, species_name is used
        :param snapshot:        (OPTIONAL) boolean: if True, add to the history
                                    a snapshot of this state being set.  Default: True
        :return:                None
nameargumentsreturns
get_system_concselfnp.array
        Retrieve the concentrations of ALL the chemicals as a Numpy array

        :return:        A Numpy array with the concentrations of ALL the chemicals,
                        in their index order
                            EXAMPLE:  array([12.3, 4.56, 0.12])
                                      The 0-th chemical has concentration 12.3, and so on...
        
nameargumentsreturns
get_chem_concself, name: strfloat
        Return the current system concentration of the given chemical, specified by its name.
        If no chemical by that name exists, an Exception is raised

        :param name:    The name of a chemical species
        :return:        The current system concentration of the above chemical
        
nameargumentsreturns
get_conc_dictself, species=None, system_data=Nonedict
        Retrieve the concentrations of the requested chemicals (by default all),
        as a dictionary indexed by the chemical's name

        :param species:     (OPTIONAL) list or tuple of names of the chemical species; by default, return all
        :param system_data: (OPTIONAL) a Numpy array of concentration values, in the same order as the
                                index of the chemical species; by default, use the SYSTEM DATA
                                (which is set and managed by various functions)
        :return:            A dictionary, indexed by chemical name, of the concentration values;
                                EXAMPLE: {"A": 1.2, "D": 4.67}
        
nameargumentsreturns
slow_rxnsself[int]
        Return a list of all the reactions that are marked as "slow"
        :return:
        
nameargumentsreturns
fast_rxnsself[int]
        Return a list of all the reactions that are marked as "fast"
        :return:
        
nameargumentsreturns
are_all_slow_rxnsselfbool
        Return True iff all the reactions are marked as "slow"
        :return:
        
nameargumentsreturns
get_rxn_speedself, rxn_index: intstr
        For the requested reaction, get the string code that it was marked with
        to classify its speed.
        If the reaction has not been previously classified, regard it as "F" (Fast)

        :param rxn_index:   The index (0-based) to identify the reaction of interest
        :return:            A 1-letter string with the code "F" (for Fast) or "S" (Slow)
        
nameargumentsreturns
set_rxn_speedself, rxn_index: int, speed: strNone
        Set a code value that classifies the reaction speed to tag the given reaction to

        :param rxn_index:   The index (0-based) to identify the reaction of interest
        :param speed:       A 1-letter string with the code "F" (for Fast) or "S" (Slow)
        :return:            None
        
nameargumentsreturns
set_rxn_speed_all_fastselfNone
        Reset all the reaction speeds to "Fast"

        :return:    None
        
nameargumentsreturns
clear_reactionsselfNone
        Get rid of all reactions; start again with "an empty slate" (but still with reference
        to the same data object about the chemicals)

        # TODO: maybe offer an option to clear just one reaction, or a list of them
        # TODO: provide support for "inactivating" reactions

        :return:    None
        

TO VISUALIZE SYSTEM

nameargumentsreturns
describe_stateselfNone
        Print out various data on the current state of the system
        :return:        None
        

TO PERFORM THE REACTIONS

nameargumentsreturns
specify_stepsself, total_duration=None, time_step=None, n_steps=None(float, int)
        If either the time_step or n_steps is not provided (but at least 1 of them must be present),
        determine the other one from total_duration

        Their desired relationship is: total_duration = time_step * n_steps

        :param total_duration:  Float with the overall time advance (i.e. time_step * n_steps)
        :param time_step:       Float with the size of each time step
        :param n_steps:         Integer with the desired number of steps
        :return:                The pair (time_step, n_steps)
        
nameargumentsreturns
set_thresholdsself, norm="norm_A", low=None, high=None, abort=NoneNone
        Over-ride default values for simulation parameters.
        The default None values can be used to eliminate some of the threshold rules, for the specified norm

        :param norm:
        :param low:
        :param high:
        :param abort:
        :return:            None
        
nameargumentsreturns
set_step_factorsself, upshift, downshift, abortNone
        Over-ride default values for simulation parameters

        :param upshift:
        :param downshift:
        :param abort:
        :return:            None
        
nameargumentsreturns
set_error_step_factorself, valueNone
        Over-ride the default value for the simulation parameter error_abort_step_factor

        :param value:
        :return:        None
        
nameargumentsreturns
single_compartment_reactself, reaction_duration=None, target_end_time=None, initial_step=None, n_steps=None, snapshots=None, silent=False, variable_steps=False, explain_variable_steps=FalseNone
        Perform ALL the reactions in the single compartment -
        based on the INITIAL concentrations,
        which are used as the basis for all the reactions.

        Update the system state and the system time accordingly
        (object attributes self.system and self.system_time)

        :param reaction_duration:  The overall time advance for the reactions (it might be exceeded in case of variable steps)
        :param target_end_time: The final time at which to stop the reaction
                                    If both target_end_time and reaction_duration are specified, an error will result

        :param initial_step:    The suggested size of the first step (it might be reduced automatically,
                                    in case of "hard" errors from large steps)

        :param n_steps:         The desired number of steps

        :param snapshots:       OPTIONAL dict that may contain any the following keys:
                                        -"frequency" (default 1)
                                        -"show_intermediates" (default True)
                                        -"species" (default None, meaning all species)
                                        -"initial_caption" (default blank)
                                        -"final_caption" (default blank)
                                    If provided, take a system snapshot after running a multiple
                                    of "frequency" reaction steps (default 1, i.e. at every step.)
                                    EXAMPLE: snapshots={"frequency": 2, "species": ["A", "H"]}

        :param silent:              If True, less output is generated

        :param variable_steps:      If True, the steps sizes will get automatically adjusted, based on thresholds
        :param explain_variable_steps:

        :return:                None.   The object attributes self.system and self.system_time get updated
        
nameargumentsreturns
reaction_step_commonself, delta_time: float, conc_array=None, variable_steps=False, explain_variable_steps=False, step_counter=1(np.array, float, float)
        This is the common entry point for both single-compartment reactions,
        and the reaction part of reaction-diffusions in 1D, 2D and 3D.

        "Compartments" may or may not correspond to the "bins" of the higher layers;
        the calling code might have opted to merge some bins into a single "compartment".

        Using the given concentration data for all the applicable species in a single compartment,
        do a single reaction time step for ALL the reactions -
        based on the INITIAL concentrations (prior to this reaction step),
        which are used as the basis for all the reactions.

        Return the increment vector for all the chemical species concentrations in the compartment

        TODO: no longer pass conc_array .  Use the object variable self.system instead

        NOTES:  * the actual system concentrations are NOT changed
                * this method doesn't decide on step sizes - except in case of ("hard" or "soft") aborts, which are
                    followed by repeats with a smaller step.  Also, it makes suggestions
                    to the calling module about the next step to best take (whether as a result of an abort,
                    or for other considerations)

        :param delta_time:      The requested time duration of the reaction step
        :param conc_array:      [OPTIONAL]All initial concentrations at the start of the reaction step,
                                    as a Numpy array for ALL the chemical species, in their index order.
                                    If not provided, self.system is used instead
        :param variable_steps:  If True, the step sizes will get automatically adjusted with an adaptive algorithm
        :param explain_variable_steps:
        :param step_counter:

        :return:                The triplet:
                                    1) increment vector for the concentrations of ALL the chemical species,
                                        in their index order, as a Numpy array
                                        EXAMPLE (for a single-reaction reactant and product with a 3:1 stoichiometry):
                                            array([7. , -21.])  TODO: is this really necessary?  Maybe update self.system here?
                                    2) time step size actually taken - which might be smaller than the requested one
                                        because of reducing the step to avoid negative-concentration errors
                                    3) recommended_next_step : a suggestions to the calling module
                                       about the next step to best take
        
nameargumentsreturns
_attempt_reaction_stepself, delta_time, variable_steps, explain_variable_steps, step_counter(np.array, float)
        Attempt to perform the core reaction step, and then raise an Exception if it needs to be aborted,
        based on various criteria.
        If variable_steps is True, determine a new value for the "recommended next step"

        :param delta_time:              The requested time duration of the reaction step
        :param variable_steps:          If True, the step sizes will get automatically adjusted with an adaptive algorithm
        :param explain_variable_steps:  If True, a brief explanation is printed about how the variable step sizes were chosen;
                                            only applicable if variable_steps is True
        :param step_counter:            A number to show in the explanations about the variable step sizes;
                                            only applicable if explain_variable_steps is True

        :return:                The pair (delta_concentrations, recommended_next_step)
        
nameargumentsreturns
norm_Aself, delta_conc: np.arrayfloat
        Return "version A" of a measure of system change, based on the average concentration changes
        of ALL chemicals across a time step, adjusted for the number of chemicals

        :param delta_conc:  A Numpy array with the concentration changes
                                of the chemicals of interest across a time step
        :return:            A measure of change in the concentrations across the simulation step
        
nameargumentsreturns
norm_Bself, baseline_conc: np.array, delta_conc: np.arrayfloat
        Return "version B" of a measure of system change, based on the max absolute relative concentration
        change of all the chemicals across a time step (based on an L infinity norm - but disregarding
        any baseline concentration that is very close to zero)

        :param baseline_conc:   A Numpy array with the concentration of the chemicals of interest
                                    at the start of a simulation time step
        :param delta_conc:      A Numpy array with the concentration changes
                                    of the chemicals of interest across a time step
        :return:                A measure of change in the concentrations across the simulation step
        
nameargumentsreturns
adjust_speedself, delta_conc: np.array, baseline_conc=None(str, Union[float, int], dict)

        :param delta_conc:
        :param baseline_conc:
        :return:                A triplet:
                                    1) String with the name of the action to take: either "low", "stay", "high" or "abort"
                                    2) A factor by which to multiple the time step at the next iteration round;
                                       if no change is deemed necessary, 1 is returned
                                    3) A dict of all the computed norms (any of the last ones, except the first one, may be missing),
                                       indexed by their names
        
nameargumentsreturns
display_thresholdsself, rule, value

        :param rule:
        :param value:
        :return:
        
nameargumentsreturns
_reaction_elemental_stepself, delta_time: float, rxn_list=Nonenp.array
        Using the system concentration data of ALL the chemical species,
        do the specified SINGLE TIME STEP for ONLY the requested reactions (by default all).

        All computations are based on the INITIAL concentrations (prior to this reaction step),
        which are used as the basis for all the reactions (in "forward Euler" approach.)

        Return the Numpy increment vector for ALL the chemical species concentrations, in their index order
        (whether involved in these reactions or not)

        NOTES:  - the actual System Concentrations and the System Time (stored in object variables) are NOT changed
                - if any of the concentrations go negative, an Exception is raised

        :param delta_time:      The time duration of this individual reaction step - assumed to be small enough that the
                                    concentration won't vary significantly during this span.
        :param rxn_list:        OPTIONAL list of reactions (specified by their indices) to include in this simulation step ;
                                    EXAMPLE: [1, 3, 7]
                                    If None, do all the reactions

        :return:            The increment vector for the concentrations of ALL the chemical species
                                (whether involved in the reactions or not),
                                as a Numpy array for all the chemical species, in their index order
                            EXAMPLE (for a single-reaction reactant and product with a 3:1 stoichiometry):   array([7. , -21.])
        
nameargumentsreturns
_reaction_elemental_step_SINGLE_REACTIONself, delta_time: float, increment_vector, rxn_index: int, delta_dict
        :param delta_time:
        :param increment_vector:
        :param rxn_index:
        :param delta_dict:
        :return:
        
nameargumentsreturns
criterion_fast_reactionself, delta_conc, fast_threshold_fraction, baseline_conc=None, use_baseline=Falsebool
        Apply a criterion to determine, from the given data,
        whether the originating reaction (the source of the data) needs to be classified as "Fast".
        All the passed data is for the concentration changes in 1 chemical from 1 reaction

        :param delta_conc:
        :param fast_threshold_fraction:
        :param baseline_conc:           # TODO: probably phase out
        :param use_baseline:            # TODO: gave poor results when formerly used for substeps

        :return:                        True if the concentration change is so large (based on some criteria)
                                            that the reaction that caused it, ought to be regarded as "fast"
        
nameargumentsreturns
validate_incrementself, delta_conc, baseline_conc: float, rxn_index: int, species_index: int, delta_timeNone
        Examine the requested concentration change given by delta_conc
        (typically, as computed by an ODE solver),
        relative to the baseline (pre-reaction) value baseline_conc,
        for the given SINGLE chemical species and SINGLE reaction.

        If the concentration change would render the concentration negative,
        raise an Exception (of custom type "ExcessiveTimeStepHard")

        :param delta_conc:              The change in concentration computed by the ode solver
                                            (for the specified chemical, in the given reaction)
        :param baseline_conc:           The initial concentration

        [The remaining arguments are ONLY USED for error printing]
        :param rxn_index:               The index (0-based) to identify the reaction of interest (ONLY USED for error printing)
        :param species_index:           The index (0-based) to identify the chemical species of interest (ONLY USED for error printing)
        :param delta_time:              The time duration of the reaction step (ONLY USED for error printing)

        :return:                        None (an Exception is raised if a negative concentration is detected)
        
nameargumentsreturns
compute_all_reaction_deltasself, delta_time: float, rxn_list=Nonedict
        For an explanation of the "reaction delta", see compute_reaction_delta().
        Compute the "reaction delta" for all the specified reaction (by default, all.)
        Return a list with an entry for each reaction, in their index order.

        For background info: https://life123.science/reactions

        :param delta_time:  The time duration of the reaction step - assumed to be small enough that the
                                concentration won't vary significantly during this span
        :param rxn_list:    OPTIONAL list of reactions (specified by their integer index);
                                if None, do all the reactions.  EXAMPLE: [1, 3, 7]

        :return:            A dict of the differences between forward and reverse "conversions" -
                                for explanation, see compute_reaction_delta().
                                The dict is indexed by the reaction number, and contains as many entries as the
                                number of reactions being investigated
        
nameargumentsreturns
compute_reaction_delta_rateself, rxnfloat
        For the SINGLE given reaction, and the current concentrations of chemicals in the system,
        compute the reaction's "forward rate" minus its "reverse rate",
        as defined in https://life123.science/reactions

        :param rxn:         An object of type "Reaction"
        :return:            The differences between the reaction's forward and reverse rates
        

MACROMOLECULE DYNAMICS

nameargumentsreturns
set_macromoleculesself, data=NoneNone
        Specify the macromolecules, and their counts, to be included in the system.
        The fractional occupancy is set to 0 at all binding sites of all the specified macromolecules.
        Any previous data gets over-written.

        Note: to set a single fractional occupancy value, use set_occupancy()

        :param data:    A dict mapping macromolecule names to their counts
                            EXAMPLE:  {"M1": 1, "M2": 3, "M3": 1}
                        If any of the requested macromolecules isn't registered, an Exception will be raised
                        If data=None, then the set of registered macromolecules is used,
                            and all their counts are set to 1
        :return:        None.
                        The object variables self.macro_system and self.macro_system_state get set
        
nameargumentsreturns
set_occupancyself, macromolecule, site_number: int, fractional_occupancy: floatNone
        Set the fractional occupancy at the given binding site of the specified macromolecule,
        using the requested value.
        If the specified macromolecule hasn't yet been added to the dynamical system state,
        automatically add it with count 1

        :param macromolecule:           Name of a previously-registered macromolecule
        :param site_number:             Integer to identify a binding site on the macromolecule
        :param fractional_occupancy:    A number between 0. and 1., inclusive
        :return:                        None
        
nameargumentsreturns
get_occupancyself, macromolecule, site_numberfloat
        Get the fractional occupancy at the given binding site of the specified macromolecule.

        :param macromolecule:           Name of a previously-registered macromolecule
        :param site_number:             Integer to identify a binding site on the macromolecule
        :return:                        A number between 0. and 1., representing the fractional occupancy
        
nameargumentsreturns
update_occupancyselfNone
        Update the fractional occupancy at all binding sites,
        based on the current system concentrations of the relevant ligands

        :return:    None
        
nameargumentsreturns
sigmoidself, conc: float, Kd: floatfloat
        Return an estimate of fractional occupancy (between 0 and 1)
        on a particular binding site on a particular macromolecule,
        from the concentration of the ligand (such as a Transcription Factor)
        and its affinity to that binding site.

        A sigmoid curve is expected.

        Based on fig. 3A of the 2019 paper "Low-Affinity Binding Sites and the
        Transcription Factor Specificity Paradox in Eukaryotes"
        (https://doi.org/10.1146/annurev-cellbio-100617-062719):

            - at extremely low concentration, the occupancy is 0
            - when the concentration is 10% of Kd, the occupancy is about 0.1
            - when the concentration matches Kd, the occupancy is 1/2 by definition
            - when the concentration is 10 times Kd, the occupancy is about 0.9
            - at concentrations beyond that, the occupancy saturates to 1.0

        :param conc:    Concentration of the ligand (such as a Transcription Factor), in microMolars
        :param Kd:      Binding-side Affinity, in microMolars
        :return:        Estimated binding-site fractional occupancy : a value between
                            0. (no occupancy at all during the previous time step) and 1. (continuous occupancy)
        
nameargumentsreturns
logisticself, x: float, x0 = 0., k = 1.float
        Compute the value of the Logistic function, in the range (0, 1), at the given point
        See: https://en.wikipedia.org/wiki/Logistic_function

        :param x:
        :param x0:
        :param k:
        :return:    The value of the Logistic function at the given point x
        

FOR DIAGNOSTICS

nameargumentsreturns
stoichiometry_checkerself, rxn_index: int, conc_arr_before: np.array, conc_arr_after: np.array, suppress_warning=Falsebool
        For the indicated reaction, investigate the change in the concentration of the involved chemicals,
        to ascertain whether the change is consistent with the reaction's stoichiometry.
        See https://life123.science/reactions

        IMPORTANT: this function is currently meant for simulations involving only 1 reaction (TODO: generalize)

        NOTE: the concentration changes in chemicals not involved in the specified reaction are ignored

        :param rxn_index:       Integer to identify the reaction of interest
        :param conc_arr_before: Numpy array with the concentrations of ALL the chemicals (whether involved
                                    in the reaction or not), in their index order, before the reaction
        :param conc_arr_after:  Same as above, but after the reaction
        :param suppress_warning:
        :return:                True if the change in reactant/product concentrations is consistent with the
                                    reaction's stoichiometry, or False otherwise
        
nameargumentsreturns
stoichiometry_checker_from_deltasself, rxn_index: int, delta_arr: np.array, suppress_warning=Falsebool
        For the indicated reaction, investigate the change in the concentration of the involved chemicals,
        to ascertain whether the change is consistent with the reaction's stoichiometry.
        See https://life123.science/reactions

        IMPORTANT: this function is currently meant for simulations involving only 1 reaction (TODO: generalize)

        NOTE: the concentration changes in chemicals not involved in the specified reaction are ignored

        :param rxn_index:   Integer to identify the reaction of interest
        :param delta_arr:   Numpy array of numbers, with the concentrations changes of ALL the chemicals (whether involved
                                in the reaction or not), in their index order,
                                as a result of JUST the reaction of interest
                            TODO: maybe also accept a Panda's data frame row
        :param suppress_warning:
        :return:            True if the change in reactant/product concentrations is consistent with the
                                reaction's stoichiometry, or False otherwise
                                Note: if any of the elements of the passed Numpy array is NaN, then True is returned
                                      (because NaN values are indicative of aborted steps; can't invalidate the stoichiometry
                                      check because of that)
        
nameargumentsreturns
stoichiometry_checker_entire_runselfbool
        Verify that the stoichiometry is satisfied in all the reaction (sub)steps,
        using the diagnostic data from an earlier run

        IMPORTANT: this function is currently meant for simulations involving only 1 reaction (TODO: generalize)

        :return:    True if everything checks out, or False otherwise
        
nameargumentsreturns
_delta_namesself[str]
        Return a list of strings, with the names of ALL the registered chemicals,
        in their index order, each prefixed by the string "Delta "
        EXAMPLE: ["Delta A", "Delta B", "Delta X"]

        :return:    A list of strings
        
nameargumentsreturns
_delta_conc_dictself, delta_conc_arr: np.ndarraydict
        Convert a Numpy array into a dict, based on all the registered chemicals.
        The keys are the chemical names, prefixed by "Delta "

        :param delta_conc_arr:  A Numpy array of "delta concentrations".  EXAMPLE: array[1.23, 52.2]
        :return:                A dictionary such as {"Delta A": 1.23, "Delta X": 52.2}
        
nameargumentsreturns
set_diagnosticsself

nameargumentsreturns
unset_diagnosticsself

nameargumentsreturns
save_diagnostic_rxn_dataself, time_step, increment_vector_single_rxn: Union[np.array, None], rxn_index: int, caption=""None
        Save up diagnostic data for 1 reaction, for a simulation step
        (by convention, regardless of whether the step is completed or aborted)

        :param time_step:                   The duration of the current simulation step
        :param increment_vector_single_rxn: A Numpy array of size equal to the total number of chemical species,
                                                containing the "delta concentrations" for
                                                ALL the chemicals (whether involved in the reaction or not)
        :param rxn_index:                   An integer that indexes the reaction of interest (numbering starts at 0)
        :param caption:                     OPTIONAL string to describe the snapshot
        :return:                            None
        
nameargumentsreturns
comment_diagnostic_rxn_dataself, msg: strNone
        Set the comment field of the last record for EACH of the reaction-specific dataframe

        :param msg: Value to set the comment field to
        :return:    None
        
nameargumentsreturns
get_diagnostic_rxn_dataself, rxn_index: int, head=None, tail=None, t=None, print_reaction=Truepd.DataFrame
        Return a Pandas dataframe with the diagnostic run data of the requested SINGLE reaction,
        from the time that the diagnostics were activated by a call to set_diagnostics().

        In particular, the dataframe contains the "Delta" values for each of the chemicals
        involved in the reaction - i.e. the change in their concentrations
        over the time interval that *STARTS* at the value in the "TIME" column.
        (So, there'll be no row with the final current System Time)

        Note: entries are always added, even if an interval run is aborted, and automatically re-done.

        Optionally, print out a brief description of the reaction.

        Optionally, limit the dataframe to a specified numbers of rows at the end,
        or just return one entry corresponding to a specific time
        (the row with the CLOSEST time to the requested one, which will appear in an extra column
        called "search_value")

        :param rxn_index:       An integer that indexes the reaction of interest (numbering starts at 0)
                                TODO: if not specified, show all reactions in turn

        :param head:            (OPTIONAL) Number of records to return,
                                    from the start of the diagnostic dataframe.
        :param tail:            (OPTIONAL) Number of records to return,
                                    from the end of the diagnostic dataframe.
                                    If either the "head" arguments is passed, this argument will get ignored

        :param t:               (OPTIONAL) Individual time to pluck out from the dataframe;
                                    the row with closest time will be returned.
                                    If this parameter is specified, an extra column - called "search_value" -
                                    is inserted at the beginning of the dataframe
                                    If either the "head" or the "tail" arguments are passed, this argument will get ignored

        :param print_reaction:  (OPTIONAL) If True (default), concisely print out the requested reaction

        :return:                A Pandas data frame with (all or some of)
                                    the diagnostic data of the specified reaction.
                                    Columns of the dataframes:
                                    'START_TIME' 'Delta A' 'Delta B'... 'time_step' 'caption'
        
nameargumentsreturns
save_diagnostic_conc_dataself, system_dataNone
        To save the diagnostic concentration data during the run, indexed by the current System Time.
        Note: if an interval run is aborted, by convention NO entry is created here

        :return: None
        
nameargumentsreturns
get_diagnostic_conc_dataselfpd.DataFrame
        Return the diagnostic concentration data saved during the run.
        This will be a complete set of simulation steps,
        even if we only saved part of the history during the run

        Note: if an interval run is aborted, by convention NO entry is created here

        :return: A Pandas dataframe, with the columns:
                    'TIME' 	'A' 'B' ... 'caption'
                 where 'A', 'B', ... are all the chemicals
        
nameargumentsreturns
save_diagnostic_decisions_dataself, data, caption=""None
        Used to save the diagnostic concentration data during the run, indexed by the current System Time.
        Note: if an interval run is aborted, by convention an entry is STILL created here

        :return: None
        
nameargumentsreturns
get_diagnostic_decisions_dataselfpd.DataFrame
        Determine and return the diagnostic data about concentration changes at every step - EVEN aborted ones

        :return:    A Pandas dataframe with a "TIME" column, and columns for all the "Delta concentration" values
        
nameargumentsreturns
get_diagnostic_decisions_data_ALTselfpd.DataFrame
        Determine and return the diagnostic data about concentration changes at every step - EVEN aborted ones
        TODO: OBSOLETE - BEING PHASED OUT

        :return:    A Pandas dataframe with a "TIME" column, and columns for all the "Delta concentration" values
        
nameargumentsreturns
explain_reactionsselfbool
        Provide a detailed explanation of all the steps of the reactions,
        from the saved diagnostic data

        WARNING: Currently designed only for exactly 2 reactions!  TODO: generalize to any number of reactions

        TODO: test and validate usefulness, now that substeps got eliminated
        TODO: allow arguments to specify the min and max reaction times during which to display the explanatory data

        :return:    True if the diagnostic data is consistent for all the steps of all the reactions,
                    or False otherwise
        
nameargumentsreturns
_explain_reactions_helperself, active_list, row_baseline, row_listbool
        Helper function for explain_reactions()

        :param active_list:
        :param row_baseline:
        :param row_list:
        :return:            True is the diagnostic data is consistent for this step, or False otherwise
        
nameargumentsreturns
explain_time_advanceself, return_times=False, silent=False, use_history=FalseUnion[None, tuple]
        Use the saved-up diagnostic data, to print out details of the timescales of the reaction run

        If diagnostics weren't enabled ahead of calling this function, an Exception is raised

        EXAMPLE of output:
            From time 0 to 0.0304, in 17 FULL steps of 0.0008
            (for a grand total of 38 FULL steps)

        :param return_times:    If True, all the critical times (times where the interval steps change)
                                    are saved and returned as a list
        :param silent:          If True, nothing gets printed out
        :param use_history:     If True, use the system history in lieu of the diagnostic data;
                                    to keep in mind is the fact that the user might only have asked
                                    for PART of the history to be saved
        :return:                Depending on the argument return_times, either None, or a pair with 2 lists:
                                        1 - list of time values
                                        2 - list of step sizes  (will have one less element than the first list)
nameargumentsreturns
_explain_time_advance_helperself, t_start, t_end, delta_baseline, silent: boolUnion[int, float]
        Using the provided data, about a group of same-size steps, create and print a description of it for the user

        :param t_start:
        :param t_end:
        :param delta_baseline:
        :param silent:          If True, nothing gets printed; otherwise, a line is printed out
        :return:                The corresponding number of FULL steps taken
        

GRAPHICS

nameargumentsreturns
plot_curvesself, chemicals=None, colors=None, title=None, title_prefix=None, range_x=None, vertical_lines=None, show_intervals=False, suppress=FalseUnion[None, go.Figure]
        Using plotly, draw the plots of concentration values over time, based on the saved history data.
        TODO: allow alternate labels for x-axis

        EXAMPLE - to combine plots:

            import plotly.graph_objects as go
            fig0 = plot_curves(chemicals=["A", "B", "C"], suppress=True)
            fig1 = px.line(x=[2,2], y=[0,100], color_discrete_sequence = ['gray'])
            all_fig = go.Figure(data=fig0.data + fig1.data, layout = fig0.layout)    # Note that the + is concatenating lists
            all_fig.update_layout(title="My title")
            all_fig.show()

        :param chemicals:   (OPTIONAL) List of the names of the chemicals to plot;
                                if None, then display all
        :param colors:      (OPTIONAL) List of the colors names to use;
                                if None, then use the hardwired defaults
        :param title:       (OPTIONAL) Title for the plot;
                                if None, use default titles that will vary based on the # of reactions; EXAMPLES:
                                    "Changes in concentrations for 5 reactions"
                                    "Reaction `A <-> 2 B` .  Changes in concentrations with time"
                                    "Changes in concentration for `2 S <-> U` and `S <-> X`"
        :param title_prefix: (OPTIONAL) If present, it gets prefixed (followed by ".  ") to the title,
                                    whether the title is specified by the user or automatically generated
        :param range_x:         (OPTIONAL) list with of the form [t_start, t_end], to only show a part of the timeline
        :param vertical_lines:  (OPTIONAL) List or tuple or Numpy array or Pandas series
                                    of x-coordinates at which to draw thin vertical dotted gray lines
        :param show_intervals:  (OPTIONAL) If True, it over-rides any value in vertical_lines, and draws
                                    thin vertical dotted gray lines at all the x-coords of the data points in the saved history data;
                                    also, it adds a comment to the title
        :param suppress:    If True, nothing gets shown - and a plotly "Figure" object gets returned instead;
                                this is useful to combine multiple plots (see example above)

        :return:            None or a plotly "Figure" object, depending on the "suppress" flag
        
nameargumentsreturns
plot_step_sizesself, show_intervals=FalseNone
        Using plotly, draw the plot of the step sizes vs. time
        (only meaningful when the variable-step option was used).
        The same scale as plot_curves() will be used.
        This function requires the diagnostics option to be turned on, prior to running the simulation

        :param show_intervals:  If True, will add to the plot thin vertical dotted gray lines
                                    at the time steps
        :return:                None
        

HISTORY

nameargumentsreturns
add_snapshotself, species=None, caption="", time=None, system_data=NoneNone
        Preserve some or all the chemical concentrations into the history,
        linked to the passed time (by default the current System Time),
        with an optional caption.

        EXAMPLES:  add_snapshot()
                    add_snapshot(species=['A', 'B']), caption="Just prior to infusion")

        :param species:     (OPTIONAL) list of name of the chemical species whose concentrations we want to preserve for later use.
                                If not specified, save all
        :param caption:     (OPTIONAL) caption to attach to this preserved data
        :param time:        (OPTIONAL) time value to attach to the snapshot (default: current System Time)
        :param system_data: (OPTIONAL) a Numpy array of concentration values, in the same order as the
                                index of the chemical species; by default, use the SYSTEM DATA
                                (which is set and managed by various functions)
        :return:            None
        
nameargumentsreturns
get_historyself, t_start=None, t_end=None, head=None, tail=None, t=Nonepd.DataFrame
        Retrieve and return a Pandas dataframe with the system history that had been saved
        using add_snapshot()
        Optionally, restrict the result with a start and/or end times,
        or by limiting to a specified numbers of rows at the end

        :param t_start: (OPTIONAL) Start time in the "SYSTEM TIME" column
        :param t_end:   (OPTIONAL) End time
        :param head:    (OPTIONAL) Number of records to return,
                                   from the start of the diagnostic dataframe.
        :param tail:    (OPTIONAL) Number of records to consider, from the end of the dataframe
        :param t:       (OPTIONAL) Individual time to pluck out from the dataframe;
                                   the row with closest time will be returned.
                                   If this parameter is specified, an extra column - called "search_value" -
                                   is inserted at the beginning of the dataframe.
                                   If either the "head" or the "tail" arguments are passed, this argument will get ignored
        :return:        A Pandas dataframe
        
nameargumentsreturns
get_historical_concentrationsself, row: int, df=Nonenp.array
        Return a Numpy array with ALL the chemical concentrations (in their index order)
        from the specified row number of given Pandas data frame (by default, the system history)

        :param row: Integer with the zero-based row number of the system history (which is a Pandas data frame)
        :param df:  (OPTIONAL) A Pandas data frame with concentration information in columns that have
                        the names of the chemicals (if None, the system history is used)
        :return:    A Numpy array of floats.  EXAMPLE: array([200., 40.5])
        

RESULT ANALYSIS

nameargumentsreturns
is_in_equilibriumself, rxn_index=None, conc=None, tolerance=1, explain=TrueUnion[bool, dict]
        Ascertain whether the given concentrations are in equilibrium for the specified reactions
        (by default, for all reactions)
        TODO: optionally display last lines in diagnostic data

        :param rxn_index:   The index (0-based integer) to identify the reaction of interest;
                                if None, then check all the reactions
        :param conc:        Dict with the concentrations of the species involved in the reaction(s).
                            The keys are the chemical names
                                EXAMPLE: {'A': 23.9, 'B': 36.1}
                            If None, then use the current System concentrations
        :param tolerance:   Allowable relative tolerance, as a PERCENTAGE, to establish satisfactory equality
        :param explain:     If True, print out details about the analysis,
                                incl. the formula(s) being used to check the equilibrium
                                EXAMPLES:   "([C][D]) / ([A][B])"
                                            "[B] / [A]^2"

        :return:            Return True if ALL the reactions are close enough to an equilibrium,
                                as allowed by the requested tolerance;
                                otherwise, return a dict of the form {False: [list of reaction index]}
                                for all the reactions that failed the criterion
                                (EXAMPLE:  {False:  [3, 6:]})
        
nameargumentsreturns
reaction_in_equilibriumself, rxn_index, conc, tolerance, explain: boolbool
        Ascertain whether the given concentrations are in equilibrium for the specified SINGLE reaction;
        return True or False, accordingly.

        :param rxn_index:   The index (0-based integer) to identify the reaction of interest
        :param conc:        Dict with the concentrations of the species involved in the reaction.
                            The keys are the chemical names
                                EXAMPLE: {'A': 23.9, 'B': 36.1}
        :param tolerance:   Allowable relative tolerance, as a PERCENTAGE, to establish satisfactory equality
        :param explain:     If True, print out the formula being used, as well as some other info.
                                EXAMPLES of formulas:   "([C][D]) / ([A][B])"
                                                        "[B] / [A]^2"
        :return:            True if the given reaction is close enough to an equilibrium,
                            as allowed by the requested tolerance
        
nameargumentsreturns
extract_delta_concentrationsself, df, row_from: int, row_to: int, chem_list: [str]np.array
        Extract the concentration changes of the specified chemical species from a Pandas dataframe
        of concentration values

        EXAMPLE:  extract_delta_concentrations(my_dataframe, 7, 8, ['A', 'B'])

        :param df:
        :param row_from:
        :param row_to:
        :param chem_list:
        :return:            A Numpy array of floats
        
nameargumentsreturns
curve_intersectionself, chem1, chem2, t_start, t_end(float, float)
        Find and return the intersection of the 2 curves in the columns var1 and var2,
        in the time interval [t_start, t_end]
        If there's more than one intersection, only one - in an unpredictable choice - is returned
        TODO: the current implementation fails in cases where the 2 curves stay within some distance of each other,
              and then one curve jumps on the opposite side of the other curve, at at BIGGER distance.
              See the missed intersection at the end of experiment "reactions_single_compartment/up_regulate_1"

        :param chem1:   The name of the 1st chemical of interest
        :param chem2:   The name of the 2nd chemical of interest
        :param t_start: The start of the time interval being considered
        :param t_end:   The end of the time interval being considered
        :return:        The pair (time of intersection, common value)