#!/usr/bin/env python3

r"""
Simulates a second-order ΔΣ modulator with the following parameter values:

    Fclk = 50MHz            -- clock frequency
    -- Fin = 37.5kHz           -- input frequency
    Fin = 200kHz            -- input frequency
    Vin = 950mV             -- input voltage amplitude (peak)
    gain1 = 0.5V/V          -- gain of first integrator
    gain2 = 0.5V/V          -- gain of second integrator
    Vmax ($V_{\rm max}$) = 1V           -- quantizer maximum input voltage
    Vmin ($V_{\rm min}$) = -1V          -- quantizer minimum input voltage
    # levels = 16             -- quantizer output levels
    levels = 5              -- quantizer output levels
    Tstop = 21/Fin "s"      -- how many input periods to simulate
    Tstop = 1/Fin "s"       -- simulation stop time
    Tstart = -1/Fin "s"     -- simulation start time (points with t<0 are discarded)
    vin_file = 'vin.wave'   -- output data file for vin
    vout_file = 'vout.wave' -- output data file for vout
    dout_file = 'dout.wave' -- output data file for dout

The values given above are used in the simulation, no further modification of the
code given below is required when changing these parameters.
"""

from quantiphy import Quantity
from math import sin, tau
from inform import display, error, os_error


class Integrator:
    def __init__(self, gain=1):
        self.state = 0
        self.gain = gain

    def update(self, vin):
        self.state += self.gain*vin
        return self.state


class Quantizer:
    def __init__(self, v_max, v_min, levels):
        self.v_min = v_min
        self.levels = levels
        self.delta = (v_max - v_min)/(levels - 1)

    def update(self, v_in):
        level = (v_in - self.v_min) // self.delta
        level = 0 if level < 0 else level
        level = self.levels-1 if level >= self.levels else level
        return int(level), self.delta*level + self.v_min


class Source:
    def __init__(self, f_in, amp):
        self.omega = tau*f_in
        self.amp = amp

    def update(self, t):
        return self.amp*sin(self.omega*t)


# read simulation parameters and load into module namespace
parameters = Quantity.extract(__doc__)
globals().update(parameters)

# display the simulation parameters
display('Simulation parameters:')
for k, v in parameters.items():
    try:
        display('   ', v.render(show_label='f'))
    except AttributeError:
        display(f'    {k} = {v}')

# instantiate components
integrator1 = Integrator(gain1)
integrator2 = Integrator(gain2)
quantizer = Quantizer(Vmax, Vmin, levels)
sine = Source(Fin, Vin)

# run simulation
t = Tstart
dt = 1/Fclk
v_out = 0
t_stop = Tstop
try:
    fvin = open(vin_file, 'w')
    fvout = open(vout_file, 'w')
    fdout = open(dout_file, 'w')
    while t < t_stop:
        v_in = sine.update(t)
        v_int1 = integrator1.update(v_in - v_out)
        v_int2 = integrator2.update(v_int1 - v_out)
        d_out, v_out = quantizer.update(v_int2)
        if (t >= 0):
            print(t, v_in, file=fvin)
            print(t, v_out, file=fvout)
            print(t, d_out, file=fdout)
        t += dt
except OSError as e:
    error(os_error(e))
