mirror of
https://github.com/Ikatono/CommTools.git
synced 2025-10-28 20:45:33 -05:00
comvolutional encoder/viterbi
This commit is contained in:
280
commtools.py
280
commtools.py
@@ -5,7 +5,9 @@ from math import log2
|
|||||||
#import sympy as sp
|
#import sympy as sp
|
||||||
from sympy.polys.domains import ZZ
|
from sympy.polys.domains import ZZ
|
||||||
from sympy.polys.galoistools import gf_factor
|
from sympy.polys.galoistools import gf_factor
|
||||||
|
import itertools as it
|
||||||
|
|
||||||
|
#TODO rewrite this, no longer accurate
|
||||||
#basic structure of objects used in this package
|
#basic structure of objects used in this package
|
||||||
#feed() is used to send data to the object
|
#feed() is used to send data to the object
|
||||||
#it stores whatever state data it needs to determine future bits
|
#it stores whatever state data it needs to determine future bits
|
||||||
@@ -17,8 +19,8 @@ from sympy.polys.galoistools import gf_factor
|
|||||||
#Hard decoders return 1 and -1. True and False are interpretted as 1 and -1 respectively
|
#Hard decoders return 1 and -1. True and False are interpretted as 1 and -1 respectively
|
||||||
#some objects allow other data types, but they may behave strangely if you use Iterables as single data points
|
#some objects allow other data types, but they may behave strangely if you use Iterables as single data points
|
||||||
#because of this, DO NOT USE STRINGS AS DATA POINTS
|
#because of this, DO NOT USE STRINGS AS DATA POINTS
|
||||||
class CommObject:
|
class CommObject(object):
|
||||||
def feed(self):
|
def feed(self, data=()):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
@@ -26,6 +28,13 @@ class CommObject:
|
|||||||
|
|
||||||
def getinverse(self):
|
def getinverse(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def load(self, data):
|
||||||
|
self.buff.add(data)
|
||||||
|
|
||||||
|
class Decoder(object):
|
||||||
|
def geterrors(self):
|
||||||
|
return self.errors.pop(-1)
|
||||||
|
|
||||||
#turns a sequence of CommObjects into a single CommObject
|
#turns a sequence of CommObjects into a single CommObject
|
||||||
class Chain(CommObject):
|
class Chain(CommObject):
|
||||||
@@ -38,11 +47,10 @@ class Chain(CommObject):
|
|||||||
self.comms = list(comms)
|
self.comms = list(comms)
|
||||||
|
|
||||||
#feeds data through each CommObject in sequence
|
#feeds data through each CommObject in sequence
|
||||||
def feed(self, data):
|
def feed(self, data=()):
|
||||||
if not isinstance(data, Iterable):
|
#putsdata through a buffer first because all CommObjects should support load()
|
||||||
out = (data,)
|
self.load(data)
|
||||||
else:
|
out = self.buff.pop(-1)
|
||||||
out = data
|
|
||||||
|
|
||||||
for i in self.comms:
|
for i in self.comms:
|
||||||
out = i.feed(out)
|
out = i.feed(out)
|
||||||
@@ -71,7 +79,7 @@ class LinearBlockEncoder(CommObject):
|
|||||||
self.buff = Queue()
|
self.buff = Queue()
|
||||||
self.dist = self.distance(self.gen)
|
self.dist = self.distance(self.gen)
|
||||||
|
|
||||||
def feed(self, data):
|
def feed(self, data=()):
|
||||||
self.buff.add(data)
|
self.buff.add(data)
|
||||||
out = []
|
out = []
|
||||||
while len(self.buff) >= self.datalen:
|
while len(self.buff) >= self.datalen:
|
||||||
@@ -86,7 +94,6 @@ class LinearBlockEncoder(CommObject):
|
|||||||
def checkmat(self):
|
def checkmat(self):
|
||||||
if not self.standard(self.gen):
|
if not self.standard(self.gen):
|
||||||
raise ValueError('Currently only standard matrices can automatically generate check matrices')
|
raise ValueError('Currently only standard matrices can automatically generate check matrices')
|
||||||
raise ValueError('Currently only standard matrices can automatically generate check matrices')
|
|
||||||
(h,w) = self.gen.shape
|
(h,w) = self.gen.shape
|
||||||
return np.concatenate((self.gen[:,h:].T, np.identity(w-h)), 1)
|
return np.concatenate((self.gen[:,h:].T, np.identity(w-h)), 1)
|
||||||
|
|
||||||
@@ -109,6 +116,7 @@ class LinearBlockEncoder(CommObject):
|
|||||||
|
|
||||||
#literally useless, why did I make this?
|
#literally useless, why did I make this?
|
||||||
#LINEAR ALGEBRA MAKES LINEAR CODES!
|
#LINEAR ALGEBRA MAKES LINEAR CODES!
|
||||||
|
#TODO rework this to test linear independence of rows (copy, tostandard()?)
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate(mat):
|
def validate(mat):
|
||||||
if not isinstance(mat, np.matrix):
|
if not isinstance(mat, np.matrix):
|
||||||
@@ -126,10 +134,133 @@ class LinearBlockEncoder(CommObject):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
#implements single-shift register convolutional encoder
|
||||||
|
#each row of mat represents an output
|
||||||
|
#columns represent input, delay1, delay2... in that order
|
||||||
|
#recursive follows the same structure, always start it with a 1
|
||||||
|
class ConvolutionalEncoder(CommObject):
|
||||||
|
def __init__(self, mat, recursive=None):
|
||||||
|
if isinstance(mat, np.matrix):
|
||||||
|
self.mat = mat
|
||||||
|
else:
|
||||||
|
self.mat = np.matrix(mat)
|
||||||
|
self.shiftlen = len(self.mat.T)-1
|
||||||
|
if recursive is None:
|
||||||
|
self.recursive = np.matrix([1] + [0]*self.shiftlen)
|
||||||
|
else:
|
||||||
|
self.recursive = np.matrix(recursive)
|
||||||
|
self.reg = deque([0]*self.shiftlen,self.shiftlen)
|
||||||
|
|
||||||
|
def feed(self, data=()):
|
||||||
|
if not isinstance(data, Iterable):
|
||||||
|
data = (data,)
|
||||||
|
#TODO replace with empty list of correct size
|
||||||
|
out = deque([])
|
||||||
|
for i in data:
|
||||||
|
state = np.matrix([i]+list(self.reg)).T
|
||||||
|
outs = mul(self.mat, state)
|
||||||
|
self.reg.appendleft(mul(self.recursive, state)[0,0])
|
||||||
|
#turn vertical vector into list
|
||||||
|
outs = [i[0] for i in outs.tolist()]
|
||||||
|
out.extend(outs)
|
||||||
|
return list(out)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
return self.feed([0]*self.shiftlen)
|
||||||
|
|
||||||
|
#relatively slow maximum-likelyhood decoder for convolutional codes
|
||||||
|
#ViterbiDecoder( convEncoder ) automatically creates a decoder for convEncoder
|
||||||
|
#ViterbiDecoder( matrix [, recursive [, puncturing ]])
|
||||||
|
class ViterbiDecoder(CommObject):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.buff = Queue()
|
||||||
|
if len(args) < 1:
|
||||||
|
raise TypeError('class ViterbiDecoder requires at least one argument, received 0')
|
||||||
|
#TODO change this logic for new init format
|
||||||
|
if isinstance(args[0], ConvolutionalEncoder):
|
||||||
|
self.mat = args[0].mat
|
||||||
|
self.recursive = args[0].recursive
|
||||||
|
self.shiftlen = args[0].shiftlen
|
||||||
|
self.block = args[0].block
|
||||||
|
else:
|
||||||
|
self.mat = np.matrix(args[0])
|
||||||
|
if len(args) >=2:
|
||||||
|
self.recursive = np.matrix(args[1])
|
||||||
|
if not self.recursive:
|
||||||
|
self.recursive = np.matrix([1] + [0]*self.mat.shape[1])
|
||||||
|
else:
|
||||||
|
self.recursive = np.matrix([1] + [0]*self.mat.shape[1])
|
||||||
|
self.shiftlen = self.mat.shape[1]-1
|
||||||
|
#trellis forward and reverse links
|
||||||
|
self.tf = [None]*self.shiftlen
|
||||||
|
self.tr = [[]]*self.shiftlen
|
||||||
|
self.datawords = [None] * 2**self.shiftlen
|
||||||
|
self.codewords = list(self.datawords)
|
||||||
|
for i in range(2**self.shiftlen):
|
||||||
|
imat = tobin(i)
|
||||||
|
datawords[i] = tobin(i, self.shiftlen+1).T[0]
|
||||||
|
codewords[i] = mul(self.mat, datawords[i])
|
||||||
|
#bit entering shift register if input is 0
|
||||||
|
shiftin = mul(self.recursive[0,1:],imat)
|
||||||
|
if shiftin:
|
||||||
|
self.tf[i] = [i//2, i//2 + 2**(self.shiftlen-1)]
|
||||||
|
self.tr[i//2].append(i)
|
||||||
|
self.tr[i//2 + 2**(self.shiftlen-1)].append(i)
|
||||||
|
else:
|
||||||
|
self.tf[i] = [i//2 + 2**(self.shiftlen-1), i//2]
|
||||||
|
self.tr[i//2].append(i)
|
||||||
|
self.tr[i//2 + 2**(self.shiftlen-1)].append(i)
|
||||||
|
self.dists = [(0,None)] + [(float('inf'),None)]*(2**self.shiftlen - 2)
|
||||||
|
#use deque for efficient appends, plus it works without specified block length
|
||||||
|
self.trell = deque()
|
||||||
|
#self.trellc = deque()
|
||||||
|
|
||||||
|
#TODO make all data go through same trellis-building code
|
||||||
|
#TODO make single function to build maximum-likelyhood list
|
||||||
|
#TODO make trellv only record links forward, store a SINGLE column of net distances elsewhere
|
||||||
|
def feed(self, data):
|
||||||
|
self.buff.add(data)
|
||||||
|
out = []
|
||||||
|
while self.block and len(self.buff) + len(self.trellc) >= self.block*self.mat.size[0]:
|
||||||
|
l = self.buff.pop(self.block*self.mat.size[0] - len(self.buff) - len(self.trellc))
|
||||||
|
blockdata = [l[i:i + n] for i in range(0, len(l), n)]
|
||||||
|
for dat in len(blockdata):
|
||||||
|
trellcol = [None]*len(self.datawords)
|
||||||
|
#TODO implement more efficient euclidean distance (calculate distance from 1/0 beforehand)
|
||||||
|
for num in range(len(datawords)):
|
||||||
|
#chooses reverse link with least distance
|
||||||
|
choices = [self.trell[-1][i] for i in self.tr[num]]
|
||||||
|
#TODO implement random decision if reverse links have equal distance
|
||||||
|
choice = self.tr[num][1 * (choices[1] > choices[0])]
|
||||||
|
dist = edist(tobinl(num), blockdata)
|
||||||
|
dist = dist + self.trell[-1][choice]
|
||||||
|
trellcol[num] = (dist, choice)
|
||||||
|
self.trell.append(trellcol)
|
||||||
|
#TODO make this better
|
||||||
|
links = [self.trell[-1][i] for i in self.tr[self.trell[-1].index(min(self.trell[-1]))]]
|
||||||
|
statelist = [None]*len(self.block)
|
||||||
|
statelist[0] = index(min(self.trell[-1]))
|
||||||
|
ind = min(self.trell[-1])[1]
|
||||||
|
|
||||||
|
for i, col in it.izip(it.count(), reversed(self.trell)):
|
||||||
|
#TODO find a better way to skip first index of trell
|
||||||
|
if not i:
|
||||||
|
continue
|
||||||
|
statelist[i] = col[ind][1]
|
||||||
|
ind = col[ind][1]
|
||||||
|
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.buff = Queue()
|
||||||
|
trellfirst = [0] + [float('inf')]*(2**self.shiftlen - 2)
|
||||||
|
self.trell = deque((trellfirst,))
|
||||||
|
#self.trellc = deque()
|
||||||
|
|
||||||
|
|
||||||
#return behavior is only defined for syndromes with weight <= (d-1)/2
|
#return behavior is only defined for syndromes with weight <= (d-1)/2
|
||||||
#where d is the min Hamming distance of the generator matrix
|
#where d is the min Hamming distance of the generator matrix
|
||||||
class LinearBlockDecoder(CommObject):
|
class LinearBlockDecoder(CommObject):
|
||||||
def __init__(self, chk, error=False):
|
def __init__(self, gen=None, chk=None, error=False):
|
||||||
if not isinstance(chk, np.matrix):
|
if not isinstance(chk, np.matrix):
|
||||||
chk = np.matrix(chk)
|
chk = np.matrix(chk)
|
||||||
self.chk = chk
|
self.chk = chk
|
||||||
@@ -137,12 +268,14 @@ class LinearBlockDecoder(CommObject):
|
|||||||
self.codelen = self.gen.shape[1]
|
self.codelen = self.gen.shape[1]
|
||||||
self.datalen = self.codelen - self.gen.shape[0]
|
self.datalen = self.codelen - self.gen.shape[0]
|
||||||
|
|
||||||
def feed(self, data):
|
def feed(self, data=()):
|
||||||
self.buff.append(data)
|
self.buff.append(data)
|
||||||
|
out = Queue()
|
||||||
while len(self.buff) >= self.codelen:
|
while len(self.buff) >= self.codelen:
|
||||||
code = np.matrix(self.buff.pop(self.codelen)).T
|
code = np.matrix(self.buff.pop(self.codelen)).T
|
||||||
synd = self.chk * code
|
synd = self.chk * code
|
||||||
#data =
|
#TODO finish this
|
||||||
|
|
||||||
#creates a cyclic encoder, a type of linear block code
|
#creates a cyclic encoder, a type of linear block code
|
||||||
#can be used in two different ways:
|
#can be used in two different ways:
|
||||||
#CyclicEncoder(n, k) creates an [n,k] code from a code polynomial of max distance
|
#CyclicEncoder(n, k) creates an [n,k] code from a code polynomial of max distance
|
||||||
@@ -158,6 +291,10 @@ class CyclicEncoder(LinearBlockEncoder):
|
|||||||
tostand = kwargs['standard']
|
tostand = kwargs['standard']
|
||||||
else:
|
else:
|
||||||
tostand = True
|
tostand = True
|
||||||
|
if 'left' in kwargs:
|
||||||
|
left = kwargs['left']
|
||||||
|
else:
|
||||||
|
left = False
|
||||||
if isinstance(args[0], list):
|
if isinstance(args[0], list):
|
||||||
#TODO implement polynomial loading
|
#TODO implement polynomial loading
|
||||||
pass
|
pass
|
||||||
@@ -166,8 +303,6 @@ class CyclicEncoder(LinearBlockEncoder):
|
|||||||
outerpoly = [1] + [0]*(n-1) + [1]
|
outerpoly = [1] + [0]*(n-1) + [1]
|
||||||
factors = gf_factor(ZZ.map(outerpoly), 2, ZZ)
|
factors = gf_factor(ZZ.map(outerpoly), 2, ZZ)
|
||||||
doms = [len(factor[0])-1 for factor in factors[1]]
|
doms = [len(factor[0])-1 for factor in factors[1]]
|
||||||
#print(factors)
|
|
||||||
#print(doms)
|
|
||||||
if n-k not in doms:
|
if n-k not in doms:
|
||||||
raise ValueError('there is no cyclic code with these values')
|
raise ValueError('there is no cyclic code with these values')
|
||||||
#list of all factors of the correct order
|
#list of all factors of the correct order
|
||||||
@@ -191,15 +326,22 @@ class CyclicEncoder(LinearBlockEncoder):
|
|||||||
self.buff = Queue()
|
self.buff = Queue()
|
||||||
self.datalen = self.gen.shape[0]
|
self.datalen = self.gen.shape[0]
|
||||||
self.codelen = self.gen.shape[1]
|
self.codelen = self.gen.shape[1]
|
||||||
|
if tostand:
|
||||||
|
tostandard(self.gen, left)
|
||||||
|
|
||||||
|
def getinverse(self, errors=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
#TODO more efficiect, cycleic-specific feee() algorithm
|
||||||
|
|
||||||
|
#TODO make this
|
||||||
class HammingEncoder(LinearBlockEncoder):
|
class HammingEncoder(LinearBlockEncoder):
|
||||||
def __init__(self, paritybits=3):
|
def __init__(self, paritybits=3):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
#basic queue structure for CommObject buffers
|
#basic deque-based FIFO structure for CommObject buffers
|
||||||
class Queue:
|
class Queue(object):
|
||||||
def __init__(self, data=()):
|
def __init__(self, data=()):
|
||||||
self.data = deque(data)
|
self.data = deque(data)
|
||||||
|
|
||||||
@@ -220,6 +362,9 @@ class Queue:
|
|||||||
self.data.append(data)
|
self.data.append(data)
|
||||||
|
|
||||||
def pop(self, num=1):
|
def pop(self, num=1):
|
||||||
|
#-1 pops all, -2 all but one, etc.
|
||||||
|
if num < 0:
|
||||||
|
num = max(0, len(self.data)+num+1)
|
||||||
if num > len(self.data):
|
if num > len(self.data):
|
||||||
raise IndexError('Cannot pop more values than are in queue')
|
raise IndexError('Cannot pop more values than are in queue')
|
||||||
|
|
||||||
@@ -242,6 +387,37 @@ def mul(x1, x2):
|
|||||||
x2 = np.matrix(x2)
|
x2 = np.matrix(x2)
|
||||||
return x1.dot(x2) % 2
|
return x1.dot(x2) % 2
|
||||||
|
|
||||||
|
#DEPRECATED: I'm actually better off not indexing so a deque is fine
|
||||||
|
#very basic shift register for use in convolutional codes
|
||||||
|
#size is determined by the size of the iterable passed at initialization
|
||||||
|
#supports indexing arbitrary values by list or tuple
|
||||||
|
#pushing data is O(1), indexing is O(1)
|
||||||
|
class ShiftRegister(object):
|
||||||
|
def __init__(self, data):
|
||||||
|
self.length = len(data)
|
||||||
|
self.data = [i for i in data]
|
||||||
|
self.ind = 0
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.length
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, int):
|
||||||
|
return self.data[(key+self.ind)%len(self)]
|
||||||
|
elif isinstance(key, (tuple, list)):
|
||||||
|
return [self.data[(i+self.ind)%len(self)] for i in key]
|
||||||
|
#TODO implement slice indexing
|
||||||
|
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield [self.data[(i+self.ind)%len(self)] for i in range(len(self))]
|
||||||
|
|
||||||
|
def push(self, datum):
|
||||||
|
self.ind -= 1
|
||||||
|
if self.ind == -1:
|
||||||
|
self.ind = len(self)-1
|
||||||
|
self.data[self.ind] = datum
|
||||||
|
|
||||||
#converts each value to 1 (if True or >0) or 0
|
#converts each value to 1 (if True or >0) or 0
|
||||||
#for use with hard decoders
|
#for use with hard decoders
|
||||||
#TODO finish this
|
#TODO finish this
|
||||||
@@ -269,22 +445,78 @@ def tobin(num, bits=0):
|
|||||||
num -= 2**i
|
num -= 2**i
|
||||||
return np.matrix(out).T
|
return np.matrix(out).T
|
||||||
|
|
||||||
|
def tobinl(num, bits=0):
|
||||||
|
if num == 0:
|
||||||
|
return np.matrix([0] * max(bits,1)).T
|
||||||
|
if not isinstance(num, int):
|
||||||
|
raise TypeError('tobin only takes integers')
|
||||||
|
bitsmax = int(log2(num))+1
|
||||||
|
if not bits:
|
||||||
|
bits = bitsmax
|
||||||
|
if bitsmax > bits:
|
||||||
|
raise ValueError('insufficient bits')
|
||||||
|
out = [0] * bits
|
||||||
|
for i in range(bitsmax-1,-1,-1):
|
||||||
|
if num >= 2**i:
|
||||||
|
out[i] = 1
|
||||||
|
num -= 2**i
|
||||||
|
return out
|
||||||
|
|
||||||
#I stole this from stackoverflow
|
#I stole this from stackoverflow
|
||||||
#https://stackoverflow.com/questions/5488307/numpy-array-in-python-list/
|
#https://stackoverflow.com/questions/5488307/numpy-array-in-python-list/
|
||||||
#thanks Sven Marnach!
|
#thanks Sven Marnach!
|
||||||
def arreq_in_list(myarr, list_arrays):
|
def arreq_in_list(myarr, list_arrays):
|
||||||
return any((myarr == x).all() for x in list_arrays)
|
return any((myarr == x).all() for x in list_arrays)
|
||||||
|
|
||||||
#adds row a to row b, mod 2
|
#adds row a to row b, mod 2, in place
|
||||||
def rowadd(mat, a, b):
|
def rowadd(mat, a, b):
|
||||||
mat[a] += mat[b]
|
mat[a] += mat[b]
|
||||||
mat %= 2
|
mat %= 2
|
||||||
|
|
||||||
|
#calculates euclidean distance for two iterables
|
||||||
|
def edist(x1, x2):
|
||||||
|
if not len(x1) == len(x2):
|
||||||
|
raise ValueError('iterables must have same length')
|
||||||
|
dist = 0
|
||||||
|
for i in range(x1):
|
||||||
|
dist += (x1[i]-x2[i])**2
|
||||||
|
return dist**.5
|
||||||
|
|
||||||
#TODO finish this
|
#TODO finish this
|
||||||
#left=True if identity matrix is on the left side
|
#left=True if identity matrix is on the left side
|
||||||
def standard(mat, left):
|
def tostandard(mat, left):
|
||||||
k = mat.shape[1]
|
k, n = mat.shape
|
||||||
for i in mat.T[:k]:
|
#TODO validate input
|
||||||
if not i.any():
|
#for i in mat.T:
|
||||||
pass
|
#if not i.any():
|
||||||
|
#pass
|
||||||
|
#first column of identity matrix
|
||||||
|
s = (n-k) * (not left)
|
||||||
|
for i in range(k):
|
||||||
|
#places a 1 in the appropriate cell if necessary
|
||||||
|
if not mat[i,s+i]:
|
||||||
|
for j in [l for l in range(k) if l != i]:
|
||||||
|
if mat[i,s+j]:
|
||||||
|
rowadd(mat, j, i)
|
||||||
|
break
|
||||||
|
#places 0s in the rest of the column
|
||||||
|
for j in [l for l in range(k) if l != i]:
|
||||||
|
if mat[i,s+j]:
|
||||||
|
rowadd(mat, i, j)
|
||||||
|
|
||||||
|
#0 no; 1 left; 2 right
|
||||||
|
def isstandard(mat):
|
||||||
|
n, k = mat.shape
|
||||||
|
#test left
|
||||||
|
left = True
|
||||||
|
for i in range(k):
|
||||||
|
if not (mat[i,i] and mat[:,i].sum() == 1):
|
||||||
|
left = False
|
||||||
|
break
|
||||||
|
if left:
|
||||||
|
return 1
|
||||||
|
s = n-k
|
||||||
|
for i in range(k):
|
||||||
|
if not (mat[i,s+i] and mat[:,s+i].sum() == 1):
|
||||||
|
return 0
|
||||||
|
return 2
|
||||||
|
|||||||
Reference in New Issue
Block a user