package module.decode.p25; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import bits.BinaryMessage; import dsp.symbol.Dibit; public class Trellis_3_4_Rate { private final static Logger mLog = LoggerFactory.getLogger( Trellis_3_4_Rate.class ); /* Hamming distance (bit match count) between constellation pairs */ private static final int[][] CONSTELLATION_METRICS = { { 4,3,3,2,3,2,2,1,3,2,2,1,2,1,1,0 }, { 3,4,2,3,2,3,1,2,2,3,1,2,1,2,0,1 }, { 3,2,4,3,2,1,3,2,2,1,3,2,1,0,2,1 }, { 2,3,3,4,1,2,2,3,1,2,2,3,0,1,1,2 }, { 3,2,2,1,4,3,3,2,2,1,1,0,3,2,2,1 }, { 2,3,1,2,3,4,2,3,1,2,0,1,2,3,1,2 }, { 2,1,3,2,3,2,4,3,1,0,2,1,2,1,3,2 }, { 1,2,2,3,2,3,3,4,0,1,1,2,1,2,2,3 }, { 3,2,2,1,2,1,1,0,4,3,3,2,3,2,2,1 }, { 2,3,1,2,1,2,0,1,3,4,2,3,2,3,1,2 }, { 2,1,3,2,1,0,2,1,3,2,4,3,2,1,3,2 }, { 1,2,2,3,0,1,1,2,2,3,3,4,1,2,2,3 }, { 2,1,1,0,3,2,2,1,3,2,2,1,4,3,3,2 }, { 1,2,0,1,2,3,1,2,2,3,1,2,3,4,2,3 }, { 1,0,2,1,2,1,3,2,2,1,3,2,3,2,4,3 }, { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 } }; /* Even valued state tribits can only produce even valued constellations * and odd valued state tribits can only produce odd constellations. */ public static Con[] EVEN_CONSTELLATIONS = new Con[] { Con.C0,Con.C2,Con.C4, Con.C6,Con.C8,Con.CA,Con.CC,Con.CE }; public static Con[] ODD_CONSTELLATIONS = new Con[] { Con.C1,Con.C3,Con.C5, Con.C7,Con.C9,Con.CB,Con.CD,Con.CF }; /* Constellation and state tribit lookup map to find the input tribit */ public static HashMap<Con,Tribit[]> INPUT_FROM_CONSTELLATION_MAP; private ArrayList<Con> mTransmittedConstellations = new ArrayList<Con>(); private ArrayList<Path> mSurvivorPaths = new ArrayList<Path>(); private ArrayList<Path> mNewPaths = new ArrayList<Path>(); private PathMetrics mPathMetrics = new PathMetrics(); /** * Implements the Viterbi algorithm to decode 3/4 rate trellis encoded 196-bit * packet data messages. */ public Trellis_3_4_Rate() { createConstellationToTribitMap(); } /** * Creates a lookup map for state to input tribit values for a given * constellation. Input tribits are contained in the array using the state * tribit's value as the lookup index. Null values indicate illegal state * and input combinations for the specified constellation. */ private void createConstellationToTribitMap() { INPUT_FROM_CONSTELLATION_MAP = new HashMap<Con,Tribit[]>(); INPUT_FROM_CONSTELLATION_MAP.put( Con.CB, new Tribit[] { null, null, Tribit.T5, Tribit.T3, Tribit.T1, Tribit.T7, null, null } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.CC, new Tribit[] { Tribit.T3, Tribit.T1, null, null, null, null, Tribit.T7, Tribit.T5 } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C0, new Tribit[] { Tribit.T0, Tribit.T6, null, null, null, null, Tribit.T4, Tribit.T2 } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C7, new Tribit[] { null, null, Tribit.T6, Tribit.T4, Tribit.T2, Tribit.T0, null, null } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.CE, new Tribit[] { Tribit.T7, Tribit.T5, null, null, null, null, Tribit.T3, Tribit.T1 } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C9, new Tribit[] { null, null, Tribit.T1, Tribit.T7, Tribit.T5, Tribit.T3, null, null } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C5, new Tribit[] { null, null, Tribit.T2, Tribit.T0, Tribit.T6, Tribit.T4, null, null } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C2, new Tribit[] { Tribit.T4, Tribit.T2, null, null, null, null, Tribit.T0, Tribit.T6 } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.CA, new Tribit[] { Tribit.T5, Tribit.T3, null, null, null, null, Tribit.T1, Tribit.T7 } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.CD, new Tribit[] { null, null, Tribit.T3, Tribit.T1, Tribit.T7, Tribit.T5, null, null } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C1, new Tribit[] { null, null, Tribit.T0, Tribit.T6, Tribit.T4, Tribit.T2, null, null } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C6, new Tribit[] { Tribit.T6, Tribit.T4, null, null, null, null, Tribit.T2, Tribit.T0 } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.CF, new Tribit[] { null, null, Tribit.T7, Tribit.T5, Tribit.T3, Tribit.T1, null, null } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C8, new Tribit[] { Tribit.T1, Tribit.T7, null, null, null, null, Tribit.T5, Tribit.T3 } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C4, new Tribit[] { Tribit.T2, Tribit.T0, null, null, null, null, Tribit.T6, Tribit.T4 } ); INPUT_FROM_CONSTELLATION_MAP.put( Con.C3, new Tribit[] { null, null, Tribit.T4, Tribit.T2, Tribit.T0, Tribit.T6, null, null } );; } /** * Member object cleanup prior to deleting */ public void dispose() { reset(); } /** * Decodes a 196-bit 3/4 rate convolutional encoded message located between * start and end indexes and returns the decoded 144-bit message overlayed * upon the original message with the remaining 52 bits cleared to zero. * * @return - original message with decoded message bits.. */ public boolean decode( BinaryMessage message, int start, int end ) { reset(); /* Load and decode each of the transmitted constellations */ for( int index = 0; index < 49; index++ ) { Con c = getConstellation( message, start + index * 4 ); add( c ); } /* The final decoded survivor path should be at path metrics tribit 0 */ Path path = mPathMetrics.getPath( Tribit.T0 ); /* We should have a path with 50 nodes, counting the starting 000 node * and the flushing 000 node, otherwise there was an error */ if( path != null && path.getNodes().size() == 50 ) { /* Clear the original message bits */ message.clear( start, end ); List<Node> nodes = path.getNodes(); /* Load each of the nodes' state tribit values into the original message */ for( int x = 1; x < 50; x++ ) { message.load( start + ( ( x - 1 ) * 3 ), 3, nodes.get( x ).getState().getValue() ); } return true; } return false; } /** * Resets the decoder before decoding a new data packet. */ private void reset() { mTransmittedConstellations.clear(); mNewPaths.clear(); mPathMetrics.reset(); mSurvivorPaths.clear(); /* Tribit 000 is the only legal start point */ mSurvivorPaths.add( new Path( new Node( 0, Tribit.T0, Con.C0 ) ) ); } private void add( Con con ) { mTransmittedConstellations.add( con ); /* Add in any newly created survivor paths */ mSurvivorPaths.addAll( mNewPaths ); mNewPaths.clear(); /* Set a culling threshold for any path metrics 3 below the best path */ int survivorThreshold = mPathMetrics.getSurvivorThreshold(); /* Reset path metrics */ mPathMetrics.reset(); /* Add constellation to each survivor path. If a survivor path metric * falls below the culling threshold, remove it */ Iterator<Path> it = mSurvivorPaths.iterator(); Path path; while( it.hasNext() ) { path = it.next(); if( path.isDead() || path.getPathMetric() < survivorThreshold ) { it.remove(); } else { path.add( con ); } } } private Con getConstellation( BinaryMessage message, int index ) { int value = message.getInt( index, index + 3 ); return Con.fromTransmittedValue( value ); } /** * Path metrics maintains a map of the current best path metrics that end * at each of the 8 valid tribit values. Each new path is evaluated against * the current best path for retention (survivor) or disposal (dead). A * best metric is maintained to evaluate the Walking Dead for disposal. * Walking dead is a path lagging the best path metric by more than 3. */ public class PathMetrics { private HashMap<Tribit,Path> mMetrics = new HashMap<Tribit,Path>(); private int mBestMetric = 0; public PathMetrics() { } public int getSurvivorThreshold() { return mBestMetric - 3; } /** * Removes all path metrics */ public void reset() { mMetrics.clear(); mBestMetric = 0; } public int getMetric( Tribit tribit ) { if( mMetrics.containsKey( tribit ) ) { return mMetrics.get( tribit ).getPathMetric(); } return 0; } public Path getPath( Tribit tribit ) { return mMetrics.get( tribit ); } /** * Evaluates the path's metric against the current best path metric for * the final state tribit of the path. If the path's metric is higher * than the current best path metric, the current path is marked for * termination and the new path is promoted to best path. If the new * path's metric is less than the best path metric, then the new path * metric is marked for termination. If the metrics are equal, the new * path is allowed to survive. * * @param path/metric to evaluate */ public void evalute( Path path ) { Node lastNode = path.getLastNode(); Path bestPath = mMetrics.get( lastNode.getState() ); if( bestPath == null ) { mMetrics.put( lastNode.getState(), path ); } else { if( path.getPathMetric() < bestPath.getPathMetric() ) { path.setSurvivor( false ); } else if( path.getPathMetric() > bestPath.getPathMetric() ) { mMetrics.put( lastNode.getState(), path ); bestPath.setSurvivor( false ); } /* Equal metric paths are allowed to survive */ } /* Capture the best path metric */ if( path.getPathMetric() > mBestMetric ) { mBestMetric = path.getPathMetric(); } } } /** * Path is a sequence of nodes where the starting node always contains * tribit 0 and constellation 0. Path maintains a running path metric * derived from the sum of each of the node's branch metrics. */ public class Path { private boolean mSurvivor = true; private int mPathMetric = 0; private ArrayList<Node> mNodes = new ArrayList<Node>(); public Path( Node first ) { mNodes.add( first ); mPathMetric = first.getBranchMetric(); mPathMetrics.evalute( this ); } public Path( ArrayList<Node> nodes, int metric ) { mNodes = nodes; mPathMetric = metric; } public List<Node> getNodes() { return mNodes; } public int getBitErrorCount() { return ( getLastNode().getIndex() * 4 ) - mPathMetric; } public void setSurvivor( boolean survivor ) { mSurvivor = survivor; } public boolean isSurvivor() { return mSurvivor; } public boolean isDead() { return !mSurvivor; } @SuppressWarnings( "unchecked" ) public Path copyOf() { return new Path( (ArrayList<Node>)mNodes.clone(), mPathMetric ); } /** * Adds the constellation and returns any new paths created as a result * of the constellation being detected as an errant constellation. */ public void add( Con con ) { Node current = getLastNode(); Tribit input = INPUT_FROM_CONSTELLATION_MAP .get( con )[ current.getState().getValue() ]; /* A non-null input tribit from the lookup table is valid, otherwise * create 8 alternate branches from the original path to explore * where the error occurred. */ if( input != null ) { add( new Node( mNodes.size(), input, con ) ); } else { /* Create new paths for each of the valid constellations that * correspond to the parity of the current state tribit */ Con[] constellations = current.getState().getType() == Type.EVEN ? EVEN_CONSTELLATIONS : ODD_CONSTELLATIONS; for( int x = 0; x < 8; x++ ) { /* Get the input tribit corresponding to the state tribit * and constellation combination */ Tribit tribit = INPUT_FROM_CONSTELLATION_MAP .get( constellations[ x ] )[ current.getState().getValue() ]; Node candidate = new Node( mNodes.size(), tribit, constellations[ x ] ); /* If the current path metric plus the candidate node's * branch metric is equal or better than the current best * metric for the given tribit, add it as a potential path, * otherwise discard it */ if( this.getPathMetric() + candidate.getBranchMetric() >= mPathMetrics.getMetric( tribit ) ) { /* If this is the final constellation of the set, simply * add it to the existing path ... */ if( x == 7 ) { add( candidate ); } /* ... otherwise create a copy of the current path */ else { Path path = this.copyOf(); path.add( candidate ); mNewPaths.add( path ); } } } } } public void add( Node node ) { mNodes.add( node ); mPathMetric += node.getBranchMetric(); mPathMetrics.evalute( this ); } public int getPathMetric() { return mPathMetric; } public Node getLastNode() { return mNodes.get( mNodes.size() - 1 ); } public String toString() { StringBuilder sb = new StringBuilder(); sb.append( "PATH [" ); sb.append( mNodes.size() ); sb.append( "-" ); sb.append( mPathMetric ); sb.append( "] " ); for( Node node: mNodes ) { sb.append( node.getState().name() ); sb.append( "/" ); sb.append( node.getConstellation().name() ); sb.append( " " ); } sb.append( mSurvivor ? "SURVIVOR" : "DEAD" ); return sb.toString(); } } /** * Trellis node containing the constellation used to produce the node and * the input tribit for the constellation. */ public class Node { private int mIndex; private int mBranchMetric; private Tribit mTribit; private Con mConstellation; public Node( int index, Tribit tribit, Con constellation ) { mIndex = index; mTribit = tribit; mConstellation = constellation; if( mIndex > 0 ) { Con transmitted = mTransmittedConstellations.get( index - 1 ); if( transmitted == null ) { throw new IllegalArgumentException( "Cannot calculate " + "metric - transmitted constellation symbol is null" ); } mBranchMetric = transmitted.getMetricTo( constellation ); } } public int getIndex() { return mIndex; } public Tribit getState() { return mTribit; } public Con getConstellation() { return mConstellation; } public int getBranchMetric() { return mBranchMetric; } public String toString() { StringBuilder sb = new StringBuilder(); sb.append( "Node " ); sb.append( mIndex ); sb.append( " Metric:" ); sb.append( mBranchMetric ); sb.append( " Con:" ); sb.append( mConstellation.name() ); sb.append( " State:" ); if( mTribit == null ) { sb.append( "null" ); } else { sb.append( mTribit.name() ); } return sb.toString(); } } /** * Constellation Type. Constellations are classified as even or add based * on the the composition/comparison of the transmitted (representative) * dibits: * * EVEN: - first dibit differs from second dibit in exactly one bit position * * ODD: - first dibit differs from second dibit in zero or two bit positions * following three patterns: * 1) repeat (10 10), mirror (01 10), or invert(00 11) */ public enum Type { EVEN, ODD }; /** * Constellations, ordered by transmitted value. Transmitted value is the * value of the dibit pair that represents the constellation when transmitted. */ public enum Con { CB( 0 ), CC( 1 ), C0( 2 ), C7( 3 ), CE( 4 ), C9( 5 ), C5( 6 ), C2( 7 ), CA( 8 ), CD( 9 ), C1( 10 ), C6( 11 ), CF( 12 ), C8( 13 ), C4( 14 ), C3( 15 ); private int mTransmittedValue; private Con( int transmittedValue ) { mTransmittedValue = transmittedValue; } public int getTransmittedValue() { return mTransmittedValue; } public static Con fromTransmittedValue( int value ) { if( 0 <= value && value <= 15 ) { return values()[ value ]; } return null; } public static Con fromTransmittedDibits( Dibit left, Dibit right ) { return fromTransmittedValue( left.getHighValue() + right.getLowValue() ); } /** * Returns the metric or hamming distance to the other constellation * using the values from the constellation costs table. A perfect match * has a metric of 4 and a complete miss has a value of 0. */ public int getMetricTo( Con other ) { return CONSTELLATION_METRICS[ getTransmittedValue() ][ other.getTransmittedValue() ]; } } /** * Tribit. Used to define both the state and the input bit sequences that * produce the transmitted constellation bit sequences. Each tribit is * either even or odd valued producing a direct correlation to the types * of valid constellations containing the tribit as the state tribit. */ public enum Tribit { T0( 0, Type.EVEN ), T1( 1, Type.EVEN ), T2( 2, Type.ODD ), T3( 3, Type.ODD ), T4( 4, Type.ODD ), T5( 5, Type.ODD ), T6( 6, Type.EVEN ), T7( 7, Type.EVEN ); private int mValue; private Type mType; private Tribit( int value, Type type ) { mValue = value; mType = type; } public Type getType() { return mType; } public int getValue() { return mValue; } } /** * Test harness */ public static void main( String[] args ) { String raw = "12:58:51.635 DEBUG dsp.fsk.P25MessageFramer - AFTER DEINTERLEAVE: 00100110000011001010010101111101000101010110100111111100101011000111011011000110000000000000000000000111111101011000001000001111000110000000000010101011110110010000000001011011000000010100011110110001000010100100011111000000000100000000000000000000010000000000001100000000000000011010101010101010101010100010001010000110010111111101101000001010000010100000101000001010000010100000101000001010000010100000101000001010000010100000101000001010000010100000010111001001000011100101011010101010101001010011"; BinaryMessage message = BinaryMessage.load( raw ); mLog.debug( "MSG: " + message.toString() ); Trellis_3_4_Rate t = new Trellis_3_4_Rate(); t.decode( message, 0, 196 ); mLog.debug( "DEC: " + message.toString() ); mLog.debug( "Finished!" ); } }