Source: chemistry/SpeciesNode.js

/**
 * @module SpeciesNode
 * @description Represents a species node in a chemical solution, handling the logical representation
 * of a Species including its quantity, state, and behavior within a solution.
 * 
 * @requires ./SpecieState
 * @requires ./Species
 * 
 * @property {Species} archetype - Species of which this is a quantity
 * @property {Solution} solvent - Reference to the solution containing this SpeciesNode
 * @property {number} moles - Number of moles of this SpeciesNode
 * @property {boolean} unitActivity - Indicates if this species node is a solid (has unit activity)
 * @property {boolean} infinite - Indicates if there's a functionally infinite quantity available
 * 
 * @example
 * const speciesNode = new SpeciesNode(speciesTemplate, 0.5);
 */

/**
 * Creates a new Node based off of the the Species provided. As no quantity
 * has been provided for the number of moles, this constructor will assume
 * both infinite amount and unitActivity.
 *
 * @param template								{Species}
 * 		The Species for the SpeciesNode
 * @param noMoles								{Number}
 * 		The amount of species in moles
 *
 * @constructor
 */
define([
    './SpecieState',
    './Species'
], function (SpecieState, Species) {
    function SpeciesNode(template, noMoles) {
        if (template === null || !(template instanceof Species)) {
            throw "RunTimeException : Pass a valid Species argument to the SpeciesNode constructor";
        }
        this.archetype = template;
        this.solvent = null; // Type solution! At some point, this must be added...
        if (noMoles === null) {
            this.unitActivity = true;
            this.infinite = true;
            this.moles = 0.0;
        } else {
            this.moles = noMoles;
            this.infinite = template.infinite;
            this.unitActivity = template.unitActivity;
        }
    }

    /**
     * Takes as an argument the fraction of the current number of moles that it
     * should removed from this node. Returns the moles removed.
     *
     * @param fractionToRemove Must be between 0 and 1
     *
     * @returns {Number}
     */
    SpeciesNode.prototype.removeFraction = function (fractionToRemove) {
        if (fractionToRemove < 0.0 || fractionToRemove > 1.0) {
            throw "RunTimeException : fractionToRemove must be between 0 and 1";
        }
        var removed = this.moles * fractionToRemove;
        this.moles = (1.0 - fractionToRemove) * this.moles;
        return removed;
    };

    /**
     * Merge two SpeciesNode
     *
     * @param other {SpeciesNode}
     */
    SpeciesNode.prototype.merge = function (other) {
        if ((other instanceof SpeciesNode) && other !== null && this.equals(other)) {
            this.moles = this.moles + other.moles;
        }
    };

    /**
     * Returns the name of the Species that this node represents in addition to
     * the number of moles, and if the solvent is non-null, the concentration.
     */
    SpeciesNode.prototype.toString = function () {
        var returnValue = "";
        returnValue = returnValue + this.archetype.name + "\t" + this.moles;

        if (this.solvent !== null) {
            returnValue = returnValue + "\t" + this.getConcentration();
        }

        return returnValue;
    };

    /**
     * Checks the equality of two SpeciesNodes. This will return true if the two
     * Species that these nodes represent are equal.
     *
     * @param other
     *
     * @returns {Boolean}
     */
    SpeciesNode.prototype.equals = function (other) {
        if (other === null) {
            throw "NullPointerException : Can't compare SpeciesNode to null";
        }

        var returnValue = false;
        if (other instanceof SpeciesNode) {
            returnValue = this.archetype.equals(other.archetype);
        }
        else if (other instanceof Species) {
            returnValue = this.archetype.equals(other);
        }
        return returnValue;
    };

    /**
     * Deep copy, but the reference to the node's solvent is not copied.
     *
     * @returns other {SpeciesNode}
     */
    SpeciesNode.prototype.clone = function () {
        var other = new SpeciesNode(this.archetype, this.moles);
        other.infinite = this.infinite;
        other.unitActivity = this.unitActivity;

        return other;
    };

    /**
     * Changes the number of moles of this species within it's solution.
     *
     * @param moles {Number}
     */
    SpeciesNode.prototype.setMoles = function (moles) {
        if (moles < 0.0) {
            throw "IllegalArgumentException : moles in SpeciesNode must be positive";
        }

        this.moles = moles;
    };

    /**
     * Returns the concentration of this species with respect to it's solvent.
     * This should be different for solids, liquids, and gases...
     * @returns {Number}
     */
    SpeciesNode.prototype.getConcentration = function () {
        if (this.archetype.state === 'g') {
            return this.moles * 0.083145 * this.solvent.getTemperature() / this.solvent.getGasVolume();
        }
        // else if (this.archetype.state === 'l') {
        //     return 1;
        // }
        else {
            return this.moles / this.solvent.liquidVolume;
        }
    };

    
    /**
     * Calculates the derivative of concentration with respect to moles (dC/dn).
     * This represents how the concentration changes when the number of moles changes.
     * 
     * For gases, this uses the ideal gas law where dC/dn = RT/V
     * For liquids and aqueous species, dC/dn = 1/V
     * 
     * @returns {Number} The derivative of concentration with respect to moles in mol/L per mol
     */
    SpeciesNode.prototype.dConc_dmoles = function () {
        if (this.archetype.state === 'g') {
            return 0.083145 * this.solvent.getTemperature() / this.solvent.getGasVolume();
        } else {
            return 1.0 / this.solvent.liquidVolume;
        }
    }

    /**
     * Sets the concentration of this species with respect to it's solvent.
     * NOTE : This has the effect of changing the number of moles.
     *
     * @param concentration {Number}
     */
    SpeciesNode.prototype.setConcentration = function (concentration) {
        if (this.solvent === null) {
            throw "NullPointerException : Solvent is null in SpeciesNode";
        }

        if (concentration < 0.0) {
            throw "IllegalArgumentException : concentration in SpeciesNode must be positive";
        }

        if (this.archetype.state === 'g') {
            this.setMoles(concentration * this.solvent.getGasVolume() / (0.083145  * this.solvent.getTemperature()));
        } else {
            this.setMoles(concentration * this.solvent.liquidVolume);
        }
    };

    /**
     * Returns the weight of this SpeciesNode in grams.
     *
     * @returns {Number}
     */
    SpeciesNode.prototype.getWeight = function () {
        return this.moles * this.archetype.molecularWeight;
    };

    /**
     * Returns the number of moles of water replaced.
     *
     * @returns {Number}
     */
    SpeciesNode.prototype.getWaterReplaced = function () {
        // 0 for solids, calculated for liquids and aqueous species
        return (this.archetype.state === SpecieState.SOLID) ? 0.0 :
        this.moles * this.archetype.waterReplacement;
    };

    /**
     * Returns the amount of volume taken up by this SpeciesNode in Liters.
     * Again, mostly useful for solids (real volume).
     *
     * @returns {Number}
     */
    SpeciesNode.prototype.getVolume = function () {
        var volume = 0.0;	// for liquids or aqueous species, confounded with water
        if (this.archetype.state === SpecieState.SOLID) {
            // for solids, real volume
            volume = (this.getWeight() / this.archetype.getDensity()) / 1000.0;
        }

        return volume;
    };

    /**
     * 
     * Currently assumes that the solvent moles are calculated correctly (solventFinite = true)
     * 
     * @returns {Number}
     */
    SpeciesNode.prototype.getMoleFraction = function (amountObj={}) {
        const moles = amountObj[this.archetype.id] || this.moles;
        let totalMoles = 0;
        for (const species of this.solvent.speciesInSolution) {
            const state = species.archetype.state;
            if (state === 'aq' || state === 'l') {
                const id = species.archetype.id;
            if (amountObj.hasOwnProperty(id)) {
                totalMoles += amountObj[id];
            } else {
                totalMoles += species.moles;
            }
            }
        }
        return moles / totalMoles;
    }

    /**
     * Get the density of this SpeciesNode's Species.
     *
     * @returns {Number}
     */
    SpeciesNode.prototype.getDensity = function () {
        return this.archetype.getDensity();
    };

    /**
     * Get the molecular weight of this SpeciesNode's Species.
     *
     * @returns {Number}
     */
    SpeciesNode.prototype.getMolecularWeight = function () {
        return this.archetype.molecularWeight;
    };
    return SpeciesNode;
});