/**
* @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;
});