Source code for cbadc.analog_system.modulator

import numpy as np
import scipy.signal
import scipy.linalg
import logging
from typing import Union
import sympy as sp
from .analog_system import AnalogSystem

logger = logging.getLogger(__name__)


[docs]class SineWaveModulator(AnalogSystem): """Sinewave modulator This class represents a sinewave modulator with a given modulation frequency and permutation matrix. Parameters ---------- modulation_frequency : `float` the modulation frequency permuation_matrix : `array_like`, shape=(N, N) the permutation matrix """ pre_computable: bool = False def __init__( self, analog_system: AnalogSystem, modulation_frequency: float, ): super().__init__( analog_system.A, analog_system.B, analog_system.CT, analog_system.Gamma, analog_system.Gamma_tildeT, analog_system.D, analog_system.B_tilde, analog_system.A_tilde, ) self.angular_modulation_frequency = 2 * np.pi * modulation_frequency if self.N % 2 != 0: raise ValueError("A must be even sized.") self._N_half = self.N // 2 self._x_shuffler = np.zeros(self.N, dtype=int) _indices = np.arange(self._x_shuffler.size) self._x_shuffler[::2] = _indices[: self._N_half] self._x_shuffler[1::2] = _indices[self._N_half :] self._x_de_shuffler = np.zeros_like(self._x_shuffler) self._x_de_shuffler[self._x_shuffler] = _indices self._M_half = self.M // 2 self._s_shuffler = np.zeros(self.M, dtype=int) _indices = np.arange(self._s_shuffler.size) self._s_shuffler[::2] = _indices[: self._M_half] self._s_shuffler[1::2] = _indices[self._M_half :] self._s_de_shuffler = np.zeros_like(self._s_shuffler) self._s_de_shuffler[self._s_shuffler] = _indices self._rotation_matrix_data = np.eye(self.N)
[docs] def derivative( self, x: np.ndarray, t: float, u: np.ndarray, s: np.ndarray ) -> np.ndarray: """Compute the derivative of the analog system. Specifically, produces the state derivative :math:`\dot{\mathbf{x}}(t) = \mathbf{A} \mathbf{x}(t) + \mathbf{B} \mathbf{u}(t) + \mathbf{\Gamma} \mathbf{s}(t)` as a function of the state vector :math:`\mathbf{x}(t)`, the given time :math:`t`, the input signal value :math:`\mathbf{u}(t)`, and the control contribution value :math:`\mathbf{s}(t)`. Parameters ---------- x : `array_like`, shape=(N,) the state vector evaluated at time t. t : `float` the time t. u : `array_like`, shape=(L,) the input signal vector evaluated at time t. s : `array_like`, shape=(M,) the control contribution evaluated at time t. Returns ------- `array_like`, shape=(N,) the derivative :math:`\dot{\mathbf{x}}(t)`. """ return ( np.dot(self.A, x).flatten() + np.dot(self.B, u).flatten() + np.dot( self.Gamma, np.dot(self.modulate(t)[: self.M, : self.M], s[self._s_shuffler])[ self._s_de_shuffler ], ).flatten() )
[docs] def control_observation( self, t: float, x: np.ndarray, u: np.ndarray = None, s: np.ndarray = None ) -> np.ndarray: """Computes the control observation for a given state vector :math:`\mathbf{x}(t)` evaluated at time :math:`t`. Specifically, returns :math:`\\tilde{\mathbf{s}}(t) = \\tilde{\mathbf{\Gamma}}^\mathsf{T} \mathbf{x}(t) + \\tilde{\mathbf{D}} \mathbf{u}(t)` Parameters ---------- x : `array_like`, shape=(N,) the state vector. u : `array_like`, shape=(L,) the input vector s : `array_like`, shape=(M,) the control signal Returns ------- `array_like`, shape=(M_tilde,) the control observation. """ x = np.dot(self.demodulate(t), x[self._x_shuffler])[ self._x_de_shuffler ].flatten() if u is None: return np.dot(self.Gamma_tildeT, x) if s is None: np.dot(self.Gamma_tildeT, x) + np.dot(self.B_tilde, u) return ( np.dot(self.Gamma_tildeT, x) + np.dot(self.B_tilde, u) + np.dot(self.A_tilde, s) )
[docs] def modulate(self, t: float) -> np.ndarray: """Upmodulate the given signal phi.""" return self._rotation_matrix(self.angular_modulation_frequency * t)
[docs] def demodulate(self, t: float) -> np.ndarray: """Downmodulate the given signal phi.""" return self._rotation_matrix(-self.angular_modulation_frequency * t)
def _rotation_matrix(self, phi: float) -> np.ndarray: self._rotation_matrix_data[: self.N, : self.N] = np.eye(self.N) * np.cos(phi) self._rotation_matrix_data[: self._N_half, self._N_half :] = -np.eye( self._N_half ) * np.sin(phi) self._rotation_matrix_data[self._N_half :, : self._N_half] = np.eye( self._N_half ) * np.sin(phi) return self._rotation_matrix_data
[docs]class SquareWaveModulator(SineWaveModulator): """Square-wave modulator This class represents a sinewave modulator with a given modulation frequency and permutation matrix. Parameters ---------- modulation_frequency : `float` the modulation frequency permuation_matrix : `array_like`, shape=(N, N) the permutation matrix """ pre_computable: bool = False def __init__( self, analog_system: AnalogSystem, modulation_frequency: float, permuation_matrix: np.ndarray, base_band_bandwidth: float, ): self.omega_q = 2 * np.pi * base_band_bandwidth A = np.zeros((2 * analog_system.N, 2 * analog_system.N)) A[: analog_system.N, : analog_system.N] = analog_system.A A[analog_system.N :, analog_system.N :] = -self.omega_q * np.eye( analog_system.N ) B = np.vstack((analog_system.B, np.zeros_like(analog_system.B))) CT = np.hstack((analog_system.CT, np.zeros_like(analog_system.CT))) Gamma = np.vstack((analog_system.Gamma, np.zeros_like(analog_system.Gamma))) Gamma_tildeT = np.hstack( (np.zeros_like(analog_system.Gamma_tildeT), analog_system.Gamma_tildeT) ) super().__init__( AnalogSystem( A, B, CT, Gamma, Gamma_tildeT, ), modulation_frequency, permuation_matrix, ) self._square_rotation = np.zeros((2, 2)) self._full_rotation_matrix = np.zeros((self.N // 2, self.N // 2)) def _rotation_matrix(self, phi: float) -> np.ndarray: cos_wave = 2 * (np.cos(phi) > 0) - 1 sin_wave = 2 * (np.sin(phi) > 0) - 1 self._square_rotation[0, 0] = cos_wave self._square_rotation[1, 1] = cos_wave self._square_rotation[0, 1] = -sin_wave self._square_rotation[1, 0] = sin_wave for n in range(self.N // 4): self._full_rotation_matrix[ 2 * n : 2 * (n + 1), 2 * n : 2 * (n + 1) ] = self._square_rotation return self._full_rotation_matrix def _rotation_matrix_half_clock_cycle(self, phi: float) -> np.ndarray: cos_wave = int(np.cos(phi) > 0) sin_wave = int(np.sin(phi) > 0) self._square_rotation[0, 0] = cos_wave self._square_rotation[1, 1] = cos_wave self._square_rotation[0, 1] = -sin_wave self._square_rotation[1, 0] = sin_wave for n in range(self.N // 4): self._full_rotation_matrix[ 2 * n : 2 * (n + 1), 2 * n : 2 * (n + 1) ] = self._square_rotation return self._full_rotation_matrix
[docs] def demodulate(self, t: float) -> np.ndarray: """Downmodulate the given signal phi.""" return np.dot( self.inverted_permuation_matrix, np.dot( self._rotation_matrix_half_clock_cycle( -2 * np.pi * self.angular_modulation_frequency * t ), self._permuation_matrix, ), )
[docs] def derivative( self, x: np.ndarray, t: float, u: np.ndarray, s: np.ndarray ) -> np.ndarray: """Compute the derivative of the analog system. Specifically, produces the state derivative :math:`\dot{\mathbf{x}}(t) = \mathbf{A} \mathbf{x}(t) + \mathbf{B} \mathbf{u}(t) + \mathbf{\Gamma} \mathbf{s}(t)` as a function of the state vector :math:`\mathbf{x}(t)`, the given time :math:`t`, the input signal value :math:`\mathbf{u}(t)`, and the control contribution value :math:`\mathbf{s}(t)`. Parameters ---------- x : `array_like`, shape=(N,) the state vector evaluated at time t. t : `float` the time t. u : `array_like`, shape=(L,) the input signal vector evaluated at time t. s : `array_like`, shape=(M,) the control contribution evaluated at time t. Returns ------- `array_like`, shape=(N,) the derivative :math:`\dot{\mathbf{x}}(t)`. """ N_2 = self.N // 2 self.A[N_2:, :N_2] = self.omega_q * self.demodulate(t) return ( np.dot(self.A, x).flatten() + np.dot(self.B, u).flatten() + np.dot(self.Gamma, np.dot(self.modulate(t), s)).flatten() )
[docs] def control_observation( self, t: float, x: np.ndarray, u: np.ndarray = None, s: np.ndarray = None ) -> np.ndarray: """Computes the control observation for a given state vector :math:`\mathbf{x}(t)` evaluated at time :math:`t`. Specifically, returns :math:`\\tilde{\mathbf{s}}(t) = \\tilde{\mathbf{\Gamma}}^\mathsf{T} \mathbf{x}(t) + \\tilde{\mathbf{D}} \mathbf{u}(t)` Parameters ---------- x : `array_like`, shape=(N,) the state vector. u : `array_like`, shape=(L,) the input vector s : `array_like`, shape=(M,) the control signal Returns ------- `array_like`, shape=(M_tilde,) the control observation. """ if u is None: return np.dot(self.Gamma_tildeT, x) if s is None: np.dot(self.Gamma_tildeT, x) + np.dot(self.B_tilde, u) return ( np.dot(self.Gamma_tildeT, x) + np.dot(self.B_tilde, u) + np.dot(self.A_tilde, s) )