Source: chemistry/Solution.js

/**
 * @module
 * 
 * @description This class represents a single solution, and contains representations of the
 * species and reactions contained therein. It invokes the balancing of the
 * reactions, and handles mixing and pouring of solutions.
 * 
 *     Solution heat capacity uses
 *        getSpecificHeat()
 *            calls SpecificHeatSolutionModel.getValue(this)
 *                    SpecificHeatSolutionModel: a few possible models.
 *                                    Is there any way to set which one?
 *                        SolventSolutionModeler: aqueous or non-aqueous
 *                            getSolventDensity
 *                            getSolventMW
 *                            getSolventSpecificHeat
 *                            (not really necessary - these can all be obtained
 *                             from the species information, as long as we make
 *                             sure to add in the values for water if they are not
 *                             provided in the json file)
 *
 *                        SolventConcentrationSolutionModeler: solvent finite or
 *                            solvent infinite. This setting is set in the
 *                            KnowledgeBase (I think). This class could be
 *                            replaced by a simple true/false (waterFinite
 *                            or waterInfinite)...
 *
 *
 *        getSolutionWeight(): Note: This is the TOTAL weight (includes solids)
 *            calls WeightSolutionModeler.getValue(this)
 *                WeightSolutionModeler: Depends on whether it is solventFinite
 *                    if solventFinite, just total weight of all species
 *                    if solventInfinite, "estimate" weight of solvent assuming
 *                    weightSolvent =
 *                        solution.liquidVolume * 1000 mL/L *
 *                            solventDensity (from SolventSolutionModeler)
 *                    Then subtract off each aq or liquid substance's getWaterReplaced()
 *                        (which is a moles of water replaced by that substance,
 *                         defined by moles × waterReplacement [mol / mol ratio
 *                         from the substance archetype]
 *                         waterReplacement is usually zero, so the "normal" idea
 *                         is basically molality, that a 1 L solution contains
 *                         1000 g of water + any other stuff dissolved (which
 *                         just increases the solution's density without reducing
 *                         the amount of water present in the 1 L solution)
 *
 *                    solventInfinite is the "normal" model
 *        
 *        LiquidVolumeSolutionModeler is where solution.liquidVolume is updated...
 *
 *
 *    Everything is set in configuration.json -> solutionModellers
 *
 *
 *   
 *   // BP elevation, FP depression used in findEquilibrium code
 *   BoilingPointSolutionModeler.getValue(this)
 *   FreezingPointSolutionModeler.getValue(this)
*/

/* 
    SpecieState is unnecessary - could be replaced with just comparison against
    the strings s, l, g, aq.
*/
define([
    'underscore',
    './KnowledgeBase',
    './Species',
    './ReactionKinetics',
    './SpeciesNode',
    './ReactionNodeKinetics',
    './Constants',
    './LiquidVolumeSolutionModeler',
    './SpecieState',
    './Cooling',
    './WeightSolutionModeler',
    './BoilingPointSolutionModeler',
    './FreezingPointSolutionModeler',
    './SpecificHeatSolutionModeler', 
    'tinycolor',
    './ColorCHSV',
    './SpectrumColor',
    './getIndex',
    '../util/logger'
], function (_,
             KnowledgeBase,
             Species,
             Reaction,
             SpeciesNode,
             ReactionNode,
             Constants,
             LiquidVolumeSolutionModeler,
             SpecieState,
             Cooling,
             WeightSolutionModeler,
             BoilingPointSolutionModeler,
             FreezingPointSolutionModeler,
             SpecificHeatSolutionModeler,
             tinycolor,
             ColorCHSV,
             SpectrumColor,
             getIndex,
             logger) {
    /**
     * @class
     * @param dataIn {Object}
     * @param dataIn.name {String} Name of the solution
     * @param dataIn.description {String} Description of the solution
     * @param dataIn.temperature {Number} Temperature of the solution
     * @param dataIn.volume {Number} Volume of the solution (liquid, in liters)
     * @param dataIn.insulated {Boolean} True if the solution is insulated
     * @param dataIn.kb {KnowledgeBase} KnowledgeBase to use for this solution
     * @param dataIn.species {Object[]} Array of species information for the solution; can specify speciesInSolution instead
     * @param dataIn.speciesInSolution {SpeciesNode[]} Array of speciesNodes for the solution
     * @param dataIn.equilibriumSettings {Object} Settings for equilibrium calculations
     */
    Solution = function(dataIn) {
        var s1, sp, _i, _j, _len, _len1, _ref, _ref1;
        this.name = dataIn.name || 'Solution';
        this.description = dataIn.description || 'Description';
        this.temperature = 298.15;
        if (dataIn.temperature) {
            if (isNaN(dataIn.temperature)) {
                var params = dataIn.temperature.split(',');
                var tmin = parseFloat(params[2]);
                var tmax = parseFloat(params[3]);
                this.temperature = tmin + Math.random() * (tmax - tmin);
            } else {
            this.temperature = dataIn.temperature;
            }
        }
        this.kineticsTimers = {};
        this.liquidVolume = parseFloat(dataIn.volume) || dataIn.liquidVolume || 0;
        this.insulated = dataIn.insulated || false;
        this.initialized = false;
        this.variation = 2.0 * Math.random() - 1.0;
        this.container = dataIn.container || null;
        this.DEFAULT_NUMERICAL_TOLERANCE = 1e-4;
        this.MAX_ITERATIONS = 300;
        this.EXTRA_ITERATIONS_CONSTANT_TEMPERATURE = 200;
        this.kb = dataIn.kb || new KnowledgeBase();
        this.reactionsInSolution = [];
        this.reactionsEvaluated = [];
        this.speciesInSolution = [];
        this.thermal = (this.insulated) ? null : new Cooling();
        this.equilibriumSettings = {liquidUnitActivity: true, ...dataIn.equilibriumSettings};
        if (dataIn.species) {
            _ref = dataIn.species;
            for (_i = 0, _len = _ref.length; _i < _len; _i++) {
                s1 = _ref[_i];
                sp = {};
                sp['id'] = parseInt(s1.id, 10);
                if (!s1.amount) {
                    sp['moles'] = 0.0;
                } else {
                    sp['moles'] = parseFloat(s1.amount);
                }
                this.speciesInSolution.push(sp);
            }
        } else {
            _ref1 = dataIn.speciesInSolution;
            for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
                s1 = _ref1[_j];
                sp = {};
                sp['id'] = s1.archetype.id;
                sp['moles'] = s1.moles;
                this.speciesInSolution.push(sp);
            }
        }

        this.colors = [];
        for (_i = Constants.MIN_WAVELENGTH; _i < Constants.MAX_WAVELENGTH; _i += 2) {
            this.colors.push({startWavelength: _i, endWavelength: _i + 2});
        }
    };

    /**
     * Since Solutions are created in mass, we delay the initialization from
     * instantiation. This method does the time-consuming initialization steps.
     */
    Solution.prototype.initialize = function () {
        var i, moles1, s, snode, sp1;
        if (this.initialized === true) {
            throw "RunTimeException : Solution.initialized is suppose to be false";
        }
        this.initialized = true;
        s = this.speciesInSolution;
        i = void 0;
        this.speciesInSolution = [];
        this.reactionsInSolution = [];
        this.reactionsEvaluated = 0;
        i = 0;
        while (i < s.length) {
            sp1 = this.kb.getSpecies(s[i].id);
            moles1 = s[i].moles;
            snode = new SpeciesNode(sp1, moles1);
            this.add(snode);
            i++;
        }
        if (this.liquidVolume !== 0) {
            this.findEquilibriumTemp(true);
        }
    };

    /**
     * Create a copy of the current solution, not a reference
     *
     * @returns other	{Solution}
     */
    Solution.prototype.clone = function () {
        if (!this.initialized) {
            this.initialize();
        }

        // Local Variables
        var other = new Solution(this),
            current,
            i;

        other.initialized = true;
        other.speciesInSolution = [];
        other.reactionsInSolution = [];

        for (i = 0; i < this.speciesInSolution.length; i++) {
            current = this.speciesInSolution[i];
            current = current.clone();
            current.solvent = other;
            other.speciesInSolution.push(current);
        }

        for (i = 0; i < this.reactionsInSolution.length; i++) {
            current = this.reactionsInSolution[i];
            other.addReaction(current.archetype);
        }

        other.reactionsEvaluated = this.reactionsEvaluated;

        if (!this.isAtRoomTemperature() && !this.insulated) {
            other.thermal = new Cooling();
            other.thermal.start();
        }

        return other;
    };

    /**
     * Mixes a solution into the current solution. If volumeRequested > volume
     * of the solution, only the available volume is poured. The actual
     * volume poured is returned. Notify if this action should be replicate
     * for collaborative mode.
     *
     * @param s					{Solution}	the solution to be mixed
     * @param volumeRequested	{Number}	how much to mix
     *
     * @returns {Number}	actual volume poured
     */
    Solution.prototype.mix = function (s, volumeRequested) {
        if (!this.initialized) {
            this.initialize();
        }

        if (!s.initialized) {
            s.initialize();
        }

        var orig = this.clone(),
            i;

        orig.container = this.container;
        var other = s.clone();
        other.container = s.container;

        // gets the filtrate of the source for calculating purposes
        var sourceCopyNoSolids = s.getFiltrate();

        // makes the requested volume possible
        var volume = volumeRequested;

        if(volume <= 0.0) {
            return 0.0;
        }

        // find the amount of solid in the source solution
        var sSolidVolume = s.getSolidVolume();

        // determine how much volume of solid to pour
        var solidFraction = 0.0;
        var solidPourVol = 0.0;

        if (Math.abs(volume - s.liquidVolume) < Constants.PRETTY_SMALL_NUMBER) {
            volume = s.liquidVolume;
        }

        // don't transfer solid if their volume
        // is really small
        if (volume > s.liquidVolume && sSolidVolume > Constants.PRETTY_SMALL_NUMBER) {
            solidFraction = (volume - s.liquidVolume) / sSolidVolume;
            solidPourVol = solidFraction * sSolidVolume;
        }
        solidFraction = (solidFraction > 1) ? 1.0 : solidFraction;

        // gets properties for the mixed solutions

        // the recipient

        var rVolume = this.getVolume(); // in L
        var rSpecificHeat = this.getSpecificHeat(); // in cal/g/K
        var rTemperature = this.getTemperature(); // in K
        var rDensity = this.getDensity(); // in g/mL

        // the full source
        var sSpecificHeat = s.getSpecificHeat();
        var sDensity = s.getDensity();
        var sTemperature = s.getTemperature();

        // the source without solids
        var nsSpecificHeat = sourceCopyNoSolids.getSpecificHeat();
        var nsDensity = sourceCopyNoSolids.getDensity();

        // the source
        if (sourceCopyNoSolids.liquidVolume !== 0 && !isNaN(nsSpecificHeat) && !isNaN(nsDensity)) {
            sSpecificHeat = solidFraction * sSpecificHeat + (1 - solidFraction) * nsSpecificHeat;
            sDensity = solidFraction * sDensity + (1 - solidFraction) * nsDensity;
        }

        // Local Variables
        var thisNode,
            ratio,
            copyNode,
            nodePourVol,
            molFrac

        // get an appropriate amount of each SpeciesNode and pour
        for (i = 0; i < s.speciesInSolution.length; i++) {
            thisNode = s.speciesInSolution[i];

            copyNode = thisNode.clone();
            ratio = (volume - solidPourVol) / s.liquidVolume;

            if (s.liquidVolume === 0 || volume === 0) {
                ratio = 0.0;
            }

            // Trying to be careful about s.getVolume() being small enough
            // to lead to large roundoff errors. If ratio < 1, we probably
            // won't get into too much trouble.
            if (thisNode.archetype.state !== SpecieState.SOLID && volume !== 0) {
                if (ratio <= 1) {
                    copyNode.moles *= ratio;
                } else {
                    // Don't pour anything.
                }
            } else if (thisNode.archetype.state === SpecieState.SOLID && copyNode.moles > 0) {
                nodePourVol = solidPourVol * (thisNode.getVolume() / sSolidVolume);
                molFrac = nodePourVol / copyNode.getVolume();

                molFrac = (molFrac > 1.0) ? 1.0 : molFrac;
                molFrac = (molFrac < 0.0) ? 0.0 : molFrac;

                copyNode.setMoles(molFrac * copyNode.moles);
                thisNode.removeFraction(molFrac);
            }
            this.add(copyNode);
        }

        // pour away liquid
        if (volume - solidPourVol > 0) {
            s.pourAwayLiquid(volume - solidPourVol);
        }

        // if we poured anything, inform listeners, add liquid, find equilibrium
        if (volume > 0) {
            this.liquidVolume += (volume - solidPourVol);
            if (this.liquidVolume < Constants.ROUNDOFF_TOLERANCE) {
                this.liquidVolume = 0;
            }

            if (rVolume > 0.0 && (sSpecificHeat !== 0 || rSpecificHeat !== 0)) {
                var transferVolume = volume;
                var t =
                    (sSpecificHeat * transferVolume * sDensity * sTemperature +
                        rSpecificHeat * rVolume * rTemperature * rDensity) /
                            (sSpecificHeat * transferVolume * sDensity +
                                rSpecificHeat * rVolume * rDensity);

                this.setTemperature(t);

            } else {
                this.setTemperature(sTemperature);
            }
            // Stir very, very quickly. For now, don't do anything unless
            // there's some liquid in the solution.
            this.findEquilibriumTemp(false);
        }

        return volume;
    };

    /**
     * Removes all the solids for this solution
     */
    Solution.prototype.filtrate = function () {
            var i,
            sn;

        for (i = 0; i < this.speciesInSolution.length; i++) {
            sn = this.speciesInSolution[i];

            if (sn.archetype.state === SpecieState.SOLID) {
                sn.removeFraction(1.0);
            }
        }

        this.findEquilibriumTemp(true);
    };

    /**
     * Returns a filtrated clone of the solution
     *
     * @returns noSolidsSolution {Solution} the filtrated solution
     */
    Solution.prototype.getFiltrate = function () {
        var noSolidsSolution = this.clone();
        noSolidsSolution.filtrate();

        return noSolidsSolution;
    };

    /**
     * Returns the color of the liquid solution.
     */
    Solution.prototype.getColor = function () {
        if (!this.initialized) {
            this.initialize();
        }

        var rAbsTot = 0;
        var bAbsTot = 0;
        var gAbsTot = 0;
        var r, g, b;

        var i;
        for (i = 0; i < this.speciesInSolution.length; i++) {
            var sn = this.speciesInSolution[i];
            var s = sn.archetype;

            var solid = s.state === SpecieState.SOLID;

            if (s.standardColorConcentration > 0 && !solid && 'hue' in s && 'saturation' in s && 'value' in s) {

                // Obtain species data
                var concentration = sn.getConcentration();
                var colorConc = s.standardColorConcentration;
                var hue = s.hue / 360;
                var saturation = s.saturation / 100;
                var value = s.value / 100.0;
                var baseColorValue, finalColorValue, finalColor;

                var rgbColor = tinycolor.fromRatio({h: hue, s: saturation, v: value});
                var baseColor = new ColorCHSV(rgbColor.toRgb());

                baseColorValue = (baseColor.CV > 200) ? 200 : baseColor.CV;
                baseColorValue = (baseColor.CV < 40) ? 40 : baseColor.CV;

                // Obtain value from concentration
                finalColorValue = 240.0 * Math.pow(baseColorValue / 240.0, concentration / colorConc);

                if (finalColorValue === Infinity) {
                    finalColorValue = 240;
                }

                finalColor = baseColor;
                finalColor.CV = (finalColorValue > 240) ? 240 : finalColorValue;

                var c = finalColor.getRGBColor();

                var rAbsorbed = 1 - c.r / 255.0;
                var gAbsorbed = 1 - c.g / 255.0;
                var bAbsorbed = 1 - c.b / 255.0;

                rAbsTot += rAbsorbed;
                gAbsTot += gAbsorbed;
                bAbsTot += bAbsorbed;
            }
        }

        r = (rAbsTot > 1) ? 0 : 1 - rAbsTot;
        g = (gAbsTot > 1) ? 0 : 1 - gAbsTot;
        b = (bAbsTot > 1) ? 0 : 1 - bAbsTot;

        r *= 255;
        g *= 255;
        b *= 255;


        return tinycolor({r: r, g: g, b: b});
    };

    /**
     * Returns the density of this solution
     *
     * @return Density in g/mL.
     */
    Solution.prototype.getDensity = function () {
        if (!this.initialized) {
            this.initialize();
        }

        var density = this.getSolutionWeight() / this.getVolume() / 1000.0;
        return (isNaN(density) || !isFinite(density)) ? 1.0 : density;
    };

    /**
     * Returns the weight (in grams) of the solution exact and without the flask
     *
     * @returns {Number}
     */
    Solution.prototype.getSolutionWeight = function () {
        return WeightSolutionModeler.getValue(this);
    };

    /**
        Needs testing! What about solids? Looks like solids are included
        in the "solution", so this should work.
     */
    Solution.prototype.getSolventWeight = function () {
        let solutionWeight = this.getSolutionWeight();
        // Solvent has id = 0,
        let weightOfSolutes = this.getSpeciesData()
            .filter(x => x.id != 0)
            .map(x => x.weight)
            .reduce( (x, y) => x+y,0); // Total...
        return solutionWeight - weightOfSolutes;
    };
    /**
     * Calls the full findEquilibrium with default convergenceCriterion, and
     * maximumIterations, but allows the user to specify whether or not the
     * equilibrium is found with constant temperature or not.
     *
     * @param constantTemp	{Boolean}
     */
    Solution.prototype.findEquilibriumTemp = function (constantTemperature=false) {
        var iter = 0;
        if (constantTemperature) {
            iter = this.EXTRA_ITERATIONS_CONSTANT_TEMPERATURE;
        }

        this.findEquilibrium(this.DEFAULT_NUMERICAL_TOLERANCE, this.MAX_ITERATIONS + iter, constantTemperature);
    };

    Solution.prototype.pruneReactionTree = function (reactions) {
        let combinations = [];
        let parents = [];
        const rSize = reactions.length;
        
        for (i = 0; i < rSize; i++) {
            let current = reactions[i];

            for (j = i + 1; j < rSize; j++) {
                let compare = reactions[j];

                // Our evaluations is more efficient if we know which
                // reaction is smaller.
                let r1 = current.archetype;
                let r2 = compare.archetype;

                if (r2.getSpeciesCount() > r1.getSpeciesCount()) {
                    // Reverse the order;
                    r2 = r1;
                    r1 = compare.archetype;
                }

                // What exactly does this function do? Finds species in common?
                let c = r1.getOverlappingCoefficients(r2);

                if (c.length === 0) {
                    continue;
                }

                // For each coefficient in common...
                let n;
                for (n = 0; n < c.length; n++) {
                    let f = c[n];

                    n++;

                    let s = c[n];

                    let temp = r1.scale(s);
                    let sum = temp.add(r2.scale(f * -1));

                    if (sum === null || sum.getSpeciesCount() > 5 || sum.isBetweenSolids) {
                        continue;
                    }

                    // Is this a duplicate?
                    let duplicate = false;
                    for (k = 0; k < combinations.length; k++) {
                        if (combinations[k].equals(sum)) {
                            duplicate = true;
                            break;
                        }
                    }

                    if (duplicate) {
                        continue;
                    }

                    // Convert to ReactionNode.
                    let species = [];
                    for (k = 0; k < sum.getSpeciesCount(); k++) {
                        let sk = sum.getSpeciesAt(k);
                        let other = this.getSpeciesNode(sk);
                        // Important to set solvent = Solution - needed for concentration calculations and
                        // must be done manually.
                        other.solvent = this;
                        species.push(other);
                    }

                    let combo = new ReactionNode(sum, species);
                    combinations.push(combo);

                    parents.push(i * rSize + j);
                }
            }
        }
        return [combinations, parents];
    }
    
    /**
     * Calls the full findEquilibrium with default convergenceCriterion, and
     * maximumIterations, but allows the user to specify whether or not the
     * equilibrium is found with constant temperature or not.
     *
     * @param convergenceCriterion	{Number}
     * @param maxIter				{Number}
     * @param constantTemperature	{Boolean}
     */
    Solution.prototype.findEquilibrium = function (convergenceCriterion, maxIter, constantTemperature) {
        if (!this.initialized) {
            this.initialize();
        }
        var i,
            j,
            k,
            r;

        // Check for a liquid in the solution
        var isThereAnyLiquid = false;

        // There's no equilibrium if there isn't any liquid specie
        isThereAnyLiquid = (this.liquidVolume === 0) ? false : true;

        // Temperature change...
        // What is this.thermal (only for)
        if (this.thermal !== null) {
            var energy = this.thermal.getHeatSinceLastCall(this);
            // in K

            var newTemperature = this.getTemperature();

            if (this.getSpecificHeat() > Constants.PRETTY_SMALL_NUMBER) {
                var dT = energy * 1000 / (this.getSolutionWeight() * this.getSpecificHeat()) / Constants.CAL_TO_J;
                newTemperature += dT;
                var bp = BoilingPointSolutionModeler.getValue(this);
                var fp = FreezingPointSolutionModeler.getValue(this);
                newTemperature = (newTemperature > bp) ? bp : newTemperature;
                newTemperature = (newTemperature < fp) ? fp : newTemperature;
            }
            else if (this.thermal !== null) {
                // if (_.isEqual(this.thermal, new Cooling())) {
                //     newTemperature = Constants.ROOM_TEMPERATURE;
                // }
                // else {
                //     newTemperature = 373.15;//BoilingPointSolutionModeler.getValue(this);
                // }
            }

            if (!constantTemperature) {
                this.setTemperature(newTemperature);
            }
        }

        // Calculate elapsed time since last call

        if (Object.keys(this.kineticsTimers).length > 0) {
            const time =  new Date().getTime();
            this.kineticsDeltaT = Object.fromEntries(
                Object.entries(this.kineticsTimers).map( ([key, val]) => [key, (time - val)/1000.0])
            );
            this.kineticsTimers = Object.fromEntries(Object.entries(this.kineticsTimers).map(
                ([key, val]) => [key, time]
            ));
        }

        
        // Finds the equilibrium, if there's any liquid
        if (isThereAnyLiquid) {
            // Copy of reactionsInSolution array
            let reactions = this.reactionsInSolution.slice(0);
      

            /**
             * Checks if a reaction involves any gas species
             * @param {ReactionNode} reaction - The reaction to check
             * @returns {boolean} - True if the reaction involves gas species, false otherwise
             */
            function hasGas(reaction) {
                for (let s = 0; s < reaction.archetype.getSpeciesCount(); s++) {
                    const species = reaction.archetype.getSpeciesAt(s);
                    if (species.state === SpecieState.GAS) {
                        return true;
                    }
                }
                return false;
            }

            // First, check if there are any gas phase reactions at all
            const hasGasReactions = reactions.some(r => hasGas(r));

            // Only check gas volume if there are gas reactions
            if (hasGasReactions) {
                const gasVolume = this.getGasVolume();
                if (gasVolume <= 0) {
                    reactions = reactions.filter(r => !hasGas(r));
                }
            }
        
            // Update rSize with filtered array length
            let rSize = reactions.length;

            var relChangeInConc = 0.0;
            var iter = 0;

            var iterationsTillNextPruning = 0;
            var anyReactionLimited = false;
            var deltaH = 0;

            do {
                anyReactionLimited = false;

                // Is it time to prune the reaction tree?
                if (iterationsTillNextPruning === 0) {
                    // Prune the tree function ...
                    // Create neighboring linear combination reactions.
                    // Only include reactions that are not kinetic reactions...
                    var [combinations, parents] = this.pruneReactionTree(reactions.filter(x=>!x.kinetic_reaction));
                }

                // Push all of our reactions, tallying the change in
                // concentration and heat.
                deltaH = 0.0;
                relChangeInConc = 0.0;

                for (j = 0; j < rSize; j++) {
                    r = reactions[j];

                    if (r.kinetic_reaction) {
                        if (iter === 0 ) {
                            // Only advance kinetic reactions on the first iteration.
                            relChangeInConc += r.iteration(constantTemperature, this.kineticsDeltaT[j]);
                        } else {
                            // Otherwise, just prevent any further iteration...
                            r.xPreviousIteration = 0; 
                        }
                    } else {
                        relChangeInConc += r.iteration(constantTemperature);
                    }
                    anyReactionLimited = anyReactionLimited || r.isShiftLimited;
                    deltaH += r.archetype.getHo() * r.xPreviousIteration;
                    // 
                    deltaH +=
                        r.archetype.getHeatCapacity() / 1000 * r.xPreviousIteration
                        * (this.getTemperature() - Constants.ROOM_TEMPERATURE);
                }


                // If we are pruning this iteration, this will loop over the new
                // reactions and iterate them as well.
                for (j = 0; j < combinations.length; j++) {
                    r = combinations[j];
                    relChangeInConc += r.iteration(constantTemperature);
                    anyReactionLimited |= r.isShiftLimited;
                    
                    deltaH += r.archetype.getHo() * r.xPreviousIteration;
                    deltaH +=
                        r.archetype.getHeatCapacity() / 1000 * r.xPreviousIteration
                        * (this.getTemperature() - Constants.ROOM_TEMPERATURE);

                }

                // Update temperature.
                if (!constantTemperature) {
                    var newTemperature = Constants.ROOM_TEMPERATURE;
                    // deltaH in kJ, dT in K
                    if (this.getSolutionWeight() * this.getSpecificHeat() !== 0) {
                        var dT = -deltaH * 1000 / (this.getSolutionWeight() * this.getSpecificHeat()) / Constants.CAL_TO_J;
                        newTemperature = this.getTemperature() + dT;

                        var bp = BoilingPointSolutionModeler.getValue(this);
                        var fp = FreezingPointSolutionModeler.getValue(this);
                        newTemperature = (newTemperature > bp) ? bp : newTemperature;
                        newTemperature = (newTemperature < fp) ? fp : newTemperature;
                    }
                    this.setTemperature(newTemperature);
                }

                // Updates liquid volume
                this.liquidVolume = this.calculateLiquidVolume();

                iter++;
                iterationsTillNextPruning--;

                // If we are pruning, swap more efficient reactions into the
                // reaction tree.
                // This part does nothing...
                // if (combinations.length > 0) {
                //     // Sort.
                //     // Sort could create a new range array...
                //     var originalSorted = [];
                //     for (i = 0; i < rSize; i++) {
                //         originalSorted[i] = i;
                //     }

                //     this.sort(this.reactionsInSolution, originalSorted);

                //     var combinationsSorted = [];
                //     for (i = 0; i < combinations.length; i++) {
                //         combinationsSorted[i] = i;
                //     }

                //     this.sort(combinations, combinationsSorted);

                //     // Choose replacements.
                //     var replacements = [];
                //     for (i = 0; i < rSize; i++) {
                //         replacements[i] = -1;
                //     }

                //     var changed = 0;

                //     for (i = 0; i < rSize; i++) {
                //         var sIndex = originalSorted[i];
                //         var singleton = reactions[sIndex];

                //         // We're looking at a low placed singleton.
                //         // Find a better combination.
                //         for (j = combinations.length - 1; j > -1; j--) {
                //             var index = combinationsSorted[j];

                //             var combo = combinations[index];

                //             if (Math.abs(combo.xPreviousIteration < Math.abs(singleton.xPreviousIteration))) {
                //                 break;
                //             }

                //             // Get its two parents.
                //             var parent = parents[index];

                //             var p1 = parent / rSize;
                //             var p2 = parent - rSize * p1;

                //             if (p1 !== sIndex && p2 !== sIndex) {
                //                 continue;
                //             }

                //             // One of these two must be us.
                //             var other = (p1 === sIndex) ? p2 : p1;

                //             // Accept the combination if p2 hasn't been
                //             // upgraded.
                //             if (replacements[other === -1]) {
                //                 changed++;
                //                 replacements[sIndex] = index;
                //                 break;
                //             }
                //         }
                //     }

                //     for (i = 0; i < rSize; i++) {
                //         var current = replacements[i];

                //         if (current !== -1) {
                //             var r = combinations[current];

                //             this.reactionsInSolution[i] = r;
                //             reactions[i] = r;
                //         }
                //     }

                //     iterationsTillNextPruning = 5 - Math.floor(5 * changed / rSize);
                // }
            } while ((anyReactionLimited || (relChangeInConc > convergenceCriterion)) && (iter < maxIter));
            
            if (iter >= maxIter) {
                console.log("Exceeded Maximum # of iterations: " + iter + " (xDiff=" + relChangeInConc + ", " + anyReactionLimited + ")");
                for (i = 0; i < reactions.length; i++) {
                    r = reactions[i];
                    console.log("Reaction " + i + ": " + r.toString() + ", xDiff=" + r.xPreviousIteration);
                }
                console.log("Liquid Volume: " + this.liquidVolume);
                console.log("Temperature: " + this.getTemperature());
                console.log("Solution Weight: " + this.getSolutionWeight());
                console.log("Specific Heat: " + this.getSpecificHeat());
            }

            // Iterate over the reactions and give information about any that are not at equilibrium or shift limited.
            // Iterate over reactions and log information about those not at equilibrium
            for (let j = 0; j < reactions.length; j++) {
                const r = reactions[j];
                
                // Calculate log(K) - log(Q)
                const logKQ = r.f(0);
                
                // Check if the reaction is shift limited and why...
                if ((Math.abs(logKQ) > 1e-4) && (r.depletionState * logKQ <= 0) && (!r.kinetic_reaction)) {
                    console.log(`Reaction ${r.toString()}: shift limited = ${r.isShiftLimited}, depletionState=${r.depletionState}, log(K/Q) = ${logKQ}`);
                }
            }


            // Updates the volume for the liquid
            this.liquidVolume = this.calculateLiquidVolume();
        }
    };

    /**
     * Return the liquid volume of the solution
     */
    Solution.prototype.calculateLiquidVolume = function () {
        return LiquidVolumeSolutionModeler.getValue(this);
    };

    /**
     * Return the specific heat of the solution in cal/g/K
     *
     * @returns {Number}	Specific heat in cal/g/K
     */
    Solution.prototype.getSpecificHeat = function () {
        return SpecificHeatSolutionModeler.getValue(this);
    };

    Solution.prototype.getConductivity = function () {  
        // No need for a separate conductivity modeler
        // Just iterate through the aqueous species, and use x.archetype.conductivity * concentration
        // to get the conductivity of the solution.
        // Then sum them all up.
        if (this.liquidVolume > 0)  {
        const conductivityArray = this.speciesInSolution
            .filter(x => x.archetype.state === SpecieState.AQUEOUS)
            .map(x => x.archetype.conductivity * x.getConcentration());
        const conductivity = conductivityArray.reduce((acc, val) => acc + val, 0);
        return conductivity;
        }
        else {
        // On the other hand, if solutionVolume is 0, we can leave it blank (not 0) or
        // report the conductivity of a pure solid if present
        const conductivitySolid = this.speciesInSolution
            .filter(x => x.archetype.state === SpecieState.SOLID)
            .map(x => x.archetype.conductivity);
        
        if (conductivitySolid.length === 1) {
            return conductivitySolid[0];
            } else {
            return NaN;
        }
        }
    }

    Solution.prototype.sort = function (input, map) {
        this.sort(input, map, 0, map.length - 1);
    }

    Solution.prototype.sort = function (input, map, begin, end) {
        if (begin < end) {
            var pivotLocation = this.partition(input, map, begin, end);
            this.sort(input, map, begin, pivotLocation);
            this.sort(input, map, pivotLocation + 1, end);
        }
    }
    
    // What does partition, sort, etc do?
    Solution.prototype.partition = function (input, map, begin, end) {
        var rand = Math.random();
        rand *= (begin - end + 1);
        rand -= .5;

        var chosen = Math.round(rand);

        chosen = (chosen < begin) ? begin : chosen;
        chosen = (chosen > end) ? end : chosen;

        var temp = map[chosen];
        map[chosen] = map[begin];
        map[begin] = temp;

        var i = begin - 1;
        var j = end + 1;

        var pivot = input[map[begin]];
        var current = null;

        var x = Math.abs(pivot.xPreviousIteration);

        while (true) {
            do {
                --j;
                current = input[map[j]];
            } while (Math.abs(current.xPreviousIteration) < x);

            do {
                ++i;
                current = input[map[i]];
            } while (Math.abs(current.xPreviousIteration) < x);

            if (i < j) {
                temp = map[j];
                map[j] = map[i];
                map[i] = temp;
            }
            else {
                return j;
            }
        }
    }

    /**
     * This method adds the argument SpeciesNode, first checking to see if it is
     * already present, and if it is not, checking it with other species.json to see
     * if it incorporates a new reaction. If so that reaction is created.
     *
     * @param s {SpeciesNode}
     */
    Solution.prototype.add = function (sn) {
        var allProductsPresent, allReactantsPresent, current, i, rIndex, reactions, siblings, speciesIndex;
        speciesIndex = getIndex(this.speciesInSolution, sn);
        if (speciesIndex === -1) {
            this.speciesInSolution.push(sn);
            sn.solvent = this;
            reactions = this.kb.getReactionsContaining(sn.archetype);
            rIndex = 0;
            while (rIndex < reactions.length) {
                current = reactions[rIndex];
                siblings = current.getProducts();
                allProductsPresent = true;
                for (i = 0; i < siblings.length; i++) {
                    if (!this.isPresent(siblings[i])) {
                        allProductsPresent = false;
                        break;
                    }
                }
                siblings = current.getReactants();
                allReactantsPresent = true;
                for (i = 0; i < siblings.length; i++) {
                    if (!this.isPresent(siblings[i])) {
                        allReactantsPresent = false;
                        break;
                    }
                }
                if (allProductsPresent || allReactantsPresent) {
                    this.addReaction(current);
                }
                rIndex++;
            }
        } else {
            this.speciesInSolution[speciesIndex].merge(sn);
        }
    };

    /**
     * Checks to see if a reaction is already present in the solution (which
     * should never occur; if it's already present, then all reacting species.json
     * would already be present, but it never hurts to check), exiting if the
     * reaction is found.
     *
     * It then checks to see if each individual component to the Reaction is
     * already present. If so, it is included in the ReactionNode. If not, a new
     * SpeciesNode is produced for it.
     *
     * @param r
     */
    Solution.prototype.addReaction = function (r) {
        var index = getIndex(this.reactionsInSolution, r),
            sIndex,
            placeHolder,
            species,
            i,
            current,
            sNode,
            rNode;

        if (index === -1) {
            // We store the reaction in the reaction vector as a placeholder
            // to avoid the result of our species.json additions from adding in
            // the same Reaction.
            placeHolder = this.reactionsInSolution.length;
            // FIXME according to rest of code reactionsInSolution should be
            // Vector<ReactionNode>
            // this one line has reactionsInSolutions of type Vector<Reaction>
            this.reactionsInSolution.push(r);

            species = [];

            for (i = 0; i < r.getSpeciesCount(); i++) {
                current = r.getSpeciesAt(i);
                for(sIndex = 0; sIndex < this.speciesInSolution.length; sIndex++) {
                    if(this.speciesInSolution[sIndex].archetype.id === current.id) {
                        species.push(this.speciesInSolution[sIndex]);
                        break;
                    }
                }
                if(sIndex >= this.speciesInSolution.length) {
                    // Create and add the species.json node.
                    sNode = new SpeciesNode(current, 0.0);
                    this.add(sNode);
                    species.push(sNode);
                }
            }

            // Replace the Reaction we used as a placeholder with a real
            // reaction node.
            rNode = new ReactionNode(r, species);
            this.reactionsInSolution[placeHolder] = rNode;
            // We create the reaction node, then add it to the list of reactions here; as soon as we use addReaction, we need to initialize a timer if kinetic data is needed...
            // Should the solution own this timer? Probably?
            if (rNode.kinetic_reaction) {
                this.kineticsTimers[placeHolder] = new Date().getTime(); // Start the timer for this reaction
                console.log(`Started kinetics timer for reaction ${r.toString()}`);
            }
        }

    };

    /**
     * Returns the total volume in Liters (including solid volume).
     *
     * @return Volume in L
     */
    Solution.prototype.getVolume = function () {
        var totalVolume = this.liquidVolume + this.getSolidVolume();

        return totalVolume;
    };

    /**
     * Returns the volume of the solids in the solution in Liters
     */
    Solution.prototype.getSolidVolume = function () {
        var solidVolume = 0.0,
            i,
            solid;

        for (i = 0; i < this.getSolidCount(); i++) {
            solid = this.getSolidAt(i);
            solidVolume += solid.getVolume();
        }

        return solidVolume;
    };

    Solution.prototype.getGasVolume = function () {
        // Should I more prominently store the maxVolume in the Solution?
        return this.container.attributes.vessel.maxVolume - this.getVolume();
    }

    /**
     * Returns the temperature in Kelvin.
     */
    Solution.prototype.getTemperature = function () {
        if (!this.initialized) {
            this.initialize();
        }

        return this.temperature;
    };

    /**
     * Sets the temperature, which may result in a change in the thermal device.
     * This differs from changeTemperature in that it should only be called by
     * the Solution. Hence it will not inform listeners of the change, or check
     * for initialization.
     */
    Solution.prototype.setTemperature = function (solutionTemperature) {
        if(solutionTemperature <= 0.0) {
            throw "InvalidArgumentException : solutionTemperature is less than 0 in Solution.setTemperature";
        }

        this.temperature = solutionTemperature;

        if (this.thermal === null && !this.isAtRoomTemperature() && !this.insulated) {
            this.thermal = new Cooling();
            this.thermal.start();
        }
    };

    /**
     * Sets the temperature, which will result in a re-equilibration. Similar to
     * setTemperature, this method will modify the thermal devices if necessary.
     * @param solutionTemperature
     */
    Solution.prototype.changeTemperature = function (solutionTemperature) {
        if (!this.initialized) {
            this.initialize();
        }

        this.setTemperature(solutionTemperature);

        this.findEquilibriumTemp(true);
    };

    /**
     * Returns true if this solution is at room temperature.
     * @returns {boolean}
     */
    Solution.prototype.isAtRoomTemperature = function () {
        var T = this.temperature;
        var Ta = Constants.ROOM_TEMPERATURE;

        return (T > Ta - .01 && T < Ta + .01);
    }

    /**
     * Sets the thermal device. Only one thermal device can be active at any one
     * time. This will automatically start the device.
     * @param device
     */
    Solution.prototype.setThermal = function (device) {
        if (!this.initialized) {
            this.initialize();
        }
        var T = this.getTemperature();
        var Ta = Constants.ROOM_TEMPERATURE;
        var counter;
        if (device === null) {
            counter++;
            try {
                if (counter === 1) {
                    this.findEquilibrium(false);
                }
            }
            catch (ignored) {}
            counter = 0;
        }

        if (device === null && (T > Ta + .01 || T < Ta - .01) && !this.insulated) {
            device = new Cooling();
        }

        if (device !== null) {
            device.start();
        }

        this.thermal = device;
    };

    /**
     * Sets the insulated state of the solution
     * @param b
     */
    Solution.prototype.setInsulated = function (b) {
        var orig = this.clone();
        this.insulated = b;

        if (b && this.thermal !== null && this.thermal instanceof Cooling) {
            this.thermal.stop();
            this.thermal = null;
        }
        else if (!b && this.thermal === null && !this.isAtRoomTemperature()) {
            this.thermal = new Cooling();
            this.thermal.start();
        }
    };

    /**
     * Returns a Vector containing all SpeciesNodes in this solution that are
     * solids.
     */
    Solution.prototype.getSolidCount = function () {
            var thisNode,
            count = 0,
            i;

        for (i = 0; i < this.speciesInSolution.length; i++) {
            thisNode = this.speciesInSolution[i];

            if (thisNode.archetype.state === SpecieState.SOLID) {
                count++;
            }
        }

        return count;
    };

    /**
     * Get the i'th solid in this solution, null if bad index.
     */
    Solution.prototype.getSolidAt = function (i) {
        var count = 0,
            returnValue = null,
            thisNode,
            j;

        for (j = 0; j < this.speciesInSolution.length; j++) {
            thisNode = this.speciesInSolution[j];

            if (thisNode.archetype.state === SpecieState.SOLID) {
                if (count === i) {
                    returnValue = thisNode;
                    break;
                } else {
                    count++;
                }
            }
        }

        return returnValue;
    };

    /**
     * Gets the color for the solids in the solution
     */
    Solution.prototype.getSolidColor = function () {
        var solidVolume = this.getSolidVolume();

        // Compute solid color.
        var r = 255;
        var g = 255;
        var b = 255;
        var rRef = 0;
        var gRef = 0;
        var bRef = 0;
        var scale = 0;

        var i;
        for (i = 0; i < this.getSolidCount(); i++) {
            var sn = this.getSolidAt(i);
            var s = sn.archetype;
            scale = sn.getVolume() / solidVolume;

            if (s.standardColorConcentration !== 0.0) {
                var hue = s.hue / 360.0;
                var saturation = s.saturation / 100;
                var value = s.value / 100.0;

                var c = tinycolor.fromRatio({h: hue, s: saturation, v: value}).toRgb();

                rRef += c.r * scale;
                gRef += c.g * scale;
                bRef += c.b * scale;
            }
            else {
                rRef += r * scale;
                gRef += g * scale;
                bRef += b * scale;
            }
        }

        rRef = (rRef > 255) ? 255 : rRef;
        gRef = (gRef > 255) ? 255 : gRef;
        bRef = (bRef > 255) ? 255 : bRef;

        return tinycolor({r: rRef, g: gRef, b: bRef});
    };

    /**
     * Dumps out the specified amount of the liquid in the solution.
     *
     * @param volumePoured the amount of the solution to be poured
     */
    Solution.prototype.pourAwayLiquid = function (volumePoured) {
        var f,
            i,
            s

        if (!this.initialized) {
            this.initialize();
        }

        if (volumePoured > 0) {
            if (volumePoured > this.liquidVolume) {
                volumePoured = this.liquidVolume;
            }

            f = volumePoured / this.liquidVolume;

            for (i = 0; i < this.speciesInSolution.length; i++) {
                s = this.speciesInSolution[i];

                // Solids handled in mix()
                if (s.archetype.state !== SpecieState.SOLID && !s.infinite && s.moles !== 0) {
                    s.removeFraction(f);
                }
            }

            this.liquidVolume -= volumePoured;
        }
    };

    /**
     * Returns the number of Species in this solution.
     */
    Solution.prototype.getSpeciesCount = function () {
        if (!this.initialized) {
            this.initialize();
        }

        return this.speciesInSolution.length;
    };

    /**
     * Returns the SpeciesNode at the specified index.
     */
    Solution.prototype.getSpeciesAt = function (i) {
        if (!this.initialized) {
            this.initialize();
        }

        return this.speciesInSolution[i];
    };

    /**
     * TODO: Remove this method, used purely for testing
     */
    Solution.prototype.toggleClone = function () {
        this.isClone = true;
    };

    /**
     * Gets the pH of the solution.
     * @returns {*}
     */
    Solution.prototype.getPH = function () {
        var pH, sp, _i, _len, _ref;
        _ref = this.speciesInSolution;
        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
            sp = _ref[_i];
            if (sp.archetype.name === 'H<sup>+</sup>' || sp.archetype.name === 'H<sub>3</sub>O<sup>+</sup>') {
                pH = -Math.log10(sp.getConcentration());
                break;
            }
        }
        return pH;
    };

    /**
     * Returns an array containing useful information about the present species.json.
     * @returns {Array}
     */
    Solution.prototype.getSpeciesData = function () {
        var regex, sdata, slist, sp, _i, _len, _ref;
        slist = [];
        _ref = this.speciesInSolution;
        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
            sp = _ref[_i];
            sdata = {};
            sdata.id = sp.archetype.id;
            sdata.name = sp.archetype.name;
            sdata.amount = sp.getConcentration();
            sdata.weight = sp.getWeight();
            sdata.moles = sdata.weight / sp.archetype.molecularWeight;
            sdata.unknown = sp.archetype.unknown;
            sdata.state = sp.archetype.state;
            regex = /(<([^>]+)>)/g;
            sdata.simpleName = sp.archetype.name.replace(regex, "");
            slist.push(sdata);
        }
        return slist;
    };

    /**
     * Whether a species.json is present in the solution
     * @param species a Species object
     * @returns {boolean}
     */
    Solution.prototype.isPresent = function (species) {
        var i;
        for (i = 0; i < this.speciesInSolution.length; i++) {
            if (this.speciesInSolution[i].archetype.id === species.id) {
                return true;
            }
        }
        return false;
    };

    /**
     * Returns the corresponding SpeciesNode in the solution for the input Species, if it is in the solution
     * @param species a Species object
     * @returns {SpeciesNode}
     */
    Solution.prototype.getSpeciesNode = function (species) {
        var i;
        for (i = 0; i < this.speciesInSolution.length; i++) {
            if (this.speciesInSolution[i].archetype.id === species.id) {
                return this.speciesInSolution[i];
            }
        }
        return null;
    };

    Solution.prototype.getAbsorbanceTable = function () {
        var newDataValues = [];
        var abs = Array.apply(null, Array(Constants.MAX_WAVELENGTH - Constants.MIN_WAVELENGTH + 1)).map(Number.prototype.valueOf,0);

        var i;
        for (i = 0; i < this.getSpeciesCount(); i++) {
            var curr = this.getSpeciesAt(i);
            var conc = curr.getConcentration();
            var individual_abs = curr.archetype.getAbsSpectrum();

            if (individual_abs !== null) {
                var j;
                for (j = 0; j < abs.length; j++) {
                    abs[j] += individual_abs[j] * conc;
                }
            }
        }

        for (i = 0; i < this.colors.length; i++) {
            var color = this.colors[i];
            var val = 0;
            var count = 0;
            var wavelength;
            for (wavelength = color.startWavelength; wavelength < color.endWavelength; wavelength++) {
                val += abs[wavelength - Constants.MIN_WAVELENGTH];
                count++;
            }
            val /= count;
            newDataValues.push(val);
        }

        return newDataValues;
    };

    Solution.prototype.getAbsorbance = function (wavelength) {
        var i;
        var difference = this.colors[this.colors.length - 1].endWavelength - this.colors[0].startWavelength;
        var abs = this.getAbsorbanceTable();
        for (i = 0; i < this.colors.length - 1; i++) {
            if (Math.abs(wavelength - this.colors[i].startWavelength) < difference) {
                difference = Math.abs(wavelength - this.colors[i].startWavelength);
            }
            else {
                break;
            }
        }
        return ((abs[i] - abs[i - 1]) / (this.colors[i].startWavelength - this.colors[i - 1].startWavelength)) * difference + abs[i - 1];
    };

    return Solution;
});