EMAT30008: Scientific Computing

Code testing

Remember: Commit Often, Perfect Later, Publish Once and make (lots of) notes in your code about the decisions you are making!

You should be committing your code to the Git repository after completing each question as a minimum. You should push your code to the cloud server (GitHub) at the end of each lab and/or week[1].

This is an exercise in code testing[2]. When writing numerical code, it invariably will not do what you intend it to do. As such, it is important to write tests to ensure that your code behaves correctly.

In their most basic form, a test will simply check that a function produces the correct output for a given input. For example,

def myfunc(x):
    return sin(x)+2

result = myfunc(0.5)
if abs(result - 2.4794255) < 1e-6:  # value computed with a calculator
    print("Test passed")
else:
    print("Test failed")

Well written tests will test all the lines of code that you write. While it is (generally) impossible to test all possible inputs to your code, you should aim to cover a wide variety of situations.

For your numerical shooting code, there are many tests possible; simply test your code with ODEs that have known analytical solutions. For example, you can use the ODE for the Hopf bifurcation normal form (you don't need to know what this is).

du1dt=βu1u2+σu1(u12+u22),du2dt=u1+βu2+σu2(u12+u22),\begin{aligned} \frac{\mathrm{d} u_1}{\mathrm{d} t} &= \beta u_1 - u_2 + \sigma u_1\left(u_1^2 + u_2^2\right),\\ \frac{\mathrm{d} u_2}{\mathrm{d} t} &= u_1 + \beta u_2 + \sigma u_2\left(u_1^2 + u_2^2\right), \end{aligned}

which for σ=1\sigma=-1 (a supercritical Hopf bifurcation) has an explicit solution

u1(t)=βcos(t+θ),u2(t)=βsin(t+θ),\begin{aligned} u_1(t) &= \sqrt{\beta}\cos(t+\theta),\\ u_2(t) &= \sqrt{\beta}\sin(t+\theta), \end{aligned}

where θ\theta is the phase[3].

[1] You are being marked on your use of Git; I expect regular commits in the Git history. I will become increasingly strict on this as the weeks go on.
[2] Code testing occurs in many different forms, ranging from unit tests through to full-blown test-driven development.
[3] Since there is no explicit time dependency in (1), the phase is arbitrary. In a simulation, it will be determined by the initial conditions. Hence the need for a phase condition when using numerical shooting.

Steps

  1. Adapt your shooting code from last week to work with general ODEs (such as (1)). Define and document[4] an appropriate API[5] for interacting with your shooting code.

    An (incomplete[6]) example interface with documentation[7] might be

    def shooting(ode, u0):
        """
        A function that uses numerical shooting to find limit cycles of
        a specified ODE.
    
        Parameters
        ----------
        ode : function
            The ODE to apply shooting to. The ode function should take
            a single parameter (the state vector) and return the
            right-hand side of the ODE as a numpy.array.
        u0 : numpy.array
            An initial guess at the initial values for the limit cycle.
    
        Returns
        -------
        Returns a numpy.array containing the corrected initial values
        for the limit cycle. If the numerical root finder failed, the
        returned array is empty.
        """
        # Here is the code that does the shooting
  2. Write a test script[8] that runs your shooting code on (1) and checks it against the explicit solution (2)[9].

  3. As part of the same test script, test your shooting code against other examples where analytical solutions are known. Vary the number of dimensions. For example, try

    du1dt=βu1u2+σu1(u12+u22),du2dt=u1+βu2+σu2(u12+u22),du3dt=u3.\begin{aligned} \frac{\mathrm{d} u_1}{\mathrm{d} t} &= \beta u_1 - u_2 + \sigma u_1\left(u_1^2 + u_2^2\right),\\ \frac{\mathrm{d} u_2}{\mathrm{d} t} &= u_1 + \beta u_2 + \sigma u_2\left(u_1^2 + u_2^2\right),\\ \frac{\mathrm{d} u_3}{\mathrm{d} t} &= -u_3. \end{aligned}
  4. Add tests to check that your code handles errors gracefully[10]. Consider errors such as

    • providing initial values where the dimensions don't match those of the ODE, or

    • providing inputs such that the numerical root finder does not converge.

[4] A good introduction to documenting your code using docstrings can be found on the Real Python website.
[5] Application Programming Interface; see for example the API of solve\_ivp is defined and documented in the SciPy documentation.
[6] Other parameters to consider: the period of oscillation, parameters for the ODE, a phase condition, options for the numerical integrator, options for the numerical root finder, ...
[7] It's also good to include an Example section in the docstring showing how to run the code with a particular example.
[8] A test script is typically a separate Python file that loads in your code as a module. It then runs a sequence of examples, checking them against known results and indicating whether the tests have passed or failed.
[9] You will have to account for numerical accuracy — your shooting solution will not exactly match the explicit solution. See the isclose and allclose functions in Python (Numpy) or isapprox function in Julia.
[10] That is, it doesn't crash and, instead, provides a nice error message or appropriate return value (see the example docstring above).