Skip to content

Creating an Experiment

There are two ways in which we talk about kinematics results: paths and stability envelopes. Path describes exactly how something moved. Envelope describes the difference between paths. We will focus on the code behind Envelope, but most of methods are also implemented for Path in such a way that usage for both is similar.

Most commonly we are interested in the comparison between what happens when we apply no forces (neutral) and pulling vs pushing, or rotating one way vs the other way. From these comparisons, we perform statistical analyses and produce graphs.

Visual representation of difference between path and envelope

Let's start by modelling these ideas. Given an experiment, we want to be able to create a stability envelope and a path.

% Experiment.m
classdef Experiment
    methods
        % ...
        function envelope = create_stability_envelope(self, directions, other_arguments)
            % Some internal processing
            is_neutral = % ...
            neutral = self.Data(is_neutral);
            envelope = Envelope(self, directions, neutral)
        end
        function path = create_path(self)
            % Do any internal processing
            path = Path(self)
        end
    end
end
What goes in Experiment.create_stability_envelope() vs in Envelope class

Generally the constructor for an object should require the information ready for it to use, without having to know anything about internal details from other objects.

For example, if you want to create a cube from information provided by a square, ideally you don't have

my_square = Square(2);
my_cube = Cube(square)
but instead you extract the information out of square that is relevant for Cube.
my_cube = Cube(square.side)
The specific logic needed to create a Cube is entirely contained within that class, without knowing anything about Square.

That being said, sometimes there's a lot of data that you need to pass, and it makes sense to use the structured object. Imagine, for example, passing a long list of configurations. This is the case with the example, which is why we pass self, which contains all the data in a particular structure.

Convenience

In some cases there is enough complexity in creating objects that you might want some convenience functions. For example, an anterior-posterior (AP) envelope is extremely common, so providing the function that fills those arguments is helpful.

% Experiment.m
classdef Experiment
    methods
    % ...
        function create_ap_envelope(self)
            % Generate other arguments from default
            % other_arguments = ...
            self.create_stability_envelope(["ant", "pos"], other_arguments)
        end
    end
end

What this means is that if I'm using the typical usage of the code with the normal names for the data files, it becomes easier to read and more convenient. Usage goes from:

directions = ["ant", "pos"];
other_arguments = ...
my_experiment.create_stability_envelope(directions, other_arguments)

to

my_experiment.create_ap_envelope();

Creating an Envelope

The key is to think about how your data type is modelling the real-world concept.

An Envelope describes motion in two opposite directions (push--pull, anterior--posterior), so a logical choice would be to make Directions a property of the class. The data are also stored as a property, together with states, specimen names, etc.

% Envelope.m
classdef Envelope
    properties
        Data
        Directions
        States
        %...
    end
    methods
        function self = Envelope(trajectory, directions, other_arguments)
            self.Directions = directions;
            % Some internal processing to determine specimens, which data refers to neutral, etc
            % specimens = ...
            % neutral = ...
            self.Data = subtract_neutral(specimens, neutral); % Heavy logic creating envelopes
            % ...
        end
    end
end

Changing data types

It's really important to notice that we are intentionally changing data types. This allows us to clearly know when we're talking about experiments, paths or envelopes. How classes change with functions calls

It also helps to have code that looks really similar, but have very distinct implementations. Both lines of code produce graphs, but one produces Envelope graphs and the other, Plot graphs.

% (1)                  (2)                 (3)
my_experiment.create_stability_envelope().plot();

% (4)             (5)        (6)
my_experiment.create_path().plot();
  1. Experiment
  2. Envelope
  3. Envelope implementation of plot()
  4. Experiment
  5. Path
  6. Path implementation of plot()
An example using dogs instead

Imagine that a Dog might have a name, when it last visited the vet and a list of documents tied to their health records.

classdef Dog
    properties
        name
        last_vet_visit
        documents
    end
    methods
        function document = get_documents(self)
            document = self.documents
        end
        % etc
    end
end

We need enough justification for a document to be an actual class. Maybe both Dogs and Cats have the same types of documents tied to them, and we want to tie functions to documents, so we create a class to represent this generic Document

classdef Document
    properties
        kind
        date_issued
        issuing_organ
        contents
    end
end

The important thing to notice is that calling get_documents() gives you a list of Document related to a specific Dog. In a similar way that when we call create_ap_envelope() or create_stability_envelope() on an Experiment, we produce an Envelope.

Statistics

We can add more detail to the workflow and focus only on data processing and output. Overview of statistical analysis

We gather data about how body parts move over time, which lends itself to the use of statistical parametric mapping (SPM). Our usual routine allows us to use a simpler, one-dimensional implementation of it, which define their own versions of ANOVA and post hoc analysis.

We apply the same principles discussed previously, namely changing data types in order to represent a different state of the data (1). Envelope has a function calculate_spm() that applies SPM-based analysis to the data, instantiating the SPM class. Performing the ANOVA and inference are all done within the constructor for SPM. Most importantly, Envelope doesn't know any details about implementation of statistics -- it only knows how to call the statistics package.

  1. Envelope and Path are tables with the same fields, but they represent two different ways the data has been processed; two different states of the data.
classdef Envelope
    methods
    % ...
        function stats = calculate_spm(self)
            data = self.Data;
            specimens = self.Specimens;
            directions = self.Directions;
            stats = SPM(data, specimens, directions);
        end
    end
end
classdef SPM
    properties
        Anova
        Inference
    end
    methods
        function self = SPM(data, specimens, directions)
            % Format the data as required by spm1d
            % Loop over directions, etc ...

            self.Anova.(direction) = spm1d.stats.anova1(data_formatted, states)
            self.Inference.(direction) = anova.inference(0.05);
        end
    end
end