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}