Source: chemistry/ReactionKinetics.js

const Species = require('./Species.js');
const Constants = require('./Constants.js');
const getIndex = require('./getIndex.js');

/**
 * This class represents a reaction in general, detailing the characteristics
 * that do not vary between different instances.
 *
 * Variables
 * 		species				{Array}			The species involved in the reaction.
 *
 * 		coefficients		{Array}			The coefficients of the species in the species Vector. Each element
 * 											corresponds directly to an element in the species Vector, and therefore
 * 											these vectors must have exactly the same size.
 * 											The values of the coefficient vector are stored as Numbers. Positive
 * 											values denote Products, while negative coef's denote Reactants values.
 */

/**
 * Constructs a new reaction based off of the given species objects as well
 * as their list of coefficients.
 *
 * @param species		{Array}		The species involved in the reaction.
 * @param coefficients	{Array}		The coefficients of the species in the species Vector.
 * 
 */
class ReactionKinetics{
    constructor(species, coefficients, kinetics=null, orders=null) {
        if (species === null || coefficients === null) {
            throw "NullPointerException : One of the arguments passed to Reaction is null";
        }

        if (species.length <= 0) {
            throw "IllegalArgumentException : Species passed to Reaction has non-positive length";
        }

        if (species.length !== coefficients.length) {
            throw "IllegalArgumentException : Length of species does not match length of coefficients";
        }

        this.species = species;
        this.coefficients = coefficients;
        if (kinetics) {
            this.kinetics = true;
            this.orders = orders;
            this.useEquilibrium = kinetics.useEquilibrium || false;
            this.Ea = parseFloat(kinetics.Ea);
            this.k298 = parseFloat(kinetics.k298);
        }
        this.isBetweenSolids = this.evaluateBetweenSolids();
    }

    /**
     * Evaluates if the reactions includes only solid species
     *
     * @returns isBetweenSolids {Boolean}
     */
    evaluateBetweenSolids() {
        var isBetweenSolids = true,
            s,
            i

        for (i = 0; i < this.species.length; i++) {
            s = this.species[i];
            if (!s instanceof Species) {
                throw "IllegalTypeException : species in Reaction contains non-Species objects";
            }
            else {
                if (s.state !== "s") {
                    isBetweenSolids = false;
                    return isBetweenSolids;
                }
            }
        }

        return isBetweenSolids;
    };

    /**
     * Checks the equality between two reactions.
     * ASSUMPTION: There cannot exist another reaction with the same species,
     * but different coefficients.
     *
     * @param other    {Object}
     *
     * @returns returnValue {Boolean}
     */
    equals(other) {
        var returnValue = true,
            i,
            exist;

        if (other === null) {
            throw "NullPointerException : other is null in Reaction.equals";
        }
        if (other instanceof ReactionKinetics) {
            if(this.species.length === other.species.length) {
                for (i = 0; i < this.species.length; i++) {
                    if (!(this.species[i].equals(other.species[i]))) {
                        returnValue = false;
                        break;
                    }
                }
            }
            else {
                returnValue = false;
            }
        }
        else if (other.archetype) {
            returnValue = this.equals(other.archetype);
        }
        else {
            returnValue = false;
        }
        return returnValue;
    };

    /**
     * Returns a new reaction that represents the reverse of this reaction, as
     * computed by converting all reactants to products, and all products to
     * reactants.
     *
     * @returns {Reaction}
     */
    reverseReaction() {
        const specie = this.cloneSpecies();
        const coefficient = this.coefficients.map(x => -x);
        return new ReactionKinetics(specie, coefficient);
    };

    /**
     * Returns a copy, not a reference, of the species in this reaction
     *
     * @returns specie {Array} a copy of species in this reaction
     */
    cloneSpecies() {
        return this.species.slice();
    };

    /**
     * Returns a copy, not a reference, of the coefficients in this reaction
     *
     * @returns coefficient {Array} a copy of coefficients in this reaction
     */
    cloneCoefficients() {
        return this.coefficients.slice();
    };

    /**
     * Adds a Reaction to this reaction. If the sum of these two is a valid
     * reaction (there is both a reactant and a product present) then a new
     * Reaction will be returned. Else, null will be returned.
     *
     * ASSUMPTION: All reactions are normalized. I.E., there is never the case
     * that the same species is both a reactant and a product.
     *
     * @param other {Reaction}    the other reaction to be added
     *
     * @returns {Reaction} if valid, else null
     */
    add(other) {
        var specie = this.cloneSpecies(),
            coefficient = this.cloneCoefficients(),
            i,
            j,
            k,
            otherSpecie,
            otherCoef,
            index,
            sum,
            returnValue,
            reactant,
            product,
            gcd,
            a,
            b,
            temp,
            l;

        if (other === null) {
            throw "NullPointerException : Argument is null in Reaction.add";
        }

        // Add the species and coefficients of the other species to our
        // cloned species/coefficient Vectors.
        for (i = 0; i < other.species.length; i++) {
            otherSpecie = other.species[i];
            otherCoef = other.coefficients[i];

            // Find the current species within ourself.
            index = getIndex(specie, otherSpecie);

            if (index > -1) {
                sum = coefficient[index] + otherCoef;

                // There might be the possibility that these terms canceled.
                if (sum === 0.0) {
                    specie.splice(index, 1);
                    coefficient.splice(index, 1);
                }
                // If they did not cancel, update the coefficient.
                else {
                    coefficient[index] = sum;
                }
            }
            else {
                specie.push(otherSpecie);
                coefficient.push(otherCoef);
            }
        }

        // If the two species are non-identical, return a new Reaction that
        // represents their addition.
        returnValue = null;

        if (specie.length > 0) {
            // Is there both a reactant and a product?
            reactant = false;
            product = false;

            for (k = 0; k < specie.length && (!reactant || !product); k++) {
                if (coefficient[k] < 0) {
                    reactant = true;
                }
                else {
                    product = true;
                }
            }

            if (reactant && product) {
                // Normalize the coefficients.
                // First, we find the gcd;
                gcd = Math.abs(coefficient[0]);

                for (j = 1; j < coefficient.length; j++) {
                    a = gcd;
                    b = Math.abs(coefficient[j]);

                    while (b !== 0) {
                        temp = a;

                        a = b;
                        b = temp % b;
                    }

                    gcd = a;
                }

                for (l = 0; gcd !== 1.0 && l < coefficient.length; l++) {
                    coefficient[l] = coefficient[l] / gcd;
                }

                returnValue = new ReactionKinetics(specie, coefficient);
            }
        }

        return returnValue;
    };

    /**
     * Returns a new reaction, scaled by the given factor.
     *
     * @param factor {Number}
     *
     * @returns {Reaction} the new scaled reaction
     */
    scale(factor) {
        var specie = this.cloneSpecies(),
            coefficient = this.cloneCoefficients(),
            i;

        if (factor === 0.0) {
            throw "IllegalArgumentException : Argument is 0 in Reaction.scale";
        }

        for (i = 0; i < coefficient.length; i++) {
            coefficient[i] = coefficient[i] * factor;
        }

        return new ReactionKinetics(specie, coefficient);
    };

    /**
     * Searches for an overlapping species in the other Reaction, and if it
     * finds one, we return the coefficients.
     *
     * If there is an overlap, we return two Doubles per overlap, the first
     * being the coefficient of our overlapping species, and the second being
     * the coef of the other reaction.
     *
     * If there is no overlap, null will be returned.
     *
     * @param other {Reaction}
     *
     * @returns returnValue {Array}
     */
    getOverlappingCoefficients(other) {
        var returnValue = [],
            i,
            index;

        for (i = 0; i < other.species.length; i++) {
            index = getIndex(this.species, other.species[i]);
            if (index !== -1) {
                returnValue.push(this.coefficients[index]);
                returnValue.push(other.coefficients[i]);
            }
        }

        return returnValue;
    };

    /**
     * Returns the String representation of this Reaction.
     *
     * @returns returnValue {String}
     */
    toString() {
        var returnValue = "",
            i;

        returnValue = returnValue + "Reaction:";

        for (i = 0; i < this.species.length; i++) {
            returnValue = returnValue + "\n\t" + this.coefficients[i] + "\t" + this.species[i].toString();
        }

        return returnValue;
    };

    /**
     * Returns the specific heat for the reaction in J/K/mol.
     *
     * @returns {Number}
     */
    getHeatCapacity() {
        var sum = 0,
            i;

        for (i = 0; i < this.species.length; i++) {
            sum += this.getCoefficientAt(i) * this.getSpeciesAt(i).cp *
                Constants.CAL_TO_J * this.getSpeciesAt(i).molecularWeight;
        }

        return sum;
    };





    /**
     * Returns the standard Enthalpy for the reaction.
     *
     * @returns {Number}
     */
    getHo() {
        var sum = 0,
            i;

        for (i = 0; i < this.species.length; i++) {
            sum += this.getCoefficientAt(i) * this.getSpeciesAt(i).enthalpy;
        }

        return sum;
    };

    /**
     * Returns the standard Entropy of the reaction.
     *
     * @returns {Number}
     */
    getSo() {
        var sum = 0,
            i;

        for (i = 0; i < this.species.length; i++) {
            sum += this.getCoefficientAt(i) * this.getSpeciesAt(i).entropy / 1000;
        }

        return sum;
    };

    /**
     * This returns the standard Gibb's Free energy of the reaction, for use in
     * calculating K.
     *
     * @param temp {Number} the temperature
     *
     * @returns {Number}
     */
    getGo(temp) {
        if (temp < 0.0) {
            throw "IllegalArgumentException : temp is less than zero in Reaction.getGo";
        }

        return this.getHo() - temp * this.getSo();
    };

    /**
     * Returns the K of the reaction for the given temperature.
     *
     * @param temperature
     *
     * @returns {Number}
     */
    getK(temperature) {
        if (temperature < 0.0) {
            throw "IllegalArgumentException : temperature is less than zero in Reaction.getK";
        }

        return Math.exp(-1.0 * this.getGo(temperature) / (0.00831451 * temperature));
    };

    /**
     * Returns the rate constant for a reaction at a given temperature
     */
    getRateConstant(temperature) {
        if (temperature < 0.0) {
            throw "IllegalArgumentException : temperature is less than zero in Reaction.getK";
        }
        const Ea_over_R = this.Ea/(0.0083145);
        return this.k298 * Math.exp(-Ea_over_R*(1/temperature-1/298.15));
    }

    /**
     * Returns the species specified at the given index.
     *
     * @param index
     *
     * @returns {Species}
     */
    getSpeciesAt(index) {
        return this.species[index];
    };

    /**
     * Returns the number of the species in the Reaction.
     *
     * @returns {Number}
     */
    getSpeciesCount() {
        return this.species.length;
    };

    /**
     * Returns the index of the specified Species.
     * This method will return -1 if the Species is not
     * present in this reaction.
     *
     * @param specie {Species} the specie we are looking for
     *
     * @returns {Number}
     */
    getSpeciesIndex(specie) {
        var i;

        for (i = 0; i < this.species.length; i++) {
            if (this.species[i].equals(specie)) {
                return i;
            }
        }
        return -1;
    };

    /**
     * Stores the referenced specie at the given index. The previous
     * specie at that index is discarded.
     *
     * @param specie {Species}
     *
     * @param index {Number}
     */
    setSpeciesAt(specie, index) {
        if (specie === null) {
            throw "NullPointerException : specie is null in Reaction.setSpeciesAt";
        }

        this.species[index] = specie;
    };

    /**
     * Returns the coefficient of the species at the specified index.
     *
     * @param index {Number}
     *
     * @returns {Number}
     */
    getCoefficientAt(index) {
        return this.coefficients[index];
    };

    /**
     * Stores the provided coefficient for the species at the given index.
     *
     * @param coef {Number}
     * @param index {Number}
     */
    setCoefficientAt(coef, index) {
        this.coefficients[index] = coef;
    };

    /**
     * Returns the products.
     * @returns {Array}
     */
    getProducts() {
        var i;
        var products = [];
        for (i = 0; i < this.species.length; i++) {
            if (this.getCoefficientAt(i) > 0) {
                products.push(this.species[i]);
            }
        }
        return products;
    };

    /**
     * Returns the reactants.
     * @returns {Array}
     */
    getReactants() {
        var i;
        var reactants = [];
        for (i = 0; i < this.species.length; i++) {
            if (this.getCoefficientAt(i) < 0) {
                reactants.push(this.species[i]);
            }
        }
        return reactants;
    };
}


module.exports = ReactionKinetics