Home Reference Source Test Repository

src/Dispersion/GaussianPlume.js

/**
 * Created by austin on 6/6/16.
 * @file GaussianPlume.js
 * Assumes:
 * Single point source
 */

"use strict";

import Source, {
    SourceType
} from './Source';
import Atmosphere from './Atmosphere';

/**
 * @typedef {Object} Stat
 * @property {number} x - meters downwind
 * @property {number} y - meters crosswind
 * @property {number} z - meters vertical
 * @property {number} stdY
 * @property {number} stdZ
 * @property {number} concentration - micrograms / cubic meter
 */

/**
 * @typedef {Object} Coord
 * @property {number} x - meters downwind 
 * @property {number} y - meters crosswind 
 * @property {number} z - meters vertical 
 */

/**
 * @typedef {Object} STD_Y_COEFF
 * @property {number} c
 * @property {number} d
 */
/**
 * 0 - 6 for atm stab grade
 * [x < 10000, x >= 10000]
 *  @type {STD_Y_COEFF}
 */
const STD_Y_COEFFS = [
    [{c: .495, d: .873}, {c: .606, d: .851}],
    [{c: .310, d: .897}, {c: .523, d: .840}],
    [{c: .197, d: .908}, {c: .285, d: .867}],
    [{c: .122, d: .916}, {c: .193, d: .865}],
    [{c: .122, d: .916}, {c: .193, d: .865}],
    [{c: .0934, d: .912}, {c: .141, d: .868}],
    [{c: .0625, d: .911}, {c: .0800, d: .884}]
];

/**
 * @typedef {Object} STD_Z_COEFF
 * @property {number} a
 * @property {number} b
 */
/** 
 * [x < 500, 500 <= x < 5000, 5000 <= x]
 * @type {STD_Z_COEFF}
 */
const STD_Z_COEFFS = [
    [{a: .0383, b: 1.281}, {a: .0002539, b: 2.089}, {a: .0002539, b: 2.089}],
    [{a: .1393, b: .9467}, {a: .04936, b: 1.114}, {a: .04936, b: 1.114}],
    [{a: .1120, b: .9100}, {a: .1014, b: .926}, {a: .1154, b: .9106}],
    [{a: .0856, b: .8650}, {a: .2591, b: .6869}, {a: .7368, b: .5642}],
    [{a: .0818, b: .8155}, {a: .2527, b: .6341}, {a: 1.297, b: .4421}],
    [{a: .1064, b: .7657}, {a: .2452, b: .6358}, {a: .9024, b: .4805}],
    [{a: .05645, b: .8050}, {a: .1930, b: .6072}, {a: 1.505, b: .3662}]
];

const G = 9.8; // gravity (m/s^2)

/**
 * A Simple Gaussian Plume. For resources, please see the github repo.
 * Calculates spread for one hour with constant conditions.
 * 
 * http://www.cerc.co.uk/environmental-software/assets/data/doc_techspec/CERC_ADMS5_P10_01_P12_01.pdf
 */
class GaussianPlume {

    /**
     * For now, each Plume contains a constant atmosphere and a single Source
     * @param {Atmosphere} atmosphere
     * @param {Source} source
     */
    constructor(atmosphere, source) {
        this.setAtmosphere(atmosphere);
        this.addSource(source);
    }

    /**
     * @override
     * @returns {string}
     */
    toString() {
        return '${this._source.toString()} in ${this._atmosphere.toString()}';
    }

    /**
     * Adds a single source to the plume
     * @param {Source} source
     * @returns {GaussianPlume} For chaining purposes
     */
    addSource(source) {
        this._source = source;
        return this;
    }

    /**
     * 
     * @returns {Source|*}
     */
    getSource() {
        return this._source;
    }

    /**
     * @type {Atmosphere}
     * @param {Atmosphere} atmosphere
     * @returns {GaussianPlume} For chaining purposes
     */
    setAtmosphere(atmosphere) {
        this._atmosphere = atmosphere;
        return this;
    }

    /**
     * @type {Atmosphere}
     * @returns {Atmosphere|*}
     */
    getAtmosphere() {
        return this._atmosphere;
    }

    /**
     * A helper function for the StdZ calculation
     * @protected
     * @param {number} x - distance downwind (m)
     * @returns {STD_Y_COEFF}
     */
    _getStdYCoeffs(x) {
        let index;
        let coeffs = STD_Y_COEFFS[this._atmosphere.getGrade()];
        if (x < 10000) {
            index = 0;
        } else {
            index = 1;
        }
        return coeffs[index];
    }

    /**
     * Brookhaven sigma
     * The crosswind distance standard deviation for a distance x downwind.
     * To be used in a Gaussian distribution
     * @param {number} x - distance downwind (m)
     * @returns {number} crosswind standard deviation at x meters downwind (m)
     */
    getStdY(x) {
        let coeffs = this._getStdYCoeffs(x);
        return coeffs.c * Math.pow(x, coeffs.d);
    }

    /**
     * A helper function for the StdZ calculation
     * @protected
     * @param {number} x - distance downwind (m)
     * @returns {STD_Z_COEFF}
     */
    _getStdZCoeffs(x) {
        let index;
        let coeffs = STD_Z_COEFFS[this._atmosphere.getGrade()];
        if (x < 500) {
            index = 0;
        } else if (x < 5000) {
            index = 1;
        } else {
            // 5000 < x
            index = 2;
        }
        return coeffs[index];
    }

    /**
     * Brookhaven sigma
     * The vertical distance standard deviation for a distance x downwind.
     * To be used in a Gaussian distribution
     * @param {number} x - distance downwind (m)
     * @returns {number}
     */
    getStdZ(x) {
        let coeffs = this._getStdZCoeffs(x);
        return coeffs.a * Math.pow(x, coeffs.b);
    }

    /**
     * 
     * @returns {number} m/s
     */
    getWindSpeedAtSourceHeight() {
        return this._atmosphere.getWindSpeedAt(this.getEffectiveSourceHeight());
    }

    /**
     * Manually set the Effective Source Height
     * @param {number} height 
     * @returns {GaussianPlume} For chaining purposes
     */
    setEffectiveSourceHeight(height) {
        this._effSrcHeight = height;
        return this;
    }
    /**
     *  Takes into account the wind and other factors into account.
     *  Should potentially move this to the Source class
     *  @returns {number} the effective source height
     *  */
    getEffectiveSourceHeight() {
        if (this._effSrcHeight) {
            return this._effSrcHeight;
        }
        let deltaH = this.getMaxRise(0);
        this._effSrcHeight = this._source.getHeight() + deltaH;
        return this._effSrcHeight;
    }

    /**
     * 
     * @param x
     * @returns {number}
     */
    getMeanHeight(x) {
        // Should use integrals but need to research how to load a nicer math library in hur

        // For large x this should be ok, between 0 (ground) and maxPlumeRise
        return this.getMaxRise(x) / 2;
    }

    /**
     * The max rise of the plume at x meters downwind
     * @param {number} x - distance (m) downwind
     * @returns {number} vertical standard deviation at x meters downwind (m)
     */
    getMaxRise(x) {
        // @see page 31
        // Grades 1 - 5 are assumed unstable/neutral, 6 - 7 are assumed stable
        // Both the momentum dominated and buoyancy dominated methods should be calculated, then use the max
        let bDeltaH, mDeltaH; // Max plume rise buoyancy, momentum dominated resp.
        const srcRad = this._source.getRadius();
        const srcTemp = this._source.getTemperature();
        const srcHeight = this._source.getHeight();
        const srcExitVel = this._source.getExitVelocity();
        const ambTemp = this._atmosphere.getTemperature();
        const F = g * srcExitVel * Math.pow(srcRad, 2) * (srcTemp - ambTemp) / srcTemp;
        const U = this._atmosphere.getWindSpeedAt(srcHeight); // wind speed at stack height

        if (this._atmosphere.getGrade() <= 5) {
            // unstable/neutral
            // Gets super funky, ugh science

            // Distance to Maximum Plume Rise
            let xStar = F < 55 ? 14 * Math.pow(F, 0.625) : 34 * Math.pow(F, .4);
            // Will use 0 if calculating from the _source. Need to read more about this.
            if (x == 0 || x > 3.5 * xStar) {
                x = xStar;
            }
            bDeltaH = 1.6 * Math.pow(F, .333) * Math.pow(3.5 * x, .667) * Math.pow(U, -1);
            mDeltaH = (3 * srcExitVel * (2 * srcRad)) / U;
        } else {
            // stable
            const s = this._atmosphere.getLetterGrade() === 'E' ? 0.018: 0.025; //  g/ambientTemp
            bDeltaH = 2.6 * Math.pow(F / (U * s), .333);
            mDeltaH = 1.5 * Math.pow(srcExitVel * srcRad, .667) * Math.pow(U, -0.333) * Math.pow(s, -0.166);
        }

        // console.log("bDeltaH: " + bDeltaH);
        // console.log("mDeltaH: " + mDeltaH);
        // Return the max
        if (bDeltaH > mDeltaH) {
            // console.log("Buoyancy dominated.");
            return bDeltaH;
        }
        // console.log("Momentum dominated.");
        return mDeltaH;
    }

    /**
     * Calculates the maximum concentration dispersed
     * @returns {number} micrograms / cubic meters
     */
    getMaxConcentration() {
        let x = this.getMaxConcentrationX();
        let stdY = this.getStdY(x);
        let stdZ = this.getStdZ(x);
        let H = this.getEffectiveSourceHeight();

        let a = (this._source.getEmissionRate() * 1000000) / (Math.PI * stdY * stdZ * this.getWindSpeedAtSourceHeight());
        let b = Math.exp((-0.5) * Math.pow(H / stdZ, 2));

        return a * b;
    }

    /**
     * Calculates the distance downwind of the maximum concentration
     * @returns {number} micrograms / cubic meter
     */
    getMaxConcentrationX() {
        // If unknown, set x to 5000 meters
        let stdYCoeffs = this._getStdYCoeffs(5000);  // c , d
        let stdZCoeffs = this._getStdZCoeffs(5000);  // a , b
        let H = this.getEffectiveSourceHeight();

        let pt1 = (stdZCoeffs.b * Math.pow(H, 2)) / (Math.pow(stdZCoeffs.a, 2) * (stdYCoeffs.d + stdZCoeffs.b));
        return Math.pow(pt1, (1 / (2 * stdZCoeffs.b)));
    }

    /**
     * Calculates the concentration at a given x,y,z coordinate.
     * Must be downwind
     * @param {number} x - Meters downwind of _source, greater than 0
     * @param {number} y - Meters crosswind of _source
     * @param {number} z - Meters vertical of ground
     * @returns {number} micrograms / cubic meter
     *
     * @example
     * getConcentration(200, 300, 10)
     * Calculates at 200 meters downwind, 300 east, 10 high
     */
    getConcentration(x, y, z) {
        // First part of Gaussian equation 1 found on page 2
        let stdY = this.getStdY(x);
        let stdZ = this.getStdZ(x);
        // Effective stack height
        let H = this.getEffectiveSourceHeight();
        let U = this.getWindSpeedAtSourceHeight();

        let a = this._source.getEmissionRate() / (2 * Math.PI * stdY * stdZ * U);
        let b = Math.exp(-1 * Math.pow(y, 2) / (2 * Math.pow(stdY, 2)));
        let c = Math.exp(-1 * Math.pow(z - H, 2) / (2 * Math.pow(stdZ, 2)));
        let d = Math.exp(-1 * Math.pow(z + H, 2) / (2 * Math.pow(stdZ, 2)));
        
        // Put it all together! get
        return a * b * (c + d);
    }

    /**
     * Calculates the stdY, stdZ, and concentrations for a list of x coordinates
     *  directly downwind of the _source
     * Useful in creating graphs / processing large amounts of data at once
     * @param {number[]} xs - a list of x's
     * @returns {Stat[]} a list of stats
     */
    getStatsForXs(xs) {
        let stats = [];
        for (let i = 0; i < xs.length; i++) {
            stats.push({
                x: xs[i],
                y: 0,
                z: 0,
                stdY: this.getStdY(xs[i]),
                stdZ: this.getStdZ(xs[i]),
                concentration: this.getConcentration(xs[i], 0, 0)
            })
        }
        return stats;
    }

    /**
     * Same as getStatsForXs, but for 3d coordinates
     * @param {Coord[]} coords - a list of objects with x,y,z params
     * @returns {Stat[]}
     */
    getStatsForCoords(coords) {
        let stats = [];
        for (let i = 0; i < coords.length; i++) {
            stats.push({
                x: coords[i].x,
                y: coords[i].y,
                z: coords[i].z,
                stdY: this.getStdY(xs[i]),
                stdZ: this.getStdZ(xs[i]),
                concentration: this.getConcentration(coords[i].x, coords[i].y, coords[i].z)
            })
        }
        return stats;
    }
}

export { SourceType }
export { Source };
export { Atmosphere };
export default GaussianPlume;