electronics.py 6.22 KB
Newer Older
1
import numpy as np
Daniel Brown's avatar
Daniel Brown committed
2

3
from finesse.components.general import Connector
4
from finesse.components.workspace import ConnectorWorkspace
5
from finesse.components.node import NodeDirection, NodeType
6
from finesse.parameter import model_parameter
7

Samuel Rowlinson's avatar
Samuel Rowlinson committed
8

Daniel Brown's avatar
Daniel Brown committed
9 10 11
class FilterWorkspace(ConnectorWorkspace):
    pass

12

13
@model_parameter("gain", "Gain", 1, 0)
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
class ZPKNodeActuator(Connector):
    def __init__(self, name, mechanical_node, gain=1, z=[], p=[], k=1):
        super().__init__(name)

        if mechanical_node.type is not NodeType.MECHANICAL:
            raise Exception("Actuation must be on a mechanical node.")

        self.gain = gain
        self.z = z
        self.p = p
        self.k = k

        self.__node = mechanical_node

        self._add_port("p1", NodeType.ELECTRICAL)
        self.p1._add_node("i", NodeDirection.INPUT)

        self._add_port("mech", NodeType.MECHANICAL)
        self.mech._add_node("actuation", None, mechanical_node)

        self._register_node_coupling("P1_ACT", self.p1.i, mechanical_node)

    def _get_workspace(self, sim):
37
        if sim.signal:
38 39 40
            refill = sim.model.fsig.f.is_changing or any(
                p.is_changing for p in self.parameters
            )
41 42
            ws = FilterWorkspace(self, sim, refill, refill)
            ws.signal.set_fill_function(self.fill)
43 44 45 46 47 48 49 50
            ws.frequencies = sim.electrical_frequencies[self.p1.i].frequencies
            return ws
        else:
            return None

    def fill(self, ws):
        for _ in ws.frequencies:
            # Assumes only couples to first mech frequency
51
            with ws.sim.signal.component_edge_fill3(
Daniel Brown's avatar
Daniel Brown committed
52
                ws.owner_id, ws.connections.P1_ACT_idx, _.index, 0,
53 54 55 56
            ) as mat:
                mat[:] = ws.values.gain


57
@model_parameter("gain", "Gain", 1, 0)
58
class Amplifier(Connector):
59 60 61 62 63 64 65 66 67 68 69 70 71
    def __init__(self, name, gain=1):
        super().__init__(name)
        self.gain = gain

        self._add_port("p1", NodeType.ELECTRICAL)
        self.p1._add_node("i", NodeDirection.INPUT)

        self._add_port("p2", NodeType.ELECTRICAL)
        self.p2._add_node("o", NodeDirection.OUTPUT)

        self._register_node_coupling("P1_P2", self.p1.i, self.p2.o)

    def _get_workspace(self, sim):
72
        if sim.signal:
73 74 75
            refill = sim.model.fsig.f.is_changing or any(
                p.is_changing for p in self.parameters
            )
76 77
            ws = FilterWorkspace(self, sim, refill, refill)
            ws.signal.set_fill_function(self.fill)
78 79 80 81 82 83 84
            ws.frequencies = sim.electrical_frequencies[self.p1.i].frequencies
            return ws
        else:
            return None

    def fill(self, ws):
        for _ in ws.frequencies:
85
            with ws.sim.signal.component_edge_fill3(
Daniel Brown's avatar
Daniel Brown committed
86
                ws.owner_id, ws.connections.P1_P2_idx, 0, 0,
87 88 89
            ) as mat:
                mat[:] = ws.values.gain

90 91
    def eval(self, f):
        return float(self.gain)
92 93


94
@model_parameter("gain", "Gain", 1, 0)
Daniel Brown's avatar
Daniel Brown committed
95 96 97 98
class Filter(Connector):
    def __init__(self, name, gain=1):
        super().__init__(name)
        self.gain = gain
99 100

        self._add_port("p1", NodeType.ELECTRICAL)
Samuel Rowlinson's avatar
Samuel Rowlinson committed
101
        self.p1._add_node("i", NodeDirection.INPUT)
102 103

        self._add_port("p2", NodeType.ELECTRICAL)
Samuel Rowlinson's avatar
Samuel Rowlinson committed
104
        self.p2._add_node("o", NodeDirection.OUTPUT)
105

Daniel Brown's avatar
Daniel Brown committed
106 107 108
        self._register_node_coupling("P1_P2", self.p1.i, self.p2.o)

    def _get_workspace(self, sim):
109
        if sim.signal:
Daniel Brown's avatar
Daniel Brown committed
110 111 112
            refill = sim.model.fsig.f.is_changing or any(
                p.is_changing for p in self.parameters
            )
113 114 115
            ws = FilterWorkspace(self, sim, refill, refill)
            ws.signal.set_fill_function(self.fill)
            ws.frequencies = sim.signal.electrical_frequencies[self.p1.i].frequencies
Daniel Brown's avatar
Daniel Brown committed
116 117 118 119 120
            return ws
        else:
            return None

    def fill(self, ws):
121
        Hz = self.eval(ws.sim.model_data.fsig)
Daniel Brown's avatar
Daniel Brown committed
122 123

        for _ in ws.frequencies:
124
            with ws.sim.signal.component_edge_fill3(
Daniel Brown's avatar
Daniel Brown committed
125
                ws.owner_id, ws.connections.P1_P2_idx, 0, 0,
Daniel Brown's avatar
Daniel Brown committed
126 127 128
            ) as mat:
                mat[:] = Hz

Daniel Brown's avatar
Daniel Brown committed
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
    def bode_plot(self, f=None, return_axes=False):
        """
        Plots Bode for this filter.

        Parameters
        ----------
        f : optional
            Frequencies to plot for in Hz (Not radians)

        Returns
        -------
        axis : Matplotlib axis for plot if return_axes=True
        """
        import matplotlib.pyplot as plt
        import scipy

        if f is not None:
146
            w = 2 * np.pi * f
Daniel Brown's avatar
Daniel Brown committed
147 148 149
        else:
            w = None

Daniel Brown's avatar
Daniel Brown committed
150 151 152
        w, mag, phase = scipy.signal.bode(
            tuple(np.array(_, dtype=float) for _ in self.sys)
        )
Daniel Brown's avatar
Daniel Brown committed
153 154

        fig, axs = plt.subplots(2, 1, sharex=True)
155
        axs[0].semilogx(w / 2 / np.pi, mag)
Daniel Brown's avatar
Daniel Brown committed
156 157
        axs[0].set_ylabel("Amplitude [dB]")

158
        axs[1].semilogx(w / 2 / np.pi, phase)
Daniel Brown's avatar
Daniel Brown committed
159 160 161 162 163 164 165 166
        axs[1].set_xlabel("Frequency [Hz]")
        axs[1].set_ylabel("Phase [Deg]")

        fig.suptitle(f"Bode plot for {self.name}")

        if return_axes:
            return axs

Daniel Brown's avatar
Daniel Brown committed
167

168
@model_parameter("gain", "Gain", 1, 0)
Daniel Brown's avatar
Daniel Brown committed
169 170 171 172 173 174 175
class ZPKFilter(Filter):
    def __init__(self, name, z, p, k, *, gain=1):
        super().__init__(name, gain)

        self.z = z
        self.p = p
        self.k = k
176

Daniel Brown's avatar
Daniel Brown committed
177 178 179 180
    @property
    def sys(self):
        return (self.z, self.p, self.k)

Daniel Brown's avatar
Daniel Brown committed
181
    def eval(self, f):
Daniel Brown's avatar
Daniel Brown committed
182
        import scipy.signal as signal
Samuel Rowlinson's avatar
Samuel Rowlinson committed
183

184 185 186 187
        return (
            float(self.gain)
            * signal.freqs_zpk(self.z, self.p, self.k, 2 * np.pi * f)[1]
        )
188

189 190

@model_parameter("gain", "Gain", 1, 0)
Daniel Brown's avatar
Daniel Brown committed
191 192 193 194 195 196 197 198 199 200 201 202 203 204
class ButterFilter(ZPKFilter):
    def __init__(self, name, order, btype, frequency, *, gain=1, analog=True):
        import scipy.signal as signal

        zpk = signal.butter(
            order,
            2 * np.pi * np.array(frequency),
            btype=btype,
            analog=analog,
            output="zpk",
        )
        super().__init__(name, *zpk, gain=gain)


205
@model_parameter("gain", "Gain", 1, 0)
Daniel Brown's avatar
Daniel Brown committed
206 207 208
class Cheby1Filter(ZPKFilter):
    def __init__(self, name, order, rp, btype, frequency, *, gain=1, analog=True):
        import scipy.signal as signal
209

Daniel Brown's avatar
Daniel Brown committed
210 211 212 213 214 215 216 217 218
        zpk = signal.cheby1(
            order,
            rp,
            2 * np.pi * np.array(frequency),
            btype=btype,
            analog=analog,
            output="zpk",
        )
        super().__init__(name, *zpk, gain=gain)