001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021
022
023package org.biojava.bio.dist;
024
025import java.io.Serializable;
026import java.util.Iterator;
027
028import org.biojava.bio.BioError;
029import org.biojava.bio.symbol.Alphabet;
030import org.biojava.bio.symbol.AtomicSymbol;
031import org.biojava.bio.symbol.FiniteAlphabet;
032import org.biojava.bio.symbol.IllegalAlphabetException;
033import org.biojava.bio.symbol.IllegalSymbolException;
034import org.biojava.bio.symbol.ReversibleTranslationTable;
035import org.biojava.bio.symbol.Symbol;
036import org.biojava.utils.AbstractChangeable;
037import org.biojava.utils.ChangeEvent;
038import org.biojava.utils.ChangeForwarder;
039import org.biojava.utils.ChangeListener;
040import org.biojava.utils.ChangeSupport;
041import org.biojava.utils.ChangeType;
042import org.biojava.utils.ChangeVetoException;
043
044/**
045 * A translated view of some underlying distribution.  The <code>getWeight</code>
046 * method returns the result of calling <code>getWeight</code> on the underlying
047 * distribution, having first translated the <code>Symbol</code> parameter using
048 * the supplied <code>ReversibleTranslationTable</code>.  All changes to the
049 * underlying distribution are reflected by the <code>TranslatedDistribution</code>.
050 *
051 * <p>
052 * The <code>TranslatedDistribution</code> is not directly mutable: calling
053 * <code>setWeight</code> will result in a <code>ChangeVetoException</code>.
054 * However, a <code>DistributionTrainer</code> may be registered for a
055 * <code>TranslatedDistribution</code>.  Any counts received by this trainer
056 * are untranslated then forwarded to the underlying distribution.  It is
057 * valid to add counts to both a <code>TranslatedDistribution</code> and
058 * its underlying distribution in a single training session, so
059 * <code>TranslatedDistribution</code> objects are useful for tying
060 * parameters together when training Markov Models.
061 * </p>
062 *
063 * <h2>Example usage</h2>
064 *
065 * <pre>
066 * Distribution d = DistributionFactory.DEFAULT.createDistribution(DNATools.getDNA());
067 * d.setWeight(DNATools.a(), 0.7);
068 * d.setWeight(DNATools.c(), 0.1);
069 * d.setWeight(DNATools.g(), 0.1);
070 * d.setWeight(DNATools.t(), 0.1);
071 * Distribution complemented = new TranslatedDistribution(
072 *     DNATools.complementTable(),
073 *     d,
074 *     DistributionFactory.DEFAULT
075 * );
076 * System.out.println(
077 *    "complemented.getWeight(DNATools.t()) = " +
078 *    complemented.getWeight(DNATools.t())
079 * );  // Should print 0.7
080 * </pre>
081 * 
082 *
083 * @author Matthew Pocock
084 * @author Thomas Down
085 * @since 1.1
086 */
087public class TranslatedDistribution
088  extends
089    AbstractChangeable
090  implements
091    Distribution,
092    Serializable
093{
094  private final Distribution other;
095  private final Distribution delegate;
096  private final ReversibleTranslationTable table;
097  private transient ChangeListener forwarder;
098  private transient ChangeListener delegateUpdate;
099
100  /**
101   * Create a new TranslatedDistribution. Make these things via getDistribuiton.
102   *
103   * @param table    a ReversibleTranslationTable used to map the symbols
104   * @param other    the underlying ditribution
105   * @param distFact a DistributionFactory used to create a delegate for
106   *    stooring mapped weights
107   */
108  public TranslatedDistribution(
109    ReversibleTranslationTable table,
110    Distribution other,
111    DistributionFactory distFact
112  ) throws IllegalAlphabetException {
113    if (! (other.getAlphabet() instanceof FiniteAlphabet)) {
114        throw new IllegalAlphabetException("The current implementation of TranslatedDistribution is only valid for distributions over finite alphabets");
115    }
116      
117    if(!table.getTargetAlphabet().equals(other.getAlphabet())) {
118      throw new IllegalAlphabetException(
119        "Table target alphabet and distribution alphabet don't match: " +
120        table.getTargetAlphabet().getName() + " and " +
121        other.getAlphabet().getName() + " without symbol "
122      );
123    }
124    this.other = other;
125    this.table = table;
126    this.delegate = distFact.createDistribution(table.getSourceAlphabet());
127    
128    syncDelegate();
129    
130    delegateUpdate = new ChangeListener() {
131        public void preChange(ChangeEvent ce) {}
132        public void postChange(ChangeEvent ce) {
133            ChangeType ct = ce.getType();
134            Object change = ce.getChange();
135            if(ct == Distribution.WEIGHTS) {
136                boolean synced = false;
137                if((change != null) && (change instanceof Object[]) ) {
138                    Object[] ca = (Object[]) change;
139                    if( (ca.length == 2) && (ca[0] instanceof Symbol) && (ca[1] instanceof Number)) {
140                        try {
141                            delegate.setWeight(
142                                (Symbol) ca[0],
143                                ((Number) ca[1]).doubleValue()
144                            );
145                            synced = true;
146                        } catch (Exception ise) {
147                            throw new BioError("Couldn't synchronize weight", ise);
148                        }
149                    }
150                }
151                if (!synced) {
152                    // Weights have changed, but we can't understand the event, so re-sync them
153                    // all.
154                    syncDelegate();
155                }
156            }
157        }
158    } ;
159    addChangeListener(delegateUpdate);
160  }
161  
162  private void syncDelegate() {
163      for (Iterator i = ((FiniteAlphabet) delegate.getAlphabet()).iterator(); i.hasNext(); ) {
164        Symbol s = (Symbol) i.next();
165        try {
166            delegate.setWeight(s, other.getWeight(table.untranslate(s)));
167        } catch (Exception ex) {
168            throw new BioError(ex, "Assertion failed: couldn't map distributions");
169        }
170    }
171  }
172
173  public Alphabet getAlphabet() {
174    return table.getSourceAlphabet();
175  }
176
177  public double getWeight(Symbol sym)
178    throws IllegalSymbolException
179  {
180    return delegate.getWeight(sym);
181  }
182
183  public void setWeight(Symbol sym, double weight)
184    throws IllegalSymbolException, ChangeVetoException 
185  {
186    throw new ChangeVetoException("Can't directly edit a TranslatedDistribution");
187  }
188
189  public Symbol sampleSymbol() {
190    return delegate.sampleSymbol();
191  }
192
193  public Distribution getNullModel() {
194    return delegate.getNullModel();
195  }
196
197  public void setNullModel(Distribution dist)
198  throws IllegalAlphabetException, ChangeVetoException {
199    delegate.setNullModel(dist);
200  }
201
202  /**
203   * Retrieve the translation table encapsulating the map from this emission
204   * spectrum to the underlying one.
205   *
206   * @return a ReversibleTranslationtTable
207   */
208  public ReversibleTranslationTable getTable() {
209    return table;
210  }
211
212  public void registerWithTrainer(DistributionTrainerContext dtc) {
213    dtc.registerDistribution(other);
214
215    dtc.registerTrainer(this, new DistributionTrainer() {
216      public void addCount(
217        DistributionTrainerContext dtc,
218        AtomicSymbol s,
219        double count
220      ) throws IllegalSymbolException {
221        dtc.addCount(other, table.translate(s), count);
222      }
223
224      public double getCount(
225        DistributionTrainerContext dtc,
226        AtomicSymbol s
227      ) throws IllegalSymbolException {
228        return dtc.getCount(other, table.translate(s));
229      }
230
231      public void train(DistributionTrainerContext dtc, double weight)
232        throws ChangeVetoException 
233      {
234          // This is a no-op, since our counts have already been passed on to
235          // the sister Distribution.
236      }
237
238      public void clearCounts(DistributionTrainerContext dtc) {
239      }
240    });
241  }
242
243  protected ChangeSupport getChangeSupport(ChangeType ct) {
244    ChangeSupport cs = super.getChangeSupport(ct);
245
246    if(forwarder == null &&
247       (Distribution.WEIGHTS.isMatchingType(ct) || ct.isMatchingType(Distribution.WEIGHTS)))
248    {
249      forwarder = new Forwarder(this, cs);
250      other.addChangeListener(forwarder, Distribution.WEIGHTS);
251    }
252
253    return cs;
254  }
255
256  private class Forwarder extends ChangeForwarder {
257    public Forwarder(Object source, ChangeSupport changeSupport) {
258      super(source, changeSupport);
259    }
260
261    protected ChangeEvent generateChangeEvent(ChangeEvent ce) {
262      ChangeType ct = ce.getType();
263      Object change = ce.getChange();
264      Object previous = ce.getPrevious();
265      if(ct == Distribution.WEIGHTS) {
266        if( (change != null) && (change instanceof Object[]) ) {
267          Object[] ca = (Object[]) change;
268          if( (ca.length == 2) && (ca[0] instanceof Symbol) ) {
269            try {
270              change = new Object[] { table.translate((Symbol) ca[0]), ca[1] };
271            } catch (IllegalSymbolException ise) {
272              throw new BioError("Couldn't translate symbol", ise);
273            }
274          }
275        }
276        if( (previous != null) && (previous instanceof Object[]) ) {
277          Object[] pa = (Object[]) previous;
278          if( (pa.length == 2) && (pa[0] instanceof Symbol) ) {
279            try {
280              previous = new Object[] { table.translate((Symbol) pa[0]), pa[1] };
281            } catch (IllegalSymbolException ise) {
282              throw new BioError("Couldn't translate symbol", ise);
283            }
284          }
285        }
286      } else if(ct == Distribution.NULL_MODEL) {
287        change = null;
288        previous = null;
289      }
290      return new ChangeEvent(
291        TranslatedDistribution.this, ct,
292        change, previous, ce
293      );
294    }
295  }
296}