43 Commits

Author SHA1 Message Date
wendtalexander
ff2d8ebb50 [ui] adding checks, refactoring 2024-10-10 15:11:40 +02:00
wendtalexander
57d3a83243 [ui/plot] refactoring 2024-10-10 15:11:15 +02:00
wendtalexander
2539fa7e25 [dataio/daq] adding stop signal from gui, commentin out 2024-10-10 15:10:50 +02:00
wendtalexander
b5f8d7663d [ui] updating autorang button 2024-10-10 14:25:50 +02:00
wendtalexander
fe3e142452 [ui/plots] removing plotitem if the function gets called again, setting autorang 2024-10-10 14:22:03 +02:00
wendtalexander
bbc8def460 [dataio/buffer] setting mutex lock while appending 2024-10-10 14:19:41 +02:00
wendtalexander
25d16cc5fd [ui] removing unessassery stuff 2024-10-10 09:59:42 +02:00
wendtalexander
523f5dc346 [ui/plot] rewriting continous plot, plotting chucks of the buffer 2024-10-10 09:58:54 +02:00
wendtalexander
b6bd9e23a0 [dataio/sinus] commenting debugging 2024-10-10 09:57:46 +02:00
wendtalexander
e100dac5ea [dataio/buffer] adding time for both channels, reading range alows for overflows reads 2024-10-10 09:57:03 +02:00
wendtalexander
22b899e723 [ui] adding stop button 2024-10-09 17:15:58 +02:00
wendtalexander
3ca48d11fe [ui/plot] plotting chuncks of the data 2024-10-09 17:15:33 +02:00
wendtalexander
32c79ff47b [dataio] adding stop button 2024-10-09 17:14:56 +02:00
wendtalexander
e3ed301252 [ui/plot] adding plot, but plots after while loop is finished 2024-10-09 08:58:18 +02:00
wendtalexander
0b36e31135 [ui] changing samplerate 2024-10-09 08:02:57 +02:00
wendtalexander
b7d92259a9 [producer] adding more time 2024-10-09 07:58:56 +02:00
wendtalexander
8378add874 [time] adding time to the buffer and retruning it with the item 2024-10-09 07:58:41 +02:00
wendtalexander
aaa42db2ae [dataio/test] adding a sin producer for testing 2024-10-07 13:51:46 +02:00
wendtalexander
52a0821601 [project] adding pglive 2024-10-04 15:33:27 +02:00
wendtalexander
5ec5cc8644 [ui] adding pglive, trying to plot continious data 2024-10-04 15:32:57 +02:00
wendtalexander
bf72f90009 [ui/plots] moving continous plot outside 2024-10-04 15:32:16 +02:00
wendtalexander
949767c45e [refactoring] spelling 2024-10-04 15:31:51 +02:00
wendtalexander
b251ee13c4 [refactoring] spelling 2024-10-04 15:31:41 +02:00
wendtalexander
cd936b1ed1 [ui] updating plot for continously plotting 2024-10-04 11:50:05 +02:00
wendtalexander
85fd70f8ca [dataio] adding chunking for reading daq input 2024-10-04 11:49:33 +02:00
wendtalexander
8910305262 [dataio] adding samplerate to circular buffer 2024-10-04 11:48:59 +02:00
wendtalexander
e43bb16bd4 [ui] adding plot that reads after pressing run the input of the daqproducer 2024-10-03 14:06:23 +02:00
wendtalexander
e555573f09 [daq] adding a producer that reads from one channel and saves it to the buffer 2024-10-03 14:05:41 +02:00
wendtalexander
4d53a0f51d [buffer] adding samplerate 2024-10-03 14:05:02 +02:00
wendtalexander
d127750e7b [buffer] removing logging, adding samplerate, adding check for getting the item at current index 2024-10-03 08:52:25 +02:00
wendtalexander
b18f870a6b [buffer] adding True for fixing test 2024-10-02 15:55:00 +02:00
wendtalexander
0b067df69c [project] adding pytest 2024-10-02 15:52:41 +02:00
wendtalexander
e3ed2fcc75 [test] adding testcases for get method of buffer 2024-10-02 15:52:29 +02:00
wendtalexander
ff84d63fe1 [buffer] adding edge cases 2024-10-02 15:51:57 +02:00
19c6b90d5c [test] add buffer test stub 2024-10-02 10:28:50 +02:00
a7b62c5b3a [ringbuffer] first implementation 2024-10-02 10:28:36 +02:00
wendtalexander
54f0d61fc9 [dataio] commenting out databuffer 2024-10-02 08:10:08 +02:00
wendtalexander
815838eab7 [dataio] adding class for buffer 2024-10-02 08:09:45 +02:00
wendtalexander
314e609472 [refactoring] removing imports 2024-10-01 18:16:33 +02:00
wendtalexander
2719d49eb0 [refacorting] adding DataBuffer 2024-10-01 12:04:47 +02:00
wendtalexander
a0c524326d [refactroring] renaming class 2024-10-01 12:04:06 +02:00
wendtalexander
c2285f3750 [project] adding buffer class 2024-10-01 12:03:41 +02:00
wendtalexander
9327d1bac9 [refactoring] renaming file 2024-10-01 12:03:25 +02:00
11 changed files with 880 additions and 45 deletions

96
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]] [[package]]
name = "asttokens" name = "asttokens"
@@ -153,6 +153,20 @@ files = [
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
] ]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]] [[package]]
name = "executing" name = "executing"
version = "2.1.0" version = "2.1.0"
@@ -270,6 +284,17 @@ files = [
[package.dependencies] [package.dependencies]
numpy = ">=1.19.3" numpy = ">=1.19.3"
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]] [[package]]
name = "ipython" name = "ipython"
version = "8.27.0" version = "8.27.0"
@@ -284,6 +309,7 @@ files = [
[package.dependencies] [package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""} colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*" decorator = "*"
exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
jedi = ">=0.16" jedi = ">=0.16"
matplotlib-inline = "*" matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""}
@@ -291,6 +317,7 @@ prompt-toolkit = ">=3.0.41,<3.1.0"
pygments = ">=2.4.0" pygments = ">=2.4.0"
stack-data = "*" stack-data = "*"
traitlets = ">=5.13.0" traitlets = ">=5.13.0"
typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""}
[package.extras] [package.extras]
all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"]
@@ -661,6 +688,21 @@ files = [
[package.dependencies] [package.dependencies]
ptyprocess = ">=0.5" ptyprocess = ">=0.5"
[[package]]
name = "pglive"
version = "0.7.6"
description = "Live plot for PyqtGraph"
optional = false
python-versions = "<3.13,>=3.9"
files = [
{file = "pglive-0.7.6-py3-none-any.whl", hash = "sha256:6203e377954725d6602ba5a08055f92bef1688d2236c76cf4e1b35c9bb896ff4"},
{file = "pglive-0.7.6.tar.gz", hash = "sha256:5e3a91a0bb800a8c9c8513a595d742eb0aba5b8b783a6f1bbb51ab7b0d2528ec"},
]
[package.dependencies]
numpy = ">=1.26.0,<2.0.0"
pyqtgraph = ">=0.13.3,<0.14.0"
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "10.4.0" version = "10.4.0"
@@ -758,6 +800,21 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
typing = ["typing-extensions"] typing = ["typing-extensions"]
xmp = ["defusedxml"] xmp = ["defusedxml"]
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "prompt-toolkit" name = "prompt-toolkit"
version = "3.0.48" version = "3.0.48"
@@ -903,6 +960,28 @@ files = [
[package.dependencies] [package.dependencies]
numpy = ">=1.22.0" numpy = ">=1.22.0"
[[package]]
name = "pytest"
version = "8.3.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@@ -1026,6 +1105,17 @@ pure-eval = "*"
[package.extras] [package.extras]
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]] [[package]]
name = "tomlkit" name = "tomlkit"
version = "0.13.2" version = "0.13.2"
@@ -1104,5 +1194,5 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = ">=3.10, <3.13"
content-hash = "b1076b7f750e8f7e66542918ec746e74544fbfdb376875158f083fb573d18107" content-hash = "e5e345e3e4d28ac0e7de7727be130454201be1949be08158e744cdc6089a3f66"

View File

@@ -21,7 +21,7 @@ include = [
] ]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = ">=3.10, <3.13"
uldaq = "^1.2.3" uldaq = "^1.2.3"
typer = "^0.12.5" typer = "^0.12.5"
matplotlib = "^3.9.2" matplotlib = "^3.9.2"
@@ -31,6 +31,8 @@ tomlkit = "^0.13.2"
scipy = "^1.14.1" scipy = "^1.14.1"
nixio = "^1.5.3" nixio = "^1.5.3"
pyqtgraph = "^0.13.7" pyqtgraph = "^0.13.7"
pytest = "^8.3.3"
pglive = "^0.7.6"
[tool.poetry.scripts] [tool.poetry.scripts]
pyrelacs = "pyrelacs.app:main" pyrelacs = "pyrelacs.app:main"

81
pyrelacs/dataio/buffer.py Normal file
View File

@@ -0,0 +1,81 @@
import time
import faulthandler
from collections import deque
from pyqtgraph import transformToArray
import uldaq
import numpy as np
from IPython import embed
import matplotlib.pyplot as plt
from pyrelacs.util.logging import config_logging
log = config_logging()
faulthandler.enable()
class DataBuffer:
def __init__(self, channels, samples):
self.channels = channels
self.samples = samples
def read_analog_continously(
self,
device: uldaq.DaqDevice,
samplerate: float = 40_000.0,
):
data_array = []
max_len_buffer = self.channels * self.samples
self.buffer = deque(maxlen=max_len_buffer)
samples_per_channel = 40_000
self.device = device
self.ai_device = self.device.get_ai_device()
data_analog_input = uldaq.create_float_buffer(
self.channels, samples_per_channel
)
er = self.ai_device.a_in_scan(
0,
1,
uldaq.AiInputMode.SINGLE_ENDED,
uldaq.Range.BIP10VOLTS,
samples_per_channel,
samplerate,
uldaq.ScanOption.CONTINUOUS,
uldaq.AInScanFlag.DEFAULT,
data=data_analog_input,
)
daq_status = uldaq.ScanStatus.IDLE
while daq_status == uldaq.ScanStatus.IDLE:
daq_status = self.ai_device.get_scan_status()[0]
prev_count = 0
prev_index = 0
while daq_status != uldaq.ScanStatus.IDLE:
daq_status, transfer_status = self.ai_device.get_scan_status()
# The index into the data buffer immediately following the last sample transferred.
curren_index = transfer_status.current_index
# total samples since start of the scan
total_samples = transfer_status.current_total_count
# The number of samples per channel transferred since the scan started
channel_samples = transfer_status.current_scan_count
self.ai_device.scan_stop()
if __name__ == "__main__":
devices = uldaq.get_daq_device_inventory(uldaq.InterfaceType.USB)
log.debug(f"Found daq devices {len(devices)}, connecting to the first one")
try:
daq_device = uldaq.DaqDevice(devices[0])
except uldaq.ul_exception.ULException as e:
log.error("Did not found daq devices, please connect one")
raise e
daq_device.connect()
buf = DataBuffer(channels=2, samples=100_000)
buf.read_analog_continously(daq_device)

View File

@@ -0,0 +1,192 @@
from typing import Tuple
import numpy as np
from IPython import embed
from pyqtgraph.Qt.QtCore import QMutex
class CircBuffer:
def __init__(
self,
size: int,
mutex: QMutex,
channels: int = 1,
samplerate: int = 40_000,
):
self.__size = size
self.__channels = channels
self.__samplereate = samplerate
self.__buffer = np.zeros(
(channels, size), dtype=np.double
) # or dtype of your choice
self.__time = np.zeros((channels, size), dtype=np.double)
self.__index = [0 for i in range(channels)]
self.__is_full = [False for i in range(channels)]
self.__totalcount = [0 for i in range(channels)]
self.__overflows = [0 for i in range(channels)]
self.mutex = mutex
@property
def size(self):
return self.__size
@property
def samplerate(self):
return self.__samplereate
@property
def channel_count(self):
return self.__channels
def totalcount(self, channel: int = 0):
return self.__totalcount[channel]
def is_full(self, channel: int = 0):
return self.__is_full[channel]
def write_index(self, channel: int = 0):
return self.__index[channel]
def append(self, item, channel: int = 0):
self.mutex.lock()
self.__buffer[channel, self.write_index(channel)] = item
self.__index[channel] = (self.write_index(channel) + 1) % self.__size
self.__totalcount[channel] += 1
self.__time[channel, self.write_index(channel)] = (
self.__time[channel, self.write_index(channel) - 1] + 1 / self.__samplereate
)
if self.__index[channel] == 0:
self.__is_full[channel] = True
self.__overflows[channel] += 1
self.mutex.unlock()
def get_all(self, channel: int = 0):
"""
Return all valid values from the specified channel
"""
if self.__is_full[channel]:
return np.concatenate(
(
self.__buffer[channel, self.__index[channel] :],
self.__buffer[channel, : self.__index[channel]],
)
)
else:
return self.__buffer[channel, : self.__index[channel]]
def has_value(self, index, channel):
if index <= 0 and self.is_full(channel):
return True
elif index < 0 and not self.is_full(channel):
return False
if index >= self.size:
return False
# test if the ring buffer is at the start but
# and the index is greater than the write index
if index > self.write_index(channel) and self.is_full(channel):
return True
elif index >= self.write_index(channel) and not self.is_full(channel):
raise IndexError("Index has no value, not written")
if index == self.write_index(channel) and self.__totalcount[channel] == 0:
return False
return True
def valid_range(self, channel: int = 0) -> Tuple[int, int]:
"""
Return the start index and the extend that are valid within the buffer
Parameters
----------
channel : int
channel of the buffer
Returns
-------
Tuple[int, int]
start, extend of the valid range
"""
start = 0
extend = 0
if self.__totalcount[channel] == 0:
return start, extend
if not self.is_full(channel):
extend = self.__totalcount[channel]
else:
extend = self.size
return start, extend
def get(self, index: int = -1, channel: int = 0) -> Tuple[np.double, float]:
# easy case first, we can spare the effort of further checking
if index >= 0 and index <= self.write_index(channel):
if self.has_value(index, channel):
return (self.__buffer[channel, index], self.__time[index])
else:
raise IndexError(
f"Invalid index {index} on ring buffer for channel{channel}"
)
if index < 0:
index = self.write_index() - 1
if self.has_value(index, channel):
return (self.__buffer[channel, index], self.__time[index])
else:
raise IndexError(
f"Invalid index {index} on ring buffer for channel{channel}"
)
def read(self, start, extend=1, channel=0):
"""Reads a numpy array from buffer"""
if extend < 0:
raise IndexError(f"Invalid extend ({extend}) for channel {channel}")
if not self.is_full(channel):
if start < 0:
raise IndexError(f"Invalid start ({start}) for channel {channel}")
else:
if start < 0:
start = start + self.size
if extend == 1:
return np.array(self.get(start, channel))
vs, vc = self.valid_range(channel)
if start > self.__totalcount[channel]:
raise IndexError(
f"Invalid start index {start} is invalid with totalcount {self.__totalcount[channel]} for channel{channel}"
)
if start > self.size:
raise IndexError(
f"Invalid start index {start} for buffer with size {self.size}"
)
if extend > vc:
extend = vc
if (start + extend) > self.__totalcount[channel]:
raise IndexError(
f" Invalid range, extended over the totalcount of the buffer {self.__totalcount[channel]}"
)
if (start + extend) < self.size:
return (
self.__time[channel, start : start + extend],
self.__buffer[channel, start : start + extend],
)
else:
return (
np.concatenate(
(
self.__time[channel, start:],
self.__time[channel, : extend - self.size + start],
)
),
np.concatenate(
(
self.__buffer[channel, start:],
self.__buffer[channel, : extend - self.size + start],
)
),
)

View File

@@ -0,0 +1,175 @@
import time
import faulthandler
import uldaq
import numpy as np
from IPython import embed
import matplotlib.pyplot as plt
from pyrelacs.dataio.circbuffer import CircBuffer
from pyrelacs.util.logging import config_logging
log = config_logging()
faulthandler.enable()
class DaqProducer:
def __init__(
self, buffer: CircBuffer, device: uldaq.DaqDevice, channels: list[int]
):
self.buffer = buffer
self.device = device
self.ai_device = self.device.get_ai_device()
self.channels = channels
self.stop = False
def read_analog_continously(
self,
*args,
**kwargs,
):
log.debug("starting acquisition")
if self.channels[0] == self.channels[1]:
channel_range = np.arange(1)
else:
channel_range = np.arange(self.channels[0], self.channels[1] + 1)
assert channel_range.size == self.buffer.channel_count, ValueError(
f"Missmatch in channel count,\n daq_channel: "
f"{channel_range.size}\n buffer_channel: {self.buffer.channel_count}"
)
# let the buffer for the daq device hold 5 seconds of data
daq_buffer_size = self.buffer.samplerate * 5
data_in = uldaq.create_float_buffer(channel_range.size, daq_buffer_size)
log.debug(f"Buffersize for daq {len(data_in)}")
log.debug(f"Buffersize {self.buffer.size}")
er = self.ai_device.a_in_scan(
self.channels[0],
self.channels[1],
uldaq.AiInputMode.SINGLE_ENDED,
uldaq.Range.BIP10VOLTS,
daq_buffer_size,
self.buffer.samplerate,
uldaq.ScanOption.CONTINUOUS,
uldaq.AInScanFlag.DEFAULT,
data=data_in,
)
chunk_size = int(daq_buffer_size / 10)
wrote_chunk = False
start_time = time.time()
daq_status = uldaq.ScanStatus.IDLE
while daq_status == uldaq.ScanStatus.IDLE:
daq_status = self.ai_device.get_scan_status()[0]
while daq_status != uldaq.ScanStatus.IDLE:
prev_count = 0
prev_index = 0
while not self.stop:
daq_status, transfer_status = self.ai_device.get_scan_status()
# The index into the data buffer immediately following the last sample transferred.
current_index = transfer_status.current_index
# total samples since start of the scan
total_samples = transfer_status.current_total_count
# The number of samples per channel transferred since the scan started
channel_samples = transfer_status.current_scan_count
new_data_count = total_samples - prev_count
# check if counts if new data is bigger than the buffer
# if that happends stop the acquisition
if new_data_count > len(data_in):
self.ai_device.scan_stop()
log.error("A Buffer overrun occurred")
break
if new_data_count > chunk_size:
wrote_chunk = True
# index wraps around the buffer
if prev_index + chunk_size > len(data_in) - 1:
log.debug("Chunk wraps around buffersize")
first_chunk = len(data_in) - prev_index
[
self.buffer.append(data_in[prev_index + i])
for i in range(first_chunk)
]
second_chunk = chunk_size - first_chunk
[
self.buffer.append(data_in[i])
for i in range(second_chunk)
]
else:
log.debug("Writing chunk to buffer")
[
self.buffer.append(data_in[prev_index + i])
for i in range(chunk_size)
]
self.buffer.append(data_in[current_index])
if total_samples - prev_count > len(data_in):
self.ai_device.scan_stop()
log.error("A Buffer overrun occurred")
break
else:
wrote_chunk = False
if wrote_chunk:
prev_count += chunk_size
prev_index += chunk_size
prev_index %= daq_buffer_size
self.ai_device.scan_stop()
daq_status, transfer_status = self.ai_device.get_scan_status()
current_index = transfer_status.current_index
log.debug(daq_status)
log.debug(transfer_status.current_index)
log.debug(transfer_status.current_total_count)
log.debug(transfer_status.current_scan_count)
log.debug(self.buffer.totalcount())
log.debug("Appending last chunk")
if prev_index + chunk_size > len(data_in) - 1:
log.debug("Chunk wraps around buffersize")
first_chunk = len(data_in) - prev_index
[
self.buffer.append(data_in[prev_index + i])
for i in range(first_chunk)
]
second_chunk = chunk_size - first_chunk
[self.buffer.append(data_in[i]) for i in range(second_chunk)]
else:
log.debug("Writing chunk to buffer")
[
self.buffer.append(data_in[prev_index + i])
for i in range(chunk_size)
]
self.buffer.append(data_in[current_index])
log.info("stopping")
break
break
return "Done. "
def stop_aquisition(self):
self.stop = True
# if __name__ == "__main__":
# devices = uldaq.get_daq_device_inventory(uldaq.InterfaceType.USB)
# log.debug(f"Found daq devices {len(devices)}, connecting to the first one")
# try:
# daq_device = uldaq.DaqDevice(devices[0])
# except uldaq.ul_exception.ULException as e:
# log.error("Did not found daq devices, please connect one")
# raise e
# daq_device.connect()
#
# buf = CircBuffer(size=1_000_000, samplerate=100)
# producer = DaqProducer(buf, daq_device, [1, 1])
# producer.read_analog_continously()

View File

@@ -0,0 +1,58 @@
from math import sin
import time
from PyQt6.QtGui import QAction
import numpy as np
import matplotlib.pyplot as plt
from IPython import embed
from pyrelacs.dataio.circbuffer import CircBuffer
from pyrelacs.util.logging import config_logging
log = config_logging()
# stopbutton: QAction
class SinProducer:
def __init__(
self,
buffer: CircBuffer,
) -> None:
self.buffer = buffer
self.stop = False
def produce_sin(
self,
*args,
**kwargs,
) -> None:
AMPLITUDE = 2
FREQUENCY = 10
self.stop = False
log.debug("producing Sin")
start_time = time.time()
t = 0
while not self.stop:
s = AMPLITUDE * np.sin(2 * np.pi * FREQUENCY * t)
self.buffer.append(s)
t += 1 / self.buffer.samplerate
time.sleep(1 / self.buffer.samplerate)
end_time = time.time()
log.debug(f"duration sinus {end_time-start_time}")
log.debug(f"Stimulation time {t}")
# data = self.buffer.get_all()
# log.debug(data.shape[0])
# log.debug(data.shape[0] / self.buffer.samplerate)
# plt.plot(np.arange(data.size) / self.buffer.samplerate, data)
# plt.show()
def stop_request(self):
self.stop = True
if __name__ == "__main__":
buf = CircBuffer(1_000_000, 1, samplerate=10_000)
pro_sin = SinProducer(buf)
pro_sin.produce_sin()

View File

@@ -11,7 +11,7 @@ from pyrelacs.util.logging import config_logging
log = config_logging() log = config_logging()
class MccDac: class MccDaq:
""" """
Represents the Digital/Analog Converter from Meassuring Computing. Represents the Digital/Analog Converter from Meassuring Computing.
provides methods for writing and reading the Analog / Digital input and output. provides methods for writing and reading the Analog / Digital input and output.
@@ -43,6 +43,7 @@ class MccDac:
except uldaq.ul_exception.ULException: except uldaq.ul_exception.ULException:
self.disconnect_dac() self.disconnect_dac()
self.connect_dac() self.connect_dac()
self.ai_device = self.daq_device.get_ai_device() self.ai_device = self.daq_device.get_ai_device()
self.ao_device = self.daq_device.get_ao_device() self.ao_device = self.daq_device.get_ao_device()
self.dio_device = self.daq_device.get_dio_device() self.dio_device = self.daq_device.get_dio_device()

View File

@@ -6,10 +6,8 @@ import uldaq
from IPython import embed from IPython import embed
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from scipy.signal import welch
from scipy.signal import find_peaks
from pyrelacs.devices.mccdac import MccDac from pyrelacs.devices.mccdaq import MccDaq
from pyrelacs.util.logging import config_logging from pyrelacs.util.logging import config_logging
log = config_logging() log = config_logging()
@@ -17,7 +15,7 @@ log = config_logging()
faulthandler.enable() faulthandler.enable()
class Calibration(MccDac): class Calibration(MccDaq):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.SAMPLERATE = 40_000.0 self.SAMPLERATE = 40_000.0

View File

@@ -1,8 +1,10 @@
import time
from pathlib import Path as path
from PyQt6.QtGui import QAction, QIcon, QKeySequence from PyQt6.QtGui import QAction, QIcon, QKeySequence
from PyQt6.QtCore import Qt, QSize, QThreadPool from PyQt6.QtCore import Qt, QSize, QThreadPool, QMutex, QTimer
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QGridLayout, QGridLayout,
QPushButton,
QToolBar, QToolBar,
QWidget, QWidget,
QMainWindow, QMainWindow,
@@ -10,19 +12,25 @@ from PyQt6.QtWidgets import (
QMenuBar, QMenuBar,
QStatusBar, QStatusBar,
) )
from pyqtgraph.Qt.QtCore import QThread
import uldaq import uldaq
import numpy as np
import nixio as nix import nixio as nix
import pyqtgraph as pg import pyqtgraph as pg
import numpy as np
from pathlib import Path as path
from scipy.signal import welch, find_peaks
from pyrelacs.dataio.daq_producer import DaqProducer
from pyrelacs.dataio.sin_producer import SinProducer
from pyrelacs.worker import Worker from pyrelacs.worker import Worker
from pyrelacs.repros.repros import Repro from pyrelacs.repros.repros import Repro
from pyrelacs.util.logging import config_logging from pyrelacs.dataio.circbuffer import CircBuffer
from pyrelacs.ui.about import AboutDialog from pyrelacs.ui.about import AboutDialog
from pyrelacs.ui.plots.calibration import CalibrationPlot from pyrelacs.ui.plots.calibration import CalibrationPlot
from pyrelacs.ui.plots.continously import Continously
from pyrelacs.util.logging import config_logging
log = config_logging() log = config_logging()
_root = path(__file__).parent.parent _root = path(__file__).parent.parent
@@ -37,14 +45,17 @@ class PyRelacs(QMainWindow):
Qt.ToolButtonStyle.ToolButtonTextBesideIcon Qt.ToolButtonStyle.ToolButtonTextBesideIcon
) # Ensure icons are displayed with text ) # Ensure icons are displayed with text
self.setWindowTitle("PyRelacs") self.setWindowTitle("PyRelacs")
self.mutex = QMutex()
self.timer = QTimer(self)
self.timer.setInterval(200)
self.figure = pg.GraphicsLayoutWidget() self.figure = pg.GraphicsLayoutWidget()
filename = path.joinpath(path.cwd(), "data.nix") filename = path.joinpath(path.cwd(), "data.nix")
if filename.exists(): if filename.exists():
self.nix_file = nix.File.open(str(filename), nix.FileMode.ReadOnly) self.nix_file = nix.File.open(str(filename), nix.FileMode.ReadOnly)
else: filename = path.joinpath(path.cwd(), "calibration.nix")
self.nix_file = nix.File.open(str(filename), nix.FileMode.Overwrite) self.nix_file = nix.File.open(str(filename), nix.FileMode.Overwrite)
self.calibration_plot = CalibrationPlot(self.figure, self.nix_file) self.calibration_plot = CalibrationPlot(self.figure, self.nix_file)
@@ -57,7 +68,6 @@ class PyRelacs(QMainWindow):
self.setMenuBar(QMenuBar(self)) self.setMenuBar(QMenuBar(self))
self.setStatusBar(QStatusBar(self)) self.setStatusBar(QStatusBar(self))
self.create_actions() self.create_actions()
self.create_buttons()
self.create_toolbars() self.create_toolbars()
layout = QGridLayout() layout = QGridLayout()
@@ -68,6 +78,30 @@ class PyRelacs(QMainWindow):
widget.setLayout(layout) widget.setLayout(layout)
self.setCentralWidget(widget) self.setCentralWidget(widget)
SAMPLERATE = 40_000
start = time.time()
BUFFERSIZE = SAMPLERATE * 10 * 60
end = time.time()
log.debug(f"Buffer allocation took {end - start}")
self.buffer = CircBuffer(
size=BUFFERSIZE, samplerate=SAMPLERATE, mutex=self.mutex
)
self.continously_plot = Continously(self.figure, self.buffer)
self.continously_plot.plot()
start = time.time()
self.connect_dac()
end = time.time()
log.debug(f"Connection to DAQ took {end - start}")
if hasattr(uldaq, "daq_device"):
log.debug("Creating Daq Generator")
self.daq_producer = DaqProducer(self.buffer, self.daq_device, [1, 1])
else:
log.debug("Creating Sinus Generator")
self.sin_producer = SinProducer(self.buffer)
def create_actions(self): def create_actions(self):
self._rlx_exitaction = QAction(QIcon(":/icons/exit.png"), "Exit", self) self._rlx_exitaction = QAction(QIcon(":/icons/exit.png"), "Exit", self)
self._rlx_exitaction.setStatusTip("Close relacs") self._rlx_exitaction.setStatusTip("Close relacs")
@@ -99,6 +133,20 @@ class PyRelacs(QMainWindow):
self._daq_calibaction.setStatusTip("Calibrate the attenuator device") self._daq_calibaction.setStatusTip("Calibrate the attenuator device")
# self._daq_calibaction.setShortcut(QKeySequence("Alt+d")) # self._daq_calibaction.setShortcut(QKeySequence("Alt+d"))
self._daq_calibaction.triggered.connect(self.calibration_plot.plot) self._daq_calibaction.triggered.connect(self.calibration_plot.plot)
self._run_action = QAction(QIcon(":/icons/record.png"), "Run", self)
self._run_action.triggered.connect(self.run_daq)
self._run_sinus_action = QAction(QIcon(":/icons/record.png"), "Sinus", self)
self._run_sinus_action.triggered.connect(self.run_sinus)
self._stop_recording = QAction(QIcon(":/icons/stop.png"), "Stop", self)
self._stop_recording.triggered.connect(self.stop_recording)
self._refresh = QAction("Recenter", self)
self._refresh.triggered.connect(self.recenter_continously_plot)
self._refresh.setShortcut(QKeySequence("Alt+r"))
self.create_menu() self.create_menu()
def create_menu(self): def create_menu(self):
@@ -117,6 +165,7 @@ class PyRelacs(QMainWindow):
device_menu.addAction(self._daq_disconnectaction) device_menu.addAction(self._daq_disconnectaction)
device_menu.addSeparator() device_menu.addSeparator()
device_menu.addAction(self._daq_calibaction) device_menu.addAction(self._daq_calibaction)
device_menu.addAction(self._run_action)
if help_menu is not None: if help_menu is not None:
help_menu.addSeparator() help_menu.addSeparator()
@@ -137,6 +186,10 @@ class PyRelacs(QMainWindow):
daq_toolbar.addAction(self._daq_connectaction) daq_toolbar.addAction(self._daq_connectaction)
daq_toolbar.addAction(self._daq_disconnectaction) daq_toolbar.addAction(self._daq_disconnectaction)
daq_toolbar.addAction(self._daq_calibaction) daq_toolbar.addAction(self._daq_calibaction)
daq_toolbar.addAction(self._run_action)
daq_toolbar.addAction(self._run_sinus_action)
daq_toolbar.addAction(self._stop_recording)
# daq_toolbar.addAction(self._refresh)
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, daq_toolbar) self.addToolBar(Qt.ToolBarArea.TopToolBarArea, daq_toolbar)
repro_toolbar = QToolBar("Repros") repro_toolbar = QToolBar("Repros")
@@ -148,55 +201,65 @@ class PyRelacs(QMainWindow):
lambda checked, n=rep, f=fn: self.run_repro(n, f) lambda checked, n=rep, f=fn: self.run_repro(n, f)
) )
repro_toolbar.addAction(repro_action) repro_toolbar.addAction(repro_action)
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, repro_toolbar) self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, repro_toolbar)
def create_buttons(self): def recenter_continously_plot(self):
self.daq_connect_button = QPushButton("Connect Daq") self.continously_plot.refresh()
self.daq_connect_button.setCheckable(True)
self.daq_connect_button.clicked.connect(self.connect_dac)
self.daq_disconnect_button = QPushButton("Disconnect Daq") def plot_continously(self):
self.daq_disconnect_button.setCheckable(True) plot_con = Worker(self.continously_plot.plot)
self.daq_disconnect_button.clicked.connect(self.disconnect_dac) plot_con.signals.result.connect(self.print_output)
plot_con.signals.finished.connect(self.thread_complete)
plot_con.signals.progress.connect(self.progress_fn)
self.threadpool.start(plot_con)
self.plot_calibration_button = QPushButton("Plot Calibration") def run_daq(self):
self.plot_calibration_button.setCheckable(True) read_daq = Worker(self.daq_producer.read_analog_continously)
self.plot_calibration_button.clicked.connect(self.calibration_plot.plot) read_daq.signals.result.connect(self.print_output)
read_daq.signals.finished.connect(self.thread_complete)
read_daq.signals.progress.connect(self.progress_fn)
self.threadpool.start(read_daq)
self.continously_plot.plot()
def run_sinus(self):
sinus_pro = Worker(self.sin_producer.produce_sin)
sinus_pro.signals.result.connect(self.print_output)
sinus_pro.signals.finished.connect(self.thread_complete)
sinus_pro.signals.progress.connect(self.progress_fn)
self.threadpool.start(sinus_pro)
self.continously_plot.plot()
def stop_recording(self):
self.add_to_textfield("Stopping the recording")
if hasattr(PyRelacs, "sin_producer"):
self.sin_producer.stop_request()
self.continously_plot.stop_plotting()
if hasattr(uldaq, "daq_device"):
self.daq_producer.stop_aquisition()
def connect_dac(self): def connect_dac(self):
devices = uldaq.get_daq_device_inventory(uldaq.InterfaceType.USB) devices = uldaq.get_daq_device_inventory(uldaq.InterfaceType.USB)
try: try:
self.daq_device = uldaq.DaqDevice(devices[0]) self.daq_device = uldaq.DaqDevice(devices[0])
log.debug(f"Found daq devices {len(devices)}, connecting to the first one") log.debug(f"Found daq devices {len(devices)}, connecting to the first one")
self.daq_device.connect()
log.debug("connected")
except IndexError: except IndexError:
log.error("DAQ is not connected") log.error("DAQ is not connected")
log.error("Please connect a DAQ device to the system") log.error("Please connect a DAQ device to the system")
if hasattr(PyRelacs, "daq_device"):
try:
self.daq_device.connect()
log.debug("Connected")
except uldaq.ul_exception.ULException as e:
log.error(f"Could not Connect to DAQ: {e}")
self.daq_connect_button.setDisabled(True)
else:
log.debug("Already handeld the error")
pass
def disconnect_dac(self): def disconnect_dac(self):
try: try:
log.debug(f"{self.daq_device}")
self.daq_device.disconnect() self.daq_device.disconnect()
self.daq_device.release() self.daq_device.release()
log.debug(f"{self.daq_device}")
self.daq_disconnect_button.setDisabled(True)
self.daq_connect_button.setEnabled(True)
except AttributeError: except AttributeError:
log.debug("DAQ was not connected") log.debug("DAQ was not connected")
def run_repro(self, n, fn): def run_repro(self, n, fn):
self.text.appendPlainText(f"started Repro {n}, {fn}") self.text.appendPlainText(f"started Repro {n}, {fn}")
worker = Worker(self.repros.run_repro, self.nix_file, n, fn) worker = Worker(self.repros.run_repro, self.nix_calibration, n, fn)
worker.signals.result.connect(self.print_output) worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete) worker.signals.finished.connect(self.thread_complete)
worker.signals.progress.connect(self.progress_fn) worker.signals.progress.connect(self.progress_fn)
@@ -208,7 +271,9 @@ class PyRelacs(QMainWindow):
def on_exit(self): def on_exit(self):
log.info("exit button!") log.info("exit button!")
self.stop_recording()
self.add_to_textfield("exiting") self.add_to_textfield("exiting")
self.disconnect_dac()
self.close() self.close()
def on_about(self, e): def on_about(self, e):

View File

@@ -0,0 +1,75 @@
import pyqtgraph as pg
from IPython import embed
import numpy as np
from pyqtgraph.Qt.QtCore import QTimer
from pyrelacs.dataio.circbuffer import CircBuffer
from pyrelacs.util.logging import config_logging
log = config_logging()
class Continously:
def __init__(self, figure: pg.GraphicsLayoutWidget, buffer: CircBuffer):
self.figure = figure
self.buffer = buffer
def plot(self, *args, **kwargs):
self.figure.setBackground("w")
prev_plot = self.figure.getItem(row=0, col=0)
if prev_plot:
self.figure.removeItem(prev_plot)
self.continous_ax = self.figure.addPlot(row=0, col=0)
pen = pg.mkPen("red")
self.time = np.zeros(self.buffer.size)
self.data = np.zeros(self.buffer.size)
self.line = self.continous_ax.plot(
self.time,
self.data,
pen=pen,
# symbol="o",
)
# self.plot_index = 0
self.timer = QTimer()
self.CHUNK_PLOT = 10_000
self.timer.setInterval(200)
self.timer.timeout.connect(self.update_plot)
self.timer.start()
def update_plot(self):
# log.debug(self.buffer.totalcount())
if self.buffer.totalcount() > self.CHUNK_PLOT:
log.debug(self.buffer.totalcount())
try:
times, items = self.buffer.read(
self.buffer.write_index() - self.CHUNK_PLOT - 1_000,
extend=self.CHUNK_PLOT,
)
except IndexError as e:
items = np.zeros(self.CHUNK_PLOT)
times = np.zeros(self.CHUNK_PLOT)
log.debug("No Data Available")
log.debug(f"Index Error {e}")
# self.time = np.roll(self.time, -len(items))
# self.data = np.roll(self.data, -len(items))
#
# self.time[-len(times) :] = times
# self.data[-len(items) :] = items
self.line.setData(
times,
items,
)
# self.plot_index += len(items)
else:
pass
def stop_plotting(self):
self.timer.stop()
def refresh(self):
self.continous_ax.enableAutoRange()

98
test/test_buffer.py Normal file
View File

@@ -0,0 +1,98 @@
import pytest
import numpy as np
from IPython import embed
from pyrelacs.dataio.circbuffer import CircBuffer
def test_init():
buff = CircBuffer(1000, 2)
assert buff.size == 1000
assert buff.channel_count == 2
def test_hasvalue():
buff = CircBuffer(1000, 2)
assert buff.has_value(0, 0) == False
assert buff.has_value(-1, 0) == False
buff.append(10, 0)
assert buff.write_index(0) == 1
assert buff.write_index(1) == 0
assert buff.has_value(0, 0) == True
assert buff.has_value(0, 1) == False
buff.append(10, 1)
assert buff.write_index(1) == 1
assert buff.has_value(0, 1) == True
for i in range(1100):
buff.append(i, 0)
buff.append(i, 1)
assert buff.write_index(0) == buff.write_index(1)
assert buff.has_value(0, 0) == True
assert buff.has_value(0, 1) == True
assert buff.has_value(buff.write_index(0), 0) == True
assert buff.has_value(buff.write_index(1), 1) == True
def test_validrange():
buff = CircBuffer(1000, 2)
# without any values the range is (0, 0)
assert buff.valid_range() == (0, 0)
buff.append(0, 0)
assert buff.valid_range() == (0, 1)
for i in range(100):
buff.append(i, 0)
assert buff.valid_range() == (0, 101)
for i in range(1000):
buff.append(i, 0)
assert buff.valid_range() == (0, 1000)
def test_get():
buff = CircBuffer(1000, 2)
# with no items written to the buffer
with pytest.raises(IndexError):
item = buff.get(index=-1)
buff.append(10, 0)
item = buff.get(index=-1)
assert item == 10
# Check if index is not written jet
with pytest.raises(IndexError):
item = buff.get(index=10)
for i in range(1000):
buff.append(i, 0)
item = buff.get(index=-1)
# the first item should be 999.0 because of we append a value in the earlier test
assert item == 999.0
with pytest.raises(IndexError):
item = buff.get(10001)
def test_read():
pass
def test_write():
buff = CircBuffer(1000, 2)
samplecount = 1000
if __name__ == "__main__":
test_get()