modules/post/automotive_assessments.mjs

import { Measurement } from "../shared/measurement.mjs";
import { OccupantBodyPart, OccupantEntity } from "../shared/occupant.mjs";
import { WorkflowOccupant } from "../shared/workflow_occupant.mjs";

import { THisHelper } from "./this.mjs";
import { WorkflowUnits } from "../../../modules/units.mjs";
import { Protocol, Protocols } from "../shared/protocols.mjs";
import { AssessmentType } from "../shared/assessment_types.mjs";
import { AssessmentDatums } from "./assessment_datums.mjs";
import { Structure } from "../shared/structure.mjs";
import { OccupantSupplier, OccupantProduct, OccupantPhysiology } from "../shared/occupant.mjs";
import { BaseEntity } from "../shared/base.mjs";

export {
    ProtocolAssessment,
    DoOccupantAssessmentOccupantData,
    DoStructureAssessmentStructureData,
    DoAssessmentOptions
};

/**
 * Class to do assessments for a protocol<br><br>
 * It extends the Protocol class and adds the assessment functions<br><br>
 * It is separated like this because it uses T/HIS JS-API functions
 * that if they were in the Protocol class would stop it from
 * working in PRIMER.
 * @extends Protocol
 * @param {string} regulation Regulation, e.g. Regulation.CNCAP
 * @param {string} crash_test Crash test, e.g. CrashTest.ODB
 * @param {string} version Version, e.g. "7.1.2"
 * @param {AssessmentDatums[]} assessment_datums Array of AssessmentDatums instances
 * @example
 * let protocol = new ProtocolAssessment(Regulation.CNCAP, CrashTest.ODB, "7.1.2", []);
 */
class ProtocolAssessment extends Protocol {
    constructor(regulation, crash_test, version, vehicle, assessment_datums) {
        super(regulation, crash_test, version, vehicle, assessment_datums);
    }

    /**
     * Create a ProtocolAssessment instance with the default AssessmentDatums for
     * the given regulation, test and year
     * @param {string} regulation Protocol regulation
     * @param {string} crash_test Protocol test
     * @param {string} version Version, e.g. "7.1.2"
     * @returns {ProtocolAssessment}
     * @example
     * let p = ProtocolAssessment.CreateDefaultProtocol(Regulation.CNCAP, CrashTest.ODB, "7.1.2");
     */
    static CreateDefaultProtocol(regulation, crash_test, version) {
        let vehicle = Protocols.GetProtocolVehicle(regulation, crash_test, version);
        let assessment_datums = super.CreateDefaultAssessmentDatums(regulation, crash_test, version);

        return new ProtocolAssessment(regulation, crash_test, version, vehicle, assessment_datums);
    }

    /* Instance methods */

    /**
     * Do calculations on an OccupantBodyPart
     * @param {DoOccupantAssessmentOccupantData[]} occupants_data Array of DoOccupantAssessmentOccupantData instances
     * @param {string[]} assessment_types Array of assessment types, e.g. AssessmentType.NECK_AXIAL
     * @param {DoAssessmentOptions} [options=null] Options
     * @returns {DoAssessmentResults}
     * @example
     * let p = new ProtocolAssessment(Regulation.CNCAP, CrashTest.ODB, "7.1.2", []);
     * let m = Model.GetFromID(1);
     * let o = new WorkflowOccupant(WorkflowOccupant.HUMANETICS_HIII_50M_V1_5,
     *                              WorkflowOccupant.DRIVER,
     *                              WorkflowOccupant.LEFT,
     *                              WorkflowOccupant.FRONT,
     *                              []);
     * let occupants_data = [new DoOccupantAssessmentOccupantData(o, m, Workflow.UNIT_SYSTEM_U2)];
     * let assessment_types = [AssessmentType.NECK_AXIAL, AssessmentType.NECK_SHEAR];
     * let options = new DoAssessmentOptions("separate_pages");
     *
     * let assessment = p.DoOccupantAssessment(occupants_data,
     *                                         assessment_types,
     *                                         options);
     */
    DoOccupantAssessment(occupants_data, assessment_types, options = null) {
        if (!(occupants_data instanceof Array)) {
            throw new Error(`<occupants_data> must be an array in <ProtocolAssessment.DoOccupantAssessment>`);
        }

        for (let occupant_data of occupants_data) {
            if (!(occupant_data instanceof DoOccupantAssessmentOccupantData)) {
                throw new Error(
                    "Not all of the items in the <occupants_data> array are DoOccupantAssessmentOccupantData instances in <ProtocolAssessment.DoOccupantAssessment>"
                );
            }
        }

        /* Object to return values from assessments */

        let results = new DoAssessmentResults();

        /* Check that there are models to extract data from */

        for (let occupant_data of occupants_data) {
            let model = Model.GetFromID(occupant_data.model_id);

            if (model == null) {
                ErrorMessage(
                    `Model ${occupant_data.model_id} for ${occupant_data.occupant} not found in <ProtocolAssessment.DoOccupantAssessment>`
                );
                return results;
            }
        }

        /* Blank all the curves and datums */

        if (options.blank_all) {
            THisHelper.BlankAllCurves();
            THisHelper.BlankAllDatums();
        }

        /* Get the first graph to plot on */

        let graph_id = options.first_graph_id;
        let graph_ids = [];

        /* Multiple assessments are done on the head acceleration curve (HIC, 3ms, peak)
         * Initialise an array to flag whether it has been read in for each occupant */

        let read_head_curve_acceleration = [];

        for (let i = 0; i < occupants_data.length; i++) {
            read_head_curve_acceleration[i] = false;
        }

        /* Do each assessment for this protocol and occupant */

        for (let assessment_type of assessment_types) {
            /* Create new graph if required */
            if (graph_id > Graph.Total()) {
                if (graph_id >= THisHelper.MAX_GRAPHS) {
                    WarningMessage(
                        `Unable to do any more assessments, the graph limit of ${THisHelper.MAX_GRAPHS} has been reached`
                    );
                    break;
                }

                new Graph();
            }

            /* REPORTER uses same graph over and over so blank curves and datums */
            if (options.graph_layout == DoAssessmentOptions.GRAPH_LAYOUT_REPORTER) {
                THisHelper.BlankAllCurves();
                THisHelper.BlankAllDatums();
            }

            /* Flag to say whether to add a graph for this assessment
             * For most assessments this is true, but for the head acceleration
             * where one curve is used for multiple assessments we only want to
             * add it once */

            let add_graph = true;

            /* Get the datums */
            let assessment_datums = this.GetDatumsByAssessment(assessment_type);

            /** @type {ReadBodyPartOutput} */
            let output = null;

            let occupant_index = -1;
            let occupants_str = "";

            for (let occupant_data of occupants_data) {
                ++occupant_index;

                let occupant = occupant_data.occupant;
                let model = Model.GetFromID(occupant_data.model_id);
                let unit_system = occupant_data.unit_system;

                /* Concatenated list of occupants used for REPORTER image filenames */

                if (occupant_index > 0) occupants_str += `_`;
                occupants_str += `${occupant}`;

                /* Only do the assessment if it's applicable to this occupant */

                let occupant_assessment_types = this.UniqueOccupantAssessmentTypes(
                    [occupant],
                    OccupantBodyPart.Types()
                );

                if (occupant_assessment_types.indexOf(assessment_type) == -1) continue;

                /* Get options for head acceleration assessments so they can all be done on one curve. */

                let head_acceleration_options = {};

                for (let assessment_type of assessment_types) {
                    if (assessment_type == AssessmentType.HEAD_HIC) {
                        head_acceleration_options.hic = true;
                        head_acceleration_options.hic_window =
                            this.HICWindow(occupant) * WorkflowUnits.TimeToSecondsFactor(unit_system);
                    } else if (assessment_type == AssessmentType.HEAD_THREE_MS_EXCEEDENCE) {
                        head_acceleration_options.three_ms = true;
                    } else if (assessment_type == AssessmentType.HEAD_PEAK_ACCELERATION) {
                        head_acceleration_options.peak_acceleration = true;
                    }
                }

                /* HEAD */
                if (
                    assessment_type == AssessmentType.HEAD_HIC ||
                    assessment_type == AssessmentType.HEAD_THREE_MS_EXCEEDENCE ||
                    assessment_type == AssessmentType.HEAD_PEAK_ACCELERATION
                ) {
                    if (!read_head_curve_acceleration[occupant_index]) {
                        /* Read in the head acceleration */

                        output = this.ReadHeadAcceleration(
                            model,
                            occupant,
                            unit_system,
                            occupant_index,
                            head_acceleration_options
                        );

                        /* Flag it so we don't read in the acceleration curve for this occupant again */

                        read_head_curve_acceleration[occupant_index] = true;
                    } else {
                        /* No need to add a graph again - the assessment has already been carried out */

                        add_graph = false;
                    }

                    /* NECK */
                } else if (
                    assessment_type == AssessmentType.NECK_EXTENSION ||
                    assessment_type == AssessmentType.NECK_EXTENSION_PASSENGER ||
                    assessment_type == AssessmentType.NECK_EXTENSION_REAR_PASSENGER
                ) {
                    output = this.ReadNeckBending(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_SHEAR_EXCEEDENCE) {
                    output = this.ReadNeckShearExceedence(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_TENSION_EXCEEDENCE) {
                    output = this.ReadNeckTensionExceedence(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_COMPRESSION_EXCEEDENCE) {
                    output = this.ReadNeckCompressionExceedence(model, occupant, unit_system, occupant_index);
                } else if (
                    assessment_type == AssessmentType.NECK_SHEAR ||
                    assessment_type == AssessmentType.NECK_SHEAR_PASSENGER
                ) {
                    output = this.ReadNeckShear(model, occupant, unit_system, occupant_index);
                } else if (
                    assessment_type == AssessmentType.NECK_AXIAL ||
                    assessment_type == AssessmentType.NECK_AXIAL_PASSENGER
                ) {
                    output = this.ReadNeckAxial(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_NIJ) {
                    output = this.ReadNeckNIJ(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_UPPER_AXIAL) {
                    output = this.ReadNeckUpperAxial(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_LOWER_AXIAL) {
                    output = this.ReadNeckLowerAxial(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_UPPER_FLEXION) {
                    output = this.ReadNeckUpperFlexion(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_LOWER_FLEXION) {
                    output = this.ReadNeckLowerFlexion(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_UPPER_EXTENSION) {
                    output = this.ReadNeckUpperExtension(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.NECK_LOWER_EXTENSION) {
                    output = this.ReadNeckLowerExtension(model, occupant, unit_system, occupant_index);

                    /* CHEST */
                } else if (
                    assessment_type == AssessmentType.CHEST_COMPRESSION ||
                    assessment_type == AssessmentType.CHEST_COMPRESSION_PASSENGER ||
                    assessment_type == AssessmentType.CHEST_COMPRESSION_REAR_PASSENGER
                ) {
                    output = this.ReadChestCompression(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.CHEST_COMPRESSION_LEFT) {
                    output = this.ReadChestCompression(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.CHEST_COMPRESSION_RIGHT) {
                    output = this.ReadChestCompression(model, occupant, unit_system, occupant_index, "Right");
                } else if (assessment_type == AssessmentType.CHEST_COMPRESSION_RATE) {
                    output = this.ReadChestCompressionRate(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.CHEST_THREE_MS_EXCEEDENCE) {
                    output = this.ReadChestAcceleration(model, occupant, unit_system, occupant_index, {
                        three_ms: true
                    });
                } else if (assessment_type == AssessmentType.CHEST_VISCOUS_CRITERION) {
                    output = this.ReadChestViscousCriterion(model, occupant, unit_system, occupant_index);

                    /* SHOULDER */
                } else if (assessment_type == AssessmentType.SHOULDER_DEFLECTION) {
                    output = this.ReadShoulderDeflection(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.SHOULDER_DEFLECTION_RATE) {
                    output = this.ReadShoulderDeflectionRate(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.SHOULDER_VISCOUS_CRITERION) {
                    output = this.ReadShoulderViscousCriterion(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.SHOULDER_LATERAL_FORCES) {
                    output = this.ReadShoulderLateralForces(model, occupant, unit_system, occupant_index);

                    /* ABDOMEN */
                } else if (assessment_type == AssessmentType.ABDOMEN_COMPRESSION) {
                    output = this.ReadAbdomenCompression(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.ABDOMEN_COMPRESSION_LEFT) {
                    output = this.ReadAbdomenCompression(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.ABDOMEN_COMPRESSION_RIGHT) {
                    output = this.ReadAbdomenCompression(model, occupant, unit_system, occupant_index, "Right");
                } else if (assessment_type == AssessmentType.ABDOMEN_VISCOUS_CRITERION) {
                    output = this.ReadAbdomenViscousCriterion(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.ABDOMEN_FORCE) {
                    output = this.ReadAbdomenForce(model, occupant, unit_system, occupant_index);

                    /* LUMBAR */
                } else if (assessment_type == AssessmentType.LUMBAR_SHEAR) {
                    output = this.ReadLumbarShear(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.LUMBAR_AXIAL) {
                    output = this.ReadLumbarAxial(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.LUMBAR_TORSION) {
                    output = this.ReadLumbarTorsion(model, occupant, unit_system, occupant_index);

                    /* PELVIS */
                } else if (assessment_type == AssessmentType.ACETABULAR_FORCE) {
                    output = this.ReadAcetabularForce(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.ACETABULAR_FORCE_LEFT) {
                    output = this.ReadAcetabularForce(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.ACETABULAR_FORCE_RIGHT) {
                    output = this.ReadAcetabularForce(model, occupant, unit_system, occupant_index, "Right");
                } else if (assessment_type == AssessmentType.ILIUM_FORCE) {
                    output = this.ReadIliumForce(model, occupant, unit_system, occupant_index);
                } else if (assessment_type == AssessmentType.PELVIS_FORCE) {
                    output = this.ReadPelvisForce(model, occupant, unit_system, occupant_index);

                    /* FEMUR */
                } else if (assessment_type == AssessmentType.LEFT_FEMUR_AXIAL) {
                    output = this.ReadFemurAxial(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.RIGHT_FEMUR_AXIAL) {
                    output = this.ReadFemurAxial(model, occupant, unit_system, occupant_index, "Right");
                } else if (assessment_type == AssessmentType.LEFT_FEMUR_FORCE) {
                    output = this.ReadFemurForce(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.RIGHT_FEMUR_FORCE) {
                    output = this.ReadFemurForce(model, occupant, unit_system, occupant_index, "Right");
                } else if (assessment_type == AssessmentType.LEFT_FEMUR_COMPRESSION_EXCEEDENCE) {
                    output = this.ReadFemurCompressionExceedence(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.RIGHT_FEMUR_COMPRESSION_EXCEEDENCE) {
                    output = this.ReadFemurCompressionExceedence(model, occupant, unit_system, occupant_index, "Right");
                } else if (assessment_type == AssessmentType.LEFT_FEMUR_COMPRESSION_VS_IMPULSE) {
                    output = this.ReadFemurCompressionVsImpulse(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.RIGHT_FEMUR_COMPRESSION_VS_IMPULSE) {
                    output = this.ReadFemurCompressionVsImpulse(model, occupant, unit_system, occupant_index, "Right");

                    /* KNEE */
                } else if (assessment_type == AssessmentType.LEFT_KNEE_DISPLACEMENT) {
                    output = this.ReadKneeDisplacement(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.RIGHT_KNEE_DISPLACEMENT) {
                    output = this.ReadKneeDisplacement(model, occupant, unit_system, occupant_index, "Right");

                    /* TIBIA */
                } else if (assessment_type == AssessmentType.LEFT_TIBIA_COMPRESSION) {
                    output = this.ReadTibiaCompression(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.RIGHT_TIBIA_COMPRESSION) {
                    output = this.ReadTibiaCompression(model, occupant, unit_system, occupant_index, "Right");
                } else if (assessment_type == AssessmentType.LEFT_TIBIA_INDEX) {
                    output = this.ReadTibiaIndex(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.RIGHT_TIBIA_INDEX) {
                    output = this.ReadTibiaIndex(model, occupant, unit_system, occupant_index, "Right");

                    /* FOOT */
                } else if (assessment_type == AssessmentType.LEFT_FOOT_ACCELERATION) {
                    output = this.ReadFootAcceleration(model, occupant, unit_system, occupant_index, "Left");
                } else if (assessment_type == AssessmentType.RIGHT_FOOT_ACCELERATION) {
                    output = this.ReadFootAcceleration(model, occupant, unit_system, occupant_index, "Right");
                } else {
                    ErrorMessage(
                        `Unknown assessment type '${assessment_type}' in <ProtocolAssessment.DoOccupantAssessment>`
                    );
                }

                /* Add curves to graph, extend datums to last curve point and output values */
                if (output) {
                    for (let curve of output.curves) {
                        curve.AddToGraph(graph_id);

                        if (assessment_datums) {
                            let p = curve.GetPoint(curve.npoints);
                            assessment_datums.ExtendLastYValueToX(p[0]);
                        }
                    }
                    THisHelper.SetGraphTitle(graph_id, output.graph_title);

                    for (let p in output.values) {
                        results.output[`M${model.id} ${output.body_part.toUpperCase()} ${occupant} ${p}`] =
                            output.values[p];
                    }
                } else {
                    if (add_graph) {
                        WarningMessage(
                            `No output from assessment type '${assessment_type}' for occupant M${model.id} '${occupant}'`
                        );
                    }
                }
            }

            /* Plot the datums and scale the graph */
            if (add_graph) {
                graph_ids.push(graph_id);
                if (assessment_datums) assessment_datums.Plot(graph_id);
                THisHelper.ScaleGraph(graph_id, assessment_datums);

                /* Special logic for the Femur compression v impulse graphs so the graph
                 * always starts x=0.0 and ends at x=10.0 */
                if (
                    assessment_type == AssessmentType.LEFT_FEMUR_COMPRESSION_VS_IMPULSE ||
                    assessment_type == AssessmentType.RIGHT_FEMUR_COMPRESSION_VS_IMPULSE
                ) {
                    let graph = Graph.GetFromID(graph_id);

                    if (graph) {
                        if (graph.xmin > 0.0) graph.xmin = 0.0;
                        if (graph.xmax < 10.0) graph.xmax = 10.0;
                    }
                }

                if (options.graph_layout != DoAssessmentOptions.GRAPH_LAYOUT_REPORTER) {
                    ++graph_id;
                }
            }

            /* Capture an image */
            if (options.graph_layout == DoAssessmentOptions.GRAPH_LAYOUT_REPORTER) {
                Plot();

                /* Assemble a descriptive filename for the images used by REPORTER */

                let at = assessment_type;
                let output_dir = options.output_dir;
                let fname = `${output_dir}/${this.regulation}~${this.crash_test}~${this.version}~${occupants_str}~${at}.png`;

                THisHelper.CaptureImage(fname);
            }
        }

        /* Layout the graphs and plot them */

        results.last_page_id = this.ApplyOptionsToGraphs(options, graph_ids);

        Plot();

        /* Return the last graph id used */

        results.last_graph_id = Math.max(...graph_ids);

        return results;
    }

    /**
     * Do calculations on a Structure
     * @param {DoStructureAssessmentStructureData[]} structures_data Array of DoStructureAssessmentStructureData instances
     * @param {string[]} assessment_types Array of assessment types, e.g. AssessmentType.DOOR_INTRUSION
     * @param {DoAssessmentOptions} [options=null] Options
     * @returns {DoAssessmentResults}
     * @example
     * let p = new ProtocolAssessment(Regulation.CNCAP, CrashTest.ODB, "7.1.2", []);
     * let m = Model.GetFromID(1);
     * let s = new Structure(Structure.A_PILLAR, []);
     * let structures_data = [new DoStructureAssessmentStructureData(s, m, Workflow.UNIT_SYSTEM_U2)];
     * let assessment_types = [AssessmentType.A_PILLAR_INTRUSION];
     * let options = new DoAssessmentOptions("separate_pages");
     *
     * let assessment = p.DoStructureAssessment(structures_data,
     *                                         assessment_types,
     *                                         options);
     */
    DoStructureAssessment(structures_data, assessment_types, options = null) {
        if (!(structures_data instanceof Array)) {
            throw new Error(`<structures_data> must be an array in <ProtocolAssessment.DoStructureAssessment>`);
        }

        for (let structure_data of structures_data) {
            if (!(structure_data instanceof DoStructureAssessmentStructureData)) {
                throw new Error(
                    "Not all of the items in the <structures_data> array are DoStructureAssessmentStructureData instances in <ProtocolAssessment.DoStructureAssessment>"
                );
            }
        }

        /* Object to return values from assessments */

        let results = new DoAssessmentResults();

        /* Check that there are models to extract data from */

        for (let structure_data of structures_data) {
            let model = Model.GetFromID(structure_data.model_id);

            if (model == null) {
                ErrorMessage(
                    `Model ${structure_data.model_id} for ${structure_data.structure} not found in <ProtocolAssessment.DoStructureAssessment>`
                );
                return results;
            }
        }

        /* Blank all the curves and datums */

        if (options.blank_all) {
            THisHelper.BlankAllCurves();
            THisHelper.BlankAllDatums();
        }

        /* Get the first graph to plot on */

        let graph_id = options.first_graph_id;
        let graph_ids = [];

        /* Do each assessment for this protocol and occupant */

        for (let assessment_type of assessment_types) {
            /* Create new graph if required */
            if (graph_id > Graph.Total()) {
                if (graph_id >= THisHelper.MAX_GRAPHS) {
                    WarningMessage(
                        `Unable to do any more assessments, the graph limit of ${THisHelper.MAX_GRAPHS} has been reached`
                    );
                    break;
                }

                new Graph();
            }

            /* REPORTER uses same graph over and over so blank curves and datums */
            if (options.graph_layout == DoAssessmentOptions.GRAPH_LAYOUT_REPORTER) {
                THisHelper.BlankAllCurves();
                THisHelper.BlankAllDatums();
            }

            /* Flag to say whether to add a graph for this assessment
             * For most assessments this is true, but for the head acceleration
             * where one curve is used for multiple assessments we only want to
             * add it once */

            let add_graph = true;

            /* Get the datums */
            let assessment_datums = this.GetDatumsByAssessment(assessment_type);

            /** @type {ReadStructureOutput} */
            let output = null;

            let structure_index = -1;
            let structures_str = "";

            for (let structure_data of structures_data) {
                ++structure_index;

                let structure = structure_data.structure;
                let model = Model.GetFromID(structure_data.model_id);
                let unit_system = structure_data.unit_system;

                /* Concatenated list of structures used for REPORTER image filenames */

                if (structure_index > 0) structures_str += `_`;
                structures_str += `${structure}`;

                /* Only do the assessment if it's applicable to this structure */

                let structure_assessment_types = this.StructureAssessmentTypes(structure);

                if (structure_assessment_types.indexOf(assessment_type) == -1) continue;

                if (assessment_type == AssessmentType.PEDAL_VERTICAL_INTRUSION) {
                    output = this.ReadVerticalIntrusion(model, structure, unit_system, structure_index, "Pedal");
                } else if (assessment_type == AssessmentType.PEDAL_LATERAL_INTRUSION) {
                    output = this.ReadLateralIntrusion(model, structure, unit_system, structure_index, "Pedal");
                } else if (assessment_type == AssessmentType.PEDAL_FORE_AFT_INTRUSION) {
                    output = this.ReadForeAftIntrusion(model, structure, unit_system, structure_index, "Pedal");
                } else if (
                    assessment_type == AssessmentType.STEERING_COLUMN_LATERAL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_ROCKER_PANEL_1_LATERAL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_ROCKER_PANEL_2_LATERAL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_ROCKER_PANEL_3_LATERAL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_ROCKER_PANEL_1_LATERAL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_ROCKER_PANEL_2_LATERAL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_ROCKER_PANEL_3_LATERAL_INTRUSION
                ) {
                    output = this.ReadLateralIntrusion(model, structure, unit_system, structure_index);
                } else if (
                    assessment_type == AssessmentType.STEERING_COLUMN_FORE_AFT_INTRUSION ||
                    assessment_type == AssessmentType.LEFT_INSTRUMENT_PANEL_FORE_AFT_INTRUSION ||
                    assessment_type == AssessmentType.RIGHT_INSTRUMENT_PANEL_FORE_AFT_INTRUSION ||
                    assessment_type == AssessmentType.DOOR_FORE_AFT_INTRUSION
                ) {
                    output = this.ReadForeAftIntrusion(model, structure, unit_system, structure_index);
                } else if (
                    assessment_type == AssessmentType.A_PILLAR_FORE_AFT_INTRUSION ||
                    assessment_type == AssessmentType.STEERING_COLUMN_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_LEFT_TOEPAN_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_CENTRE_TOEPAN_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_RIGHT_TOEPAN_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_LEFT_TOEPAN_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_CENTRE_TOEPAN_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_RIGHT_TOEPAN_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_FOOTREST_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_FOOTREST_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_LOWER_HINGE_1_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_LOWER_HINGE_2_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_LOWER_HINGE_3_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_UPPER_HINGE_1_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_UPPER_HINGE_2_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_UPPER_HINGE_3_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_LOWER_HINGE_1_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_LOWER_HINGE_2_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_LOWER_HINGE_3_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_UPPER_HINGE_1_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_UPPER_HINGE_2_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_UPPER_HINGE_3_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_LEFT_LOWER_DASH_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_RIGHT_LOWER_DASH_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_CENTRE_DASH_ALL_INTRUSION ||
                    assessment_type == AssessmentType.DRIVER_UPPER_DASH_ALL_INTRUSION ||
                    assessment_type == AssessmentType.PASSENGER_UPPER_DASH_ALL_INTRUSION
                ) {
                    output = this.ReadAllIntrusion(model, structure, unit_system, structure_index);
                } else {
                    ErrorMessage(
                        `Unknown assessment type '${assessment_type}' in <ProtocolAssessment.DoStructureAssessment>`
                    );
                }

                /* Add curves to graph, extend datums to last curve point and output values */
                if (output) {
                    for (let curve of output.curves) {
                        curve.AddToGraph(graph_id);

                        if (assessment_datums) {
                            let p = curve.GetPoint(curve.npoints);
                            assessment_datums.ExtendLastYValueToX(p[0]);
                        }
                    }
                    THisHelper.SetGraphTitle(graph_id, output.graph_title);

                    for (let p in output.values) {
                        results.output[`M${model.id} ${structure} ${p}`] = output.values[p];
                    }
                } else {
                    if (add_graph) {
                        WarningMessage(
                            `No output from assessment type '${assessment_type}' for structure M${model.id} '${structure}'`
                        );
                    }
                }
            }

            /* Plot the datums and scale the graph */
            if (add_graph) {
                graph_ids.push(graph_id);
                if (assessment_datums) assessment_datums.Plot(graph_id);
                THisHelper.ScaleGraph(graph_id, assessment_datums);

                if (options.graph_layout != DoAssessmentOptions.GRAPH_LAYOUT_REPORTER) {
                    ++graph_id;
                }
            }

            /* Capture an image */
            if (options.graph_layout == DoAssessmentOptions.GRAPH_LAYOUT_REPORTER) {
                Plot();

                /* Assemble a descriptive filename for the images used by REPORTER */

                let at = assessment_type.toLowerCase();
                let output_dir = options.output_dir;
                let fname = `${output_dir}/${this.regulation}~${this.crash_test}~${this.version}~${structures_str}~${at}.png`;

                THisHelper.CaptureImage(fname);
            }
        }

        /* Layout the graphs and plot them */

        results.last_page_id = this.ApplyOptionsToGraphs(options, graph_ids);

        Plot();

        /* Return the last graph id used */

        results.last_graph_id = Math.max(...graph_ids);

        return results;
    }

    /**
     * Apply the selected options to the graphs
     * @param {DoAssessmentOptions} options Options to specify what to do for the graphs
     * @param {number[]} graph_ids Graph ids to apply options to
     * @return {number} The last page id used
     */
    ApplyOptionsToGraphs(options, graph_ids) {
        /* Set the layout */

        let last_page = 1;

        if (options.graph_layout == DoAssessmentOptions.GRAPH_LAYOUT_SAME_PAGE) {
            THisHelper.PutGraphsOnPage(graph_ids, options.first_page_id, options.remove_existing_graphs);
            last_page = options.first_page_id;
        } else if (options.graph_layout == DoAssessmentOptions.GRAPH_LAYOUT_SEPARATE_PAGES) {
            last_page = THisHelper.PutGraphsOnSeparatePages(graph_ids, options.first_page_id);
            if (Page.ReturnGraphs(options.first_page_id).length > 0) Page.SetActivePage(options.first_page_id);
        } else if (options.graph_layout == DoAssessmentOptions.GRAPH_LAYOUT_REPORTER) {
            /* Do nothing for REPORTER (images already captured) */
        } else {
            ErrorMessage(`Invalid graph_layout ${options.graph_layout} in <ProtocolAssessment.ApplyOptionsToGraphs>`);
        }

        return last_page;
    }

    /**
     * Returns a list of the assessment types for the given occupants and body parts
     * @param {WorkflowOccupant[]} occupants WorkflowOccupant instances
     * @param {string[]} body_part_types Body part types
     * @returns {string[]}
     */
    UniqueOccupantAssessmentTypes(occupants, body_part_types) {
        let assessment_types = new Set();

        for (let body_part_type of body_part_types) {
            for (let occupant of occupants) {
                let at = this.OccupantAssessmentTypes(occupant, body_part_type.toLowerCase());

                for (let a of at) {
                    assessment_types.add(a);
                }
            }
        }

        return [...assessment_types];
    }

    /**
     * Returns a list of the assessment types for the given structures
     * @param {Structure[]} structures Structure instances
     * @returns {string[]}
     */
    UniqueStructureAssessmentTypes(structures) {
        let assessment_types = new Set();

        for (let structure of structures) {
            let at = this.StructureAssessmentTypes(structure);

            for (let a of at) {
                assessment_types.add(a);
            }
        }

        return [...assessment_types];
    }

    /**
     * Returns a colour to use for a curve by index
     * @param {number} index
     * @returns {number}
     */
    GetCurveColourByIndex(index) {
        let colors = [Colour.BLACK, Colour.BLUE, Colour.MAGENTA, Colour.CYAN];

        return colors[index % colors.length];
    }

    /**
     * Object to return from the occupant Read functions
     * @typedef ReadBodyPartOutput
     * @property {?Curve[]} curves Output curves
     * @property {Object} values Object with values for the output
     * @property {string} body_part Body part type
     * @property {string} graph_title Graph title (also used in curve labels)
     */

    /**
     * Reads head accelerations from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {object} [options] Options
     * @return {?ReadBodyPartOutput}
     * @example
     * let curve = p.ReadHeadAcceleration(model, occupant, unit_system);
     */
    ReadHeadAcceleration(model, occupant, unit_system, occupant_index, options) {
        /* Graph title - also used in curve labels */

        let graph_title = `Head Acceleration Magnitude`;

        /* Get the raw measurements from the head */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.HEAD);

        /* All the occupants have nodes in the head to read the accelerations,
         * so no need for occupant specific logic here */

        let curve_x = raw_measurements.GetCurve(Measurement.X_ACCELERATION);
        let curve_y = raw_measurements.GetCurve(Measurement.Y_ACCELERATION);
        let curve_z = raw_measurements.GetCurve(Measurement.Z_ACCELERATION);

        if (!curve_x) {
            WarningMessage(
                `Unable to read x acceleration from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadHeadAcceleration>`
            );
            return null;
        }
        if (!curve_y) {
            WarningMessage(
                `Unable to read y acceleration from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadHeadAcceleration>`
            );
            return null;
        }
        if (!curve_z) {
            WarningMessage(
                `Unable to read z acceleration from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadHeadAcceleration>`
            );
            return null;
        }

        /* Convert acceleration to g */

        let curve_x_g = Operate.Div(curve_x, WorkflowUnits.GravityConstant(unit_system));
        let curve_y_g = Operate.Div(curve_y, WorkflowUnits.GravityConstant(unit_system));
        let curve_z_g = Operate.Div(curve_z, WorkflowUnits.GravityConstant(unit_system));

        curve_x_g.RemoveFromGraph();
        curve_y_g.RemoveFromGraph();
        curve_z_g.RemoveFromGraph();

        /* Convert time to seconds */

        let curve_x_g_s = Operate.Dix(curve_x_g, WorkflowUnits.TimeToSecondsFactor(unit_system));
        let curve_y_g_s = Operate.Dix(curve_y_g, WorkflowUnits.TimeToSecondsFactor(unit_system));
        let curve_z_g_s = Operate.Dix(curve_z_g, WorkflowUnits.TimeToSecondsFactor(unit_system));

        curve_x_g_s.RemoveFromGraph();
        curve_y_g_s.RemoveFromGraph();
        curve_z_g_s.RemoveFromGraph();

        /* Filter with C1000 */

        let curve_x_c1000 = Operate.C1000(curve_x_g_s, 0.00001);
        let curve_y_c1000 = Operate.C1000(curve_y_g_s, 0.00001);
        let curve_z_c1000 = Operate.C1000(curve_z_g_s, 0.00001);

        curve_x_c1000.RemoveFromGraph();
        curve_y_c1000.RemoveFromGraph();
        curve_z_c1000.RemoveFromGraph();

        /* Vector combine */

        let curve_vec = Operate.Vec(curve_x_c1000, curve_y_c1000, curve_z_c1000);
        curve_vec.RemoveFromGraph();

        /* Convert back to model time */

        curve_vec = Operate.Mux(curve_vec, WorkflowUnits.TimeToSecondsFactor(unit_system));
        curve_vec.RemoveFromGraph();

        /* Values to extract from curve */

        let values = {};

        /* Do a HIC calculation */
        if (options && options.hic) {
            values["HIC"] = `${Operate.Hic(curve_vec, options.hic_window, 1.0).toPrecision(6)}`;
            values["HIC_window"] = `${options.hic_window}s`;

            curve_vec.RemoveFromGraph();
        }

        /* Do a three ms calculation */
        if (options && options.three_ms) {
            Operate.Tms(curve_vec, 0.003 * WorkflowUnits.TimeToSecondsFactor(unit_system));

            values["tms"] = `${curve_vec.tms.toPrecision(6)}g`;
            values["tms_tmin"] = `${curve_vec.tms_tmin.toPrecision(6)}g`;
            values["tms_tmax"] = `${curve_vec.tms_tmax.toPrecision(6)}g`;

            curve_vec.RemoveFromGraph();
        }

        /* Get the peak acceleration */
        if (options && options.peak_acceleration) {
            values["peak_acceleration"] = `${curve_vec.ymax.toPrecision(6)}g`;
        }

        /* Set the labels and line style */

        THisHelper.SetCurveLabels(
            curve_vec,
            `${occupant} ${graph_title}`,
            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
            "Acceleration (g)"
        );

        THisHelper.SetLineStyle(curve_vec, this.GetCurveColourByIndex(occupant_index));

        return { curves: [curve_vec], values: values, body_part: OccupantBodyPart.HEAD, graph_title: graph_title };
    }

    /**
     * Reads neck shear forces from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckShear(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Neck Shear`;

        /* Get the raw measurements from the neck */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.NECK);

        /* Process them to get shear force */

        switch (occupant.GetEntityTypeFromTag(OccupantEntity.NECK_LOADCELL)) {
            case BaseEntity.XSECTION:
                let shear = raw_measurements.GetCurve(Measurement.SHEAR);
                if (!shear) {
                    WarningMessage(
                        `Unable to read shear force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadNeckShear>`
                    );
                    return null;
                }

                /* Convert forces kN */
                let curve_shear_kn = Operate.Div(shear, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_shear_kn.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_shear_s = Operate.Dix(curve_shear_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_shear_s.RemoveFromGraph();

                /* Filter with C1000 */
                let curve_shear_c1000 = Operate.C1000(curve_shear_s, 0.00001);
                curve_shear_c1000.RemoveFromGraph();

                /* Convert back to model time */
                let curve_shear = Operate.Mux(curve_shear_c1000, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_shear.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_shear,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_shear], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_shear],
                    values: {
                        max_shear: `${curve_shear.ymax.toPrecision(6)}kN`,
                        min_shear: `${curve_shear.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckShear>`);
                return null;
        }
    }

    /**
     * Reads neck axial forces from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckAxial(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Neck Axial`;

        /* Get the raw measurements from the neck */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.NECK);

        /* Process them to get axial force */

        switch (occupant.GetEntityTypeFromTag(OccupantEntity.NECK_LOADCELL)) {
            case BaseEntity.XSECTION:
                let axial = raw_measurements.GetCurve(Measurement.AXIAL);
                if (!axial) {
                    WarningMessage(
                        `Unable to read axial force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadNeckAxial>`
                    );
                    return null;
                }

                /* Convert forces kN */
                let curve_axial_kn = Operate.Div(axial, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_axial_kn.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_axial_s = Operate.Dix(curve_axial_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial_s.RemoveFromGraph();

                /* Filter with C1000 */
                let curve_axial_c1000 = Operate.C1000(curve_axial_s, 0.00001);
                curve_axial_c1000.RemoveFromGraph();

                /* Convert back to model time */
                let curve_axial = Operate.Mux(curve_axial_c1000, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_axial,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_axial], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_axial],
                    values: {
                        max_axial: `${curve_axial.ymax.toPrecision(6)}kN`,
                        min_axial: `${curve_axial.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckAxial>`);
                return null;
        }
    }

    /**
     * Reads neck lower axial forces from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckLowerAxial(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Lower Neck Axial`;

        /* Get the raw measurements from the neck */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.NECK);

        /* Process them to get axial force */

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let lower_axial = raw_measurements.GetCurve(Measurement.LOWER_AXIAL);
                if (!lower_axial) {
                    WarningMessage(
                        `Unable to read lower axial force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadNeckLowerAxial>`
                    );
                    return null;
                }

                /* Convert forces kN */
                let curve_axial_kn = Operate.Div(lower_axial, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_axial_kn.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_axial_s = Operate.Dix(curve_axial_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_axial_c600 = Operate.C600(curve_axial_s, 0.00001);
                curve_axial_c600.RemoveFromGraph();

                /* Convert back to model time */
                let curve_axial = Operate.Mux(curve_axial_c600, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_axial,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_axial], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_axial],
                    values: {
                        max_lower_axial: `${curve_axial.ymax.toPrecision(6)}kN`,
                        min_lower_axial: `${curve_axial.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckLowerAxial>`);
                return null;
        }
    }

    /**
     * Reads neck upper axial forces from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckUpperAxial(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Upper Neck Axial`;

        /* Get the raw measurements from the neck */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.NECK);

        /* Process them to get axial force */

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let upper_axial = raw_measurements.GetCurve(Measurement.UPPER_AXIAL);
                if (!upper_axial) {
                    WarningMessage(
                        `Unable to read upper axial force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadNeckUpperAxial>`
                    );
                    return null;
                }

                /* Convert forces kN */
                let curve_axial_kn = Operate.Div(upper_axial, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_axial_kn.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_axial_s = Operate.Dix(curve_axial_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_axial_c600 = Operate.C600(curve_axial_s, 0.00001);
                curve_axial_c600.RemoveFromGraph();

                /* Convert back to model time */
                let curve_axial = Operate.Mux(curve_axial_c600, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_axial,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_axial], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_axial],
                    values: {
                        max_upper_axial: `${curve_axial.ymax.toPrecision(6)}kN`,
                        min_upper_axial: `${curve_axial.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckUpperAxial>`);
                return null;
        }
    }

    /**
     * Reads neck bending from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckBending(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Neck Extension Bending Moment`;

        /* Get the raw measurements from the neck */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.NECK);

        /* Process them to get bending moment */

        switch (occupant.GetEntityTypeFromTag(OccupantEntity.NECK_LOADCELL)) {
            case BaseEntity.XSECTION:
                let bending = raw_measurements.GetCurve(Measurement.BENDING);
                if (!bending) {
                    WarningMessage(
                        `Unable to read bending moment from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadNeckBending>`
                    );
                    return null;
                }

                /* BENDING MOMENT */

                /* Convert forces Nm */
                let curve_bending_nm = Operate.Div(bending, WorkflowUnits.MomentToNewtonMetreFactor(unit_system));
                curve_bending_nm.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_bending_s = Operate.Dix(curve_bending_nm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_bending_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_bending_c600 = Operate.C600(curve_bending_s, 0.00001);
                curve_bending_c600.RemoveFromGraph();

                /* SHEAR */
                let shear_output = this.ReadNeckShear(model, occupant, unit_system, occupant_index);

                if (!shear_output) return null;
                if (shear_output.curves.length == 0) return null;

                let curve_shear = shear_output.curves[0];

                /* Transpose the bending moment to the occupital condoyle moment
                 * Not required for the THOR occupant.
                 *
                 * TODO - check which other occupants it applies to
                 */

                /** @type {Curve} */
                let curve_bending_adj = null;

                if (occupant.product == OccupantProduct.THOR) {
                    curve_bending_adj = curve_bending_c600;
                } else {
                    let curve_shear_adj = Operate.Mul(curve_shear, 17.78);
                    curve_bending_adj = Operate.Sub(curve_bending_c600, curve_shear_adj);
                    curve_shear_adj.RemoveFromGraph();
                }
                curve_bending_adj.RemoveFromGraph();

                /* Convert back to model time */
                let curve_bending = Operate.Mux(curve_bending_adj, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_bending.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_bending,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Bending Moment (Nm)"
                );

                THisHelper.SetLineStyle([curve_bending], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_bending],
                    values: {
                        max_extension: `${curve_bending.ymax.toPrecision(6)}Nm`,
                        min_extension: `${curve_bending.ymin.toPrecision(6)}Nm`
                    },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckBending>`);
                return null;
        }
    }

    /**
     * Reads neck lower extension moment from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckLowerExtension(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Lower Neck Lateral Extension`;

        /* Get the raw measurements from the neck */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.NECK);

        /* Process them to get bending moment */

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let lower_extension = raw_measurements.GetCurve(Measurement.LOWER_EXTENSION);
                if (!lower_extension) {
                    WarningMessage(
                        `Unable to read lower extension from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadNeckLowerExtension>`
                    );
                    return null;
                }

                /* Convert forces Nm */
                let curve_extension_nm = Operate.Div(
                    lower_extension,
                    WorkflowUnits.MomentToNewtonMetreFactor(unit_system)
                );
                curve_extension_nm.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_extension_s = Operate.Dix(curve_extension_nm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_extension_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_extension_c600 = Operate.C600(curve_extension_s, 0.00001);
                curve_extension_c600.RemoveFromGraph();

                /* Convert back to model time */
                let curve_extension = Operate.Mux(curve_extension_c600, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_extension.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_extension,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Extension (Nm)"
                );

                THisHelper.SetLineStyle([curve_extension], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_extension],
                    values: {
                        max_lower_extension: `${curve_extension.ymax.toPrecision(6)}Nm`,
                        min_lower_extension: `${curve_extension.ymin.toPrecision(6)}Nm`
                    },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckLowerExtension>`);
                return null;
        }
    }

    /**
     * Reads neck upper extension moment from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckUpperExtension(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Upper Neck Lateral Extension`;

        /* Get the raw measurements from the neck */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.NECK);

        /* Process them to get bending moment */

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let upper_extension = raw_measurements.GetCurve(Measurement.UPPER_EXTENSION);
                if (!upper_extension) {
                    WarningMessage(
                        `Unable to read upper extension from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadNeckUpperExtension>`
                    );
                    return null;
                }

                /* Convert forces Nm */
                let curve_extension_nm = Operate.Div(
                    upper_extension,
                    WorkflowUnits.MomentToNewtonMetreFactor(unit_system)
                );
                curve_extension_nm.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_extension_s = Operate.Dix(curve_extension_nm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_extension_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_extension_c600 = Operate.C600(curve_extension_s, 0.00001);
                curve_extension_c600.RemoveFromGraph();

                /* Convert back to model time */
                let curve_extension = Operate.Mux(curve_extension_c600, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_extension.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_extension,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Extension (Nm)"
                );

                THisHelper.SetLineStyle([curve_extension], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_extension],
                    values: {
                        max_upper_extension: `${curve_extension.ymax.toPrecision(6)}Nm`,
                        min_upper_extension: `${curve_extension.ymin.toPrecision(6)}Nm`
                    },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckUpperExtension>`);
                return null;
        }
    }

    /**
     * Reads neck lower flexion moment from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckLowerFlexion(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Lower Neck Lateral Flexion`;

        /* Get the raw measurements from the neck */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.NECK);

        /* Process them to get bending moment */

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let lower_flexion = raw_measurements.GetCurve(Measurement.LOWER_FLEXION);
                if (!lower_flexion) {
                    WarningMessage(
                        `Unable to read lower flexion from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadNeckLowerFlexion>`
                    );
                    return null;
                }

                /* Convert forces Nm */
                let curve_flexion_nm = Operate.Div(lower_flexion, WorkflowUnits.MomentToNewtonMetreFactor(unit_system));
                curve_flexion_nm.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_flexion_s = Operate.Dix(curve_flexion_nm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_flexion_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_flexion_c600 = Operate.C600(curve_flexion_s, 0.00001);
                curve_flexion_c600.RemoveFromGraph();

                /* Convert back to model time */
                let curve_flexion = Operate.Mux(curve_flexion_c600, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_flexion.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_flexion,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Flexion (Nm)"
                );

                THisHelper.SetLineStyle([curve_flexion], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_flexion],
                    values: {
                        max_lower_flexion: `${curve_flexion.ymax.toPrecision(6)}Nm`,
                        min_lower_flexion: `${curve_flexion.ymin.toPrecision(6)}Nm`
                    },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckLowerFlexion>`);
                return null;
        }
    }

    /**
     * Reads neck upper flexion moment from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckUpperFlexion(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Upper Neck Lateral Flexion`;

        /* Get the raw measurements from the neck */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.NECK);

        /* Process them to get bending moment */

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let upper_flexion = raw_measurements.GetCurve(Measurement.UPPER_FLEXION);
                if (!upper_flexion) {
                    WarningMessage(
                        `Unable to read upper flexion from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadNeckUpperFlexion>`
                    );
                    return null;
                }

                /* Convert forces Nm */
                let curve_flexion_nm = Operate.Div(upper_flexion, WorkflowUnits.MomentToNewtonMetreFactor(unit_system));
                curve_flexion_nm.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_flexion_s = Operate.Dix(curve_flexion_nm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_flexion_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_flexion_c600 = Operate.C600(curve_flexion_s, 0.00001);
                curve_flexion_c600.RemoveFromGraph();

                /* Convert back to model time */
                let curve_flexion = Operate.Mux(curve_flexion_c600, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_flexion.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_flexion,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Flexion (Nm)"
                );

                THisHelper.SetLineStyle([curve_flexion], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_flexion],
                    values: {
                        max_upper_flexion: `${curve_flexion.ymax.toPrecision(6)}Nm`,
                        min_upper_flexion: `${curve_flexion.ymin.toPrecision(6)}Nm`
                    },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckUpperFlexion>`);
                return null;
        }
    }

    /**
     * Reads neck shear force exceedence from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckShearExceedence(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Neck Shear Exceedence`;

        switch (occupant.GetEntityTypeFromTag(OccupantEntity.NECK_LOADCELL)) {
            case BaseEntity.XSECTION:
                let shear_output = this.ReadNeckShear(model, occupant, unit_system, occupant_index);
                if (!shear_output) return null;
                if (shear_output.curves.length == 0) return null;

                let curve_shear = shear_output.curves[0];

                /* Take the absolute values in case of -ve loads */

                let curve_shear_abs = Operate.Abs(curve_shear);
                curve_shear_abs.RemoveFromGraph();

                let curve_shear_exc = Operate.Exc(curve_shear_abs, "positive");
                curve_shear_exc.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_shear_exc,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_shear_exc], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_shear_exc],
                    values: {},
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckShearExceedence>`);
                return null;
        }
    }

    /**
     * Reads neck tension force exceedence from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckTensionExceedence(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Neck Tension Exceedence`;

        switch (occupant.GetEntityTypeFromTag(OccupantEntity.NECK_LOADCELL)) {
            case BaseEntity.XSECTION:
                let axial_output = this.ReadNeckAxial(model, occupant, unit_system, occupant_index);
                if (!axial_output) return null;
                if (axial_output.curves.length == 0) return null;

                let curve_axial = axial_output.curves[0];

                let curve_tension_exc = Operate.Exc(curve_axial, "positive");
                curve_tension_exc.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_tension_exc,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_tension_exc], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_tension_exc],
                    values: {},
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckTensionExceedence>`);
                return null;
        }
    }

    /**
     * Reads neck compression force exceedence from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckCompressionExceedence(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Neck Compression Exceedence`;

        switch (occupant.GetEntityTypeFromTag(OccupantEntity.NECK_LOADCELL)) {
            case BaseEntity.XSECTION:
                let axial_output = this.ReadNeckAxial(model, occupant, unit_system, occupant_index);
                if (!axial_output) return null;
                if (axial_output.curves.length == 0) return null;

                let curve_axial = axial_output.curves[0];

                let curve_compression_exc = Operate.Exc(curve_axial, "negative");
                curve_compression_exc.RemoveFromGraph();

                let curve_compression_exc_abs = Operate.Abs(curve_compression_exc);
                curve_compression_exc_abs.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_compression_exc_abs,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_compression_exc_abs], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_compression_exc_abs],
                    values: {},
                    body_part: OccupantBodyPart.NECK,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckCompressionExceedence>`);
                return null;
        }
    }

    /**
     * Reads Neck Injury Criteria (NIJ) from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadNeckNIJ(model, occupant, unit_system, occupant_index) {
        switch (occupant.GetEntityTypeFromTag(OccupantEntity.NECK_LOADCELL)) {
            case BaseEntity.XSECTION:
                let shear_output = this.ReadNeckShear(model, occupant, unit_system, occupant_index);
                if (!shear_output) return null;
                if (shear_output.curves.length == 0) return null;

                let curve_shear = shear_output.curves[0];

                let axial_output = this.ReadNeckAxial(model, occupant, unit_system, occupant_index);
                if (!axial_output) return null;
                if (axial_output.curves.length == 0) return null;

                let curve_axial = axial_output.curves[0];

                let bending_output = this.ReadNeckBending(model, occupant, unit_system, occupant_index);
                if (!bending_output) return null;
                if (bending_output.curves.length == 0) return null;

                let curve_bending = bending_output.curves[0];

                /* Regularise the curves so they have the same number of points */

                let max_points = Math.max(curve_shear.npoints, curve_axial.npoints, curve_bending.npoints);

                let last_point = curve_shear.GetPoint(curve_shear.npoints);
                let last_time = last_point[0];
                let dt = last_time / max_points;
                let curve_shear_reg = Operate.Reg(curve_shear, dt);
                curve_shear_reg.RemoveFromGraph();

                last_point = curve_axial.GetPoint(curve_axial.npoints);
                last_time = last_point[0];
                dt = last_time / max_points;
                let curve_axial_reg = Operate.Reg(curve_axial, dt);
                curve_axial_reg.RemoveFromGraph();

                last_point = curve_bending.GetPoint(curve_bending.npoints);
                last_time = last_point[0];
                dt = last_time / max_points;
                let curve_bending_reg = Operate.Reg(curve_bending, dt);
                curve_bending_reg.RemoveFromGraph();

                /* Get the critical loads for this occupant */

                let nij_critical_loads = occupant.GetNIJCriticalLoads();

                /* Calculate the NIJ */

                let curves_nij = Operate.Nij(
                    curve_shear_reg,
                    curve_axial_reg,
                    curve_bending_reg,
                    nij_critical_loads.tension,
                    nij_critical_loads.compression,
                    nij_critical_loads.flexion,
                    nij_critical_loads.extension,
                    0.0
                );
                for (let c of curves_nij) {
                    c.RemoveFromGraph();
                }

                /* Set the labels and line style */
                for (let i = 0; i < curves_nij.length; i++) {
                    let label = "";

                    if (i == 0) label = `${occupant} NIJ - Tension-Extension`;
                    if (i == 1) label = `${occupant} NIJ - Tension-Flexion`;
                    if (i == 2) label = `${occupant} NIJ - Compression-Extension`;
                    if (i == 3) label = `${occupant} NIJ - Compression-Extension`;

                    THisHelper.SetCurveLabels(
                        curves_nij[i],
                        label,
                        `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                        "NIJ"
                    );
                }

                THisHelper.SetLineStyle(
                    [curves_nij[0], curves_nij[1]],
                    this.GetCurveColourByIndex(occupant_index),
                    LineStyle.DASH
                );
                THisHelper.SetLineStyle([curves_nij[2], curves_nij[3]], this.GetCurveColourByIndex(occupant_index));

                let nij_max = 0.0;

                for (let curve_nij of curves_nij) {
                    nij_max = Math.max(nij_max, curve_nij.ymax);
                }

                return {
                    curves: curves_nij,
                    values: { nij_max: nij_max },
                    body_part: OccupantBodyPart.NECK,
                    graph_title: `NIJ`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadNeckNIJ>`);
                return null;
        }
    }

    /**
     * Reads chest compression from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} [side] "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadChestCompression(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Chest Compression`;

        /* Get the raw measurements from the chest */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.CHEST);

        /* Process them to get compression */

        switch (occupant.product) {
            case OccupantProduct.HIII:
                let rotation = raw_measurements.GetCurve(Measurement.ROTATION);
                if (!rotation) {
                    WarningMessage(
                        `Unable to read rotations from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                /* The dummy manuals say that a negative rotation indicates compression, however testing has shown
                 * that in some versions it is actually the other way around. If the maximum absolute value is +ve
                 * then multiply the rotation by -1 so the conversion to compression works correctly. */

                let rmax = rotation.ymax;
                let rmin = rotation.ymin;

                if (Math.abs(rmax) > Math.abs(rmin)) {
                    rotation = Operate.Mul(rotation, -1);
                    rotation.RemoveFromGraph();
                }
                /* Convert rotation to deflection */

                let rotation_factors = occupant.GetChestRotationFactors();

                if (!rotation_factors) return null;

                let curve_disp_mm = null;

                if (rotation_factors.type == "linear") {
                    curve_disp_mm = Operate.Mul(rotation, rotation_factors.values[0]);
                } else if (rotation_factors.type == "third_order") {
                    let curve_id = Curve.FirstFreeID();

                    curve_disp_mm = new Curve(curve_id);

                    let n = rotation.npoints;

                    for (let i = 1; i <= n; ++i) {
                        let p_rot = rotation.GetPoint(i);

                        let time = p_rot[0];

                        let com =
                            p_rot[1] * rotation_factors.values[0] +
                            p_rot[1] * rotation_factors.values[1] +
                            p_rot[1] * rotation_factors.values[2];

                        curve_disp_mm.AddPoint(time, com);
                    }
                } else {
                    ErrorMessage(
                        `Unknown rotation factor type ${rotation_factors.type} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                curve_disp_mm.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_disp_s = Operate.Dix(curve_disp_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_s.RemoveFromGraph();

                /* Filter with C180 */
                let curve_disp_c180 = Operate.C180(curve_disp_s, 0.00001);
                curve_disp_c180.RemoveFromGraph();

                /* Convert back to model time */
                let curve_disp = Operate.Mux(curve_disp_c180, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_disp,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Compression (mm)"
                );

                THisHelper.SetLineStyle([curve_disp], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_disp],
                    values: {
                        max_chest_compression: `${curve_disp.ymax.toPrecision(6)}mm`
                    },
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: graph_title
                };

            case OccupantProduct.WSID:
                let upper_rib_rotation = raw_measurements.GetCurve(Measurement.UPPER_ROTATION);
                let mid_rib_rotation = raw_measurements.GetCurve(Measurement.MID_ROTATION);
                let lower_rib_rotation = raw_measurements.GetCurve(Measurement.LOWER_ROTATION);
                let upper_rib_translation = raw_measurements.GetCurve(Measurement.UPPER_TRANSLATION);
                let mid_rib_translation = raw_measurements.GetCurve(Measurement.MID_TRANSLATION);
                let lower_rib_translation = raw_measurements.GetCurve(Measurement.LOWER_TRANSLATION);

                if (!upper_rib_rotation) {
                    WarningMessage(
                        `Unable to read upper rib rotation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!mid_rib_rotation) {
                    WarningMessage(
                        `Unable to read mid rib rotation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!lower_rib_rotation) {
                    WarningMessage(
                        `Unable to read lower rib rotation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                if (!upper_rib_translation) {
                    WarningMessage(
                        `Unable to read upper rib translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!mid_rib_translation) {
                    WarningMessage(
                        `Unable to read mid rib translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!lower_rib_translation) {
                    WarningMessage(
                        `Unable to read lower rib translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                /* Assess each rib */

                /** @type {Curve} */
                let curve_upper_rib_disp = null;
                let upper_rib_disp = "";
                /** @type {Curve} */
                let curve_mid_rib_disp = null;
                let mid_rib_disp = "";
                /** @type {Curve} */
                let curve_bottom_rib_disp = null;
                let bottom_rib_disp = "";

                for (let rib = 0; rib < 3; rib++) {
                    /** @type {Curve} */
                    let curve_disp = null;
                    /** @type {Curve} */
                    let curve_rot = null;
                    let irtracc_length = 0;

                    if (rib == 0) {
                        curve_disp = upper_rib_translation;
                        curve_rot = upper_rib_rotation;
                        irtracc_length = occupant.upper_rib_irtracc_length;
                    } else if (rib == 1) {
                        curve_disp = mid_rib_translation;
                        curve_rot = mid_rib_rotation;
                        irtracc_length = occupant.mid_rib_irtracc_length;
                    } else if (rib == 2) {
                        curve_disp = lower_rib_translation;
                        curve_rot = lower_rib_rotation;
                        irtracc_length = occupant.bottom_rib_irtracc_length;
                    }

                    /* Add IR-TRACC length */

                    let curve_total_disp = Operate.Add(curve_disp, irtracc_length);
                    curve_total_disp.RemoveFromGraph();

                    /* Convert length to mm */

                    let curve_disp_mm = Operate.Div(
                        curve_total_disp,
                        WorkflowUnits.LengthToMillimetresFactor(unit_system)
                    );
                    curve_disp_mm.RemoveFromGraph();

                    /* Convert time to seconds */

                    let curve_disp_s = Operate.Dix(curve_disp_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_s.RemoveFromGraph();

                    let curve_rot_s = Operate.Dix(curve_rot, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_rot_s.RemoveFromGraph();

                    /* Filter with C180 */

                    let curve_disp_c180 = Operate.C180(curve_disp_s, 0.00001);
                    curve_disp_c180.RemoveFromGraph();

                    let curve_rot_c180 = Operate.C180(curve_rot_s, 0.00001);
                    curve_rot_c180.RemoveFromGraph();

                    /* Calculate the lateral displacement */

                    let curve_rot_sin_theta = Operate.Sin(curve_rot_c180);
                    curve_rot_sin_theta.RemoveFromGraph();

                    let curve_lat_disp = Operate.Mul(curve_disp_c180, curve_rot_sin_theta);
                    curve_lat_disp.RemoveFromGraph();

                    /* Assign displacement to the correct curve and calculate Viscous Criterion */

                    if (rib == 0) {
                        curve_upper_rib_disp = curve_lat_disp;

                        THisHelper.SetCurveLabels(
                            curve_upper_rib_disp,
                            `${occupant} Upper Rib Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle([curve_upper_rib_disp], this.GetCurveColourByIndex(occupant_index));

                        upper_rib_disp = `${curve_upper_rib_disp.ymax.toPrecision(6)}mm`;
                    } else if (rib == 1) {
                        curve_mid_rib_disp = curve_lat_disp;

                        THisHelper.SetCurveLabels(
                            curve_mid_rib_disp,
                            `${occupant} Middle Rib Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_mid_rib_disp],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH
                        );

                        mid_rib_disp = `${curve_mid_rib_disp.ymax.toPrecision(6)}mm`;
                    } else if (rib == 2) {
                        curve_bottom_rib_disp = curve_lat_disp;

                        THisHelper.SetCurveLabels(
                            curve_bottom_rib_disp,
                            `${occupant} Bottom Rib Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_bottom_rib_disp],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        bottom_rib_disp = `${curve_bottom_rib_disp.ymax.toPrecision(6)}mm`;
                    }
                }

                return {
                    curves: [curve_upper_rib_disp, curve_mid_rib_disp, curve_bottom_rib_disp],
                    values: {
                        upper_rib_disp: upper_rib_disp,
                        mid_rib_disp: mid_rib_disp,
                        bottom_rib_disp: bottom_rib_disp
                    },
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: graph_title
                };

            case OccupantProduct.ES2RE:
                let upper_translation = raw_measurements.GetCurve(Measurement.UPPER_TRANSLATION);
                let mid_translation = raw_measurements.GetCurve(Measurement.MID_TRANSLATION);
                let lower_translation = raw_measurements.GetCurve(Measurement.LOWER_TRANSLATION);

                if (!upper_translation) {
                    WarningMessage(
                        `Unable to read upper translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!mid_translation) {
                    WarningMessage(
                        `Unable to read mid translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!lower_translation) {
                    WarningMessage(
                        `Unable to read lower translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                /** @type {Curve} */
                let curve_upper_disp = null;
                let upper_disp = "";
                /** @type {Curve} */
                let curve_mid_disp = null;
                let mid_disp = "";
                /** @type {Curve} */
                let curve_bottom_disp = null;
                let bottom_disp = "";

                /* Assess each sensor */
                for (let sensor = 0; sensor < 3; sensor++) {
                    let curve_disp = null;

                    if (sensor == 0) {
                        curve_disp = upper_translation;
                    } else if (sensor == 1) {
                        curve_disp = mid_translation;
                    } else if (sensor == 2) {
                        curve_disp = lower_translation;
                    }

                    /* Convert length to mm */

                    let curve_disp_mm = Operate.Div(curve_disp, WorkflowUnits.LengthToMillimetresFactor(unit_system));
                    curve_disp_mm.RemoveFromGraph();

                    /* Compression is negative */

                    curve_disp_mm = Operate.Mul(curve_disp_mm, -1.0);
                    curve_disp_mm.RemoveFromGraph();

                    /* Convert time to seconds */

                    let curve_disp_s = Operate.Dix(curve_disp_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_s.RemoveFromGraph();

                    /* Filter with C180 */

                    let curve_disp_c180 = Operate.C180(curve_disp_s, 0.00001);
                    curve_disp_c180.RemoveFromGraph();

                    /* Convert back to model time */

                    curve_disp_c180 = Operate.Mux(curve_disp_c180, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_c180.RemoveFromGraph();

                    /* Assign displacement to the correct curve and calculate Viscous Criterion */

                    if (sensor == 0) {
                        curve_upper_disp = curve_disp_c180;

                        THisHelper.SetCurveLabels(
                            curve_upper_disp,
                            `${occupant} Upper Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle([curve_upper_disp], this.GetCurveColourByIndex(occupant_index));

                        upper_disp = `${curve_upper_disp.ymax.toPrecision(6)}mm`;
                    } else if (sensor == 1) {
                        curve_mid_disp = curve_disp_c180;

                        THisHelper.SetCurveLabels(
                            curve_mid_disp,
                            `${occupant} Middle Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_mid_disp],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH
                        );

                        mid_disp = `${curve_mid_disp.ymax.toPrecision(6)}mm`;
                    } else if (sensor == 2) {
                        curve_bottom_disp = curve_disp_c180;

                        THisHelper.SetCurveLabels(
                            curve_bottom_disp,
                            `${occupant} Bottom Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_bottom_disp],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        bottom_disp = `${curve_bottom_disp.ymax.toPrecision(6)}mm`;
                    }
                }

                return {
                    curves: [curve_upper_disp, curve_mid_disp, curve_bottom_disp],
                    values: {
                        upper_disp: upper_disp,
                        mid_disp: mid_disp,
                        bottom_disp: bottom_disp
                    },
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: graph_title
                };

            case OccupantProduct.SID2:
                let upper_thorax_translation = raw_measurements.GetCurve(Measurement.UPPER_TRANSLATION);
                let mid_thorax_translation = raw_measurements.GetCurve(Measurement.MID_TRANSLATION);
                let lower_thorax_translation = raw_measurements.GetCurve(Measurement.LOWER_TRANSLATION);

                if (!upper_thorax_translation) {
                    WarningMessage(
                        `Unable to read upper thorax translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!mid_thorax_translation) {
                    WarningMessage(
                        `Unable to read mid thorax translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!lower_thorax_translation) {
                    WarningMessage(
                        `Unable to read lower thorax translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                /** @type {Curve} */
                let curve_upper_thorax_disp = null;
                let upper_thorax_disp = "";
                /** @type {Curve} */
                let curve_mid_thorax_disp = null;
                let mid_thorax_disp = "";
                /** @type {Curve} */
                let curve_bottom_thorax_disp = null;
                let bottom_thorax_disp = "";

                /* Assess each sensor */
                for (let sensor = 0; sensor < 3; sensor++) {
                    let curve_disp = null;

                    if (sensor == 0) {
                        curve_disp = upper_thorax_translation;
                    } else if (sensor == 1) {
                        curve_disp = mid_thorax_translation;
                    } else if (sensor == 2) {
                        curve_disp = lower_thorax_translation;
                    }

                    /* Convert length to mm */

                    let curve_disp_mm = Operate.Div(curve_disp, WorkflowUnits.LengthToMillimetresFactor(unit_system));
                    curve_disp_mm.RemoveFromGraph();

                    /* Compression is negative */

                    curve_disp_mm = Operate.Mul(curve_disp_mm, -1.0);
                    curve_disp_mm.RemoveFromGraph();

                    /* Convert time to seconds */

                    let curve_disp_s = Operate.Dix(curve_disp_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_s.RemoveFromGraph();

                    /* Filter with C180 */

                    let curve_disp_c180 = Operate.C180(curve_disp_s, 0.00001);
                    curve_disp_c180.RemoveFromGraph();

                    /* Convert back to model time */

                    curve_disp_c180 = Operate.Mux(curve_disp_c180, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_c180.RemoveFromGraph();

                    /* Assign displacement to the correct curve and calculate Viscous Criterion */

                    if (sensor == 0) {
                        curve_upper_thorax_disp = curve_disp_c180;

                        THisHelper.SetCurveLabels(
                            curve_upper_thorax_disp,
                            `${occupant} Upper Thorax Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle([curve_upper_thorax_disp], this.GetCurveColourByIndex(occupant_index));

                        upper_thorax_disp = `${curve_upper_thorax_disp.ymax.toPrecision(6)}mm`;
                    } else if (sensor == 1) {
                        curve_mid_thorax_disp = curve_disp_c180;

                        THisHelper.SetCurveLabels(
                            curve_mid_thorax_disp,
                            `${occupant} Middle Thorax Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_mid_thorax_disp],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH
                        );

                        mid_thorax_disp = `${curve_mid_thorax_disp.ymax.toPrecision(6)}mm`;
                    } else if (sensor == 2) {
                        curve_bottom_thorax_disp = curve_disp_c180;

                        THisHelper.SetCurveLabels(
                            curve_bottom_thorax_disp,
                            `${occupant} Bottom Thorax Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_bottom_thorax_disp],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        bottom_thorax_disp = `${curve_bottom_thorax_disp.ymax.toPrecision(6)}mm`;
                    }
                }

                return {
                    curves: [curve_upper_thorax_disp, curve_mid_thorax_disp, curve_bottom_thorax_disp],
                    values: {
                        upper_thorax_disp: upper_thorax_disp,
                        mid_thorax_disp: mid_thorax_disp,
                        bottom_thorax_disp: bottom_thorax_disp
                    },
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: graph_title
                };

            case OccupantProduct.THOR:
                /** @type {Curve} */
                let upper_x1 = null;
                /** @type {Curve} */
                let upper_y1 = null;
                /** @type {Curve} */
                let upper_z1 = null;
                /** @type {Curve} */
                let lower_x1 = null;
                /** @type {Curve} */
                let lower_y1 = null;
                /** @type {Curve} */
                let lower_z1 = null;
                /** @type {Curve} */
                let upper_x2 = null;
                /** @type {Curve} */
                let upper_y2 = null;
                /** @type {Curve} */
                let upper_z2 = null;
                /** @type {Curve} */
                let lower_x2 = null;
                /** @type {Curve} */
                let lower_y2 = null;
                /** @type {Curve} */
                let lower_z2 = null;

                if (side == "Left") {
                    upper_x1 = raw_measurements.GetCurve(Measurement.UPPER_LEFT_COMPRESSION_X_1);
                    upper_y1 = raw_measurements.GetCurve(Measurement.UPPER_LEFT_COMPRESSION_Y_1);
                    upper_z1 = raw_measurements.GetCurve(Measurement.UPPER_LEFT_COMPRESSION_Z_1);

                    lower_x1 = raw_measurements.GetCurve(Measurement.LOWER_LEFT_COMPRESSION_X_1);
                    lower_y1 = raw_measurements.GetCurve(Measurement.LOWER_LEFT_COMPRESSION_Y_1);
                    lower_z1 = raw_measurements.GetCurve(Measurement.LOWER_LEFT_COMPRESSION_Z_1);

                    upper_x2 = raw_measurements.GetCurve(Measurement.UPPER_LEFT_COMPRESSION_X_2);
                    upper_y2 = raw_measurements.GetCurve(Measurement.UPPER_LEFT_COMPRESSION_Y_2);
                    upper_z2 = raw_measurements.GetCurve(Measurement.UPPER_LEFT_COMPRESSION_Z_2);

                    lower_x2 = raw_measurements.GetCurve(Measurement.LOWER_LEFT_COMPRESSION_X_2);
                    lower_y2 = raw_measurements.GetCurve(Measurement.LOWER_LEFT_COMPRESSION_Y_2);
                    lower_z2 = raw_measurements.GetCurve(Measurement.LOWER_LEFT_COMPRESSION_Z_2);
                } else if (side == "Right") {
                    upper_x1 = raw_measurements.GetCurve(Measurement.UPPER_RIGHT_COMPRESSION_X_1);
                    upper_y1 = raw_measurements.GetCurve(Measurement.UPPER_RIGHT_COMPRESSION_Y_1);
                    upper_z1 = raw_measurements.GetCurve(Measurement.UPPER_RIGHT_COMPRESSION_Z_1);

                    lower_x1 = raw_measurements.GetCurve(Measurement.LOWER_RIGHT_COMPRESSION_X_1);
                    lower_y1 = raw_measurements.GetCurve(Measurement.LOWER_RIGHT_COMPRESSION_Y_1);
                    lower_z1 = raw_measurements.GetCurve(Measurement.LOWER_RIGHT_COMPRESSION_Z_1);

                    upper_x2 = raw_measurements.GetCurve(Measurement.UPPER_RIGHT_COMPRESSION_X_2);
                    upper_y2 = raw_measurements.GetCurve(Measurement.UPPER_RIGHT_COMPRESSION_Y_2);
                    upper_z2 = raw_measurements.GetCurve(Measurement.UPPER_RIGHT_COMPRESSION_Z_2);

                    lower_x2 = raw_measurements.GetCurve(Measurement.LOWER_RIGHT_COMPRESSION_X_2);
                    lower_y2 = raw_measurements.GetCurve(Measurement.LOWER_RIGHT_COMPRESSION_Y_2);
                    lower_z2 = raw_measurements.GetCurve(Measurement.LOWER_RIGHT_COMPRESSION_Z_2);
                } else {
                    ErrorMessage(`Invalid side '${side}' in <ProtocolAssessment.ReadChestCompression>`);
                    return null;
                }

                if (!upper_x1) {
                    WarningMessage(
                        `Unable to read upper ${side} x1 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!upper_y1) {
                    WarningMessage(
                        `Unable to read upper ${side} y1 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!upper_z1) {
                    WarningMessage(
                        `Unable to read upper ${side} z1 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                if (!lower_x1) {
                    WarningMessage(
                        `Unable to read lower ${side} x1 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!lower_y1) {
                    WarningMessage(
                        `Unable to read lower ${side} y1 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!lower_z1) {
                    WarningMessage(
                        `Unable to read lower ${side} z1 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                if (!upper_x2) {
                    WarningMessage(
                        `Unable to read upper ${side} x2 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!upper_y2) {
                    WarningMessage(
                        `Unable to read upper ${side} y2 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!upper_z2) {
                    WarningMessage(
                        `Unable to read upper ${side} z2 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                if (!lower_x2) {
                    WarningMessage(
                        `Unable to read lower ${side} x2 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!lower_y2) {
                    WarningMessage(
                        `Unable to read lower ${side} y2 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }
                if (!lower_z2) {
                    WarningMessage(
                        `Unable to read lower ${side} z2 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestCompression>`
                    );
                    return null;
                }

                /* Get the difference between each node */
                let curve_upper_diff_x = Operate.Sub(upper_x1, upper_x2);
                let curve_upper_diff_y = Operate.Sub(upper_y1, upper_y2);
                let curve_upper_diff_z = Operate.Sub(upper_z1, upper_z2);
                curve_upper_diff_x.RemoveFromGraph();
                curve_upper_diff_y.RemoveFromGraph();
                curve_upper_diff_z.RemoveFromGraph();

                let curve_lower_diff_x = Operate.Sub(lower_x1, lower_x2);
                let curve_lower_diff_y = Operate.Sub(lower_y1, lower_y2);
                let curve_lower_diff_z = Operate.Sub(lower_z1, lower_z2);
                curve_lower_diff_x.RemoveFromGraph();
                curve_lower_diff_y.RemoveFromGraph();
                curve_lower_diff_z.RemoveFromGraph();

                /* Get resultant */
                let curve_upper_vec = Operate.Vec(curve_upper_diff_x, curve_upper_diff_y, curve_upper_diff_z);
                let curve_lower_vec = Operate.Vec(curve_lower_diff_x, curve_lower_diff_y, curve_lower_diff_z);
                curve_upper_vec.RemoveFromGraph();
                curve_lower_vec.RemoveFromGraph();

                /* Convert displacement to mm */
                let curve_upper_mm = Operate.Div(curve_upper_vec, WorkflowUnits.LengthToMillimetresFactor(unit_system));
                let curve_lower_mm = Operate.Div(curve_lower_vec, WorkflowUnits.LengthToMillimetresFactor(unit_system));
                curve_upper_mm.RemoveFromGraph();
                curve_lower_mm.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_upper_s = Operate.Dix(curve_upper_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                let curve_lower_s = Operate.Dix(curve_lower_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_upper_s.RemoveFromGraph();
                curve_lower_s.RemoveFromGraph();

                /* Filter with C180 */
                let curve_upper_c180 = Operate.C180(curve_upper_s, 0.00001);
                let curve_lower_c180 = Operate.C180(curve_lower_s, 0.00001);
                curve_upper_c180.RemoveFromGraph();
                curve_lower_c180.RemoveFromGraph();

                /* Convert back to model time */
                let curve_upper = Operate.Mux(curve_upper_c180, WorkflowUnits.TimeToSecondsFactor(unit_system));
                let curve_lower = Operate.Mux(curve_lower_c180, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_upper.RemoveFromGraph();
                curve_lower.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_upper,
                    `${occupant} Chest Upper ${side} Compression`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Compression (mm)"
                );

                THisHelper.SetLineStyle([curve_upper], this.GetCurveColourByIndex(occupant_index));

                THisHelper.SetCurveLabels(
                    curve_lower,
                    `${occupant} Chest Lower ${side} Compression`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Compression (mm)"
                );

                THisHelper.SetLineStyle([curve_lower], this.GetCurveColourByIndex(occupant_index), LineStyle.DASH);

                let values = {};
                values[`${side}_upper_max_compression`] = `${curve_upper.ymax.toPrecision(6)}mm`;
                values[`${side}_lower_max_compression`] = `${curve_lower.ymax.toPrecision(6)}mm`;

                return {
                    curves: [curve_upper, curve_lower],
                    values: values,
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadChestCompression>`);
                return null;
        }
    }

    /**
     * Reads chest compression rate from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadChestCompressionRate(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Chest Compression Rate`;

        /* Get the compression output */

        let compression_output = this.ReadChestCompression(model, occupant, unit_system, occupant_index);
        if (!compression_output) return null;

        switch (occupant.product) {
            case OccupantProduct.HIII:
                if (compression_output.curves.length == 0) return null;

                let curve_disp = compression_output.curves[0];

                /* Convert time to seconds */
                let curve_disp_s = Operate.Dix(curve_disp, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_s.RemoveFromGraph();

                /* Convert from mm to metres */
                let curve_disp_m = Operate.Div(curve_disp_s, 1000.0);
                curve_disp_m.RemoveFromGraph();

                /* Differentiate to get deflection rate in m/s */
                let curve_disp_rate = Operate.Dif(curve_disp_m);
                curve_disp_rate.RemoveFromGraph();

                /* Convert back to model time */
                curve_disp_rate = Operate.Mux(curve_disp_rate, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_rate.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_disp_rate,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Compression Rate (m/s)"
                );

                THisHelper.SetLineStyle([curve_disp_rate], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_disp_rate],
                    values: {
                        max_chest_compression_rate: `${curve_disp_rate.ymax.toPrecision(6)}m/s`
                    },
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: graph_title
                };

            case OccupantProduct.WSID:
            case OccupantProduct.ES2RE:
            case OccupantProduct.SID2:
                if (compression_output.curves.length < 3) return null;

                /** @type {Curve} */
                let curve_upper_disp_rate = null;
                let upper_disp_rate = "";
                /** @type {Curve} */
                let curve_mid_disp_rate = null;
                let mid_disp_rate = "";
                /** @type {Curve} */
                let curve_bottom_disp_rate = null;
                let bottom_disp_rate = "";

                /* Assess each rib */
                for (let rib = 0; rib < 3; rib++) {
                    let curve_disp = compression_output.curves[rib];

                    /* Convert time to seconds */
                    let curve_disp_s = Operate.Dix(curve_disp, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_s.RemoveFromGraph();

                    /* Convert from mm to metres */
                    let curve_disp_m = Operate.Div(curve_disp_s, 1000.0);
                    curve_disp_m.RemoveFromGraph();

                    /* Differentiate to get deflection rate in m/s */
                    let curve_disp_rate = Operate.Dif(curve_disp_m);
                    curve_disp_rate.RemoveFromGraph();

                    /* Convert back to model time */
                    curve_disp_rate = Operate.Mux(curve_disp_rate, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_rate.RemoveFromGraph();

                    /* Assign to the correct curve */

                    if (rib == 0) {
                        curve_upper_disp_rate = curve_disp_rate;
                        THisHelper.SetCurveLabels(
                            curve_upper_disp_rate,
                            `${occupant} Upper Compression Rate`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression Rate (m/s)"
                        );

                        THisHelper.SetLineStyle([curve_disp_rate], this.GetCurveColourByIndex(occupant_index));

                        upper_disp_rate = `${curve_upper_disp_rate.ymax.toPrecision(6)}m/s`;
                    } else if (rib == 1) {
                        curve_mid_disp_rate = curve_disp_rate;
                        THisHelper.SetCurveLabels(
                            curve_mid_disp_rate,
                            `${occupant} Middle Compression Rate`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression Rate (m/s)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_disp_rate],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH
                        );

                        mid_disp_rate = `${curve_mid_disp_rate.ymax.toPrecision(6)}m/s`;
                    } else if (rib == 2) {
                        curve_bottom_disp_rate = curve_disp_rate;
                        THisHelper.SetCurveLabels(
                            curve_bottom_disp_rate,
                            `${occupant} Bottom Compression Rate`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression Rate (m/s)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_disp_rate],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        bottom_disp_rate = `${curve_bottom_disp_rate.ymax.toPrecision(6)}m/s`;
                    }
                }

                return {
                    curves: [curve_upper_disp_rate, curve_mid_disp_rate, curve_bottom_disp_rate],
                    values: {
                        upper_disp_rate: upper_disp_rate,
                        mid_disp_rate: mid_disp_rate,
                        bottom_disp_rate: bottom_disp_rate
                    },
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadChestCompressionRate>`);
                return null;
        }
    }

    /**
     * Reads chest viscous criterion from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadChestViscousCriterion(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Chest Viscous Criterion`;

        /* Get the compression output */

        let compression_output = this.ReadChestCompression(model, occupant, unit_system, occupant_index);
        if (!compression_output) return null;

        /* Viscous Criterion constants */

        let viscous_criterion_constants = this.GetViscousCriterionConstants();
        if (!viscous_criterion_constants) return null;

        switch (occupant.product) {
            case OccupantProduct.HIII:
                if (compression_output.curves.length == 0) return null;
                let curve_disp = compression_output.curves[0];

                /* Convert time to seconds */
                let curve_disp_s = Operate.Dix(curve_disp, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_s.RemoveFromGraph();

                /* Convert from mm to metres */
                let curve_disp_m = Operate.Div(curve_disp_s, 1000.0);
                curve_disp_m.RemoveFromGraph();

                let curve_viscous_criterion = Operate.Vc(
                    curve_disp_m,
                    viscous_criterion_constants.A,
                    viscous_criterion_constants.B,
                    "ECER95"
                );
                curve_viscous_criterion.RemoveFromGraph();

                /* Convert back to model time */
                curve_viscous_criterion = Operate.Mux(
                    curve_viscous_criterion,
                    WorkflowUnits.TimeToSecondsFactor(unit_system)
                );
                curve_viscous_criterion.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_viscous_criterion,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Velocity (m/s)"
                );

                THisHelper.SetLineStyle([curve_viscous_criterion], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_viscous_criterion],
                    values: {
                        max_viscous_criterion: `${curve_viscous_criterion.ymax.toPrecision(6)}m/s`
                    },
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: graph_title
                };

            case OccupantProduct.WSID:
            case OccupantProduct.ES2RE:
            case OccupantProduct.SID2:
                if (compression_output.curves.length < 3) return null;

                /** @type {Curve} */
                let curve_upper_vc = null;
                let upper_vc = "";
                /** @type {Curve} */
                let curve_mid_vc = null;
                let mid_vc = "";
                /** @type {Curve} */
                let curve_bottom_vc = null;
                let bottom_vc = "";

                /* Assess each rib */
                for (let rib = 0; rib < 3; rib++) {
                    let curve_disp = compression_output.curves[rib];

                    /* Convert time to seconds */
                    let curve_disp_s = Operate.Dix(curve_disp, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_s.RemoveFromGraph();

                    /* Convert from mm to metres */
                    let curve_disp_m = Operate.Div(curve_disp_s, 1000.0);
                    curve_disp_m.RemoveFromGraph();

                    let curve_viscous_criterion = Operate.Vc(
                        curve_disp_m,
                        viscous_criterion_constants.A,
                        viscous_criterion_constants.B,
                        "ECER95"
                    );
                    curve_viscous_criterion.RemoveFromGraph();

                    /* Convert back to model time */
                    curve_viscous_criterion = Operate.Mux(
                        curve_viscous_criterion,
                        WorkflowUnits.TimeToSecondsFactor(unit_system)
                    );
                    curve_viscous_criterion.RemoveFromGraph();

                    /* Assign displacement to the correct curve and calculate Viscous Criterion */

                    if (rib == 0) {
                        curve_upper_vc = curve_viscous_criterion;
                        THisHelper.SetCurveLabels(
                            curve_upper_vc,
                            `${occupant} Upper Chest Viscous Criterion`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Velocity (m/s)"
                        );

                        THisHelper.SetLineStyle([curve_viscous_criterion], this.GetCurveColourByIndex(occupant_index));

                        upper_vc = `${curve_upper_vc.ymax.toPrecision(6)}m/s`;
                    } else if (rib == 1) {
                        curve_mid_vc = curve_viscous_criterion;
                        THisHelper.SetCurveLabels(
                            curve_mid_vc,
                            `${occupant} Middle Chest Viscous Criterion`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Velocity (m/s)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_viscous_criterion],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH
                        );

                        mid_vc = `${curve_mid_vc.ymax.toPrecision(6)}m/s`;
                    } else if (rib == 2) {
                        curve_bottom_vc = curve_viscous_criterion;
                        THisHelper.SetCurveLabels(
                            curve_bottom_vc,
                            `${occupant} Bottom Chest Viscous Criterion`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Velocity (m/s)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_viscous_criterion],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        bottom_vc = `${curve_bottom_vc.ymax.toPrecision(6)}m/s`;
                    }
                }

                return {
                    curves: [curve_upper_vc, curve_mid_vc, curve_bottom_vc],
                    values: {
                        upper_vc: upper_vc,
                        mid_vc: mid_vc,
                        bottom_vc: bottom_vc
                    },
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadChestViscousCriterion>`);
                return null;
        }
    }

    /**
     * Reads chest acceleration from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {object} [options] Options
     * @returns {?ReadBodyPartOutput}
     */
    ReadChestAcceleration(model, occupant, unit_system, occupant_index, options) {
        /* Graph title - also used in curve labels */

        let graph_title = `Chest Acceleration`;

        /* Get the raw measurements from the chest */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.CHEST);

        /* Process them to get compression */

        switch (`${occupant.product}${occupant.physiology}`) {
            case `${OccupantProduct.HIII}${OccupantPhysiology.M50}`:
                let x_acceleration = raw_measurements.GetCurve(Measurement.X_ACCELERATION);
                let y_acceleration = raw_measurements.GetCurve(Measurement.Y_ACCELERATION);
                let z_acceleration = raw_measurements.GetCurve(Measurement.Z_ACCELERATION);

                if (!x_acceleration) {
                    WarningMessage(
                        `Unable to read x acceleration from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestAcceleration>`
                    );
                    return null;
                }
                if (!y_acceleration) {
                    WarningMessage(
                        `Unable to read y acceleration from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestAcceleration>`
                    );
                    return null;
                }
                if (!z_acceleration) {
                    WarningMessage(
                        `Unable to read z acceleration from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadChestAcceleration>`
                    );
                    return null;
                }
                /* Convert acceleration to g */

                let curve_x_g = Operate.Div(x_acceleration, WorkflowUnits.GravityConstant(unit_system));
                curve_x_g.RemoveFromGraph();

                let curve_y_g = Operate.Div(y_acceleration, WorkflowUnits.GravityConstant(unit_system));
                curve_y_g.RemoveFromGraph();

                let curve_z_g = Operate.Div(z_acceleration, WorkflowUnits.GravityConstant(unit_system));
                curve_z_g.RemoveFromGraph();

                /* Convert time to seconds */

                let curve_x_g_s = Operate.Dix(curve_x_g, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_x_g_s.RemoveFromGraph();

                let curve_y_g_s = Operate.Dix(curve_y_g, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_y_g_s.RemoveFromGraph();

                let curve_z_g_s = Operate.Dix(curve_z_g, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_z_g_s.RemoveFromGraph();

                /* Filter with C1000 */

                let curve_x_c1000 = Operate.C1000(curve_x_g_s, 0.00001);
                curve_x_c1000.RemoveFromGraph();

                let curve_y_c1000 = Operate.C1000(curve_y_g_s, 0.00001);
                curve_y_c1000.RemoveFromGraph();

                let curve_z_c1000 = Operate.C1000(curve_z_g_s, 0.00001);
                curve_z_c1000.RemoveFromGraph();

                /* Vector combine */

                let curve_vec = Operate.Vec(curve_x_c1000, curve_y_c1000, curve_z_c1000);
                curve_vec.RemoveFromGraph();

                /* Convert back to model time */

                let curve_acceleration = Operate.Mux(curve_vec, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_acceleration.RemoveFromGraph();

                let values = {};

                /* Do a three ms calculation */
                if (options && options.three_ms) {
                    Operate.Tms(curve_acceleration, 0.003 * WorkflowUnits.TimeToSecondsFactor(unit_system));

                    values["tms"] = `${curve_acceleration.tms.toPrecision(6)}g`;
                    values["tms_tmin"] = `${curve_acceleration.tms_tmin.toPrecision(6)}g`;
                    values["tms_tmax"] = `${curve_acceleration.tms_tmax.toPrecision(6)}g`;

                    curve_acceleration.RemoveFromGraph();
                }

                THisHelper.SetCurveLabels(
                    curve_acceleration,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Acceleration (g)"
                );

                THisHelper.SetLineStyle([curve_acceleration], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_acceleration],
                    values: values,
                    body_part: OccupantBodyPart.CHEST,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadChestAcceleration>`);
                return null;
        }
    }

    /**
     * Reads shoulder deflection from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadShoulderDeflection(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Shoulder Compression`;

        /* Get the raw measurements from the shoulder */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.SHOULDER);

        /* Process them to get deflections */

        switch (occupant.product) {
            case OccupantProduct.SID2:
                let deflection = raw_measurements.GetCurve(Measurement.DEFLECTION);

                if (!deflection) {
                    WarningMessage(
                        `Unable to read deflection from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadShoulderDeflection>`
                    );
                    return null;
                }

                /* Convert length to mm */
                let curve_disp_mm = Operate.Div(deflection, WorkflowUnits.LengthToMillimetresFactor(unit_system));
                curve_disp_mm.RemoveFromGraph();

                /* Compression is negative */
                curve_disp_mm = Operate.Mul(curve_disp_mm, -1.0);
                curve_disp_mm.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_disp_s = Operate.Dix(curve_disp_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_s.RemoveFromGraph();

                /* Filter with C180 */
                let curve_disp_c180 = Operate.C180(curve_disp_s, 0.00001);
                curve_disp_c180.RemoveFromGraph();

                /* Convert back to model time */
                let curve_disp = Operate.Mux(curve_disp_c180, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_disp,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Compression (mm)"
                );

                THisHelper.SetLineStyle([curve_disp], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_disp],
                    values: {
                        max_compression: `${curve_disp.ymax.toPrecision(6)}mm`
                    },
                    body_part: OccupantBodyPart.SHOULDER,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadShoulderDeflection>`);
                return null;
        }
    }

    /**
     * Reads shoulder deflection rate from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadShoulderDeflectionRate(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Shoulder Compression Rate`;

        /* Get the deflection output */

        let deflection_output = this.ReadShoulderDeflection(model, occupant, unit_system, occupant_index);
        if (!deflection_output) return null;

        switch (occupant.product) {
            case OccupantProduct.SID2:
                if (deflection_output.curves.length == 0) return null;
                let curve_disp = deflection_output.curves[0];

                /* Convert time to seconds */
                let curve_disp_s = Operate.Dix(curve_disp, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_s.RemoveFromGraph();

                /* Convert from mm to metres */
                let curve_disp_m = Operate.Div(curve_disp_s, 1000.0);
                curve_disp_m.RemoveFromGraph();

                /* Differentiate to get deflection rate in m/s */
                let curve_disp_rate = Operate.Dif(curve_disp_m);
                curve_disp_rate.RemoveFromGraph();

                /* Convert back to model time */
                curve_disp_rate = Operate.Mux(curve_disp_rate, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_rate.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_disp_rate,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Compression Rate (m/s)"
                );

                THisHelper.SetLineStyle([curve_disp_rate], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_disp_rate],
                    values: {
                        max_compression_rate: `${curve_disp_rate.ymax.toPrecision(6)}m/s`
                    },
                    body_part: OccupantBodyPart.SHOULDER,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadShoulderCompressionRate>`);
                return null;
        }
    }

    /**
     * Reads shoulder viscous criterion from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadShoulderViscousCriterion(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Shoulder Viscous Criterion`;

        /* Get the deflection output */

        let deflection_output = this.ReadShoulderDeflection(model, occupant, unit_system, occupant_index);
        if (!deflection_output) return null;

        /* Viscous Criterion constants */

        let viscous_criterion_constants = this.GetViscousCriterionConstants();
        if (!viscous_criterion_constants) return null;

        switch (occupant.product) {
            case OccupantProduct.SID2:
                if (deflection_output.curves.length == 0) return null;
                let curve_disp = deflection_output.curves[0];

                /* Convert time to seconds */
                let curve_disp_s = Operate.Dix(curve_disp, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_s.RemoveFromGraph();

                /* Convert from mm to metres */
                let curve_disp_m = Operate.Div(curve_disp_s, 1000.0);
                curve_disp_m.RemoveFromGraph();

                let curve_viscous_criterion = Operate.Vc(
                    curve_disp_m,
                    viscous_criterion_constants.A,
                    viscous_criterion_constants.B,
                    "ECER95"
                );
                curve_viscous_criterion.RemoveFromGraph();

                /* Convert back to model time */
                curve_viscous_criterion = Operate.Mux(
                    curve_viscous_criterion,
                    WorkflowUnits.TimeToSecondsFactor(unit_system)
                );
                curve_viscous_criterion.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_viscous_criterion,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Velocity (m/s)"
                );

                THisHelper.SetLineStyle([curve_viscous_criterion], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_viscous_criterion],
                    values: {
                        max_viscous_criterion: `${curve_viscous_criterion.ymax.toPrecision(6)}m/s`
                    },
                    body_part: OccupantBodyPart.SHOULDER,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadShoulderViscousCriterion>`);
                return null;
        }
    }

    /**
     * Reads shoulder lateral forces from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadShoulderLateralForces(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Shoulder Lateral Force`;

        /* Get the raw measurements from the shoulder */

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.SHOULDER);

        /* Process them to get deflections */

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let left_force = raw_measurements.GetCurve(Measurement.LEFT_FORCE);
                let right_force = raw_measurements.GetCurve(Measurement.RIGHT_FORCE);

                if (!left_force) {
                    WarningMessage(
                        `Unable to read left force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadShoulderLateralForces>`
                    );
                    return null;
                }
                if (!right_force) {
                    WarningMessage(
                        `Unable to read right force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadShoulderLateralForces>`
                    );
                    return null;
                }

                /** @type {Curve} */
                let curve_left_force = null;
                /** @type {Curve} */
                let curve_right_force = null;

                for (let side = 0; side < 2; side++) {
                    /** @type {Curve} */
                    let curve_force = null;

                    if (side == 0) {
                        curve_force = left_force;
                    } else if (side == 1) {
                        curve_force = right_force;
                    }

                    /* Convert force to kN */
                    let curve_force_kn = Operate.Div(curve_force, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                    curve_force_kn.RemoveFromGraph();

                    /* Convert time to seconds */
                    let curve_force_s = Operate.Dix(curve_force_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_force_s.RemoveFromGraph();

                    /* Filter with C600 */
                    let curve_force_c600 = Operate.C600(curve_force_s, 0.00001);
                    curve_force_c600.RemoveFromGraph();

                    /* Convert back to model time */
                    let curve_lateral_force = Operate.Mux(
                        curve_force_c600,
                        WorkflowUnits.TimeToSecondsFactor(unit_system)
                    );
                    curve_lateral_force.RemoveFromGraph();

                    /* Set the labels and line style */

                    if (side == 0) {
                        curve_left_force = curve_lateral_force;

                        THisHelper.SetCurveLabels(
                            curve_left_force,
                            `${occupant} Left Shoulder Lateral Force`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compressive Force (kN)"
                        );

                        THisHelper.SetLineStyle([curve_left_force], this.GetCurveColourByIndex(occupant_index));
                    } else if (side == 1) {
                        curve_right_force = curve_lateral_force;

                        THisHelper.SetCurveLabels(
                            curve_right_force,
                            `${occupant} Right Shoulder Lateral Force`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compressive Force (kN)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_right_force],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );
                    }
                }

                return {
                    curves: [curve_left_force, curve_right_force],
                    values: {
                        max_left_force: `${curve_left_force.ymax.toPrecision(6)}kN`,
                        max_right_force: `${curve_right_force.ymax.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.SHOULDER,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadShoulderLateralForces>`);
                return null;
        }
    }

    /**
     * Reads abdomen compression from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} [side] "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadAbdomenCompression(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Abdomen Compression`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.ABDOMEN);

        /* Process them to get compression */

        switch (occupant.product) {
            case OccupantProduct.WSID: {
                let upper_rotation = raw_measurements.GetCurve(Measurement.UPPER_ROTATION);
                let lower_rotation = raw_measurements.GetCurve(Measurement.LOWER_ROTATION);
                let upper_translation = raw_measurements.GetCurve(Measurement.UPPER_TRANSLATION);
                let lower_translation = raw_measurements.GetCurve(Measurement.LOWER_TRANSLATION);

                if (!upper_rotation) {
                    WarningMessage(
                        `Unable to read upper abdomen rotation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }
                if (!lower_rotation) {
                    WarningMessage(
                        `Unable to read lower abdomen rotation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }

                if (!upper_translation) {
                    WarningMessage(
                        `Unable to read upper abdomen translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }
                if (!lower_translation) {
                    WarningMessage(
                        `Unable to read lower abdomen translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }

                /* Assess upper and lower abdomen */

                /** @type {Curve} */
                let curve_upper_disp = null;
                let upper_disp = "";
                /** @type {Curve} */
                let curve_bottom_disp = null;
                let bottom_disp = "";

                for (let i = 0; i < 2; i++) {
                    /** @type {Curve} */
                    let curve_disp = null;
                    /** @type {Curve} */
                    let curve_rot = null;
                    let irtracc_length = 0;

                    if (i == 0) {
                        curve_disp = upper_translation;
                        curve_rot = upper_rotation;
                        irtracc_length = occupant.upper_abdomen_irtracc_length;
                    } else if (i == 1) {
                        curve_disp = lower_translation;
                        curve_rot = lower_rotation;
                        irtracc_length = occupant.bottom_abdomen_irtracc_length;
                    }

                    /* Add IR-TRACC length */

                    let curve_total_disp = Operate.Add(curve_disp, irtracc_length);
                    curve_total_disp.RemoveFromGraph();

                    /* Convert length to mm */

                    let curve_disp_mm = Operate.Div(
                        curve_total_disp,
                        WorkflowUnits.LengthToMillimetresFactor(unit_system)
                    );
                    curve_disp_mm.RemoveFromGraph();

                    /* Convert time to seconds */

                    let curve_disp_s = Operate.Dix(curve_disp_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_s.RemoveFromGraph();

                    let curve_rot_s = Operate.Dix(curve_rot, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_rot_s.RemoveFromGraph();

                    /* Filter with C180 */

                    let curve_disp_c180 = Operate.C180(curve_disp_s, 0.00001);
                    curve_disp_c180.RemoveFromGraph();

                    let curve_rot_c180 = Operate.C180(curve_rot_s, 0.00001);
                    curve_rot_c180.RemoveFromGraph();

                    /* Calculate the lateral displacement */

                    let curve_rot_sin_theta = Operate.Sin(curve_rot_c180);
                    curve_rot_sin_theta.RemoveFromGraph();

                    let curve_lat_disp = Operate.Mul(curve_disp_c180, curve_rot_sin_theta);
                    curve_lat_disp.RemoveFromGraph();

                    /* Assign displacement to the correct curve and calculate Viscous Criterion */

                    if (i == 0) {
                        curve_upper_disp = curve_lat_disp;

                        THisHelper.SetCurveLabels(
                            curve_upper_disp,
                            `${occupant} Upper Abdomen Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle([curve_upper_disp], this.GetCurveColourByIndex(occupant_index));

                        upper_disp = `${curve_upper_disp.ymax.toPrecision(6)}mm`;
                    } else if (i == 1) {
                        curve_bottom_disp = curve_lat_disp;

                        THisHelper.SetCurveLabels(
                            curve_bottom_disp,
                            `${occupant} Bottom Abdomen Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_bottom_disp],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        bottom_disp = `${curve_bottom_disp.ymax.toPrecision(6)}mm`;
                    }
                }

                return {
                    curves: [curve_upper_disp, curve_bottom_disp],
                    values: {
                        upper_abdomen_disp: upper_disp,
                        bottom_abdomen_disp: bottom_disp
                    },
                    body_part: OccupantBodyPart.ABDOMEN,
                    graph_title: graph_title
                };
            }

            case OccupantProduct.SID2: {
                let upper_translation = raw_measurements.GetCurve(Measurement.UPPER_TRANSLATION);
                let lower_translation = raw_measurements.GetCurve(Measurement.LOWER_TRANSLATION);

                if (!upper_translation) {
                    WarningMessage(
                        `Unable to read upper abdomen translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }
                if (!lower_translation) {
                    WarningMessage(
                        `Unable to read lower abdomen translation from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }

                /** @type {Curve} */
                let curve_upper_disp = null;
                let upper_disp = "";
                /** @type {Curve} */
                let curve_bottom_disp = null;
                let bottom_disp = "";

                /* Assess upper and lower abdomen */
                for (let i = 0; i < 2; i++) {
                    let curve_disp = null;

                    if (i == 0) {
                        curve_disp = upper_translation;
                    } else if (i == 1) {
                        curve_disp = lower_translation;
                    }

                    /* Convert length to mm */

                    let curve_disp_mm = Operate.Div(curve_disp, WorkflowUnits.LengthToMillimetresFactor(unit_system));
                    curve_disp_mm.RemoveFromGraph();

                    /* Compression is negative */

                    curve_disp_mm = Operate.Mul(curve_disp_mm, -1.0);
                    curve_disp_mm.RemoveFromGraph();

                    /* Convert time to seconds */

                    let curve_disp_s = Operate.Dix(curve_disp_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_s.RemoveFromGraph();

                    /* Filter with C180 */

                    let curve_disp_c180 = Operate.C180(curve_disp_s, 0.00001);
                    curve_disp_c180.RemoveFromGraph();

                    /* Convert back to model time */

                    curve_disp_c180 = Operate.Mux(curve_disp_c180, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_c180.RemoveFromGraph();

                    /* Assign displacement to the correct curve and calculate Viscous Criterion */

                    if (i == 0) {
                        curve_upper_disp = curve_disp_c180;

                        THisHelper.SetCurveLabels(
                            curve_upper_disp,
                            `${occupant} Upper Abdomen Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle([curve_upper_disp], this.GetCurveColourByIndex(occupant_index));

                        upper_disp = `${curve_upper_disp.ymax.toPrecision(6)}mm`;
                    } else if (i == 1) {
                        curve_bottom_disp = curve_disp_c180;

                        THisHelper.SetCurveLabels(
                            curve_bottom_disp,
                            `${occupant} Bottom Abdomen Compression`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Compression (mm)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_bottom_disp],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        bottom_disp = `${curve_bottom_disp.ymax.toPrecision(6)}mm`;
                    }
                }

                return {
                    curves: [curve_upper_disp, curve_bottom_disp],
                    values: {
                        upper_abdomen_disp: upper_disp,
                        bottom_abdomen_disp: bottom_disp
                    },
                    body_part: OccupantBodyPart.ABDOMEN,
                    graph_title: graph_title
                };
            }

            case OccupantProduct.THOR:
                /** @type {Curve} */
                let x1 = null;
                /** @type {Curve} */
                let y1 = null;
                /** @type {Curve} */
                let z1 = null;
                /** @type {Curve} */
                let x2 = null;
                /** @type {Curve} */
                let y2 = null;
                /** @type {Curve} */
                let z2 = null;

                if (side == "Left") {
                    x1 = raw_measurements.GetCurve(Measurement.LEFT_COMPRESSION_X_1);
                    y1 = raw_measurements.GetCurve(Measurement.LEFT_COMPRESSION_Y_1);
                    z1 = raw_measurements.GetCurve(Measurement.LEFT_COMPRESSION_Z_1);

                    x2 = raw_measurements.GetCurve(Measurement.LEFT_COMPRESSION_X_2);
                    y2 = raw_measurements.GetCurve(Measurement.LEFT_COMPRESSION_Y_2);
                    z2 = raw_measurements.GetCurve(Measurement.LEFT_COMPRESSION_Z_2);
                } else if (side == "Right") {
                    x1 = raw_measurements.GetCurve(Measurement.RIGHT_COMPRESSION_X_1);
                    y1 = raw_measurements.GetCurve(Measurement.RIGHT_COMPRESSION_Y_1);
                    z1 = raw_measurements.GetCurve(Measurement.RIGHT_COMPRESSION_Z_1);

                    x2 = raw_measurements.GetCurve(Measurement.RIGHT_COMPRESSION_X_2);
                    y2 = raw_measurements.GetCurve(Measurement.RIGHT_COMPRESSION_Y_2);
                    z2 = raw_measurements.GetCurve(Measurement.RIGHT_COMPRESSION_Z_2);
                } else {
                    ErrorMessage(`Invalid side '${side}' in <ProtocolAssessment.ReadAbdomenCompression>`);
                    return null;
                }

                if (!x1) {
                    WarningMessage(
                        `Unable to read ${side} x1 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }
                if (!y1) {
                    WarningMessage(
                        `Unable to read ${side} y1 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }
                if (!z1) {
                    WarningMessage(
                        `Unable to read ${side} z1 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }

                if (!x2) {
                    WarningMessage(
                        `Unable to read ${side} x2 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }
                if (!y2) {
                    WarningMessage(
                        `Unable to read ${side} y2 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }
                if (!z2) {
                    WarningMessage(
                        `Unable to read ${side} z2 displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenCompression>`
                    );
                    return null;
                }

                /* Get the difference between each node */
                let curve_diff_x = Operate.Sub(x1, x2);
                let curve_diff_y = Operate.Sub(y1, y2);
                let curve_diff_z = Operate.Sub(z1, z2);
                curve_diff_x.RemoveFromGraph();
                curve_diff_y.RemoveFromGraph();
                curve_diff_z.RemoveFromGraph();

                /* Get resultant */
                let curve_vec = Operate.Vec(curve_diff_x, curve_diff_y, curve_diff_z);
                curve_vec.RemoveFromGraph();

                /* Convert displacement to mm */
                let curve_mm = Operate.Div(curve_vec, WorkflowUnits.LengthToMillimetresFactor(unit_system));
                curve_mm.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_s = Operate.Dix(curve_mm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_s.RemoveFromGraph();

                /* Filter with C180 */
                let curve_c180 = Operate.C180(curve_s, 0.00001);
                curve_c180.RemoveFromGraph();

                /* Convert back to model time */
                let curve_disp = Operate.Mux(curve_c180, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_disp,
                    `${occupant} Abdomen ${side} Compression`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Compression (mm)"
                );

                THisHelper.SetLineStyle([curve_disp], this.GetCurveColourByIndex(occupant_index));

                let values = {};
                values[`${side}_max_compression`] = `${curve_disp.ymax.toPrecision(6)}mm`;

                return {
                    curves: [curve_disp],
                    values: values,
                    body_part: OccupantBodyPart.ABDOMEN,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadAbdomenCompression>`);
                return null;
        }
    }

    /**
     * Reads abdomen viscous criterion from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadAbdomenViscousCriterion(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Abdomen Viscous Criterion`;

        /* Get the compression output */

        let compression_output = this.ReadAbdomenCompression(model, occupant, unit_system, occupant_index);
        if (!compression_output) return null;

        /* Viscous Criterion constants */

        let viscous_criterion_constants = this.GetViscousCriterionConstants();
        if (!viscous_criterion_constants) return null;

        switch (occupant.product) {
            case OccupantProduct.SID2:
            case OccupantProduct.WSID:
                if (compression_output.curves.length < 2) return null;

                /** @type {Curve} */
                let curve_upper_vc = null;
                let upper_vc = "";
                /** @type {Curve} */
                let curve_bottom_vc = null;
                let bottom_vc = "";

                /* Assess upper and lower abdomen */
                for (let i = 0; i < 2; i++) {
                    let curve_disp = compression_output.curves[i];

                    /* Convert time to seconds */
                    let curve_disp_s = Operate.Dix(curve_disp, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_disp_s.RemoveFromGraph();

                    /* Convert from mm to metres */
                    let curve_disp_m = Operate.Div(curve_disp_s, 1000.0);
                    curve_disp_m.RemoveFromGraph();

                    let curve_viscous_criterion = Operate.Vc(
                        curve_disp_m,
                        viscous_criterion_constants.A,
                        viscous_criterion_constants.B,
                        "ECER95"
                    );
                    curve_viscous_criterion.RemoveFromGraph();

                    /* Convert back to model time */
                    curve_viscous_criterion = Operate.Mux(
                        curve_viscous_criterion,
                        WorkflowUnits.TimeToSecondsFactor(unit_system)
                    );
                    curve_viscous_criterion.RemoveFromGraph();

                    /* Assign displacement to the correct curve and calculate Viscous Criterion */

                    if (i == 0) {
                        curve_upper_vc = curve_viscous_criterion;
                        THisHelper.SetCurveLabels(
                            curve_upper_vc,
                            `${occupant} Upper Abdomen Viscous Criterion`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Velocity (m/s)"
                        );

                        THisHelper.SetLineStyle([curve_viscous_criterion], this.GetCurveColourByIndex(occupant_index));

                        upper_vc = `${curve_upper_vc.ymax.toPrecision(6)}m/s`;
                    } else if (i == 1) {
                        curve_bottom_vc = curve_viscous_criterion;
                        THisHelper.SetCurveLabels(
                            curve_bottom_vc,
                            `${occupant} Bottom Abdomen Viscous Criterion`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Velocity (m/s)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_viscous_criterion],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        bottom_vc = `${curve_bottom_vc.ymax.toPrecision(6)}m/s`;
                    }
                }

                return {
                    curves: [curve_upper_vc, curve_bottom_vc],
                    values: {
                        upper_vc: upper_vc,
                        bottom_vc: bottom_vc
                    },
                    body_part: OccupantBodyPart.ABDOMEN,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadAbdomenViscousCriterion>`);
                return null;
        }
    }

    /**
     * Reads abdomen forces from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadAbdomenForce(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Abdomen Force`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.ABDOMEN);

        /* Process them to get force */

        switch (occupant.product) {
            case OccupantProduct.ES2RE:
                let front_force = raw_measurements.GetCurve(Measurement.FRONT_FORCE);
                let mid_force = raw_measurements.GetCurve(Measurement.MID_FORCE);
                let back_force = raw_measurements.GetCurve(Measurement.BACK_FORCE);

                if (!front_force) {
                    WarningMessage(
                        `Unable to read front abdomen force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenForce>`
                    );
                    return null;
                }
                if (!mid_force) {
                    WarningMessage(
                        `Unable to read mid abdomen force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenForce>`
                    );
                    return null;
                }
                if (!back_force) {
                    WarningMessage(
                        `Unable to read back abdomen force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAbdomenForce>`
                    );
                    return null;
                }

                /* Convert time to seconds */
                let curve_front_s = Operate.Dix(front_force, WorkflowUnits.TimeToSecondsFactor(unit_system));
                let curve_mid_s = Operate.Dix(mid_force, WorkflowUnits.TimeToSecondsFactor(unit_system));
                let curve_back_s = Operate.Dix(back_force, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_front_s.RemoveFromGraph();
                curve_mid_s.RemoveFromGraph();
                curve_back_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_front_c600 = Operate.C600(curve_front_s, 0.00001);
                let curve_mid_c600 = Operate.C600(curve_mid_s, 0.00001);
                let curve_back_c600 = Operate.C600(curve_back_s, 0.00001);
                curve_front_c600.RemoveFromGraph();
                curve_mid_c600.RemoveFromGraph();
                curve_back_c600.RemoveFromGraph();

                /* Convert to kN */
                let curve_front_kn = Operate.Div(curve_front_c600, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                let curve_mid_kn = Operate.Div(curve_mid_c600, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                let curve_back_kn = Operate.Div(curve_back_c600, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_front_kn.RemoveFromGraph();
                curve_mid_kn.RemoveFromGraph();
                curve_back_kn.RemoveFromGraph();

                /* Convert back to model time */
                curve_front_kn = Operate.Mux(curve_front_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_mid_kn = Operate.Mux(curve_mid_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_back_kn = Operate.Mux(curve_back_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_front_kn.RemoveFromGraph();
                curve_mid_kn.RemoveFromGraph();
                curve_back_kn.RemoveFromGraph();

                /* Sum all the forces */
                let curve_front_add_mid_kn = Operate.Add(curve_front_kn, curve_mid_kn);
                curve_front_add_mid_kn.RemoveFromGraph();
                let curve_force = Operate.Add(curve_front_add_mid_kn, curve_back_kn);

                THisHelper.SetCurveLabels(
                    curve_force,
                    `${occupant} Resultant Abdomen Force`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );
                THisHelper.SetCurveLabels(
                    curve_front_kn,
                    `${occupant} Front Abdomen Force`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );
                THisHelper.SetCurveLabels(
                    curve_mid_kn,
                    `${occupant} Middle Abdomen Force`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );
                THisHelper.SetCurveLabels(
                    curve_back_kn,
                    `${occupant} Back Abdomen Force`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_force], this.GetCurveColourByIndex(occupant_index));
                THisHelper.SetLineStyle([curve_front_kn], this.GetCurveColourByIndex(occupant_index), LineStyle.DASH);
                THisHelper.SetLineStyle([curve_mid_kn], this.GetCurveColourByIndex(occupant_index), LineStyle.DASH2);
                THisHelper.SetLineStyle([curve_back_kn], this.GetCurveColourByIndex(occupant_index), LineStyle.DASH3);

                let max_force = `${curve_force.ymax.toPrecision(6)}kN`;
                let min_force = `${curve_force.ymin.toPrecision(6)}kN`;

                return {
                    curves: [curve_force, curve_front_kn, curve_mid_kn, curve_back_kn],
                    values: {
                        max_force: max_force,
                        min_force: min_force
                    },
                    body_part: OccupantBodyPart.ABDOMEN,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadAbdomenForce>`);
                return null;
        }
    }

    /**
     * Reads lumbar shear force from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadLumbarShear(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Lumbar Shear`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.LUMBAR);

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let shear = raw_measurements.GetCurve(Measurement.SHEAR);
                if (!shear) {
                    WarningMessage(
                        `Unable to read shear force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadLumbarShear>`
                    );
                    return null;
                }

                /* Convert time to seconds */
                let curve_shear_s = Operate.Dix(shear, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_shear_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_shear_c600 = Operate.C600(curve_shear_s, 0.00001);
                curve_shear_c600.RemoveFromGraph();

                /* Convert to kN */
                let curve_shear_kn = Operate.Div(curve_shear_c600, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_shear_kn.RemoveFromGraph();

                /* Convert back to model time */
                curve_shear_kn = Operate.Mux(curve_shear_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_shear_kn.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_shear_kn,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_shear_kn], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_shear_kn],
                    values: {
                        max_shear: `${curve_shear_kn.ymax.toPrecision(6)}kN`,
                        min_shear: `${curve_shear_kn.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.LUMBAR,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadLumbarShear>`);
                return null;
        }
    }

    /**
     * Reads lumbar axial force from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadLumbarAxial(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Lumbar Axial`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.LUMBAR);

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let axial = raw_measurements.GetCurve(Measurement.AXIAL);
                if (!axial) {
                    WarningMessage(
                        `Unable to read axial force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadLumbarAxial>`
                    );
                    return null;
                }

                /* Convert time to seconds */
                let curve_axial_s = Operate.Dix(axial, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_axial_c600 = Operate.C600(curve_axial_s, 0.00001);
                curve_axial_c600.RemoveFromGraph();

                /* Convert to kN */
                let curve_axial_kn = Operate.Div(curve_axial_c600, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_axial_kn.RemoveFromGraph();

                /* Convert back to model time */
                curve_axial_kn = Operate.Mux(curve_axial_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial_kn.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_axial_kn,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_axial_kn], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_axial_kn],
                    values: {
                        max_axial: `${curve_axial_kn.ymax.toPrecision(6)}kN`,
                        min_axial: `${curve_axial_kn.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.LUMBAR,
                    graph_title: graph_title
                };
            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadLumbarAxial>`);
                return null;
        }
    }

    /**
     * Reads lumbar torsion from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadLumbarTorsion(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Lumbar Torsion`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.LUMBAR);

        switch (occupant.product) {
            case OccupantProduct.WSID:
                let torsion = raw_measurements.GetCurve(Measurement.TORSION);
                if (!torsion) {
                    WarningMessage(
                        `Unable to read torsion from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadLumbarTorsion>`
                    );
                    return null;
                }

                /* Convert time to seconds */
                let curve_torsion_s = Operate.Dix(torsion, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_torsion_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_torsion_c600 = Operate.C600(curve_torsion_s, 0.00001);
                curve_torsion_c600.RemoveFromGraph();

                /* Convert to Nm */
                let curve_torsion_nm = Operate.Div(
                    curve_torsion_c600,
                    WorkflowUnits.MomentToNewtonMetreFactor(unit_system)
                );
                curve_torsion_nm.RemoveFromGraph();

                /* Convert back to model time */
                curve_torsion_nm = Operate.Mux(curve_torsion_nm, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_torsion_nm.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_torsion_nm,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Torsion (Nm)"
                );

                THisHelper.SetLineStyle([curve_torsion_nm], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_torsion_nm],
                    values: {
                        max_torsion: `${curve_torsion_nm.ymax.toPrecision(6)}Nm`,
                        min_torsion: `${curve_torsion_nm.ymin.toPrecision(6)}Nm`
                    },
                    body_part: OccupantBodyPart.LUMBAR,
                    graph_title: graph_title
                };
            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadLumbarTorsion>`);
                return null;
        }
    }

    /**
     * Reads acetabular force from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} [side] "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadAcetabularForce(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Acetabular Force`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.PELVIS);

        switch (occupant.product) {
            case OccupantProduct.SID2:
                let acetabulum_force = raw_measurements.GetCurve(Measurement.ACETABULUM_FORCE);

                if (!acetabulum_force) {
                    WarningMessage(
                        `Unable to read acetabulum force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAcetabularForce>`
                    );
                    return null;
                }

                /* Convert time to seconds */
                let curve_acetabulum_force_s = Operate.Dix(
                    acetabulum_force,
                    WorkflowUnits.TimeToSecondsFactor(unit_system)
                );
                curve_acetabulum_force_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_acetabulum_force_c600 = Operate.C600(curve_acetabulum_force_s, 0.00001);
                curve_acetabulum_force_c600.RemoveFromGraph();

                /* Convert to kN */
                let curve_acetabulum_force_kn = Operate.Div(
                    curve_acetabulum_force_c600,
                    WorkflowUnits.ForceToKiloNewtonFactor(unit_system)
                );
                curve_acetabulum_force_kn.RemoveFromGraph();

                /* Convert back to model time */
                let curve_force = Operate.Mux(
                    curve_acetabulum_force_kn,
                    WorkflowUnits.TimeToSecondsFactor(unit_system)
                );
                curve_force.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_force,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_force], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_force],
                    values: {
                        max_acetabular_force: `${curve_force.ymax.toPrecision(6)}kN`,
                        min_acetabular_force: `${curve_force.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.PELVIS,
                    graph_title: graph_title
                };

            case OccupantProduct.THOR:
                /** @type {Curve} */
                let force_x = null;
                /** @type {Curve} */
                let force_y = null;
                /** @type {Curve} */
                let force_z = null;

                if (side == "Left") {
                    Message("AA");
                    force_x = raw_measurements.GetCurve(Measurement.LEFT_FORCE_X);
                    force_y = raw_measurements.GetCurve(Measurement.LEFT_FORCE_Y);
                    force_z = raw_measurements.GetCurve(Measurement.LEFT_FORCE_Z);
                    Message(force_x.id);
                } else if (side == "Right") {
                    force_x = raw_measurements.GetCurve(Measurement.RIGHT_FORCE_X);
                    force_y = raw_measurements.GetCurve(Measurement.RIGHT_FORCE_Y);
                    force_z = raw_measurements.GetCurve(Measurement.RIGHT_FORCE_Z);
                } else {
                    ErrorMessage(`Invalid side '${side}' in <ProtocolAssessment.ReadAcetabularForce>`);
                    return null;
                }

                if (!force_x) {
                    WarningMessage(
                        `Unable to read ${side} X force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAcetabularForce>`
                    );
                }

                if (!force_y) {
                    WarningMessage(
                        `Unable to read ${side} Y force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAcetabularForce>`
                    );
                }

                if (!force_z) {
                    WarningMessage(
                        `Unable to read ${side} Z force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadAcetabularForce>`
                    );
                }

                /* Resultant force */
                let curve_force_res = Operate.Vec(force_x, force_y, force_z);
                curve_force_res.RemoveFromGraph();

                /* For left pelvis,     when Fx>0, c_com = 1 * c_force_res; when Fx <= 0, c_com = 0 * c_force_res = 0;
                 * For right pelvis,    when Fx<0, c_com = 1 * c_force_res; when Fx >= 0, c_com = 0 * c_force_res = 0; */
                let curve_com_res = new Curve(Curve.FirstFreeID());

                if (side == "Left") {
                    for (let i = 1; i <= force_x.npoints; i++) {
                        let com_x_data = force_x.GetPoint(i);

                        if (com_x_data[1] > 0) {
                            curve_com_res.AddPoint(com_x_data[0], curve_force_res.GetPoint(i)[1]);
                        } else {
                            curve_com_res.AddPoint(com_x_data[0], 0);
                        }
                    }
                } else if (side == "Right") {
                    for (let i = 1; i <= force_x.npoints; i++) {
                        let com_x_data = force_x.GetPoint(i);

                        if (com_x_data[1] < 0) {
                            curve_com_res.AddPoint(com_x_data[0], curve_force_res.GetPoint(i)[1]);
                        } else {
                            curve_com_res.AddPoint(com_x_data[0], 0);
                        }
                    }
                }

                curve_com_res.RemoveFromGraph();

                /* Convert forces kN */
                let curve_com_kn = Operate.Div(curve_com_res, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_com_kn.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_com_s = Operate.Dix(curve_com_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_com_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_com_c600 = Operate.C600(curve_com_s, 0.00001);
                curve_com_c600.RemoveFromGraph();

                /* Convert back to model time */
                let curve_com = Operate.Mux(curve_com_c600, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_com.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_com,
                    `${occupant} ${side} Acetabulum Compression`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Compression (kN)"
                );

                THisHelper.SetLineStyle([curve_com], this.GetCurveColourByIndex(occupant_index));

                let values = {};
                values[`${side}_max_compression`] = `${curve_com.ymax.toPrecision(6)}kN`;

                return {
                    curves: [curve_com],
                    values: values,
                    body_part: OccupantBodyPart.PELVIS,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadAcetabularForce>`);
                return null;
        }
    }

    /**
     * Reads ilium force from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadIliumForce(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Ilium Force`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.PELVIS);

        switch (occupant.product) {
            case OccupantProduct.SID2:
                let iliac_force = raw_measurements.GetCurve(Measurement.ILIAC_FORCE);

                if (!iliac_force) {
                    WarningMessage(
                        `Unable to read iliac force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadIliumForce>`
                    );
                    return null;
                }

                /* Convert time to seconds */
                let curve_iliac_force_s = Operate.Dix(iliac_force, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_iliac_force_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_iliac_force_c600 = Operate.C600(curve_iliac_force_s, 0.00001);
                curve_iliac_force_c600.RemoveFromGraph();

                /* Convert to kN */
                let curve_iliac_force_kn = Operate.Div(
                    curve_iliac_force_c600,
                    WorkflowUnits.ForceToKiloNewtonFactor(unit_system)
                );
                curve_iliac_force_kn.RemoveFromGraph();

                /* Convert back to model time */
                let curve_force = Operate.Mux(curve_iliac_force_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_force.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_force,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_force], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_force],
                    values: {
                        max_ilium_force: `${curve_force.ymax.toPrecision(6)}kN`,
                        min_ilium_force: `${curve_force.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.PELVIS,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadIliumForce>`);
                return null;
        }
    }

    /**
     * Reads pelvis force from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @returns {?ReadBodyPartOutput}
     */
    ReadPelvisForce(model, occupant, unit_system, occupant_index) {
        /* Graph title - also used in curve labels */

        let graph_title = `Pelvis Force`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.PELVIS);

        switch (occupant.product) {
            case OccupantProduct.WSID:
            case OccupantProduct.ES2RE:
                let force = raw_measurements.GetCurve(Measurement.PUBIC_SYMPHYSIS_FORCE);
                if (!force) {
                    WarningMessage(
                        `Unable to read pelvis force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadPelvisForce>`
                    );
                    return null;
                }

                /* Convert time to seconds */
                let curve_force_s = Operate.Dix(force, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_force_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_force_c600 = Operate.C600(curve_force_s, 0.00001);
                curve_force_c600.RemoveFromGraph();

                /* Convert to kN */
                let curve_force_kn = Operate.Div(curve_force_c600, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_force_kn.RemoveFromGraph();

                /* Convert back to model time */
                curve_force_kn = Operate.Mux(curve_force_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_force_kn.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_force_kn,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_force_kn], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_force_kn],
                    values: {
                        max_force: `${curve_force_kn.ymax.toPrecision(6)}kN`,
                        min_force: `${curve_force_kn.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.PELVIS,
                    graph_title: graph_title
                };

            case OccupantProduct.SID2:
                let iliac_force = raw_measurements.GetCurve(Measurement.ILIAC_FORCE);
                let acetabulum_force = raw_measurements.GetCurve(Measurement.ACETABULUM_FORCE);

                if (!iliac_force) {
                    WarningMessage(
                        `Unable to read iliac force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadPelvisForce>`
                    );
                    return null;
                }
                if (!acetabulum_force) {
                    WarningMessage(
                        `Unable to read acetabulum force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadPelvisForce>`
                    );
                    return null;
                }

                /* Convert time to seconds */
                let curve_iliac_force_s = Operate.Dix(iliac_force, WorkflowUnits.TimeToSecondsFactor(unit_system));
                let curve_acetabulum_force_s = Operate.Dix(
                    acetabulum_force,
                    WorkflowUnits.TimeToSecondsFactor(unit_system)
                );
                curve_iliac_force_s.RemoveFromGraph();
                curve_acetabulum_force_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_iliac_force_c600 = Operate.C600(curve_iliac_force_s, 0.00001);
                let curve_acetabulum_force_c600 = Operate.C600(curve_acetabulum_force_s, 0.00001);
                curve_iliac_force_c600.RemoveFromGraph();
                curve_acetabulum_force_c600.RemoveFromGraph();

                /* Convert to kN */
                let curve_iliac_force_kn = Operate.Div(
                    curve_iliac_force_c600,
                    WorkflowUnits.ForceToKiloNewtonFactor(unit_system)
                );
                let curve_acetabulum_force_kn = Operate.Div(
                    curve_acetabulum_force_c600,
                    WorkflowUnits.ForceToKiloNewtonFactor(unit_system)
                );
                curve_iliac_force_kn.RemoveFromGraph();
                curve_acetabulum_force_kn.RemoveFromGraph();

                /* Convert back to model time */
                curve_iliac_force_kn = Operate.Mux(
                    curve_iliac_force_kn,
                    WorkflowUnits.TimeToSecondsFactor(unit_system)
                );
                curve_acetabulum_force_kn = Operate.Mux(
                    curve_acetabulum_force_kn,
                    WorkflowUnits.TimeToSecondsFactor(unit_system)
                );
                curve_iliac_force_kn.RemoveFromGraph();
                curve_acetabulum_force_kn.RemoveFromGraph();

                /* Add forces together */
                let curve_force = Operate.Add(curve_iliac_force_kn, curve_acetabulum_force_kn);
                curve_force.RemoveFromGraph();

                THisHelper.SetCurveLabels(
                    curve_force,
                    `${occupant} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_force], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_force],
                    values: {
                        max_force: `${curve_force.ymax.toPrecision(6)}kN`,
                        min_force: `${curve_force.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.PELVIS,
                    graph_title: graph_title
                };

            case OccupantProduct.THOR:
                /** @type {Curve} */
                let curve_left_force = null;
                /** @type {Curve} */
                let curve_right_force = null;

                /* Assess left and right acetabulum */
                for (let i = 0; i < 2; i++) {
                    let force_x = null;
                    let force_y = null;
                    let force_z = null;
                    let side = "";

                    if (i == 0) {
                        force_x = raw_measurements.GetCurve(Measurement.LEFT_FORCE_X);
                        force_y = raw_measurements.GetCurve(Measurement.LEFT_FORCE_Y);
                        force_z = raw_measurements.GetCurve(Measurement.LEFT_FORCE_Z);
                        side = "left";
                    } else if (i == 1) {
                        force_x = raw_measurements.GetCurve(Measurement.RIGHT_FORCE_X);
                        force_y = raw_measurements.GetCurve(Measurement.RIGHT_FORCE_Y);
                        force_z = raw_measurements.GetCurve(Measurement.RIGHT_FORCE_Z);
                        side = "right";
                    }

                    if (!force_x) {
                        WarningMessage(
                            `Unable to read acetabulum ${side} X force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadPelvisForce>`
                        );
                        return null;
                    }
                    if (!force_y) {
                        WarningMessage(
                            `Unable to read acetabulum ${side} Y force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadPelvisForce>`
                        );
                        return null;
                    }
                    if (!force_z) {
                        WarningMessage(
                            `Unable to read acetabulum ${side} Z force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadPelvisForce>`
                        );
                        return null;
                    }

                    /* Vector combine the forces */
                    let force = Operate.Vec(force_x, force_y, force_z);

                    /* Convert time to seconds */
                    let curve_force_s = Operate.Dix(force, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_force_s.RemoveFromGraph();

                    /* Filter with C600 */
                    let curve_force_c600 = Operate.C600(curve_force_s, 0.00001);
                    curve_force_c600.RemoveFromGraph();

                    /* Convert to kN */
                    let curve_force_kn = Operate.Div(
                        curve_force_c600,
                        WorkflowUnits.ForceToKiloNewtonFactor(unit_system)
                    );
                    curve_force_kn.RemoveFromGraph();

                    /* Convert back to model time */
                    curve_force_kn = Operate.Mux(curve_force_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_force_kn.RemoveFromGraph();

                    if (i == 0) {
                        curve_left_force = curve_force_kn;
                        THisHelper.SetCurveLabels(
                            curve_force_kn,
                            `${occupant} Left Acetabulum Force`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Force (kN)"
                        );

                        THisHelper.SetLineStyle([curve_force_kn], this.GetCurveColourByIndex(occupant_index));
                    } else if (i == 1) {
                        curve_right_force = curve_force_kn;
                        THisHelper.SetCurveLabels(
                            curve_force_kn,
                            `${occupant} Right Acetabulum Force`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Force (kN)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_force_kn],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );
                    }
                }

                return {
                    curves: [curve_left_force, curve_right_force],
                    values: {
                        max_left_force: `${curve_left_force.ymax.toPrecision(6)}kN`,
                        min_left_force: `${curve_left_force.ymin.toPrecision(6)}kN`,
                        max_right_force: `${curve_right_force.ymax.toPrecision(6)}kN`,
                        min_right_force: `${curve_right_force.ymin.toPrecision(6)}kN`
                    },
                    body_part: OccupantBodyPart.PELVIS,
                    graph_title: graph_title
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadPelvisForce>`);
                return null;
        }
    }

    /**
     * Reads femur axial force from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} side "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadFemurAxial(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Femur Axial`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.FEMUR);

        switch (occupant.supplier) {
            case OccupantSupplier.ATD:
            case OccupantSupplier.HUMANETICS:
            case OccupantSupplier.LSTC:
                /** @type {Curve} */
                let axial = null;

                if (side == "Left") {
                    axial = raw_measurements.GetCurve(Measurement.LEFT_AXIAL_FORCE);
                } else if (side == "Right") {
                    axial = raw_measurements.GetCurve(Measurement.RIGHT_AXIAL_FORCE);
                } else {
                    ErrorMessage(`Invalid side '${side}' in <ProtocolAssessment.ReadFemurAxial>`);
                    return null;
                }

                if (!axial) {
                    WarningMessage(
                        `Unable to read axial force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadFemurAxial>`
                    );
                    return null;
                }

                /* Convert forces kN */
                let curve_axial_kn = Operate.Div(axial, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_axial_kn.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_axial_s = Operate.Dix(curve_axial_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_axial_c600 = Operate.C600(curve_axial_s, 0.00001);
                curve_axial_c600.RemoveFromGraph();

                /* Convert back to model time */
                let curve_axial = Operate.Mux(curve_axial_c600, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_axial.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_axial,
                    `${occupant} ${side} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_axial], this.GetCurveColourByIndex(occupant_index));

                let values = {};
                values[`${side}_max_axial`] = `${curve_axial.ymax.toPrecision(6)}kN`;
                values[`${side}_min_axial`] = `${curve_axial.ymin.toPrecision(6)}kN`;

                return {
                    curves: [curve_axial],
                    values: values,
                    body_part: OccupantBodyPart.FEMUR,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadFemurAxial>`);
                return null;
        }
    }

    /**
     * Reads femur resultant force from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} side "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadFemurForce(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Femur Force`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.FEMUR);

        switch (occupant.supplier) {
            case OccupantSupplier.ATD:
            case OccupantSupplier.HUMANETICS:
            case OccupantSupplier.LSTC:
                /** @type {Curve} */
                let force_x = null;
                /** @type {Curve} */
                let force_y = null;
                /** @type {Curve} */
                let force_z = null;

                if (side == "Left") {
                    force_x = raw_measurements.GetCurve(Measurement.LEFT_FORCE_X);
                    force_y = raw_measurements.GetCurve(Measurement.LEFT_FORCE_Y);
                    force_z = raw_measurements.GetCurve(Measurement.LEFT_FORCE_Z);
                } else if (side == "Right") {
                    force_x = raw_measurements.GetCurve(Measurement.RIGHT_FORCE_X);
                    force_y = raw_measurements.GetCurve(Measurement.RIGHT_FORCE_Y);
                    force_z = raw_measurements.GetCurve(Measurement.RIGHT_FORCE_Z);
                } else {
                    ErrorMessage(`Invalid side '${side}' in <ProtocolAssessment.ReadFemurForce>`);
                    return null;
                }

                if (!force_x) {
                    WarningMessage(
                        `Unable to read X force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadFemurForce>`
                    );
                    return null;
                }
                if (!force_y) {
                    WarningMessage(
                        `Unable to read Y force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadFemurForce>`
                    );
                    return null;
                }
                if (!force_z) {
                    WarningMessage(
                        `Unable to read Z force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadFemurForce>`
                    );
                    return null;
                }

                /* Convert forces kN */
                let curve_force_x_kn = Operate.Div(force_x, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                let curve_force_y_kn = Operate.Div(force_y, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                let curve_force_z_kn = Operate.Div(force_z, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                curve_force_x_kn.RemoveFromGraph();
                curve_force_y_kn.RemoveFromGraph();
                curve_force_z_kn.RemoveFromGraph();

                /* Convert time to seconds */
                let curve_force_x_s = Operate.Dix(curve_force_x_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                let curve_force_y_s = Operate.Dix(curve_force_y_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                let curve_force_z_s = Operate.Dix(curve_force_z_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_force_x_s.RemoveFromGraph();
                curve_force_y_s.RemoveFromGraph();
                curve_force_z_s.RemoveFromGraph();

                /* Filter with C600 */
                let curve_force_x_c600 = Operate.C600(curve_force_x_s, 0.00001);
                let curve_force_y_c600 = Operate.C600(curve_force_y_s, 0.00001);
                let curve_force_z_c600 = Operate.C600(curve_force_z_s, 0.00001);
                curve_force_x_c600.RemoveFromGraph();
                curve_force_y_c600.RemoveFromGraph();
                curve_force_z_c600.RemoveFromGraph();

                /* Vector combine */
                let curve_force = Operate.Vec(curve_force_x_c600, curve_force_y_c600, curve_force_z_c600);
                curve_force.RemoveFromGraph();

                /* Convert back to model time */
                curve_force = Operate.Mux(curve_force, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_force.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_force,
                    `${occupant} ${side} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_force], this.GetCurveColourByIndex(occupant_index));

                let values = {};
                values[`${side}_max_force`] = `${curve_force.ymax.toPrecision(6)}kN`;
                values[`${side}_min_force`] = `${curve_force.ymin.toPrecision(6)}kN`;

                return {
                    curves: [curve_force],
                    values: values,
                    body_part: OccupantBodyPart.FEMUR,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadFemurForce>`);
                return null;
        }
    }

    /**
     * Reads femur compression exceedence from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} side "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadFemurCompressionExceedence(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Femur Compression Exceedence`;

        switch (occupant.supplier) {
            case OccupantSupplier.ATD:
            case OccupantSupplier.HUMANETICS:
            case OccupantSupplier.LSTC:
                let axial_output = this.ReadFemurAxial(model, occupant, unit_system, occupant_index, side);
                if (!axial_output) return null;
                if (axial_output.curves.length == 0) return null;

                let curve_axial = axial_output.curves[0];

                let curve_compression_exc = Operate.Exc(curve_axial, "negative");
                curve_compression_exc.RemoveFromGraph();

                let curve_compression_exc_abs = Operate.Abs(curve_compression_exc);
                curve_compression_exc_abs.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_compression_exc_abs,
                    `${occupant} ${side} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Force (kN)"
                );

                THisHelper.SetLineStyle([curve_compression_exc_abs], this.GetCurveColourByIndex(occupant_index));

                return {
                    curves: [curve_compression_exc_abs],
                    values: {},
                    body_part: OccupantBodyPart.FEMUR,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadFemurCompressionExceedence>`);
                return null;
        }
    }

    /**
     * Reads femur compression vs impulse from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} side "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadFemurCompressionVsImpulse(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Femur Compression vs. Impulse`;

        switch (occupant.supplier) {
            case OccupantSupplier.ATD:
            case OccupantSupplier.HUMANETICS:
            case OccupantSupplier.LSTC:
                let axial_output = this.ReadFemurAxial(model, occupant, unit_system, occupant_index, side);
                if (!axial_output) return null;
                if (axial_output.curves.length == 0) return null;

                let curve_axial = axial_output.curves[0];

                /* To calculate the Knee-Thigh-Hip (KTH) rating we need the peak compressive force
                 * and the time when the force last equals zero prior to the peak compressive force.
                 *
                 * The curve is then integrated from this time up to the time after the peak force when
                 * the compressive force equals 4.050kN. This is the force impulse.
                 */

                let peak_compression = -curve_axial.ymin;
                let time_of_peak = curve_axial.x_at_ymin;

                /* Find the last time when the force equals zero prior to the peak compressive force
                 * and the last time when the compressive force equals 4050N after the peak force (this new guide line as per the May 2021 test protocol) */

                let time_pre_peak_com = 0.0;
                let time_post_peak_com = 0.0;

                for (let i = 0; i < curve_axial.npoints; i++) {
                    let p = curve_axial.GetPoint(i);

                    if (p[0] < time_of_peak) {
                        if (p[1] >= 0.0) time_pre_peak_com = p[0];
                    }

                    if (p[0] > time_of_peak) {
                        if (p[1] <= -4.05) time_post_peak_com = p[0];
                    }
                }

                /* If the peak compressive force is the very last point, then <time_post_peak_com>
                 * will still be zero, so set it to <time_of_peak> */

                if (time_post_peak_com == 0.0) time_post_peak_com = time_of_peak;

                /* Clip the curve between these times and then integrate */

                let curve_clipped = Operate.Clip(curve_axial, time_pre_peak_com, time_post_peak_com, -1e20, 0.0);
                curve_clipped.RemoveFromGraph();

                let curve_int = Operate.Int(curve_clipped);
                curve_int.RemoveFromGraph();

                /* Convert from -ve kNs to +ve Ns */

                curve_int = Operate.Mul(curve_int, -1000.0);
                curve_int.RemoveFromGraph();

                /* Get last value to get impulse */

                let p = curve_int.GetPoint(curve_int.npoints);

                let impulse = p[1];

                /* Curve to show force v impulse point */

                let curve_force_v_impulse = new Curve(Curve.FirstFreeID());

                curve_force_v_impulse.AddPoint(peak_compression, impulse);
                curve_force_v_impulse.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_force_v_impulse,
                    `${occupant} ${side} ${graph_title}`,
                    `Force (kN)`,
                    "Impulse (Ns)"
                );

                THisHelper.SetLineStyle(
                    [curve_force_v_impulse],
                    this.GetCurveColourByIndex(occupant_index),
                    LineStyle.SOLID,
                    // @ts-ignore
                    Symbol.CROSS
                );

                let values = {};
                values[`${side}_peak_compression`] = `${peak_compression.toPrecision(6)}kN`;
                values[`${side}_impulse`] = `${impulse.toPrecision(6)}Ns`;

                return {
                    curves: [curve_force_v_impulse],
                    values: values,
                    body_part: OccupantBodyPart.FEMUR,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadFemurCompressionVsImpulse>`);
                return null;
        }
    }

    /**
     * Reads knee displacement from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} side "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadKneeDisplacement(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Knee Displacement`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.KNEE);

        switch (`${occupant.product}${occupant.physiology}`) {
            case `${OccupantProduct.HIII}${OccupantPhysiology.M50}`:
            case `${OccupantProduct.THOR}${OccupantPhysiology.M50}`:
                /** @type {Curve} */
                let curve_disp = null;

                if (side == "Left") {
                    curve_disp = raw_measurements.GetCurve(Measurement.LEFT_TRANSLATION);
                } else if (side == "Right") {
                    curve_disp = raw_measurements.GetCurve(Measurement.RIGHT_TRANSLATION);
                } else {
                    ErrorMessage(`Invalid side '${side}' in <ProtocolAssessment.ReadKneeDisplacement>`);
                    return null;
                }

                if (!curve_disp) {
                    WarningMessage(
                        `Unable to read displacement from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadKneeDisplacement>`
                    );
                    return null;
                }

                /* Convert time to seconds */
                let curve_disp_s = Operate.Dix(curve_disp, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_s.RemoveFromGraph();

                /* Convert displacement to mm */
                let curve_disp_mm = Operate.Div(curve_disp_s, WorkflowUnits.LengthToMillimetresFactor(unit_system));
                curve_disp_mm.RemoveFromGraph();

                /* Filter with C180 */
                let curve_disp_c180 = Operate.C180(curve_disp_mm, 0.00001);
                curve_disp_c180.RemoveFromGraph();

                /* Convert back to model time */
                curve_disp_c180 = Operate.Mux(curve_disp_c180, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_disp_c180.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_disp_c180,
                    `${occupant} ${side} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Displacment (mm)"
                );

                THisHelper.SetLineStyle(curve_disp_c180, this.GetCurveColourByIndex(occupant_index));

                let values = {};
                values[`${side}_max_displacement`] = `${curve_disp_c180.ymax.toPrecision(6)}mm`;

                return {
                    curves: [curve_disp_c180],
                    values: values,
                    body_part: OccupantBodyPart.KNEE,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadKneeDisplacement>`);
                return null;
        }
    }

    /**
     * Reads tibia compression force from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} side "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadTibiaCompression(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Tibia Compression`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.TIBIA);

        switch (`${occupant.product}${occupant.physiology}`) {
            case `${OccupantProduct.HIII}${OccupantPhysiology.M50}`:
            case `${OccupantProduct.THOR}${OccupantPhysiology.M50}`:
                /** @type {Curve} */
                let curve_upper_com = null;
                let upper_com = "";
                /** @type {Curve} */
                let curve_lower_com = null;
                let lower_com = "";

                /* Assess upper and lower tibia */
                for (let i = 0; i < 2; i++) {
                    /** @type {Curve} */
                    let curve_com = null;

                    if (side == "Left") {
                        if (i == 0) {
                            curve_com = raw_measurements.GetCurve(Measurement.LEFT_UPPER_AXIAL_FORCE);
                        } else {
                            curve_com = raw_measurements.GetCurve(Measurement.LEFT_LOWER_AXIAL_FORCE);
                        }
                    } else if (side == "Right") {
                        if (i == 0) {
                            curve_com = raw_measurements.GetCurve(Measurement.RIGHT_UPPER_AXIAL_FORCE);
                        } else {
                            curve_com = raw_measurements.GetCurve(Measurement.RIGHT_LOWER_AXIAL_FORCE);
                        }
                    } else {
                        ErrorMessage(`Invalid side '${side}' in <ProtocolAssessment.ReadTibiaCompression>`);
                        return null;
                    }

                    if (!curve_com) {
                        WarningMessage(
                            `Unable to read tibia compression force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadTibiaCompression>`
                        );
                        return null;
                    }

                    /* Convert time to seconds */
                    let curve_com_s = Operate.Dix(curve_com, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_com_s.RemoveFromGraph();

                    /* Filter with C600 */
                    let curve_com_c600 = Operate.C600(curve_com_s, 0.00001);
                    curve_com_c600.RemoveFromGraph();

                    /* Convert force to kN */
                    let curve_com_kn = Operate.Div(curve_com_c600, WorkflowUnits.ForceToKiloNewtonFactor(unit_system));
                    curve_com_kn.RemoveFromGraph();

                    /* Convert back to model time */
                    curve_com_kn = Operate.Mux(curve_com_kn, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_com_kn.RemoveFromGraph();

                    /* For THOR, multiply by -1 to make compression +ve */
                    if (occupant.product == OccupantProduct.THOR) {
                        curve_com_kn = Operate.Mul(curve_com_kn, -1);
                        curve_com_kn.RemoveFromGraph();
                    }

                    /* Assign to the correct curve */

                    if (i == 0) {
                        curve_upper_com = curve_com_kn;
                        THisHelper.SetCurveLabels(
                            curve_upper_com,
                            `${occupant} ${side} Upper ${graph_title}`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Force (kN)"
                        );

                        THisHelper.SetLineStyle([curve_com_kn], this.GetCurveColourByIndex(occupant_index));

                        upper_com = `${curve_com_kn.ymax.toPrecision(6)}kN`;
                    } else if (i == 1) {
                        curve_lower_com = curve_com_kn;
                        THisHelper.SetCurveLabels(
                            curve_lower_com,
                            `${occupant} ${side} Lower ${graph_title}`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Force (kN)"
                        );

                        THisHelper.SetLineStyle(
                            [curve_com_kn],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        lower_com = `${curve_com_kn.ymax.toPrecision(6)}kN`;
                    }
                }

                let values = {};
                values[`${side}_upper_compression`] = upper_com;
                values[`${side}_lower_compression`] = lower_com;

                return {
                    curves: [curve_upper_com, curve_lower_com],
                    values: values,
                    body_part: OccupantBodyPart.TIBIA,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadTibiaCompression>`);
                return null;
        }
    }

    /**
     * Reads tibia index from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} side "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadTibiaIndex(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Tibia Index`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.TIBIA);

        switch (`${occupant.product}${occupant.physiology}`) {
            case `${OccupantProduct.HIII}${OccupantPhysiology.M50}`:
            case `${OccupantProduct.THOR}${OccupantPhysiology.M50}`:
                /* Get compression curves for upper and lower tibia */
                let compression_output = this.ReadTibiaCompression(model, occupant, unit_system, occupant_index, side);
                if (!compression_output) return null;
                if (compression_output.curves.length < 2) return null;

                /** @type {Curve} */
                let curve_upper_ti = null;
                let upper_ti = "";
                /** @type {Curve} */
                let curve_lower_ti = null;
                let lower_ti = "";

                /* Get critical loads for Tibia Index calc */
                let tibia_index_critical_loads = occupant.GetTibiaIndexCriticalLoads();

                /* Assess upper and lower tibia */
                for (let i = 0; i < 2; i++) {
                    /** @type {Curve} */
                    let curve_axial = compression_output.curves[i];
                    /** @type {Curve} */
                    let curve_bending_y = null;
                    /** @type {Curve} */
                    let curve_bending_z = null;

                    if (side == "Left") {
                        if (i == 0) {
                            curve_bending_y = raw_measurements.GetCurve(Measurement.LEFT_UPPER_BENDING_X);
                            curve_bending_z = raw_measurements.GetCurve(Measurement.LEFT_UPPER_BENDING_Y);
                        } else {
                            curve_bending_y = raw_measurements.GetCurve(Measurement.LEFT_LOWER_BENDING_X);
                            curve_bending_z = raw_measurements.GetCurve(Measurement.LEFT_LOWER_BENDING_Y);
                        }
                    } else if (side == "Right") {
                        if (i == 0) {
                            curve_bending_y = raw_measurements.GetCurve(Measurement.RIGHT_UPPER_BENDING_X);
                            curve_bending_z = raw_measurements.GetCurve(Measurement.RIGHT_UPPER_BENDING_Y);
                        } else {
                            curve_bending_y = raw_measurements.GetCurve(Measurement.RIGHT_LOWER_BENDING_X);
                            curve_bending_z = raw_measurements.GetCurve(Measurement.RIGHT_LOWER_BENDING_Y);
                        }
                    } else {
                        ErrorMessage(`Invalid side '${side}' in <ProtocolAssessment.ReadTibiaIndex>`);
                        return null;
                    }

                    if (!curve_axial) {
                        WarningMessage(
                            `Unable to read axial force from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadTibiaIndex>`
                        );
                        return null;
                    }
                    if (!curve_bending_y) {
                        WarningMessage(
                            `Unable to read y bending moment from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadTibiaIndex>`
                        );
                        return null;
                    }
                    if (!curve_bending_z) {
                        WarningMessage(
                            `Unable to read z bending moment from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadTibiaIndex>`
                        );
                        return null;
                    }

                    /* Convert time to seconds */
                    let curve_axial_s = Operate.Dix(curve_axial, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    let curve_bending_y_s = Operate.Dix(
                        curve_bending_y,
                        WorkflowUnits.TimeToSecondsFactor(unit_system)
                    );
                    let curve_bending_z_s = Operate.Dix(
                        curve_bending_z,
                        WorkflowUnits.TimeToSecondsFactor(unit_system)
                    );
                    curve_axial_s.RemoveFromGraph();
                    curve_bending_y_s.RemoveFromGraph();
                    curve_bending_z_s.RemoveFromGraph();

                    /* Filter with C600 (axial is already filtered) */
                    let curve_bending_y_c600 = Operate.C600(curve_bending_y_s, 0.00001);
                    let curve_bending_z_c600 = Operate.C600(curve_bending_z_s, 0.00001);
                    curve_bending_y_c600.RemoveFromGraph();
                    curve_bending_z_c600.RemoveFromGraph();

                    /* Convert moments to Nm (axial is already in kN) */
                    let curve_bending_y_nm = Operate.Div(
                        curve_bending_y_c600,
                        WorkflowUnits.MomentToNewtonMetreFactor(unit_system)
                    );
                    let curve_bending_z_nm = Operate.Div(
                        curve_bending_z_c600,
                        WorkflowUnits.MomentToNewtonMetreFactor(unit_system)
                    );
                    curve_bending_y_nm.RemoveFromGraph();
                    curve_bending_z_nm.RemoveFromGraph();

                    /* Resultant moment */
                    let curve_bending_resultant = Operate.Vec2d(curve_bending_y_nm, curve_bending_z_nm);
                    curve_bending_resultant.RemoveFromGraph();

                    /* Convert back to model time */
                    let curve_axial_kn = Operate.Mux(curve_axial_s, WorkflowUnits.TimeToSecondsFactor(unit_system));
                    curve_bending_resultant = Operate.Mux(
                        curve_bending_resultant,
                        WorkflowUnits.TimeToSecondsFactor(unit_system)
                    );
                    curve_axial_kn.RemoveFromGraph();
                    curve_bending_resultant.RemoveFromGraph();

                    /* Calculate the Tibia Index = |Mr/Mrc| + |Fz/Fzc| */

                    let curve_tibia_index = new Curve(Curve.FirstFreeID());

                    let n = curve_bending_resultant.npoints;

                    for (let p = 1; p <= n; p++) {
                        let p_axial = curve_axial_kn.GetPoint(p);
                        let p_bending = curve_bending_resultant.GetPoint(p);

                        let time = p_bending[0];
                        let ti =
                            Math.abs(p_bending[1] / tibia_index_critical_loads.bending) +
                            Math.abs(p_axial[1] / tibia_index_critical_loads.compression);

                        curve_tibia_index.AddPoint(time, ti);
                    }

                    curve_tibia_index.RemoveFromGraph();

                    /* Assign to the correct curve */

                    if (i == 0) {
                        curve_upper_ti = curve_tibia_index;
                        THisHelper.SetCurveLabels(
                            curve_upper_ti,
                            `${occupant} ${side} Upper ${graph_title}`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Tibia Index"
                        );

                        THisHelper.SetLineStyle([curve_tibia_index], this.GetCurveColourByIndex(occupant_index));

                        /* For some reason curve_upper_ti.ymax is returning 0.0,
                         * so loop over all the points to get the max */
                        let max = 0.0;
                        for (let p = 1; p <= curve_upper_ti.npoints; p++) {
                            max = Math.max(curve_upper_ti.GetPoint(p)[1], max);
                        }
                        upper_ti = `${max.toPrecision(6)}`;
                    } else if (i == 1) {
                        curve_lower_ti = curve_tibia_index;
                        THisHelper.SetCurveLabels(
                            curve_lower_ti,
                            `${occupant} ${side} Lower ${graph_title}`,
                            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                            "Tibia Index"
                        );

                        THisHelper.SetLineStyle(
                            [curve_tibia_index],
                            this.GetCurveColourByIndex(occupant_index),
                            LineStyle.DASH2
                        );

                        /* For some reason curve_lower_ti.ymax is returning 0.0,
                         * so loop over all the points to get the max */
                        let max = 0.0;
                        for (let p = 1; p <= curve_lower_ti.npoints; p++) {
                            max = Math.max(curve_lower_ti.GetPoint(p)[1], max);
                        }
                        lower_ti = `${max.toPrecision(6)}`;
                    }
                }

                let values = {};
                values[`${side}_upper_tibia_index`] = upper_ti;
                values[`${side}_lower_tibia_index`] = lower_ti;

                return {
                    curves: [curve_upper_ti, curve_lower_ti],
                    values: values,
                    body_part: OccupantBodyPart.TIBIA,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadTibiaIndex>`);
                return null;
        }
    }

    /**
     * Reads foot acceleration from the given occupant
     * processing them according to the procedures set
     * out in the occupant manual, e.g. filtering the raw
     * measurements
     * @param {Model} model Model to read data from
     * @param {WorkflowOccupant} occupant WorkflowOccupant to read data from
     * @param {number} unit_system Unit system
     * @param {number} occupant_index Index of the curve style to use
     * @param {string} side "Left" or "Right" side
     * @returns {?ReadBodyPartOutput}
     */
    ReadFootAcceleration(model, occupant, unit_system, occupant_index, side) {
        /* Graph title - also used in curve labels */

        let graph_title = `Foot Acceleration`;

        let raw_measurements = occupant.ReadRawBodyPartMeasurements(model, OccupantBodyPart.FOOT);

        switch (`${occupant.product}${occupant.physiology}`) {
            case `${OccupantProduct.HIII}${OccupantPhysiology.M50}`:
                /** @type {Curve} */
                let curve_x = null;
                /** @type {Curve} */
                let curve_y = null;
                /** @type {Curve} */
                let curve_z = null;

                if (side == "Left") {
                    curve_x = raw_measurements.GetCurve(Measurement.LEFT_ACCELERATION_X);
                    curve_y = raw_measurements.GetCurve(Measurement.LEFT_ACCELERATION_Y);
                    curve_z = raw_measurements.GetCurve(Measurement.LEFT_ACCELERATION_Z);
                } else if (side == "Right") {
                    curve_x = raw_measurements.GetCurve(Measurement.RIGHT_ACCELERATION_X);
                    curve_y = raw_measurements.GetCurve(Measurement.RIGHT_ACCELERATION_Y);
                    curve_z = raw_measurements.GetCurve(Measurement.RIGHT_ACCELERATION_Z);
                } else {
                    ErrorMessage(`Invalid side '${side}' in <ProtocolAssessment.ReadFootAcceleration>`);
                    return null;
                }

                if (!curve_x) {
                    WarningMessage(
                        `Unable to read x acceleration from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadFootAcceleration>`
                    );
                    return null;
                }
                if (!curve_y) {
                    WarningMessage(
                        `Unable to read y acceleration from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadFootAcceleration>`
                    );
                    return null;
                }
                if (!curve_z) {
                    WarningMessage(
                        `Unable to read z acceleration from occupant M${model.id} ${occupant} in <ProtocolAssessment.ReadFootAcceleration>`
                    );
                    return null;
                }

                /* Convert acceleration to g */

                let curve_x_g = Operate.Div(curve_x, WorkflowUnits.GravityConstant(unit_system));
                let curve_y_g = Operate.Div(curve_y, WorkflowUnits.GravityConstant(unit_system));
                let curve_z_g = Operate.Div(curve_z, WorkflowUnits.GravityConstant(unit_system));

                curve_x_g.RemoveFromGraph();
                curve_y_g.RemoveFromGraph();
                curve_z_g.RemoveFromGraph();

                /* Convert time to seconds */

                let curve_x_g_s = Operate.Dix(curve_x_g, WorkflowUnits.TimeToSecondsFactor(unit_system));
                let curve_y_g_s = Operate.Dix(curve_y_g, WorkflowUnits.TimeToSecondsFactor(unit_system));
                let curve_z_g_s = Operate.Dix(curve_z_g, WorkflowUnits.TimeToSecondsFactor(unit_system));

                curve_x_g_s.RemoveFromGraph();
                curve_y_g_s.RemoveFromGraph();
                curve_z_g_s.RemoveFromGraph();

                /* Filter with C1000 */

                let curve_x_c1000 = Operate.C1000(curve_x_g_s, 0.00001);
                let curve_y_c1000 = Operate.C1000(curve_y_g_s, 0.00001);
                let curve_z_c1000 = Operate.C1000(curve_z_g_s, 0.00001);

                curve_x_c1000.RemoveFromGraph();
                curve_y_c1000.RemoveFromGraph();
                curve_z_c1000.RemoveFromGraph();

                /* Vector combine */

                let curve_vec = Operate.Vec(curve_x_c1000, curve_y_c1000, curve_z_c1000);
                curve_vec.RemoveFromGraph();

                /* Convert back to model time */

                curve_vec = Operate.Mux(curve_vec, WorkflowUnits.TimeToSecondsFactor(unit_system));
                curve_vec.RemoveFromGraph();

                /* Set the labels and line style */

                THisHelper.SetCurveLabels(
                    curve_vec,
                    `${occupant} ${side} ${graph_title}`,
                    `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
                    "Acceleration (g)"
                );

                THisHelper.SetLineStyle(curve_vec, this.GetCurveColourByIndex(occupant_index));

                let values = {};
                values[`${side}_peak_acceleration`] = `${curve_vec.ymax.toPrecision(6)}g`;

                return {
                    curves: [curve_vec],
                    values: values,
                    body_part: OccupantBodyPart.FOOT,
                    graph_title: `${side} ${graph_title}`
                };

            default:
                WarningMessage(`${occupant.name} not supported in <ProtocolAssessment.ReadFootAcceleration>`);
                return null;
        }
    }

    /**
     * Object to return from the structure Read functions
     * @typedef ReadStructureOutput
     * @property {?Curve[]} curves Output curves
     * @property {Object} values Object with values for the output
     * @property {string} graph_title Graph title (also used in curve labels)
     */

    /**
     * Reads vertical intrusion from the given structure
     * @param {Model} model Model to read data from
     * @param {Structure} structure Structure to read data from
     * @param {number} unit_system Unit system
     * @param {number} structure_index Index of the curve style to use
     * @param {string} [structure_name = null] Alternative structure name to use in graph title - required for assessment types that plot results from multiple structures on one graph, e.g. pedals
     * @returns {?ReadStructureOutput}
     */
    ReadVerticalIntrusion(model, structure, unit_system, structure_index, structure_name) {
        let name = structure_name ? structure_name : structure;

        let graph_title = `${name} Vertical Intrusion`;

        let raw_measurements = structure.ReadRawStructureMeasurements(model);

        let curve_intrusion = raw_measurements.GetCurve(Measurement.VERTICAL_INTRUSION);

        if (!curve_intrusion) {
            WarningMessage(
                `Unable to read deflection for structure M${model.id} ${structure} in <ProtocolAssessment.ReadVerticalIntrusion>`
            );
            return null;
        }

        /* Convert to mm */
        let curve_intrusion_mm = Operate.Div(curve_intrusion, WorkflowUnits.LengthToMillimetresFactor(unit_system));
        curve_intrusion_mm.RemoveFromGraph();

        /* Get the last value */
        let p = curve_intrusion_mm.GetPoint(curve_intrusion_mm.npoints);

        let intrusion = p[1];

        /* Set the labels and line style */

        THisHelper.SetCurveLabels(
            curve_intrusion_mm,
            `${structure} Vertical Intrusion`,
            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
            "Intrusion (mm)"
        );

        THisHelper.SetLineStyle([curve_intrusion_mm], this.GetCurveColourByIndex(structure_index));

        return {
            curves: [curve_intrusion_mm],
            values: {
                vertical_intrusion: `${intrusion.toPrecision(6)}mm`
            },
            graph_title: graph_title
        };
    }

    /**
     * Reads lateral intrusion from the given structure
     * @param {Model} model Model to read data from
     * @param {Structure} structure Structure to read data from
     * @param {number} unit_system Unit system
     * @param {number} structure_index Index of the curve style to use
     * @param {string} [structure_name = null] Alternative structure name to use in graph title - required for assessment types that plot results from multiple structures on one graph, e.g. pedals
     * @returns {?ReadStructureOutput}
     */
    ReadLateralIntrusion(model, structure, unit_system, structure_index, structure_name) {
        let name = structure_name ? structure_name : structure;

        let graph_title = `${name} Lateral Intrusion`;

        let raw_measurements = structure.ReadRawStructureMeasurements(model);

        let curve_intrusion = raw_measurements.GetCurve(Measurement.LATERAL_INTRUSION);

        if (!curve_intrusion) {
            WarningMessage(
                `Unable to read deflection for structure M${model.id} ${structure} in <ProtocolAssessment.ReadLateralIntrusion>`
            );
            return null;
        }

        /* Convert to mm */
        let curve_intrusion_mm = Operate.Div(curve_intrusion, WorkflowUnits.LengthToMillimetresFactor(unit_system));
        curve_intrusion_mm.RemoveFromGraph();

        /* Get the last value */
        let p = curve_intrusion_mm.GetPoint(curve_intrusion_mm.npoints);

        let intrusion = p[1];

        /* Set the labels and line style */

        THisHelper.SetCurveLabels(
            curve_intrusion_mm,
            `${structure} Lateral Intrusion`,
            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
            "Intrusion (mm)"
        );

        THisHelper.SetLineStyle([curve_intrusion_mm], this.GetCurveColourByIndex(structure_index));

        return {
            curves: [curve_intrusion_mm],
            values: {
                lateral_intrusion: `${intrusion.toPrecision(6)}mm`
            },
            graph_title: graph_title
        };
    }

    /**
     * Reads fore/aft intrusion from the given structure
     * @param {Model} model Model to read data from
     * @param {Structure} structure Structure to read data from
     * @param {number} unit_system Unit system
     * @param {number} structure_index Index of the curve style to use
     * @param {string} [structure_name = null] Alternative structure name to use in graph title - required for assessment types that plot results from multiple structures on one graph, e.g. pedals
     * @returns {?ReadStructureOutput}
     */
    ReadForeAftIntrusion(model, structure, unit_system, structure_index, structure_name) {
        let name = structure_name ? structure_name : structure;

        let graph_title = `${name} Fore/Aft Intrusion`;

        let raw_measurements = structure.ReadRawStructureMeasurements(model);

        let curve_intrusion = raw_measurements.GetCurve(Measurement.FORE_AFT_INTRUSION);

        if (!curve_intrusion) {
            WarningMessage(
                `Unable to read deflection for structure M${model.id} ${structure} in <ProtocolAssessment.ReadForeAftIntrusion>`
            );
            return null;
        }

        /* Fore/aft intrusion = -ve elongation is rearward so multiply by -1.0 first */
        curve_intrusion = Operate.Mul(curve_intrusion, -1.0);
        curve_intrusion.RemoveFromGraph();

        /* Convert to mm */
        let curve_intrusion_mm = Operate.Div(curve_intrusion, WorkflowUnits.LengthToMillimetresFactor(unit_system));
        curve_intrusion_mm.RemoveFromGraph();

        /* Get the last value */
        let p = curve_intrusion_mm.GetPoint(curve_intrusion_mm.npoints);

        let intrusion = p[1];

        /* Set the labels and line style */

        THisHelper.SetCurveLabels(
            curve_intrusion_mm,
            `${structure} Fore/Aft Intrusion`,
            `Time (${WorkflowUnits.TimeUnit(unit_system)})`,
            "Intrusion (mm)"
        );

        THisHelper.SetLineStyle([curve_intrusion_mm], this.GetCurveColourByIndex(structure_index));

        return {
            curves: [curve_intrusion_mm],
            values: {
                fore_aft_intrusion: `${intrusion.toPrecision(6)}mm`
            },
            graph_title: graph_title
        };
    }

    /**
     * Reads all intrusions (vertical, lateral and fore/aft) from the given structure
     * @param {Model} model Model to read data from
     * @param {Structure} structure Structure to read data from
     * @param {number} unit_system Unit system
     * @param {number} structure_index Index of the curve style to use
     * @returns {?ReadStructureOutput}
     */
    ReadAllIntrusion(model, structure, unit_system, structure_index) {
        let graph_title = `${structure} Intrusion`;

        let vertical_output = this.ReadVerticalIntrusion(model, structure, unit_system, structure_index);
        if (!vertical_output) return null;
        if (vertical_output.curves.length == 0) return null;

        let curve_vertical = vertical_output.curves[0];

        let lateral_output = this.ReadLateralIntrusion(model, structure, unit_system, structure_index);
        if (!lateral_output) return null;
        if (lateral_output.curves.length == 0) return null;

        let curve_lateral = lateral_output.curves[0];

        let fore_aft_output = this.ReadForeAftIntrusion(model, structure, unit_system, structure_index);
        if (!fore_aft_output) return null;
        if (fore_aft_output.curves.length == 0) return null;

        let curve_fore_aft = fore_aft_output.curves[0];

        /* Set the labels and line style */

        THisHelper.SetLineStyle([curve_vertical], this.GetCurveColourByIndex(structure_index));
        THisHelper.SetLineStyle([curve_lateral], this.GetCurveColourByIndex(structure_index + 1));
        THisHelper.SetLineStyle([curve_fore_aft], this.GetCurveColourByIndex(structure_index + 2));

        /* Values */

        let values = {};

        for (let k in vertical_output.values) {
            values[k] = vertical_output.values[k];
        }
        for (let k in lateral_output.values) {
            values[k] = lateral_output.values[k];
        }
        for (let k in fore_aft_output.values) {
            values[k] = fore_aft_output.values[k];
        }

        return {
            curves: [curve_vertical, curve_lateral, curve_fore_aft],
            values: values,
            graph_title: graph_title
        };
    }
}

/**
 * Class to hold data for an occupant its model ID and unit system
 * Used as an argument in the ProtocolAssessment.DoOccupantAssessment function
 */
class DoOccupantAssessmentOccupantData {
    /**
     * @param {WorkflowOccupant} occupant WorkflowOccupant instance
     * @param {number} model_id Model ID
     * @param {number} unit_system Unit system, e.g. Workflow.UNIT_SYSTEM_U2
     * @example
     * let od = new DoOccupantAssessmentOccupantData(o, 1, Workflow.UNIT_SYSTEM_U2);
     */
    constructor(occupant, model_id, unit_system) {
        this.occupant = occupant;
        this.model_id = model_id;
        this.unit_system = unit_system;
    }

    /* Instance property getter and setters */

    /**
     * WorkflowOccupant instance
     * @type {WorkflowOccupant} */
    get occupant() {
        return this._occupant;
    }
    set occupant(new_occupant) {
        if (!(new_occupant instanceof WorkflowOccupant)) {
            throw new Error(
                `<occupant> is not a WorkflowOccupant instance in  <DoOccupantAssessmentOccupantData> constructor`
            );
        }

        this._occupant = new_occupant;
    }
    /**
     * Model ID
     * @type {number} */
    get model_id() {
        return this._model_id;
    }
    set model_id(new_model_id) {
        if (typeof new_model_id != "number") {
            throw new Error(`<model_id> is not a number in <DoOccupantAssessmentOccupantData> constructor`);
        }

        this._model_id = new_model_id;
    }
    /**
     * Unit system, e.g. Workflow.UNIT_SYSTEM_U2
     * @type {number} */
    get unit_system() {
        return this._unit_system;
    }
    set unit_system(new_unit_system) {
        if (typeof new_unit_system != "number") {
            throw new Error(`<unit_system> is not a number in <DoOccupantAssessmentOccupantData> constructor`);
        }

        this._unit_system = new_unit_system;
    }
}

/**
 * Class to hold data for a structure its model ID and unit system
 * Used as an argument in the ProtocolAssessment.DoStructureAssessment function
 */
class DoStructureAssessmentStructureData {
    /**
     * @param {Structure} structure Structure instance
     * @param {number} model_id Model ID
     * @param {number} unit_system Unit system, e.g. Workflow.UNIT_SYSTEM_U2
     * @example
     * let sd = new DoStructureAssessmentStructureData(s, 1, Workflow.UNIT_SYSTEM_U2);
     */
    constructor(structure, model_id, unit_system) {
        this.structure = structure;
        this.model_id = model_id;
        this.unit_system = unit_system;
    }

    /* Instance property getter and setters */

    /**
     * Structure instance
     * @type {Structure} */
    get structure() {
        return this._structure;
    }
    set structure(new_structure) {
        if (!(new_structure instanceof Structure)) {
            throw new Error(
                `<structure> is not a Structure instance in <DoStructureAssessmentStructureData> constructor`
            );
        }

        this._structure = new_structure;
    }
    /**
     * Model ID
     * @type {number} */
    get model_id() {
        return this._model_id;
    }
    set model_id(new_model_id) {
        if (typeof new_model_id != "number") {
            throw new Error(`<model_id> is not a number in <DoStructureAssessmentStructureData> constructor`);
        }

        this._model_id = new_model_id;
    }
    /**
     * Unit system, e.g. Workflow.UNIT_SYSTEM_U2
     * @type {number} */
    get unit_system() {
        return this._unit_system;
    }
    set unit_system(new_unit_system) {
        if (typeof new_unit_system != "number") {
            throw new Error(`<unit_system> is not a number in <DoStructureAssessmentStructureData> constructor`);
        }

        this._unit_system = new_unit_system;
    }
}

/**
 * Class to hold options that can be passed to the
 * ProtocolAssessment.DoOccupantAssessment and ProtocolAssessment.DoStructureAssessment functions
 */
class DoAssessmentOptions {
    /**
     * @param {string} graph_layout Graph layout
     * @param {number} first_graph_id First graph ID to use when plotting results
     * @param {number} first_page_id First page ID to use when plotting results
     * @param {boolean} blank_all Switch whether to blank all existing curves and datums before plotting results
     * @param {boolean} remove_existing_graphs Switch whether to remove existing graphs from a page
     * @param {string} [output_dir=""] Output directory for "reporter" images
     */
    constructor(graph_layout, first_graph_id, first_page_id, blank_all, remove_existing_graphs, output_dir = "") {
        this.graph_layout = graph_layout;
        this.first_graph_id = first_graph_id;
        this.first_page_id = first_page_id;
        this.blank_all = blank_all;
        this.remove_existing_graphs = remove_existing_graphs;
        this.output_dir = output_dir;
    }

    /* Instance property getter and setters */

    /**
     * Graph layout
     * @type {string} */
    get graph_layout() {
        return this._graph_layout;
    }
    set graph_layout(new_graph_layout) {
        if (typeof new_graph_layout != "string") {
            throw new Error(`<graph_layout> is not a string in <DoAssessmentOptions> constructor`);
        }

        if (DoAssessmentOptions.GraphLayouts().indexOf(new_graph_layout) == -1) {
            throw new Error(`Invalid graph layout: ${new_graph_layout} in <DoAssessmentOptions> constructor`);
        }

        this._graph_layout = new_graph_layout;
    }

    /** Switch whether to blank all existing curves and datums before plotting results
     * @type {boolean} */
    get blank_all() {
        return this._blank_all;
    }
    set blank_all(new_blank_all) {
        if (typeof new_blank_all != "boolean") {
            throw new Error(`<blank_all> is not a boolean in <DoAssessmentOptions>`);
        }

        this._blank_all = new_blank_all;
    }

    /** Switch whether to remove existing graphs from a page
     * @type {boolean} */
    get remove_existing_graphs() {
        return this._remove_existing_graphs;
    }
    set remove_existing_graphs(new_remove_existing_graphs) {
        if (typeof new_remove_existing_graphs != "boolean") {
            throw new Error(`<remove_existing_graphs> is not a boolean in <DoAssessmentOptions>`);
        }

        this._remove_existing_graphs = new_remove_existing_graphs;
    }

    /** First graph ID to use when plotting results
     * @type {number} */
    get first_graph_id() {
        return this._first_graph_id;
    }
    set first_graph_id(new_first_graph_id) {
        if (typeof new_first_graph_id != "number") {
            throw new Error(`<first_graph_id> is not a number in <DoAssessmentOptions> constructor`);
        }

        this._first_graph_id = new_first_graph_id;
    }

    /** First page ID to use when plotting results
     * @type {number} */
    get first_page_id() {
        return this._first_page_id;
    }
    set first_page_id(new_first_page_id) {
        if (typeof new_first_page_id != "number") {
            throw new Error(`<first_page_id> is not a number in <DoAssessmentOptions> constructor`);
        }

        this._first_page_id = new_first_page_id;
    }

    /**
     * Output directory
     * @type {string} */
    get output_dir() {
        return this._output_dir;
    }
    set output_dir(new_output_dir) {
        if (typeof new_output_dir != "string") {
            throw new Error(`<output_dir> is not a string in <DoAssessmentOptions> constructor`);
        }

        if (new_output_dir != "" && !File.IsDirectory(new_output_dir)) {
            throw new Error(`<output_dir> is not a valid directory in <DoAssessmentOptions> constructor`);
        }

        this._output_dir = new_output_dir;
    }

    /** Reporter graph layout option
     * @type {string} */
    static get GRAPH_LAYOUT_REPORTER() {
        return "reporter";
    }

    /** same page graph layout option
     * @type {string} */
    static get GRAPH_LAYOUT_SAME_PAGE() {
        return "same_page";
    }

    /** Separate pages graph layout option
     * @type {string} */
    static get GRAPH_LAYOUT_SEPARATE_PAGES() {
        return "separate_pages";
    }

    /**
     * Return a list of valid graph layouts
     * @returns {string[]}
     */
    static GraphLayouts() {
        return [
            DoAssessmentOptions.GRAPH_LAYOUT_REPORTER,
            DoAssessmentOptions.GRAPH_LAYOUT_SAME_PAGE,
            DoAssessmentOptions.GRAPH_LAYOUT_SEPARATE_PAGES
        ];
    }
}

/**
 * Class used to return results from the ProtocolAssessment.DoOccupantAssessment and
 * ProtocolAssessment.DoStructureAssessment functions
 */
class DoAssessmentResults {
    constructor() {
        this.output = {};
        this.last_graph_id = 1;
        this.last_page_id = 1;
    }

    /* Instance property getter and setters */

    /**
     * Output object. This can store any results you want, but the properties are generally
     * set to something that describes the result, e.g "M1 NECK Driver-front-left max_extension"
     * @type {object} */
    get output() {
        return this._output;
    }
    set output(new_output) {
        if (typeof new_output != "object") {
            throw new Error(`<output> is not an object in <DoAssessmentResults>`);
        }

        this._output = new_output;
    }

    /** Last graph ID. This is the last graph id used when plotting results
     * @type {number} */
    get last_graph_id() {
        return this._last_graph_id;
    }
    set last_graph_id(new_last_graph_id) {
        if (typeof new_last_graph_id != "number") {
            throw new Error(`<last_graph_id> is not a number in <DoAssessmentResults>`);
        }

        this._last_graph_id = new_last_graph_id;
    }

    /** Last page ID. This is the last page id used when plotting results
     * @type {number} */
    get last_page_id() {
        return this._last_page_id;
    }
    set last_page_id(new_last_page_id) {
        if (typeof new_last_page_id != "number") {
            throw new Error(`<last_page_id> is not a number in <DoAssessmentResults>`);
        }

        this._last_page_id = new_last_page_id;
    }
}