Source code for cbadc.circuit.lc_tank

from ..analog_frontend import AnalogFrontend
from ..analog_system import AnalogSystem
from ..digital_control import DigitalControl
from ..analog_signal import Clock
from .analog_frontend import CircuitAnalogFrontend
from ..analog_frontend import get_global_control, _analog_system_matrix_exponential
from .components.passives import Resistor, Capacitor, Inductor
from .components.opamp import OTA
import numpy as np
import scipy.integrate


def _non_homogenious_weights(
    analog_frontend: AnalogFrontend, input_function
) -> np.ndarray:
    analog_system = analog_frontend.analog_system
    digital_control = analog_frontend.digital_control

    def input_derivative(t: float, x: np.ndarray):
        return np.dot(analog_system.A, x) + analog_system.B.flatten() * input_function(
            t
        )

    y = scipy.integrate.solve_ivp(
        input_derivative,
        (0, digital_control.clock.T),
        np.zeros(analog_system.N),
    ).y[:, -1]

    A = np.zeros((analog_system.N, analog_system.M))

    for m in range(analog_system.M):
        identity_vector = np.zeros(analog_system.N)
        identity_vector[m] = 1.0

        def control_derivative(t: float, x: np.ndarray):
            return np.dot(analog_system.A, x) + identity_vector

        A[:, m] = scipy.integrate.solve_ivp(
            control_derivative,
            (0, digital_control.clock.T),
            np.zeros(analog_system.N),
        ).y[:, -1]

    return np.linalg.lstsq(A, y)[0]


def _non_homogenious_spectrum_weights(analog_frontend: AnalogFrontend, BW: float):
    size = 1000
    omega = 2 * np.pi * np.linspace(0, BW, size)
    Y = np.zeros((analog_frontend.analog_system.N, len(omega)))
    for i, w in enumerate(omega):

        def derivative(t, x):
            return np.dot(
                analog_frontend.analog_system.A, x
            ) + analog_frontend.analog_system.B.flatten() * np.cos(w * t)

        Y[:, i] = scipy.integrate.solve_ivp(
            derivative,
            (0, analog_frontend.digital_control.clock.T),
            np.zeros(analog_frontend.analog_system.N),
        ).y[:, -1]

    A = np.zeros((analog_frontend.analog_system.N, analog_frontend.analog_system.M))

    for m in range(analog_frontend.analog_system.M):
        identity_vector = np.zeros(analog_frontend.analog_system.N)
        identity_vector[m] = 1.0

        def control_derivative(t: float, x: np.ndarray):
            return np.dot(analog_frontend.analog_system.A, x) + identity_vector

        A[:, m] = scipy.integrate.solve_ivp(
            control_derivative,
            (0, analog_frontend.digital_control.clock.T),
            np.zeros(analog_frontend.analog_system.N),
        ).y[:, -1]

    return np.linalg.lstsq(
        np.vstack([A for _ in range(len(omega))]), Y.flatten(), rcond=None
    )[0]


def _non_homogenious_initial_state_weights(analog_frontend: AnalogFrontend):
    A = np.zeros((analog_frontend.analog_system.N, analog_frontend.analog_system.M))

    for m in range(analog_frontend.analog_system.M):
        identity_vector = np.zeros(analog_frontend.analog_system.N)
        identity_vector[m] = 1.0

        def control_derivative(t: float, x: np.ndarray):
            return np.dot(analog_frontend.analog_system.A, x) + identity_vector

        A[:, m] = scipy.integrate.solve_ivp(
            control_derivative,
            (0, analog_frontend.digital_control.clock.T),
            np.zeros(analog_frontend.analog_system.N),
        ).y[:, -1]

    x0 = np.array([0.5 / (1 << n) for n in np.arange(analog_frontend.analog_system.N)])
    y = np.dot(
        _analog_system_matrix_exponential(
            analog_frontend.analog_system.A, analog_frontend.digital_control.clock.T
        ),
        x0,
    )

    return np.linalg.lstsq(A, y, rcond=None)[0]


def _exponential_decay(analog_frontend: AnalogFrontend, input_function) -> np.ndarray:
    analog_system = analog_frontend.analog_system
    digital_control = analog_frontend.digital_control

    def input_derivative(t: float, x: np.ndarray):
        return np.dot(analog_system.A, x) + analog_system.B.flatten() * input_function(
            t
        )

    y = scipy.integrate.solve_ivp(
        input_derivative,
        (0, digital_control.clock.T),
        np.zeros(analog_system.N),
    ).y[:, -1]

    A = np.zeros((analog_system.N, analog_system.M))

    for m in range(analog_system.M):
        identity_vector = np.zeros(analog_system.N)
        identity_vector[m] = 1.0

        def control_derivative(t: float, x: np.ndarray):
            return np.dot(analog_system.A, x) + identity_vector

        A[:, m] = scipy.integrate.solve_ivp(
            control_derivative,
            (0, digital_control.clock.T),
            np.zeros(analog_system.N),
        ).y[:, -1]

    return np.array(
        [np.inner(y, A[:, m]) / np.inner(A[:, m], A[:, m]) for m in range(A.shape[1])]
    )


[docs]class LCFrontend(CircuitAnalogFrontend): L: float C: float Rs: float def __init__( self, M: int, L: float, C: float, gm: float, fc: float, Rin: float = 1e1, vdd_voltage: float = 1.2, in_high=0.0, in_low=0.0, ): kappa_scale: np.ndarray = np.array([gm * (0.5**m) for m in range(M)]) N = 2 * M - 1 A = np.zeros((N, N)) B = np.zeros((N, 1)) Gamma = np.zeros((N, M)) # The local feedback # A[:M, :M] = -np.eye(M) / (R * C) A[0, 0] = -1 / (Rin * C) # The resonance elements A[M:, :M] = (np.eye(M) - np.eye(M, k=1))[: (N - M), :] / (2 * L) A[:M, M:] = (-np.eye(M) + np.eye(M, k=-1))[:, : (N - M)] / C B[0, 0] = 1 / (Rin * C) Gamma[:M, :M] = np.eye(M) / C for m in range(M): Gamma[m, m] *= kappa_scale[m] # experimental def step(t: float): return 1.0 if t > 0 else 0.0 self.extended_analog_frontend = AnalogFrontend( AnalogSystem(A, B, np.eye(N), Gamma, -np.eye(N)), DigitalControl(Clock(1 / fc), M), ) # Gamma[:M, :M] = np.diag( # _non_homogenious_weights(self.extended_analog_frontend, step) * 2 # ) # BW = 1e8 # Gamma[:M, :M] = np.diag(_non_homogenious_spectrum_weights(self.extended_analog_frontend, BW)) # Gamma[:M, :M] = np.diag(_non_homogenious_weights(self.extended_analog_frontend, step))[0,0] # Gamma[:M, :M] = np.diag(_non_homogenious_initial_state_weights(self.extended_analog_frontend)) Gamma[:M, :M] = np.diag(_exponential_decay(self.extended_analog_frontend, step)) self.extended_analog_frontend = AnalogFrontend( AnalogSystem(A, B, np.eye(N), Gamma, -Gamma.transpose()), DigitalControl(Clock(1 / fc), M), ) self.extended_analog_frontend = get_global_control( AnalogSystem(A, B, np.eye(N), Gamma, -Gamma.transpose()), DigitalControl(Clock(1 / fc), M), np.linspace(0, 1 / fc, M, endpoint=False), # atol=1e-6, # rtol=1e-5, ) super().__init__( AnalogFrontend( AnalogSystem( self.extended_analog_frontend.analog_system.A[:M, :M], self.extended_analog_frontend.analog_system.B[:M, :], np.eye(M), self.extended_analog_frontend.analog_system.Gamma[:M, :M], self.extended_analog_frontend.analog_system.Gamma_tildeT[:M, :M], B_tilde=self.extended_analog_frontend.analog_system.B_tilde, A_tilde=self.extended_analog_frontend.analog_system.A_tilde, ), self.extended_analog_frontend.digital_control, ), vdd_voltage, in_high, in_low, subckt_name='LC_frontend', instance_name='Xaf', ) inductors = [Inductor(f'Lp_{i}', L) for i in range(M - 1)] + [ Inductor(f'Ln_{i}', L) for i in range(M - 1) ] capacitors = [Capacitor(f'C_{i}', C) for i in range(M)] resistors = [ Resistor('Rin_p', Rin), Resistor('Rin_n', Rin), ] otas = [ OTA( f'gm{m}_p', 'ota', np.abs(self.extended_analog_frontend.analog_system.Gamma[m, m] * C / 2), ) for m in range(M) ] + [ OTA( f'gm{m}_n', 'ota', np.abs(self.extended_analog_frontend.analog_system.Gamma[m, m] * C / 2), ) for m in range(M) ] self.add(*inductors) self.add(*capacitors) self.add(*resistors) self.add(*otas) for m in range(M): self.connects( (self['VDD'], otas[m]['VDD']), (self['VSS'], otas[m]['VSS']), (self['VDD'], otas[m]['OUT_P']), (self.xp[m], otas[m]['OUT_N']), (self[f'OUT{m}_P'], otas[m]['IN_P']), (self[f'OUT{m}_N'], otas[m]['IN_N']), ) self.connects( (self['VDD'], otas[m + M]['VDD']), (self['VSS'], otas[m + M]['VSS']), (self['VSS'], otas[m + M]['OUT_N']), (self.xn[m], otas[m + M]['OUT_P']), (self[f'OUT{m}_P'], otas[m + M]['IN_P']), (self[f'OUT{m}_N'], otas[m + M]['IN_N']), ) self.connects( (self.xp[m], capacitors[m][0]), (self.xn[m], capacitors[m][1]), ) # Input if m == 0: self.connects( (self.xp[m], resistors[0][1]), (self.xn[m], resistors[1][1]), (self['IN0_P'], resistors[0][0]), (self['IN0_N'], resistors[1][0]), ) # controls else: self.connects( (self.xp[m - 1], inductors[m - 1][0]), (self.xp[m], inductors[m - 1][1]), (self.xn[m - 1], inductors[M + m - 2][0]), (self.xn[m], inductors[M + m - 2][1]), )