/*
* BioJava development code
*
* This code may be freely distributed and modified under the
* terms of the GNU Lesser General Public Licence. This should
* be distributed with the code. If you do not have a copy,
* see:
*
* http://www.gnu.org/copyleft/lesser.html
*
* Copyright for this code is held jointly by the individual
* authors. These should be listed in @author doc comments.
*
* For more information on the BioJava project and its aims,
* or to join the biojava-l mailing list, visit the home page
* at:
*
* http://www.biojava.org/
*
*/
package org.biojava.bio.dist;
import java.io.Serializable;
import java.util.Iterator;
import org.biojava.bio.BioError;
import org.biojava.bio.symbol.Alphabet;
import org.biojava.bio.symbol.AtomicSymbol;
import org.biojava.bio.symbol.FiniteAlphabet;
import org.biojava.bio.symbol.IllegalAlphabetException;
import org.biojava.bio.symbol.IllegalSymbolException;
import org.biojava.bio.symbol.ReversibleTranslationTable;
import org.biojava.bio.symbol.Symbol;
import org.biojava.utils.AbstractChangeable;
import org.biojava.utils.ChangeEvent;
import org.biojava.utils.ChangeForwarder;
import org.biojava.utils.ChangeListener;
import org.biojava.utils.ChangeSupport;
import org.biojava.utils.ChangeType;
import org.biojava.utils.ChangeVetoException;
/**
* A translated view of some underlying distribution. The getWeight
* method returns the result of calling getWeight
on the underlying
* distribution, having first translated the Symbol
parameter using
* the supplied ReversibleTranslationTable
. All changes to the
* underlying distribution are reflected by the TranslatedDistribution
.
*
*
* The TranslatedDistribution
is not directly mutable: calling
* setWeight
will result in a ChangeVetoException
.
* However, a DistributionTrainer
may be registered for a
* TranslatedDistribution
. Any counts received by this trainer
* are untranslated then forwarded to the underlying distribution. It is
* valid to add counts to both a TranslatedDistribution
and
* its underlying distribution in a single training session, so
* TranslatedDistribution
objects are useful for tying
* parameters together when training Markov Models.
*
* Distribution d = DistributionFactory.DEFAULT.createDistribution(DNATools.getDNA()); * d.setWeight(DNATools.a(), 0.7); * d.setWeight(DNATools.c(), 0.1); * d.setWeight(DNATools.g(), 0.1); * d.setWeight(DNATools.t(), 0.1); * Distribution complemented = new TranslatedDistribution( * DNATools.complementTable(), * d, * DistributionFactory.DEFAULT * ); * System.out.println( * "complemented.getWeight(DNATools.t()) = " + * complemented.getWeight(DNATools.t()) * ); // Should print 0.7 ** * * @author Matthew Pocock * @author Thomas Down * @since 1.1 */ public class TranslatedDistribution extends AbstractChangeable implements Distribution, Serializable { private final Distribution other; private final Distribution delegate; private final ReversibleTranslationTable table; private transient ChangeListener forwarder; private transient ChangeListener delegateUpdate; /** * Create a new TranslatedDistribution. Make these things via getDistribuiton. * * @param table a ReversibleTranslationTable used to map the symbols * @param other the underlying ditribution * @param distFact a DistributionFactory used to create a delegate for * stooring mapped weights */ public TranslatedDistribution( ReversibleTranslationTable table, Distribution other, DistributionFactory distFact ) throws IllegalAlphabetException { if (! (other.getAlphabet() instanceof FiniteAlphabet)) { throw new IllegalAlphabetException("The current implementation of TranslatedDistribution is only valid for distributions over finite alphabets"); } if(!table.getTargetAlphabet().equals(other.getAlphabet())) { throw new IllegalAlphabetException( "Table target alphabet and distribution alphabet don't match: " + table.getTargetAlphabet().getName() + " and " + other.getAlphabet().getName() + " without symbol " ); } this.other = other; this.table = table; this.delegate = distFact.createDistribution(table.getSourceAlphabet()); syncDelegate(); delegateUpdate = new ChangeListener() { public void preChange(ChangeEvent ce) {} public void postChange(ChangeEvent ce) { ChangeType ct = ce.getType(); Object change = ce.getChange(); if(ct == Distribution.WEIGHTS) { boolean synced = false; if((change != null) && (change instanceof Object[]) ) { Object[] ca = (Object[]) change; if( (ca.length == 2) && (ca[0] instanceof Symbol) && (ca[1] instanceof Number)) { try { delegate.setWeight( (Symbol) ca[0], ((Number) ca[1]).doubleValue() ); synced = true; } catch (Exception ise) { throw new BioError("Couldn't synchronize weight", ise); } } } if (!synced) { // Weights have changed, but we can't understand the event, so re-sync them // all. syncDelegate(); } } } } ; addChangeListener(delegateUpdate); } private void syncDelegate() { for (Iterator i = ((FiniteAlphabet) delegate.getAlphabet()).iterator(); i.hasNext(); ) { Symbol s = (Symbol) i.next(); try { delegate.setWeight(s, other.getWeight(table.untranslate(s))); } catch (Exception ex) { throw new BioError(ex, "Assertion failed: couldn't map distributions"); } } } public Alphabet getAlphabet() { return table.getSourceAlphabet(); } public double getWeight(Symbol sym) throws IllegalSymbolException { return delegate.getWeight(sym); } public void setWeight(Symbol sym, double weight) throws IllegalSymbolException, ChangeVetoException { throw new ChangeVetoException("Can't directly edit a TranslatedDistribution"); } public Symbol sampleSymbol() { return delegate.sampleSymbol(); } public Distribution getNullModel() { return delegate.getNullModel(); } public void setNullModel(Distribution dist) throws IllegalAlphabetException, ChangeVetoException { delegate.setNullModel(dist); } /** * Retrieve the translation table encapsulating the map from this emission * spectrum to the underlying one. * * @return a ReversibleTranslationtTable */ public ReversibleTranslationTable getTable() { return table; } public void registerWithTrainer(DistributionTrainerContext dtc) { dtc.registerDistribution(other); dtc.registerTrainer(this, new DistributionTrainer() { public void addCount( DistributionTrainerContext dtc, AtomicSymbol s, double count ) throws IllegalSymbolException { dtc.addCount(other, table.translate(s), count); } public double getCount( DistributionTrainerContext dtc, AtomicSymbol s ) throws IllegalSymbolException { return dtc.getCount(other, table.translate(s)); } public void train(DistributionTrainerContext dtc, double weight) throws ChangeVetoException { // This is a no-op, since our counts have already been passed on to // the sister Distribution. } public void clearCounts(DistributionTrainerContext dtc) { } }); } protected ChangeSupport getChangeSupport(ChangeType ct) { ChangeSupport cs = super.getChangeSupport(ct); if(forwarder == null && (Distribution.WEIGHTS.isMatchingType(ct) || ct.isMatchingType(Distribution.WEIGHTS))) { forwarder = new Forwarder(this, cs); other.addChangeListener(forwarder, Distribution.WEIGHTS); } return cs; } private class Forwarder extends ChangeForwarder { public Forwarder(Object source, ChangeSupport changeSupport) { super(source, changeSupport); } protected ChangeEvent generateChangeEvent(ChangeEvent ce) { ChangeType ct = ce.getType(); Object change = ce.getChange(); Object previous = ce.getPrevious(); if(ct == Distribution.WEIGHTS) { if( (change != null) && (change instanceof Object[]) ) { Object[] ca = (Object[]) change; if( (ca.length == 2) && (ca[0] instanceof Symbol) ) { try { change = new Object[] { table.translate((Symbol) ca[0]), ca[1] }; } catch (IllegalSymbolException ise) { throw new BioError("Couldn't translate symbol", ise); } } } if( (previous != null) && (previous instanceof Object[]) ) { Object[] pa = (Object[]) previous; if( (pa.length == 2) && (pa[0] instanceof Symbol) ) { try { previous = new Object[] { table.translate((Symbol) pa[0]), pa[1] }; } catch (IllegalSymbolException ise) { throw new BioError("Couldn't translate symbol", ise); } } } } else if(ct == Distribution.NULL_MODEL) { change = null; previous = null; } return new ChangeEvent( TranslatedDistribution.this, ct, change, previous, ce ); } } }