/*
* Copyright (c) 2003-2012 Fred Hutchinson Cancer Research Center
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fhcrc.cpl.toolbox.chem;
import org.fhcrc.cpl.toolbox.datastructure.Pair;
import java.util.*;
/**
* Represents a chemical formula, with useful helper methods.
* User: dhmay
* Date: Apr 6, 2010
* Time: 4:06:07 PM
* To change this template use File | Settings | File Templates.
*/
public class ChemicalFormula
{
//mass of the "monoisotope", or technically "commonest isotope". This must be updated when any change happens
protected double commonestIsotopeMass;
//this is the most important thing. How much of what elements
protected Map<String, Integer> elementCountMap;
//peak frequencies and masses are not necessarily populated, because that's somewhat costly
protected double[] peakFrequencies;
protected double[] peakMasses;
//default number of peaks whose freqs and masses we should calculate, /if/ we calculate
public static final int DEFAULT_NUMPEAKS_TOCALC = 3;
//controls the number of peaks calculated if number not supplied
public static int numPeaksToCalculate = DEFAULT_NUMPEAKS_TOCALC;
/**
* Create a ChemicalFormula that's the same as another ChemicalFormula, but without peaks populated
* @param otherFormula
*/
public ChemicalFormula(ChemicalFormula otherFormula)
{
this(otherFormula.getElementCountMap(), 0);
}
/**
* Creates a ChemicalFormula and does not populate peak masses and frequencies
* @param formulaString
* @throws IllegalArgumentException
*/
public ChemicalFormula(String formulaString) throws IllegalArgumentException
{
this(formulaString, false);
}
/**
* Creates a ChemicalFormula, only populating peak masses and frequencies if specified
* @param elementCountMap
* @param numPeaksToPopulate
* @throws IllegalArgumentException
*/
public ChemicalFormula(Map<String, Integer> elementCountMap, int numPeaksToPopulate)
throws IllegalArgumentException
{
this.elementCountMap = new HashMap<String, Integer>(elementCountMap);
commonestIsotopeMass = ChemCalcs.calcCommonestIsotopeMass(elementCountMap);
if (numPeaksToPopulate > 0)
populatePeakMassesAndFrequencies(numPeaksToPopulate);
}
/**
* Creates a ChemicalFormula, only populating peak masses and frequencies if specified
* @param formulaString
* @param shouldPopulatePeaks
* @throws IllegalArgumentException
*/
public ChemicalFormula(String formulaString, boolean shouldPopulatePeaks) throws IllegalArgumentException
{
this(ChemCalcs.chemicalFormula2AtomCount(formulaString),
shouldPopulatePeaks ? numPeaksToCalculate : 0);
}
/**
* Creates a ChemicalFormula and populates peak masses and frequencies out to the specified # of peaks
* @param formulaString
* @param numPeaksToPopulate
* @throws IllegalArgumentException
*/
public ChemicalFormula(String formulaString, int numPeaksToPopulate) throws IllegalArgumentException
{
this(ChemCalcs.chemicalFormula2AtomCount(formulaString), numPeaksToPopulate);
}
/**
* Add additional elements to this formula. If peak masses, freqs already populated, update them
* @param additionFormula
*/
public void addFormula(ChemicalFormula additionFormula)
{
for (String atom : additionFormula.getElementCountMap().keySet())
elementCountMap.put(atom, additionFormula.getElementCountMap().get(atom) +
(elementCountMap.containsKey(atom) ? elementCountMap.get(atom) : 0));
commonestIsotopeMass = ChemCalcs.calcCommonestIsotopeMass(elementCountMap);
if (peakFrequencies != null)
populatePeakMassesAndFrequencies(peakFrequencies.length);
}
/**
* Create a new ChemicalFormula identical to this one with the additional elements added. Do not
* populate peaks
* @param additionFormula
* @return
*/
public ChemicalFormula createFormulaWithAddition(ChemicalFormula additionFormula)
{
ChemicalFormula newFormula = new ChemicalFormula(this);
newFormula.addFormula(additionFormula);
return newFormula;
}
/**
* Remove specified elements. Populate peaks if they were already populated. Throw IllegalArgumentException
* if the formula doesn't have the specified elements
* @param subtractionFormula
* @return
*/
public ChemicalFormula createFormulaWithSubtraction(ChemicalFormula subtractionFormula)
throws IllegalArgumentException
{
ChemicalFormula newFormula = new ChemicalFormula(this);
newFormula.subtractFormula(subtractionFormula);
return newFormula;
}
/**
* Create a formula identical to this one with elements removed. Populate peaks if they were already populated.
* Throw IllegalArgumentException if the formula doesn't have the specified elements
* @param subtractionFormula
* @return
*/
public void subtractFormula(ChemicalFormula subtractionFormula)
throws IllegalArgumentException
{
for (String atom : subtractionFormula.getElementCountMap().keySet())
{
if (!elementCountMap.containsKey(atom))
throw new IllegalArgumentException("Can't remove nonpresent element " + atom);
int numPresent = elementCountMap.get(atom);
int numForNewMap = numPresent - subtractionFormula.getElementCountMap().get(atom);
if (numForNewMap < 0)
throw new IllegalArgumentException("Can't remove " + subtractionFormula.getElementCountMap().get(atom) +
" of element " + atom + ", only " + numPresent + " present");
if (numForNewMap == 0)
elementCountMap.remove(atom);
else
elementCountMap.put(atom, numForNewMap);
}
commonestIsotopeMass = ChemCalcs.calcCommonestIsotopeMass(elementCountMap);
if (peakFrequencies != null)
populatePeakMassesAndFrequencies(peakFrequencies.length);
}
public double getCommonestIsotopeMass()
{
return commonestIsotopeMass;
}
public void setCommonestIsotopeMass(double mass)
{
this.commonestIsotopeMass = mass;
}
public String toString()
{
return ChemCalcs.atomCount2FormulaString(getElementCountMap());
}
public static class ComparatorMassAsc implements Comparator<ChemicalFormula>
{
public int compare(ChemicalFormula o1, ChemicalFormula o2)
{
if (o1.commonestIsotopeMass == o2.commonestIsotopeMass)
return 0;
return o1.commonestIsotopeMass == o2.commonestIsotopeMass ? 0 :
o1.commonestIsotopeMass < o2.commonestIsotopeMass ? -1 : 1;
}
}
public String getPeakMassesFrequenciesString()
{
StringBuffer buf = new StringBuffer("");
if (peakMasses != null)
{
buf.append(" all masses (frequencies): ");
for (int i=0; i< peakMasses.length; i++)
{
if (i>0)
buf.append(", ");
buf.append(peakMasses[i] + " (" + peakFrequencies[i] + ")");
}
}
return buf.toString();
}
public double[] getPeakFrequencies()
{
return getPeakFrequencies(numPeaksToCalculate);
}
public double[] getPeakMasses()
{
return getPeakMasses(numPeaksToCalculate);
}
/**
* Can be slightly costly if masses or frequencies not already calculated
* @return
*/
public double[] getPeakFrequencies(int numPeaksToCalculate)
{
if (peakFrequencies == null || peakFrequencies.length < numPeaksToCalculate)
populatePeakMassesAndFrequencies(numPeaksToCalculate);
return peakFrequencies;
}
/**
* Can be slightly costly if masses or frequencies not already calculated
* @return
*/
public double[] getPeakMasses(int numPeaksToCalculate)
{
if (peakMasses == null || peakMasses.length < numPeaksToCalculate)
populatePeakMassesAndFrequencies(numPeaksToCalculate);
return peakMasses;
}
public void populatePeakMassesAndFrequencies()
{
populatePeakMassesAndFrequencies(numPeaksToCalculate);
}
public void populatePeakMassesAndFrequencies(int numPeaksToCalc)
{
Pair<double[], double[]> massesAndFrequencies =
ChemCalcs.calcPeakMassesAndProbabilities(elementCountMap, numPeaksToCalc);
peakMasses = massesAndFrequencies.first;
peakFrequencies = massesAndFrequencies.second;
}
public Map<String, Integer> getElementCountMap()
{
return elementCountMap;
}
public static int getNumPeaksToCalculate()
{
return numPeaksToCalculate;
}
public static void setNumPeaksToCalculate(int numPeaksToCalculate)
{
ChemicalFormula.numPeaksToCalculate = numPeaksToCalculate;
}
/**
* Equality check. This is for storing things in hashtables by ChemicalFormula.
* NOTE! ChemicalFormulas with identical formulas will hash to the same thing
* @param otherFormula
* @return
*/
public boolean equals(Object otherFormula)
{
return otherFormula instanceof ChemicalFormula && toString().equals(otherFormula.toString());
}
/**
* Hash code generation. WARNING! This will generate the same code as a String with the formula contents.
* So don't put ChemicalFormulas and Strings in the same HashMap.
* @return
*/
public int hashCode()
{
return toString().hashCode();
}
/**
* Calculate the nominal mass, defined as the number of nucleons
* todo: is this the standard way to calculate it?
* @return
*/
public int getNominalMass()
{
int nominalMass = 0;
for (String symbol : elementCountMap.keySet())
{
nominalMass += elementCountMap.get(symbol) * Elements.get(symbol).getNominalMass();
//if (toString().equals("C5H7N3O1")) System.err.println(symbol + ", " + elementCountMap.get(symbol) + ", " + Elements.get(symbol).getNominalMass());
}
return nominalMass;
}
/**
* Calculate the mass defect of the entire molecule. This is NOT the unit mass defect
* @return
*/
public double calcTotalMassDefect()
{
//System.err.println(toString() + ", " + getNominalMass() + ", " + commonestIsotopeMass);
//todo: is this standard?
return commonestIsotopeMass - getNominalMass();
}
/**
* Calculate the unit mass defect.
* todo: is this standard?
* @return
*/
public double calcUnitMassDefect()
{
return calcTotalMassDefect() / getNominalMass();
}
}