<jupyter_start><jupyter_text>Verifying Kinetics Models: Part 1 - Software InfrastructureTesting provides a way to find defects in products such as cars televisions, food, and software.There are two broad objectives for testing. **Validation** determines if the product provides a useful purpose. In terms of kinetics models this means that the model is accurate, and is useful for its intended objectives. For example, to a model for predicting ICU usage for COVID-19 patients might involve: (a) checking its predictions vs. observed future data and (b) estimating the mortality, morbidity, and cost implications resulting from inaccuracies of the model. In general, validation is context and discipline specific.**Verification** is about determining if the product performs according to its specification. For kinetics models, this means that the model dynamics of the model are consistent with what is intended (even if these are not the *correct* dynamics). Validation software engineering typically takes the form of unittests, codes that detect errors in the functioning of software components.This tutorial focuses on verification of kinetics models, ensuring that the intended dynamics are produced by the model. This tutorial is divided into two parts. The first part describes the software setup for doing verification of kinetics models in Jupyter Notebooks. The second part describes an approach to writing kinetics tests using this software setup.<jupyter_code>import numpy as np
import tellurium as te
from teUtils.named_timeseries import NamedTimeseries, TIME
from teUtils.timeseries_plotter import TimeseriesPlotter, PlotOptions<jupyter_output><empty_output><jupyter_text>Lecture  Motivation and Background Testing is the process by which you exercise your code to determine if it performs as expected. The code you are testing is referred to as the code under test.There are two parts to writing tests.1. invoking the code under test so that it is exercised in a particular way;1. evaluating the results of executing code under test to determine if it behaved as expected.The collection of tests performed are referred to as the test cases.  Testing in a Jupyter Notebook<jupyter_code>simple_model = '''
    model example1
      S1 -> S2; k1*S1
      S1 = 10
      S2 = 0
      k1 = 0.1
    end
    '''
# Simulation runner
def runSimulation(model):
    """
    Runs a simulation for an antimony model
    
    Parameters
    ----------
    model: str
    
    Returns
    -------
    NamedTimeseries
    """
    rr = te.loada(model)
    data = rr.simulate()
    return NamedTimeseries(named_array=data)
# Tests return True if passed and False if fail.
def test1(ts):
    return len(ts) > 0

def test2(ts):
    s1 = ts["S1"]
    return s1[0] == 10
# Test runner
def runTests(model):
    ts = runSimulation(model)
    is_ok = True
    for test in [test1, test2]:
        is_ok = is_ok and test(ts)
    if is_ok:
        print("OK.")
    else:
        print("Problems encountered in model.")
runTests(simple_model)<jupyter_output>OK.<jupyter_text>Breakout In the following, you will create tests for a new model called ``model``.<jupyter_code>new_model = '''
        # Reactions   
        J1: S1 -> S2; k1*S1
        J2: S2 -> S3; k2*S2
        J3: S3 -> S4; k3*S3
        J4: S4 -> S5; k4*S4
        J5: S5 -> S6; k5*S5;
        # Species initializations     
        k1 = 1; k2 = 2; k3 = 3; k4 = 4; k5 = 5;
        S1 = 10; S2 = 0; S3 = 0; S4 = 0; S5 = 0; S6 = 0;
        '''<jupyter_output><empty_output><jupyter_text>Run the existing tests for this model. Did the model pass?<jupyter_code>runTests(new_model)<jupyter_output>OK.<jupyter_text>Improve the tests by including print statements that describe the error.<jupyter_code># Tests
def test1(ts):
    if len(ts) > 0:
        return True
    else:
        print("No data produced by simulation.")
        return False
    
def test2(ts):
    s1 = ts["S1"]
    if s1[0] == 10:
        return True
    else:
        print("Initial value of S1 should be 10!")
        return False
runTests(new_model)<jupyter_output>OK.<jupyter_text>Fix the error in the model and re-run the tests.<jupyter_code># Fix error in model<jupyter_output><empty_output><jupyter_text>Write additional tests.1. The maxium value of S1 is at 0.1. The maximum value of S6 is its last value.<jupyter_code>def test3(ts):
    """
    Check that the maximum value of S1 is at time 0.
    """
    s1 = ts["S1"]
    if s1[0] == max(s1):
        return True
    else:
        print("The initial value of S1 should be 0!")
        return False
    
def test4(ts):
    """
    Check that the maximum value of s6 is at the end of the simulation.
    """
    s6 = ts["S6"]
    if s6[-1] == max(s6):
        return True
    else:
        print("The maximum value of S6 should be at the last time!")
        return False
def newRunTests(model):
    ts = runSimulation(model)
    is_ok = True
    for test in [test1, test2, test3, test4]:
        is_ok = is_ok and test(ts)
    if is_ok:
        print("OK.")
    else:
        print("Problems encountered in model.")
newRunTests(new_model)<jupyter_output>OK.<jupyter_text>AdvancedA problem with the above infrastructre is that we need to revise the test runner every time we add or remove a test. Having to make coordinated changes in software is extremely bad practice and is a common cause of software errors.Another solution is to have a "test container". In python, this is done by creating a python class in which tests reside. We won't provide details about python classes here. Rather, you can just use the code below. The main changes are the ``__init__`` function at the top, the arguments to the test, and the use of ``self.ts`` to access the timeseries. The ``run`` function runs all functions in ``TestContainer`` that begin with ``test``.<jupyter_code>class TestContainer(object):
    
    def __init__(self, model):
        ts = runSimulation(model)
        self.ts = ts
    
    def test1(self):
        if len(self.ts) > 0:
            return True
        else:
            print("No data produced by simulation.")
            return False

    def test2(self):
        s1 = self.ts["S1"]
        if s1[0] == 10:
            return True
        else:
            print("Initial value of S1 should be 10!")
            return False
        
    def run(self):
        is_ok = True
        for item in dir(self):
            if item[0:4] == "test":
                # Construct the function call
                func = "self.%s()" % item
                is_ok = is_ok and eval(func)
        if is_ok:
            print("OK.")
        else:
            print("Problems encountered in model.")<jupyter_output><empty_output><jupyter_text>To run the tests, you do the follow:<jupyter_code>tester = TestContainer(simple_model)
tester.run()<jupyter_output>OK.
{'fraction_non_alphanumeric': 0.08114035087719298, 'fraction_numerical': 0.019005847953216373, 'mean_word_length': 2.958912037037037, 'pattern_counts': {'":': 1, '<': 32, '<?xml version=': 0, '>': 41, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '431411', 'license': 'MIT', 'n_tokens_mistral': 2059, 'n_tokens_neox': 1966, 'n_words': 828, 'path': 'notebooks/Verifying Kinetics Models - Part 1 - Software Infrastructure-With Exercise Answers.ipynb', 'repo_name': 'ModelEngineering/km_unit', 'size': '6840'}