Skip to content

Commit 048109d

Browse files
Updated Channel Dictionary to Channel Class (#148)
* Added channel dataclass to store channel data. * Added noqa to legacy files. * Applied channel class to psospa and ps6000a * Removed set_ylim from protocol class * Removed set_ylim from protocol class * Removed set_ylim from protocol class * Added channel dataclass to store channel data. * Added noqa to legacy files. * Applied channel class to psospa and ps6000a * Fixed merged duplications * Fixed docstring return space * Removed scaled_range in ChannelClass dataclass * Removed duplicated lines * Updated conversion_test.py to new channel class - Updated tests to use channel classes. - Updated mv_to_adc to just include channel * Updated conversions.md to reflect mv_to_adc() changes. * Added function to remember last used voltage for get_ylim.
1 parent 54d8144 commit 048109d

File tree

9 files changed

+160
-75
lines changed

9 files changed

+160
-75
lines changed

docs/docs/ref/ps6000a/conversions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ the PicoScope needs to be initialized using `scope.open_unit()` followed by the
1212
>>> import pypicosdk as psdk
1313
>>> scope = psdk.ps6000a()
1414
>>> scope.open_unit(resolution=psdk.RESOLUTION._8BIT)
15-
>>> scope.mv_to_adc(100, channel_range=psdk.RANGE.V1)
15+
>>> scope.mv_to_adc(100, channel=psdk.CHANNEL.A)
1616
3251
1717
>>> scope.close_unit()
1818
```

docs/docs/ref/psospa/conversions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ the PicoScope needs to be initialized using `scope.open_unit()` followed by the
1212
>>> import pypicosdk as psdk
1313
>>> scope = psdk.psospa()
1414
>>> scope.open_unit(resolution=psdk.RESOLUTION._8BIT)
15-
>>> scope.mv_to_adc(100, channel_range=psdk.RANGE.V1)
15+
>>> scope.mv_to_adc(100, channel=psdk.CHANNEL.A)
1616
3251
1717
>>> scope.close_unit()
1818
```

pypicosdk/_classes/__init__.py

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""
2+
This file contains a class to hold the channel data
3+
i.e. range information and probe scale information
4+
"""
5+
from dataclasses import dataclass
6+
import numpy as np
7+
if __name__ != '__main__':
8+
from .. import constants as cst
9+
else:
10+
from pypicosdk import constants as cst
11+
12+
13+
@dataclass
14+
class ChannelClass:
15+
"Dataclass containing channel information"
16+
range: cst.RANGE
17+
range_mv: int
18+
probe_scale: float
19+
ylim_mv: int
20+
ylim_v: float
21+
22+
def __init__(self, ch_range: cst.RANGE, probe_scale: float):
23+
self.range = ch_range
24+
self.probe_scale = probe_scale
25+
self.range_mv = cst.RANGE_LIST[ch_range]
26+
self.range_v = self.range_mv / 1000
27+
self.ylim_mv = np.array([-self.range_mv, self.range_mv]) * probe_scale
28+
self.ylim_v = self.ylim_mv / 1000
29+
30+
31+
if __name__ == '__main__':
32+
test = ChannelClass(ch_range=cst.RANGE.V1, probe_scale=10)
33+
print(test)

pypicosdk/base.py

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Copyright (C) 2025-2025 Pico Technology Ltd. See LICENSE file for terms.
33
"""
44

5+
# flake8: noqa
6+
# pylint: skip-file
57
import ctypes
68
import os
79
import platform
@@ -10,6 +12,7 @@
1012
import numpy as np
1113
import numpy.ctypeslib as npc
1214

15+
from ._classes._channel_class import ChannelClass
1316
from .error_list import ERROR_STRING
1417
from .constants import *
1518
from . import constants as cst
@@ -55,15 +58,13 @@ def __init__(self, dll_name, *args, **kwargs):
5558

5659
# Setup class variables
5760
self.handle = ctypes.c_short()
58-
self.range = {}
59-
self.probe_scale = {}
61+
self.channel_db: dict[int, ChannelClass] = {}
6062
self.resolution = None
6163
self.max_adc_value = None
6264
self.min_adc_value = None
6365
self.over_range = 0
6466
self._actual_interval = 0
65-
66-
self.ylim = (0, 0)
67+
self.last_used_volt_unit: str = 'mv'
6768

6869
def __exit__(self):
6970
self.close_unit()
@@ -312,7 +313,7 @@ def _get_enabled_channel_flags(self) -> int:
312313
int: Decimal of enabled channels
313314
"""
314315
enabled_channel_byte = 0
315-
for channel in self.range:
316+
for channel in self.channel_db:
316317
enabled_channel_byte += 2**channel
317318
return enabled_channel_byte
318319

@@ -831,22 +832,21 @@ def get_minimum_timebase_stateless(self) -> dict:
831832
}
832833

833834
# Data conversion ADC/mV & ctypes/int
834-
def mv_to_adc(self, mv: float, channel_range: int, channel: CHANNEL = None) -> int:
835+
def mv_to_adc(self, mv: float, channel: CHANNEL = None) -> int:
835836
"""
836837
Converts a millivolt (mV) value to an ADC value based on the device's
837838
maximum ADC range.
838839
839840
Args:
840841
mv (float): Voltage in millivolts to be converted.
841-
channel_range (int): Range of channel in millivolts i.e. 500 mV.
842842
channel (CHANNEL, optional): Channel associated with ``mv``. The
843843
probe scaling for the channel will be applied if provided.
844844
845845
Returns:
846846
int: ADC value corresponding to the input millivolt value.
847847
"""
848-
scale = self.probe_scale.get(channel, 1)
849-
channel_range_mv = RANGE_LIST[channel_range]
848+
scale = self.channel_db[channel].probe_scale
849+
channel_range_mv = self.channel_db[channel].range_mv
850850
return int(((mv / scale) / channel_range_mv) * self.max_adc_value)
851851

852852
def _adc_conversion(
@@ -857,8 +857,8 @@ def _adc_conversion(
857857
) -> float | np.ndarray:
858858
"""Converts ADC value or array to mV or V using the stored probe scaling."""
859859
unit_scale = _get_literal(output_unit, OutputUnitV_M)
860-
channel_range_mv = RANGE_LIST[self.range[channel]]
861-
channel_scale = self.probe_scale[channel]
860+
channel_range_mv = self.channel_db[channel].range_mv
861+
channel_scale = self.channel_db[channel].probe_scale
862862
return (((adc / self.max_adc_value) * channel_range_mv) * channel_scale) / unit_scale
863863

864864
def _adc_to_(
@@ -882,6 +882,10 @@ def _adc_to_(
882882
Returns:
883883
dict | float | np.ndarray: _description_
884884
"""
885+
886+
# Update last used
887+
self.last_used_volt_unit = unit
888+
885889
if isinstance(data, dict):
886890
for channel, adc in data.items():
887891
data[channel] = self._adc_conversion(adc, channel, output_unit=unit)
@@ -910,6 +914,7 @@ def adc_to_mv(
910914
Returns:
911915
dict, int, float, np.ndarray: Data converted into millivolts (mV)
912916
"""
917+
self.last_used_volt_unit = 'mv' # Update last used
913918
return self._adc_to_(data, channel, unit='mv')
914919

915920
def adc_to_volts(
@@ -931,6 +936,7 @@ def adc_to_volts(
931936
Returns:
932937
dict, int, float, np.ndarray: Data converted into volts (V)
933938
"""
939+
self.last_used_volt_unit = 'v' # Update last used
934940
return self._adc_to_(data, channel, unit='v')
935941

936942
def _thr_hyst_mv_to_adc(
@@ -941,11 +947,12 @@ def _thr_hyst_mv_to_adc(
941947
hysteresis_upper_mv,
942948
hysteresis_lower_mv
943949
) -> tuple[int, int, int, int]:
944-
if channel in self.range:
945-
upper_adc = self.mv_to_adc(threshold_upper_mv, self.range[channel], channel)
946-
lower_adc = self.mv_to_adc(threshold_lower_mv, self.range[channel], channel)
947-
hyst_upper_adc = self.mv_to_adc(hysteresis_upper_mv, self.range[channel], channel)
948-
hyst_lower_adc = self.mv_to_adc(hysteresis_lower_mv, self.range[channel], channel)
950+
if channel in self.channel_db:
951+
ch_range = self.channel_db[channel].range
952+
upper_adc = self.mv_to_adc(threshold_upper_mv, channel)
953+
lower_adc = self.mv_to_adc(threshold_lower_mv, channel)
954+
hyst_upper_adc = self.mv_to_adc(hysteresis_upper_mv, channel)
955+
hyst_lower_adc = self.mv_to_adc(hysteresis_lower_mv, channel)
949956
else:
950957
upper_adc = int(threshold_upper_mv)
951958
lower_adc = int(threshold_lower_mv)
@@ -968,43 +975,39 @@ def _change_power_source(self, state: POWER_SOURCE) -> 0:
968975
state
969976
)
970977

971-
def _set_ylim(self, ch_range: RANGE | range_literal) -> None:
972-
"""
973-
Update the scope self.ylim with the largest channel range
974-
975-
Args:
976-
ch_range (RANGE | range_literal): Range of current channel
977-
"""
978-
# Convert to mv
979-
ch_range = RANGE_LIST[ch_range]
980-
981-
# Compare largest value
982-
max_ylim = max(self.ylim[1], ch_range)
983-
min_ylim = -max_ylim
984-
self.ylim = (min_ylim, max_ylim)
985-
986-
def get_ylim(self, unit: OutputUnitV_L = 'mv') -> tuple[float, float]:
978+
def get_ylim(self, unit: str | None | OutputUnitV_L = None) -> tuple[float, float]:
987979
"""
988-
Returns the ylim of the widest channel range as a tuple.
980+
Returns the ylim of the widest channel range as a tuple. The unit is taken from
981+
the last used adc to voltage conversion, but can be overwritten by declaring a
982+
`unit` variable.
989983
Ideal for pyplot ylim function.
990984
991985
Args:
992-
unit (str): 'mv' or 'v'. Depending on whether your data is in mV
993-
or Volts.
986+
unit (str | None, optional): Overwrite the ylim unit using `'mv'` or `'v'`.
987+
If None, The unit will be taken from the last voltage unit conversion.
994988
995989
Returns:
996-
tuple[float, float]: Minium and maximum range values
990+
tuple[float,float]: Minium and maximum range values
997991
998992
Examples:
999993
>>> from matplotlib import pyplot as plt
1000994
>>> ...
1001995
>>> plt.ylim(scope.get_ylim())
1002996
"""
997+
if unit is None:
998+
# Collect last used voltage unit
999+
unit = self.last_used_volt_unit
1000+
1001+
# Get largest channel range
1002+
largest_range_index = max(
1003+
self.channel_db,
1004+
key=lambda ch: self.channel_db[ch].range_mv
1005+
)
10031006
unit = unit.lower()
1004-
if unit.lower() == 'mv':
1005-
return self.ylim
1006-
elif unit.lower():
1007-
return self.ylim[0] / 1000, self.ylim[1] / 1000
1007+
if unit == 'mv':
1008+
return self.channel_db[largest_range_index].ylim_mv
1009+
elif unit == 'v':
1010+
return self.channel_db[largest_range_index].ylim_v
10081011

10091012
def set_device_resolution(self, resolution: RESOLUTION) -> None:
10101013
"""Configure the ADC resolution using ``ps6000aSetDeviceResolution``.
@@ -1054,8 +1057,8 @@ def set_simple_trigger(
10541057
channel = _get_literal(channel, channel_map)
10551058
direction = _get_literal(direction, trigger_dir_m)
10561059

1057-
if channel in self.range:
1058-
threshold_adc = self.mv_to_adc(threshold_mv, self.range[channel], channel)
1060+
if channel in self.channel_db:
1061+
threshold_adc = self.mv_to_adc(threshold_mv, channel)
10591062
else:
10601063
threshold_adc = int(threshold_mv)
10611064

@@ -1454,12 +1457,12 @@ def set_data_buffer_for_enabled_channels(
14541457
channels_buffer = {}
14551458
# Rapid
14561459
if captures > 0:
1457-
for channel in self.range:
1460+
for channel in self.channel_db:
14581461
np_buffer = self.set_data_buffer_rapid_capture(channel, samples, captures, segment, datatype, ratio_mode, action=ACTION.ADD)
14591462
channels_buffer[channel] = np_buffer
14601463
# Single
14611464
else:
1462-
for channel in self.range:
1465+
for channel in self.channel_db:
14631466
channels_buffer[channel] = self.set_data_buffer(channel, samples, segment, datatype, ratio_mode, action=ACTION.ADD)
14641467

14651468
return channels_buffer
@@ -1763,6 +1766,9 @@ def run_simple_block_capture(
17631766
>>> buffers = scope.run_simple_block_capture(timebase=3, samples=1000)
17641767
"""
17651768

1769+
# Update last used
1770+
self.last_used_volt_unit = output_unit
1771+
17661772
# Create data buffers
17671773
channel_buffer = \
17681774
self.set_data_buffer_for_enabled_channels(samples, segment, datatype, ratio_mode)
@@ -1821,6 +1827,8 @@ def run_simple_rapid_block_capture(
18211827
tuple[dict,np.ndarray]: Dictionary of channel buffers and the time
18221828
axis (numpy array).
18231829
"""
1830+
# Update last used
1831+
self.last_used_volt_unit = output_unit
18241832

18251833
# Segment set to 0
18261834
segment = 0

pypicosdk/psospa.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
"""Copyright (C) 2025-2025 Pico Technology Ltd. See LICENSE file for terms."""
22

3+
# flake8: noqa
4+
# pylint: skip-file
35
import ctypes
46
from typing import override, Literal
57
import json
8+
from warnings import warn
69

10+
from ._classes._channel_class import ChannelClass
11+
from . import constants as cst
712
from .constants import *
813
from .constants import (
914
TIME_UNIT
1015
)
16+
from . import common as cmn
1117
from .common import PicoSDKException
1218
from .base import PicoScopeBase
1319
from .shared.ps6000a_psospa import shared_ps6000a_psospa
@@ -61,12 +67,13 @@ def open_unit(self, serial_number:str=None, resolution:RESOLUTION | resolution_l
6167
@override
6268
def set_channel_on(
6369
self,
64-
channel:CHANNEL,
65-
range:RANGE,
66-
coupling:COUPLING=COUPLING.DC,
67-
offset:float=0,
68-
bandwidth:BANDWIDTH_CH=BANDWIDTH_CH.FULL,
69-
range_type:PICO_PROBE_RANGE_INFO=PICO_PROBE_RANGE_INFO.X1_PROBE_NV
70+
channel: CHANNEL,
71+
range: RANGE,
72+
coupling: COUPLING = COUPLING.DC,
73+
offset: float = 0,
74+
bandwidth: BANDWIDTH_CH = BANDWIDTH_CH.FULL,
75+
range_type: PICO_PROBE_RANGE_INFO = PICO_PROBE_RANGE_INFO.X1_PROBE_NV,
76+
probe_scale: float = 1.0,
7077
) -> int:
7178
"""
7279
Enable and configure a specific channel on the device with given parameters.
@@ -84,8 +91,16 @@ def set_channel_on(
8491
Bandwidth limit setting for the channel. Defaults to full bandwidth.
8592
range_type (PICO_PROBE_RANGE_INFO, optional):
8693
Specifies the probe range type. Defaults to X1 probe (no attenuation).
94+
probe_scale (float, optional): Probe attenuation factor e.g. 10 for x10 probe.
95+
Default value of 1.0 (x1).
8796
"""
88-
self.range[channel] = range
97+
if probe_scale != 1:
98+
warn(
99+
f'Ensure selected channel range of {cst.range_literal.__args__[range]} ' +
100+
f'accounts for attenuation of x{probe_scale} at scope input',
101+
cmn.ProbeScaleWarning)
102+
103+
self.channel_db[channel] = ChannelClass(ch_range=range, probe_scale=probe_scale)
89104

90105
range_max = ctypes.c_int64(RANGE_LIST[range] * 1_000_000)
91106
range_min = ctypes.c_int64(-range_max.value)
@@ -103,6 +118,7 @@ def set_channel_on(
103118
)
104119
return status
105120

121+
106122
@override
107123
def get_nearest_sampling_interval(self, interval_s:float, round_faster:int=True) -> dict:
108124
"""
@@ -267,6 +283,7 @@ def set_led_colours(
267283
hue = [hue]
268284
saturation = [saturation]
269285

286+
270287
if isinstance(hue[0], str):
271288
hue = [led_colours_m[i] for i in hue]
272289

@@ -287,6 +304,7 @@ def set_led_colours(
287304
array_len,
288305
)
289306

307+
290308
def set_all_led_states(self,state:str|led_state_l):
291309
"""
292310
Sets the state of all LED's on the PicoScope.
@@ -303,6 +321,7 @@ def set_led_states(self, led:str|led_channel_l|list[led_channel_l], state:str|le
303321
Sets the state for a selected LED. Between default behaviour (auto),
304322
on or off.
305323
324+
306325
Args:
307326
led (str): The selected LED. Must be one of these values:
308327
`'A'`, `'B'`, `'C'`, `'D'`, `'E'`, `'F'`, `'G'`, `'H'`, `'AWG'`, `'AUX'`.

pypicosdk/shared/_protocol.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,3 @@
88

99
class _ProtocolBase(Protocol):
1010
"""Protocol placeholder class for shared methods"""
11-
def _set_ylim(self, *args, **kwargs): ...

0 commit comments

Comments
 (0)