# Path: ripyl/streaming.py
# class ChunkExtractor(object):
#     '''Utility class that pulls arbitrarily sized chunks from a sample stream'''
#     def __init__(self, stream):
#         self.stream = stream
#         self.sample_buf = []
#         self.buf_count = 0
#         self.stream_ended = False
#         self.buf_start_time = 0.0
#         self.sample_period = 0.0
# 
#     def next_chunk(self, chunk_size=10000):
#         '''Get a new chunk of samples from the stream
# 
#         chunk_size (int)
#             The number of samples for this chunk.
# 
#         Returns a SampleChunk object. If the stream had fewer than chunk_size samples
#           remaining then the SampleChunk.samples array is sized to hold only those samples.
# 
#         Returns None if the stream has ended.
#         '''
#         if self.stream_ended:
#             return None
# 
#         while True:
#             if self.buf_count < chunk_size: # Need to get another chunk
#                 try:
#                     sc = next(self.stream)
#                     if len(self.sample_buf) == 0:
#                         self.buf_start_time = sc.start_time
#                         self.sample_period = sc.sample_period
#                     self.sample_buf.append(sc.samples)
#                     self.buf_count += len(sc.samples)
# 
#                 except StopIteration:
#                     self.stream_ended = True
# 
#             if self.stream_ended and self.buf_count < chunk_size:
#                 # Not enough samples in buffer for a full sized chunk
#                 chunk_size = self.buf_count
#                 if chunk_size == 0:
#                     break
# 
#             if self.buf_count >= chunk_size:
#                 # We have enough buffered samples to return a new chunk
#                 out_samp = np.empty(chunk_size, dtype=float)
#                 out_count = 0
#                 for b in self.sample_buf:
#                     if out_count + len(b) <= chunk_size:
#                         use_count = len(b)
#                     else:
#                         use_count = chunk_size - out_count
# 
#                     out_samp[out_count:out_count + use_count] = b[:use_count]
#                     out_count += use_count
# 
#                 out_time = self.buf_start_time
# 
#                 if self.buf_count > chunk_size:
#                     # There are unused samples remaining in the buffer
#                     # Place these at the start of the buffer for the next call to next_chunk()
#                     self.sample_buf = [self.sample_buf[-1][use_count:]]
#                     self.buf_count = len(self.sample_buf[0])
#                     self.buf_start_time = self.buf_start_time + self.sample_period * chunk_size
#                 else:
#                     self.sample_buf = []
#                     self.buf_count = 0
# 
#                 return SampleChunk(out_samp, out_time, self.sample_period)
# 
#             if self.stream_ended:
#                 break
# 
#         return None
# 
#     def next_samples(self, sample_count=10000):
#         '''Get a new set of raw samples from the stream
# 
#         sample_count (int)
#             The number of samples for the array.
# 
#         Returns a numpy array of float. If the stream had fewer than sample_count samples
#           remaining then the array is sized to hold only those samples.
# 
#         Returns None if the stream has ended.
#         '''
# 
#         sc = self.next_chunk(sample_count)
#         if sc is not None:
#             return sc.samples
#         else:
#             return None
# 
#     def buffered_chunk(self):
#         '''Get all remaining buffered samples'''
#         return self.next_chunk(self.buf_count)
# 
# class StreamError(RuntimeError):
#     '''Custom exception class for edge and sample streams'''
#     pass
# 
# class AutoLevelError(StreamError):
#     '''Custom exception class auto-level errors'''
#     def __init__(self, msg='Unable to find avg. logic levels of waveform'):
#         StreamError.__init__(self, msg)
# 
# Path: ripyl/util/equality.py
# def relatively_equal(a, b, epsilon):
#     '''Evaluate the relative equality of two floats
# 
#     The epsilon parameter controls how close the relatve magnitudes need to be
#     for equality to be determined.
# 
#     Adapted from: http://floating-point-gui.de/errors/comparison/ 
# 
#     a (float)
#         First number to compare
# 
#     b (float)
#         Second number to compare
# 
#     epsilon (float)
#         Relative epsilon for comparison
# 
#     Returns True when a and b are nearly equal
#     '''
#     
#     if a == b: # take care of the inifinities
#         return True
#     
#     elif a * b == 0.0: # either a or b is zero
#         return abs(a - b) < epsilon ** 2
#         
#     else: # relative error
#         return abs(a - b) / (abs(a) + abs(b)) < epsilon


import numpy as np
import scipy as sp
import math
import collections
import itertools
import ripyl.util.stats as stats
from ripyl.streaming import ChunkExtractor, StreamError, AutoLevelError
from ripyl.util.equality import relatively_equal
    return symbol_rate
    
#FIX: clean up use of cur_time, cur_state, cur_state(), next_states, etc.
class EdgeSequence(object):
    '''Utility class to walk through an edge iterator in arbitrary time steps'''

    def __init__(self, edges, time_step, start_time=None):
        '''
        edges (sequence of (float, int) tuples)
            An iterable of 2-tuples representing each edge transition.
            The 2-tuples *must* be in the absolute time form (time, logic level).
        
        time_step (float)
            The default time step for advance() when it is called
            without an argument.
        
        start_time (float)
            The initial starting time for the sequence.
            
        Raises StreamError when there are less than two elements to the edges iterable
        '''
        self.edges = edges
        self.time_step = time_step
        self.it_end = False
        
        try:
            self.cur_states = next(self.edges)
            self.next_states = next(self.edges)
        except StopIteration:
            self.it_end = True

# Path: ripyl/streaming.py
# class ChunkExtractor(object):
#     '''Utility class that pulls arbitrarily sized chunks from a sample stream'''
#     def __init__(self, stream):
#         self.stream = stream
#         self.sample_buf = []
#         self.buf_count = 0
#         self.stream_ended = False
#         self.buf_start_time = 0.0
#         self.sample_period = 0.0
# 
#     def next_chunk(self, chunk_size=10000):
#         '''Get a new chunk of samples from the stream
# 
#         chunk_size (int)
#             The number of samples for this chunk.
# 
#         Returns a SampleChunk object. If the stream had fewer than chunk_size samples
#           remaining then the SampleChunk.samples array is sized to hold only those samples.
# 
#         Returns None if the stream has ended.
#         '''
#         if self.stream_ended:
#             return None
# 
#         while True:
#             if self.buf_count < chunk_size: # Need to get another chunk
#                 try:
#                     sc = next(self.stream)
#                     if len(self.sample_buf) == 0:
#                         self.buf_start_time = sc.start_time
#                         self.sample_period = sc.sample_period
#                     self.sample_buf.append(sc.samples)
#                     self.buf_count += len(sc.samples)
# 
#                 except StopIteration:
#                     self.stream_ended = True
# 
#             if self.stream_ended and self.buf_count < chunk_size:
#                 # Not enough samples in buffer for a full sized chunk
#                 chunk_size = self.buf_count
#                 if chunk_size == 0:
#                     break
# 
#             if self.buf_count >= chunk_size:
#                 # We have enough buffered samples to return a new chunk
#                 out_samp = np.empty(chunk_size, dtype=float)
#                 out_count = 0
#                 for b in self.sample_buf:
#                     if out_count + len(b) <= chunk_size:
#                         use_count = len(b)
#                     else:
#                         use_count = chunk_size - out_count
# 
#                     out_samp[out_count:out_count + use_count] = b[:use_count]
#                     out_count += use_count
# 
#                 out_time = self.buf_start_time
# 
#                 if self.buf_count > chunk_size:
#                     # There are unused samples remaining in the buffer
#                     # Place these at the start of the buffer for the next call to next_chunk()
#                     self.sample_buf = [self.sample_buf[-1][use_count:]]
#                     self.buf_count = len(self.sample_buf[0])
#                     self.buf_start_time = self.buf_start_time + self.sample_period * chunk_size
#                 else:
#                     self.sample_buf = []
#                     self.buf_count = 0
# 
#                 return SampleChunk(out_samp, out_time, self.sample_period)
# 
#             if self.stream_ended:
#                 break
# 
#         return None
# 
#     def next_samples(self, sample_count=10000):
#         '''Get a new set of raw samples from the stream
# 
#         sample_count (int)
#             The number of samples for the array.
# 
#         Returns a numpy array of float. If the stream had fewer than sample_count samples
#           remaining then the array is sized to hold only those samples.
# 
#         Returns None if the stream has ended.
#         '''
# 
#         sc = self.next_chunk(sample_count)
#         if sc is not None:
#             return sc.samples
#         else:
#             return None
# 
#     def buffered_chunk(self):
#         '''Get all remaining buffered samples'''
#         return self.next_chunk(self.buf_count)
# 
# class StreamError(RuntimeError):
#     '''Custom exception class for edge and sample streams'''
#     pass
# 
# class AutoLevelError(StreamError):
#     '''Custom exception class auto-level errors'''
#     def __init__(self, msg='Unable to find avg. logic levels of waveform'):
#         StreamError.__init__(self, msg)
# 
# Path: ripyl/util/equality.py
# def relatively_equal(a, b, epsilon):
#     '''Evaluate the relative equality of two floats
# 
#     The epsilon parameter controls how close the relatve magnitudes need to be
#     for equality to be determined.
# 
#     Adapted from: http://floating-point-gui.de/errors/comparison/ 
# 
#     a (float)
#         First number to compare
# 
#     b (float)
#         Second number to compare
# 
#     epsilon (float)
#         Relative epsilon for comparison
# 
#     Returns True when a and b are nearly equal
#     '''
#     
#     if a == b: # take care of the inifinities
#         return True
#     
#     elif a * b == 0.0: # either a or b is zero
#         return abs(a - b) < epsilon ** 2
#         
#     else: # relative error
#         return abs(a - b) / (abs(a) + abs(b)) < epsilon


# Path: ripyl/decode.py
import numpy as np
import scipy as sp
import math
import collections
import itertools
import ripyl.util.stats as stats
from ripyl.streaming import ChunkExtractor, StreamError, AutoLevelError
from ripyl.util.equality import relatively_equal

    return symbol_rate
    
#FIX: clean up use of cur_time, cur_state, cur_state(), next_states, etc.
class EdgeSequence(object):
    '''Utility class to walk through an edge iterator in arbitrary time steps'''

    def __init__(self, edges, time_step, start_time=None):
        '''
        edges (sequence of (float, int) tuples)
            An iterable of 2-tuples representing each edge transition.
            The 2-tuples *must* be in the absolute time form (time, logic level).
        
        time_step (float)
            The default time step for advance() when it is called
            without an argument.
        
        start_time (float)
            The initial starting time for the sequence.
            
        Raises StreamError when there are less than two elements to the edges iterable
        '''
        self.edges = edges
        self.time_step = time_step
        self.it_end = False
        
        try:
            self.cur_states = next(self.edges)
            self.next_states = next(self.edges)
        except StopIteration:
            self.it_end = True

            raise StreamError('Not enough edges to initialize edge_sequence() object')