diff options
author | Eddy Pedroni <eddy@0xf7.com> | 2022-06-01 19:20:10 +0200 |
---|---|---|
committer | Eddy Pedroni <eddy@0xf7.com> | 2022-06-01 19:20:10 +0200 |
commit | 2cd5778d5b49a40206220b571a4d53009348967b (patch) | |
tree | 02f83ff5261fdc6258542b85d12b2c846a49c0d4 | |
parent | 6b709f4423d132742207b5cf01a64f0707a77927 (diff) |
Initial implementation of frequency response measurement
-rw-r--r-- | doc/architecture.drawio | 2 | ||||
-rw-r--r-- | lab_control/frequency_response.py | 25 | ||||
-rw-r--r-- | lab_control/test/frequency_response_measurement_test.py | 40 | ||||
-rw-r--r-- | lab_control/test/mock_lab.py | 54 |
4 files changed, 120 insertions, 1 deletions
diff --git a/doc/architecture.drawio b/doc/architecture.drawio index 3c68dad..f32a569 100644 --- a/doc/architecture.drawio +++ b/doc/architecture.drawio @@ -1 +1 @@ -<mxfile host="Electron" modified="2022-05-26T14:43:31.818Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/18.0.6 Chrome/100.0.4896.143 Electron/18.2.3 Safari/537.36" etag="yj-ZX5OZNlBGnsCaUlsx" version="18.0.6" type="device"><diagram id="Aw8Itl_1nmNhQKuLZfUh" name="Page-1">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</diagram></mxfile>
\ No newline at end of file +<mxfile host="Electron" modified="2022-06-01T15:45:48.554Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/18.0.6 Chrome/100.0.4896.143 Electron/18.2.3 Safari/537.36" etag="QFb5ZM_4cWopSkUcfKm_" version="18.0.6" type="device"><diagram id="Aw8Itl_1nmNhQKuLZfUh" name="Page-1">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</diagram></mxfile>
\ No newline at end of file diff --git a/lab_control/frequency_response.py b/lab_control/frequency_response.py new file mode 100644 index 0000000..ca2002e --- /dev/null +++ b/lab_control/frequency_response.py @@ -0,0 +1,25 @@ +from lab_control.function_generator import FunctionGenerator +from lab_control.oscilloscope import Oscilloscope + +class FrequencyResponseMeasurement: + def __init__(self): + self.minFrequency = None + self.maxFrequency = None + self.steps = None + self.functionGeneratorChannel = None + self.oscilloscopeChannel = None + self.measurementDone = False + self.data = None + + def measure(self, osc: Oscilloscope, fg: FunctionGenerator) -> None: + frequencyRange = self.maxFrequency - self.minFrequency + self.data = [] + + for i in range(0, self.steps): + frequency = self.minFrequency + i * frequencyRange / (self.steps - 1) + fg.setFrequency(self.functionGeneratorChannel, frequency) + response = osc.measureAmplitude(self.oscilloscopeChannel) + self.data.append((frequency, response)) + + self.measurementDone = True + diff --git a/lab_control/test/frequency_response_measurement_test.py b/lab_control/test/frequency_response_measurement_test.py new file mode 100644 index 0000000..caea62a --- /dev/null +++ b/lab_control/test/frequency_response_measurement_test.py @@ -0,0 +1,40 @@ +import pytest + +from lab_control.test.mock_lab import MockLab +from lab_control.frequency_response import FrequencyResponseMeasurement + +@pytest.fixture +def mockLab(): + return MockLab() + +@pytest.fixture +def uut(mockLab): + return FrequencyResponseMeasurement() + +def test_frequencyResponseRamp(mockLab, uut): + uut.minFrequency = 100.0 + uut.maxFrequency = 200.0 + uut.steps = 11 + uut.functionGeneratorChannel = 1 + uut.oscilloscopeChannel = 1 + + # Expect a ramp response from 0 to 2 V + minAmplitude = 0.0 + maxAmplitude = 2.0 + + def amplitudeFunction(f: float) -> float: + assert f >= uut.minFrequency and f <= uut.maxFrequency + frequencyPu = (f - uut.minFrequency) / (uut.maxFrequency - uut.minFrequency) + return minAmplitude + maxAmplitude * frequencyPu + + mockLab.connectChannels(uut.functionGeneratorChannel, uut.oscilloscopeChannel) + mockLab.setAmplitudeFunction(uut.oscilloscopeChannel, amplitudeFunction) + expectedData = [(f, amplitudeFunction(f)) for f in [100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0, 180.0, 190.0, 200.0]] + + assert not uut.measurementDone + assert uut.data == None + + response = uut.measure(mockLab, mockLab) + + assert uut.measurementDone + assert uut.data == expectedData diff --git a/lab_control/test/mock_lab.py b/lab_control/test/mock_lab.py new file mode 100644 index 0000000..718f319 --- /dev/null +++ b/lab_control/test/mock_lab.py @@ -0,0 +1,54 @@ +from collections.abc import Callable + +from lab_control.function_generator import FunctionGenerator +from lab_control.oscilloscope import Oscilloscope + +class MockLab(FunctionGenerator, Oscilloscope): + class FGChannelState: + def __init__(self): + self.on = False + self.frequency = None + self.amplitude = None + + class OscChannelState: + def __init__(self): + self.amplitudeFunction = None + self.fgChannel = None + + def __init__(self): + self.fgChannels = [MockLab.FGChannelState() for i in range(0, 2)] + self.oscChannels = [MockLab.OscChannelState() for i in range(0, 4)] + + def setOn(self, channel: int) -> None: + self.fgChannels[channel - 1].on = True + + def setOff(self, channel: int) -> None: + self.fgChannels[channel - 1].on = False + + def setFrequency(self, channel: int, frequency: float) -> None: + self.fgChannels[channel - 1].frequency = frequency + + def setAmplitude(self, channel: int, amplitude: float) -> None: + self.fgChannels[channel - 1].amplitude = amplitude + + def setFunction(self, channel: int, function: int) -> None: + pass + + def measureAmplitude(self, channel: int) -> float: + fgChannel = self.oscChannels[channel - 1].fgChannel + return self.oscChannels[channel - 1].amplitudeFunction(self.fgChannels[fgChannel].frequency) + + def measurePeakToPeak(self, channel: int) -> float: + pass + + def measureRMS(self, channel: int) -> float: + pass + + def measureFrequency(self, channel: int) -> float: + pass + + def setAmplitudeFunction(self, channel: int, f: Callable[[float], float]) -> None: + self.oscChannels[channel - 1].amplitudeFunction = f + + def connectChannels(self, fg: int, osc: int) -> None: + self.oscChannels[osc - 1].fgChannel = fg - 1 |