Contributing Guide

This guide provides instructions for submitting and formatting new code in OTP.

Submitting Changes

Changes to OTP should be proposed as a pull request and undergo a review process before being merged. New code must be free of warnings and errors and adhere to the style guidelines.

Creating a Problem

Each problem defines a package under +otp that contains all files used by the problem. When creating a new problem we recommend duplicating an existing problem package, e.g., Lorenz63 or Brusselator, then renaming and editing the contents as needed.

To add a new test problem from scratch follow these steps:

  1. Check out the latest version of OTP:

git clone https://github.com/ComputationalScienceLaboratory/ODE-Test-Problems.git
cd ODE-Test-Problems/
  1. Create a new folder in the toolbox/+opt/ directory with a name that starts with + to indicate it is a package. Also create a subfolder named +presets in the new problem folder:

mkdir toolbox/+opt/+example
cd toolbox/+opt/+example
mkdir +presets
  1. The minimal set of files needed inside the problem folder to set up a new example problem are:

  • The right-hand side (RHS) function named as f.m

  • The problem class to specify properties, override plotting and solver options, and convert parameters into arguments for RHS functions

  • The parameters class

  • A Canonical.m preset inside the +presets subfolder to set standard initial condition and parameters.

touch f.m ExampleProblem.m ExampleParameters.m +presets/Canonical.m

The RHS Function

The RHS function f.m, which is the time-derivative of the state y, is defined as a function with at least two arguments. If parameters are needed, they can be added as arguments.

function dy = f(t, y, param1, ...)
  dy = ...
end

Other functions associated with an otp.RHS like the Jacobian and mass matrix should be implemented in separate .m files with the same function signature as f.m.

The Problem Class

A problem package must contain a class named <Name>Problem.m that is a subclass of otp.Problem. There are two methods that must be implemented: the constructor and onSettingsChanged. Optionally, one can override functions such as internalPlot and internalSolve to provide problem-specific defaults. Partitioned problems can add custom RHS functions as class properties with private write access. The property name should start with RHS, e.g., RHSStiff.

A basic template for a new class of problems called Example looks like

classdef ExampleProblem < otp.Problem
    methods
        function obj = ExampleProblem(timeSpan, y0, parameters)
            obj@otp.Problem('Example', [], timeSpan, y0, parameters);
            % The second argument specifies the number of variables in the problem is arbitrary
        end
    end

    methods (Access=protected)
        function onSettingsChanged(obj)
            % Parameters are stored in the obj.Parameters structure. We can assign them to individual variables to be
            % used in function calls
            param1 = obj.Parameters.Param1;

            % set up the RHS function wrapper
            obj.RHS = otp.RHS(@(t, y) otp.example.f(t, y, param1));
        end

        % set up internal plot function
        function fig = internalPlot(obj, t, y, varargin)
            fig = internalPlot@otp.Problem(obj, t, y, 'xscale', 'log', 'yscale', 'log', varargin{:});
        end

        % set up internal movie function
        function mov = internalMovie(obj, t, y, varargin)
            mov = internalMovie@otp.Problem(obj, t, y,, 'xscale', 'log', 'yscale', 'log', varargin{:});
        end

        % set up internal solver
        function sol = internalSolve(obj, varargin)
            % Set tolerances due to the very small scales
            sol = internalSolve@otp.Problem(obj, 'AbsTol', 1e-50, varargin{:});
        end
    end
end

The Parameters Class

A problem package must also contain a class named <Name>Parameters.m that is a subclass of otp.Parameters. It needs to provide public properties for each of the problem parameters and a constructor which forwards arguments to the superclass constructor; no methods are needed. Note that property validation is currently not supported in Octave. Therefore, we use a custom comment syntax that is parsed by the installer to optionally include validation. The following is an example of a parameter class with property validation:

classdef ExampleParameters
    properties
        Param1 %MATLAB ONLY: (1,1) {mustBeReal, mustBeNonnegative}
    end

    methods
        function obj = ExampleParameters(varargin)
            obj = obj@otp.Parameters(varargin{:});
        end
    end
end

Adding presets

Within a problem package, there should be a subpackage named +presets. This contains subclasses of <Name>Problem that specify the time span, initial conditions, and parameters. Typically, only the constructor needs to be implemented in a preset class.

In our example, we add a Canonical.m preset inside the +presets subfolder.

classdef Canonical < otp.example.ExampleProblem

    methods
        function obj = Canonical(varargin)
            y0 = ...
            tspan = ...

            % Specify a default value for Param1 which can be overridden by a name-value pair passed to this constructor
            params = otp.example.ExampleParameters('Param1', pi, varargin{:});

            obj = obj@otp.example.ExampleProblem(tspan, y0, params);
        end
    end
end

Style Guidelines

In order for this project to maintain a consistent coding style, the following conventions should be used. These standards largely match those most commonly used in MATLAB’s code and documentation.

Line Formatting

Four spaces are used for indentation. A line should be kept to 120 characters or less.

Variables

Variable names should be written in camel case.

% Examples
data = 4;
maxEigenvalue = eigs(rand(4), 1);
fun = @(t, y) y + sin(t);

Functions

Functions should be completely alphanumeric and written in camel case. No special character is used to distinguish between words.

% Example
function r = depthFirstSearch(tree)
    ...
end

Structures

Structures should have camel case property names.

% Example
car = struct('make', 'Ford', 'modelYear', 2020);

Packages

Package names should be completely lowercase and start with a plus symbol. No capitalization or special character is used to distinguish between words.

% Example
% Path: +otp/+utils/PhysicalConstants.m
help otp.utils.PhysicalConstants

Classes

Class names and properties should be written in Pascal case. When the name contains an acronym, all letters should be capitalized. Methods should be written in camel case.

% Examples
classdef Employee
    properties
        FirstName
        LastName
        Salary
    end

    methods
        function p = calculatePay(hours)
            ...
        end
    end
end

classdef ODETestProblems
    ...
end