# QC definition file

-
    name: "Missing shots"
    iterate: "sequences"
    labels: [ "QC" ]
    id: missing_shots
    check: |
        const sequence = currentItem;
        let results;
        if (sequence.missing_shots) {
            results = {
                shots: {}
            }
            const missing_shots = missingShotpoints.filter(i => !i.ntba);
            for (const shot of missing_shots) {
                results.shots[shot.point] = { remarks: "Missed shot", labels: [ "QC", "QCAcq" ] };
            }
        } else {
            results = true;
        }

        results;
-
    name: "Gun QC"
    disabled: false
    labels: [ "QC", "QCGuns" ]
    children:
        -
            name: "Sequences without gun data"
            iterate: "sequences"
            id: seq_no_gun_data
            check: |
                shotpoints.some(i => i.meta?.raw?.smsrc) || "Sequence has no gun data"
        -
            name: "Missing gun data"
            id: missing_gun_data
            ignoreAllFailed: true
            check: |
                !!currentItem._("raw_meta.smsrc.guns")
                    ? true
                    : "Missing gun data"

        -
            name: "No fire"
            id: no_fire
            check: |
                const currentShot = currentItem;
                const gunData = currentItem._("raw_meta.smsrc");
                (gunData && gunData.guns && gunData.guns.length != gunData.num_active)
                    ? `Source ${gunData.src_number}: No fire (${gunData.guns.length - gunData.num_active} guns)`
                    : true;

        -
            name: "Pressure errors"
            id: pressure_errors
            check: |
                const pressure=11;
                const gunData = currentItem._("raw_meta.smsrc");
                const results = gunData &&
                    gunData
                    .guns
                    .filter(gun => ((gun[2] == gunData.src_number) && (gun[pressure]/parameters.gunPressureNominal - 1) > parameters.gunPressureToleranceRatio))
                    .map(gun =>
                        `source ${gun[2]}, string ${gun[0]}, gun ${gun[1]}, pressure: ${gun[pressure]} / ${parameters.gunPressureNominal} = ${(Math.abs(gun[pressure]/parameters.gunPressureNominal - 1)*100).toFixed(2)}% > ${(parameters.gunPressureToleranceRatio*100).toFixed(2)}%`
                    ).join("  \n");
                results && results.length
                    ? results
                    : true

        -
            name: "Single gun / cluster"
            children:
                -
                    name: "Source depth"
                    id: source_depth
                    check: |
                        const currentShot = currentItem;
                        let _result_;
                        _depth=10;
                        const gunData = currentShot._("raw_meta.smsrc.guns");
                        if (!gunData) {
                            // We check for missing data elsewhere, so don't fail this test
                            _result_ = true
                        } else if (gunData.every(gun => Math.abs(gun[_depth]-parameters.gunDepth) <= parameters.gunDepthTolerance)) {
                            _result_ = true;
                        } else {
                            const bad_guns = gunData.filter(gun => Math.abs(gun[_depth]-parameters.gunDepth) > parameters.gunDepthTolerance).map(gun => {
                                return `source ${gun[2]}, string ${gun[0]}, gun ${gun[1]}, depth: ${gun[10]}`;
                            });
                            _result_ = `Depth error: ${bad_guns.join("; ")}`;
                        }
                        _result_

                -
                    name: "Synchronisation (error)"
                    id: sync_error
                    check: |
                        const currentShot = currentItem;
                        const gunData = currentShot._("raw_meta.smsrc");
                        let result = [];
                        if (gunData && gunData.num_nofire == 0) {

                            // These are the indices into the gun array for the different
                            // values of interest.
                            const subarray = 0;
                            const aimpoint = 7;
                            const firetime = 8;

                            // We only care about the source which actually fired (or was supposed to)
                            const sourceFired = gunData.guns.filter(g => g[2] == gunData.src_number);

                            // Let us check if the average delta for each string is within spec
                            let subarrayAverages = [];
                            sourceFired.forEach(g => {
                                const idx = g[subarray]-1;
                                const delta = g[firetime]-g[aimpoint];
                                if (!subarrayAverages[idx]) {
                                    subarrayAverages[idx] = [];
                                }
                                subarrayAverages[idx].push(delta);
                            });
                            subarrayAverages = subarrayAverages.map(s => s.reduce( (a, b) => a+b, 0 ) / s.length);

                            subarrayAverages.forEach((value, idx) => {
                                if (value > parameters.gunTimingSubarrayAverage) {
                                    result.push(`Average delta error: string ${idx+1}: ${value.toFixed(2)} > ${parameters.gunTimingSubarrayAverage}`);
                                }
                            });

                            // Let us see about individual guns
                            sourceFired
                            .filter(gun => Math.abs(gun[firetime]-gun[aimpoint]) > parameters.gunTiming)
                            .forEach(gun => {
                                const value = Math.abs(gun[firetime]-gun[aimpoint]);
                                result.push(`Delta error: source ${gun[2]}, string ${gun[0]}, gun ${gun[1]}: ${value.toFixed(2)} > ${parameters.gunTiming}`);
                            });
                        }
                        if (result.length) {
                            result.join("; ");
                        } else {
                            // Either there were no error or gun data was missing, which we take care of elsewhere
                            true;
                        }

                -
                    name: "Synchronisation (warning)"
                    id: sync_warn
                    check: |
                        const currentShot = currentItem;
                        const gunData = currentShot._("raw_meta.smsrc");
                        let result = [];
                        if (gunData && gunData.num_nofire == 0) {

                            // These are the indices into the gun array for the different
                            // values of interest.
                            const subarray = 0;
                            const aimpoint = 7;
                            const firetime = 8;

                            // We only care about the source which actually fired (or was supposed to)
                            const sourceFired = gunData.guns.filter(g => g[2] == gunData.src_number);

                            sourceFired
                            .filter(gun => Math.abs(gun[firetime]-gun[aimpoint]) >= parameters.gunTimingWarning && Math.abs(gun[firetime]-gun[aimpoint]) <= parameters.gunTiming)
                            .forEach(gun => {
                                const value = Math.abs(gun[firetime]-gun[aimpoint]);
                                result.push(`Delta warning: source ${gun[2]}, string ${gun[0]}, gun ${gun[1]}: ${parameters.gunTimingWarning} ≤ ${value.toFixed(2)} ≤ ${parameters.gunTiming}`);
                            });
                        }
                        if (result.length) {
                            result.join("; ");
                        } else {
                            // Either there were no error or gun data was missing, which we take care of elsewhere
                            true;
                        }

                -
                    name: "Autofire"
                    id: autofire
                    check: |
                        const currentShot = currentItem;
                        let _result_;
                        _autofire=5;
                        const gunData = currentShot._("raw_meta.smsrc.guns");
                        if (!gunData) {
                            // We check for missing data elsewhere, so don't fail this test
                            _result_ = true;
                        } else if (gunData.every(gun => gun[_autofire] == false)) {
                            _result_ = true;
                        } else {
                            const bad_guns = gunData.filter(gun => gun[_autofire]).map(gun => {
                                return `source ${gun[2]}, string ${gun[0]}, gun ${gun[1]}, depth: ${gun[10]}`;
                            });
                            _result_ = `Depth error: ${bad_guns.join(";\n")}`;
                        }
                        _result_

-
    name: "Centre of source preplot deviation (single shots)"
    labels: [ "QC", "QCNav" ]
    disabled: false
    children:
        -
            name: "Crossline"
            id: crossline
            check: |
                const currentShot = currentItem;
                Math.abs(currentShot.error_i) <= parameters.crosslineError
                    || `Crossline error (${currentShot.type}): ${currentShot.error_i.toFixed(2)} > ${parameters.crosslineError}`

        -
            name: "Inline"
            id: inline
            check: |
                const currentShot = currentItem;
                Math.abs(currentShot.error_j) <= parameters.inlineError
                    || `Inline error (${currentShot.type}): ${currentShot.error_j.toFixed(2)} > ${parameters.inlineError}`

-
    name: "Centre of source preplot deviation (moving average)"
    labels: [ "QC", "QCNav" ]
    children:
        -
            name: "Crossline"
            iterate: "sequences"
            parameters: [ "crosslineErrorAverage" ]
            id: crossline_average
            check: |
                const currentSequence = currentItem;
                //const i_err = shotpoints.filter(s => s.error_i != null).map(a => a.error_i);
                const i_err = shotpoints.map(i =>
                    (i.errorfinal?.coordinates ?? i.errorraw?.coordinates)[0]
                )
                .filter(i => !isNaN(i));

                if (i_err.length) {
                    const avg = i_err.reduce( (a, b) => a+b)/i_err.length;
                    avg <= parameters.crosslineErrorAverage ||
                        `Average crossline error: ${avg.toFixed(2)} > ${parameters.crosslineErrorAverage}`
                } else {
                    `Sequence ${currentSequence.sequence} has no shots within preplot`
                }

        -
            name: "Inline"
            iterate: "sequences"
            parameters: [ "inlineErrorRunningAverageShots" ]
            id: inline_average
            check: |
                const currentSequence = currentItem;
                const n = parameters.inlineErrorRunningAverageShots; // For brevity
                const results = shotpoints.slice(n/2, -n/2).map( (shot, index) => {
                    const shots = shotpoints.slice(index, index+n).map(i =>
                        (i.errorfinal?.coordinates ?? i.errorraw?.coordinates)[1]
                    ).filter(i => i !== null);
                    if (!shots.length) {
                        // We are outside the preplot
                        // Nothing to see here, move along
                        return true;
                    }
                    const mean = shots.reduce( (a, b) => a+b ) / shots.length;
                    return Math.abs(mean) <= parameters.inlineErrorRunningAverageValue || [
                        shot.point,
                        {
                            remarks: `Running average inline error: ${mean.toFixed(2)} > ${parameters.inlineErrorRunningAverageValue}`,
                            labels: [ "QC", "QCNav" ]
                        }
                    ]
                }).filter(i => i !== true);

                results.length == 0 || results.join("\n");
                results.length == 0 || {
                    remarks: "Sequence exceeds inline error running average limit",
                    shots: Object.fromEntries(results)
                }
