Skip to content

Added new functionality to ds8r class #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
510 changes: 289 additions & 221 deletions ds8r/ds8r.py
Original file line number Diff line number Diff line change
@@ -1,292 +1,360 @@
"""
Define python class 'DS8R' and its method 'run()' used to control DS8R device.
"""

import ctypes
import os

__all__ = ['DS8R']

api_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'DS8R_API')


class DS8R:
"""
A Python controller for DS8R device.
A Python controller for the DS8R device.
The DS8R class provides methods to configure and control the DS8R device,
which is used for delivering electrical pulses in research and clinical settings.
This class interfaces with the device's DLL to send commands and retrieve the current state.
Attributes
----------
mode : {1, 2}, optional
"Mode" indicates pulse mode.
It can be either 1 (mono-phasic) or 2 (bi-phasic) (default: 1).
mode : {DS8R.MODE_MONOPHASIC, DS8R.MODE_BIPHASIC}, optional
Indicates pulse mode. Default is DS8R.MODE_MONOPHASIC.
- In **mono-phasic** mode, only positive or negative currents are generated.
- In **bi-phasic** mode, positive and negative currents alternate.
One serves as a stimulus phase and the other serves as a recovery phase.
- **MODE_MONOPHASIC** (1): Only positive or negative currents are generated.
- **MODE_BIPHASIC** (2): Positive and negative currents alternate. One serves as a stimulus phase, and the other serves as a recovery phase.
polarity : {1, 2, 3}, optional
"Polarity" indicates pulse polarity.
It can be either 1 (positive), 2 (negative), or 3 (alternating) (default: 1).
polarity : {DS8R.POLARITY_POSITIVE, DS8R.POLARITY_NEGATIVE, DS8R.POLARITY_ALTERNATING}, optional
Indicates pulse polarity. Default is DS8R.POLARITY_POSITIVE.
- **Positive** is the standard stimulation mode.
- **Negative** reverses the polarity of all pulses.
- In **alternating** mode, each successive trigger results in a polarity reversal.
- **POLARITY_POSITIVE** (1): Standard stimulation mode.
- **POLARITY_NEGATIVE** (2): Reverses the polarity of all pulses.
- **POLARITY_ALTERNATING** (3): Each successive trigger results in a polarity reversal.
source : {1, 2}, optional
"Source" indicates the source of pulse amplitude control.
It can be either 1 (internal) or 2 (external) (default: 1).
source : {DS8R.SOURCE_INTERNAL, DS8R.SOURCE_EXTERNAL}, optional
Indicates the source of pulse amplitude control. Default is DS8R.SOURCE_INTERNAL.
- **Internal** is the front panel control (including software).
- **External** is the external analogue voltage control.
- **SOURCE_INTERNAL** (1): Front panel control (including software).
- **SOURCE_EXTERNAL** (2): External analogue voltage control.
demand : int, optional
"Demand" indicates current output.
It can have a value between 1 and 150 (default: 20).
Current output demand. Value between 1 and 150 (default: 20).
The value of 1 indicates 0.1mA
(e.g. the value of 24 indicates 2.4mA).
Due to safety issues, the current output is limited to 150 (15.0mA).
Values from 1 to 19 (0.1 ~ 1.9mA) may not be correctly implemented
due to the limitations of the device.
- The value of 1 indicates 0.1 mA (e.g., 24 indicates 2.4 mA).
- Due to safety issues, the current output is limited to 150 (15.0 mA).
- Values from 1 to 19 (0.1 ~ 1.9 mA) may not be correctly implemented due to device limitations.
pulse_width : int, optional
"Pulse_width" indicates pulse duration.
It can have a value between 50 and 2000,
which have to be a multiple of 10 (default: 100)
Pulse duration in microseconds. Value between 50 and 2000, must be a multiple of 10 (default: 100).
The value of 1 indicates 1 microsecond
(e.g. the value of 100 indicates 100 microseconds).
Since pulse duration increments by 10 microsecond steps,
the "pulse_width" value must be a multiple of 10.
- The value directly represents microseconds (e.g., 100 indicates 100 µs).
- Since pulse duration increments by 10 µs steps, the value must be a multiple of 10.
dwell : int, optional
"Dwell" indicates interphase interval in biphasic mode.
It can have a value between 1 and 990 (default: 1).
Interphase interval in biphasic mode. Value between 1 and 990 (default: 1).
Interphase interval is the interval between
the stimulus phase and the recovery phase.
The value of 1 indicates 1 microsecond
(e.g. the value of 100 indicates 100 microseconds).
- Interval between the stimulus phase and the recovery phase.
- The value directly represents microseconds (e.g., 100 indicates 100 µs).
recovery : int, optional
"Recovery" indicates the recovery phase ratio in biphasic mode.
It can have a value between 10 and 100 (default: 100)
Recovery phase ratio in biphasic mode. Value between 10 and 100 (default: 100).
At 100%, stimulus and recovery phases are the same in duration and amplitude.
As the ratio is reduced from 100% the amplitude of the recovery phase decreases,
and its duration increases to preserve charge balancing.
- At 100%, stimulus and recovery phases are the same in duration and amplitude.
- As the ratio is reduced from 100%, the amplitude of the recovery phase decreases, and its duration increases to preserve charge balancing.
enabled : {0, 1}, optional
"Enabled" indicates output status.
It can have 0 (disabled) or 1 (enabled) (default: 1).
Output status. 0 (disabled) or 1 (enabled) (default: 1).
- In **Disabled** state, the current output will not be triggered.
- In **Enabled** state, the current output will be triggered.
- **0**: The current output will not be triggered.
- **1**: The current output will be triggered.
Methods
-------
upload_parameters()
Uploads the configured parameters to the DS8R device without triggering a pulse.
Examples
--------
First, you must make an DS8R object with parameters as arguments.
If you don't pass any argument, the object will use the default values.
These parameters are not applied to the setting of the DS8R device yet.
They will be changed when you use the method `run()`.
trigger_pulse()
Triggers a pulse on the DS8R device using the current settings.
>>> c = DS8R()
set_enabled(enabled)
Sets the enabled state of the device without changing other parameters.
If you want to change a parameter value of an existing DS8R object,
you can do it just by assigning a new value to the property.
get_state(verbose=False)
Retrieves the current state of the DS8R device.
>>> c.demand = 20
run(force=False)
Uploads the parameters and triggers a pulse.
Finally, by running `run()` method as below,
you can apply the parameters to the DS8R device and trigger a current output.
Examples
--------
First, create a DS8R object with desired parameters. If no arguments are passed, default values are used.
These parameters are not applied to the DS8R device until you call `run()`.
>>> c.run()
"""
>>> device = DS8R()
def __init__(self,
mode: int = 1,
polarity: int = 1,
source: int = 1,
demand: int = 20,
pulse_width: int = 100,
dwell: int = 1,
recovery: int = 100,
enabled: int = 1):
self.mode = mode
self.polarity = polarity
self.source = source
self.demand = demand
self.pulse_width = pulse_width
self.dwell = dwell
self.recovery = recovery
self.enabled = enabled
To change a parameter value of an existing DS8R object:
@property
def mode(self) -> int:
return self.__mode
>>> device.demand = 20
@mode.setter
def mode(self, obj: int):
if not isinstance(obj, int):
raise TypeError(
'Invalid value. Every parameter value must be an integer.')
Use constants for clarity:
if obj == 1 or obj == 2:
self.__mode = obj
else:
raise ValueError(
'The parameter "mode" must be either 1 (Monophasic) or 2 (Biphasic).')
>>> device.mode = DS8R.MODE_BIPHASIC
>>> device.polarity = DS8R.POLARITY_ALTERNATING
>>> device.source = DS8R.SOURCE_INTERNAL
@property
def polarity(self) -> int:
return self.__polarity
Finally, apply the parameters to the DS8R device and trigger a current output:
@polarity.setter
def polarity(self, obj: int):
if not isinstance(obj, int):
raise TypeError(
'Invalid value. Every parameter value must be an integer.')
>>> device.run()
if obj in [1, 2, 3]:
self.__polarity = obj
else:
raise ValueError(
'The parameter "polarity" must be either 1 (Positive), 2 (Negative), or 3 (Alternating).')
To apply a demand value exceeding the safe limit (12.4 mA), use `force=True`:
@property
def source(self) -> int:
return self.__source
>>> device.demand = 130
>>> device.run(force=True)
"""

@source.setter
def source(self, obj: int):
if not isinstance(obj, int):
raise TypeError(
'Invalid value. Every parameter value must be an integer.')
_dll_loaded = False
_dll = None

if obj in [1, 2]:
self.__source = obj
else:
raise ValueError(
'The parameter "source" must be either 1 (internal) or 2 (External).')
# Constants for Mode, Polarity, Source
MODE_MONOPHASIC = 1
MODE_BIPHASIC = 2

@property
def demand(self) -> int:
return self.__demand
POLARITY_POSITIVE = 1
POLARITY_NEGATIVE = 2
POLARITY_ALTERNATING = 3

@demand.setter
def demand(self, obj: int):
if not isinstance(obj, int):
raise TypeError(
'Invalid value. Every parameter value must be an integer.')
SOURCE_INTERNAL = 1
SOURCE_EXTERNAL = 2

def __init__(self, mode=MODE_MONOPHASIC, polarity=POLARITY_POSITIVE, source=SOURCE_INTERNAL,
demand=20, pulse_width=100, dwell=1, recovery=100, enabled=1, dll_path=None):
self.mode = mode
self.polarity = polarity
self.source = source
self.demand = demand
self.pulse_width = pulse_width
self.dwell = dwell
self.recovery = recovery
self.enabled = enabled

if 1 <= obj <= 150:
self.__demand = obj
# Load the DLL only once
if not DS8R._dll_loaded:
if dll_path is None:
# Use the directory of the current script
dll_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'D128RProxy.dll')
if not os.path.exists(dll_path):
raise FileNotFoundError(f"DLL not found at {dll_path}")
try:
DS8R._dll = ctypes.WinDLL(dll_path)
DS8R._dll_loaded = True
self._define_functions()
except OSError as e:
raise OSError(f"Failed to load DLL: {e}")

def _define_functions(self):
# DGD128_Set function
DS8R._dll.DGD128_Set.argtypes = [
ctypes.c_long, # Mode
ctypes.c_long, # Polarity
ctypes.c_long, # Source
ctypes.c_long, # Demand
ctypes.c_long, # PulseWidth
ctypes.c_long, # Dwell
ctypes.c_long, # Recovery
ctypes.c_long # Enabled
]
DS8R._dll.DGD128_Set.restype = ctypes.c_int

# DGD128_Get function
DS8R._dll.DGD128_Get.argtypes = [
ctypes.POINTER(ctypes.c_long), # Mode
ctypes.POINTER(ctypes.c_long), # Polarity
ctypes.POINTER(ctypes.c_long), # Source
ctypes.POINTER(ctypes.c_long), # Demand
ctypes.POINTER(ctypes.c_long), # PulseWidth
ctypes.POINTER(ctypes.c_long), # Dwell
ctypes.POINTER(ctypes.c_long), # Recovery
ctypes.POINTER(ctypes.c_long) # Enabled
]
DS8R._dll.DGD128_Get.restype = ctypes.c_int

# DGD128_Trigger function
DS8R._dll.DGD128_Trigger.argtypes = []
DS8R._dll.DGD128_Trigger.restype = ctypes.c_int

def upload_parameters(self):
"""
Uploads the configured parameters to the DS8R device without triggering a pulse.
if 1 <= obj <= 19:
print('"demand" values from 1 to 19 may not be correctly implemented '
'due to the limitations of the device.')
else:
raise ValueError(
'The parameter "demand" must be in the range of 1 to 150.')
This method sends the current configuration parameters to the DS8R device.
It does not trigger a pulse; it only updates the device settings.
"""
try:
# Map parameters to ctypes, ensuring they are integers
mode = ctypes.c_long(int(self.mode))
polarity = ctypes.c_long(int(self.polarity))
source = ctypes.c_long(int(self.source))
demand = ctypes.c_long(int(self.demand))
pulse_width = ctypes.c_long(int(self.pulse_width))
dwell = ctypes.c_long(int(self.dwell))
recovery = ctypes.c_long(int(self.recovery))
enabled = ctypes.c_long(int(self.enabled))

# Call the DGD128_Set function to upload parameters
result = DS8R._dll.DGD128_Set(
mode,
polarity,
source,
demand,
pulse_width,
dwell,
recovery,
enabled
)

# Log the return value for debugging
print(f"DGD128_Set returned: {result}")

# Do not raise an exception based on return value
print("Parameters uploaded successfully.")
except Exception as e:
print(f"Error in upload_parameters: {e}")
raise

def trigger_pulse(self):
"""
Triggers a pulse on the DS8R device using the current settings.
@property
def pulse_width(self) -> int:
return self.__pulse_width
This method commands the DS8R device to emit a pulse based on the
parameters previously uploaded via `upload_parameters`.
"""
try:
result = DS8R._dll.DGD128_Trigger()

@pulse_width.setter
def pulse_width(self, obj: int):
if not isinstance(obj, int):
raise TypeError(
'Invalid value. Every parameter value must be an integer.')
# Log the return value for debugging
print(f"DGD128_Trigger returned: {result}")

if 50 <= obj <= 2000 and obj % 10 == 0:
self.__pulse_width = obj
else:
raise ValueError(
'The parameter "pulse_width" must be in the range of 50 to 2000, '
'and the input value must be a multiple of 10.')
# Do not raise an exception based on return value
print("Pulse triggered successfully.")
except Exception as e:
print(f"Error in trigger_pulse: {e}")
raise

@property
def dwell(self) -> int:
return self.__dwell
def set_enabled(self, enabled):
"""
Sets the enabled state of the device without changing other parameters.
@dwell.setter
def dwell(self, obj: int):
if not isinstance(obj, int):
raise TypeError(
'Invalid value. Every parameter value must be an integer.')
Parameters
----------
enabled : bool
True to enable the device, False to disable.
if 1 <= obj <= 990:
self.__dwell = obj
else:
raise ValueError(
'The parameter "dwell" must be in the range of 1 to 990')
This method updates the 'enabled' parameter and uploads it to the device.
"""
try:
# Update the 'enabled' parameter
self.enabled = 1 if enabled else 0

@property
def recovery(self) -> int:
return self.__recovery
# Call upload_parameters to update the device
self.upload_parameters()

@recovery.setter
def recovery(self, obj: int):
if not isinstance(obj, int):
raise TypeError(
'Invalid value. Every parameter value must be an integer.')
state_str = 'enabled' if enabled else 'disabled'
print(f"Device {state_str} successfully.")
except Exception as e:
print(f"Error in set_enabled: {e}")
raise

if 10 <= obj <= 100:
self.__recovery = obj
else:
raise ValueError(
'The parameter "recovery" must be in the range of 10 to 100')
def get_state(self, verbose=False):
"""
Retrieves the current state of the DS8R device.
@property
def enabled(self) -> int:
return self.__enabled
Parameters
----------
verbose : bool, optional
If True, prints the retrieved parameters. Default is False.
@enabled.setter
def enabled(self, obj):
if not isinstance(obj, int):
raise TypeError(
'Invalid value. Every parameter value must be an integer.')
Returns
-------
dict
A dictionary containing the current device parameters.
if obj == 0 or obj == 1:
self.__enabled = obj
else:
raise ValueError(
'The parameter "enabled" must be either 0 (disabled) or 1 (enabled).')
This method queries the DS8R device for its current settings and updates
the instance variables accordingly.
"""
try:
# Prepare ctypes variables to receive the parameters
mode = ctypes.c_long()
polarity = ctypes.c_long()
source = ctypes.c_long()
demand = ctypes.c_long()
pulse_width = ctypes.c_long()
dwell = ctypes.c_long()
recovery = ctypes.c_long()
enabled = ctypes.c_long()

# Call the DGD128_Get function
result = DS8R._dll.DGD128_Get(
ctypes.byref(mode),
ctypes.byref(polarity),
ctypes.byref(source),
ctypes.byref(demand),
ctypes.byref(pulse_width),
ctypes.byref(dwell),
ctypes.byref(recovery),
ctypes.byref(enabled)
)

# Create a dictionary with the parameters
state = {
'mode': mode.value,
'polarity': polarity.value,
'source': source.value,
'demand': demand.value, # Use demand as is
'pulse_width': pulse_width.value,
'dwell': dwell.value,
'recovery': recovery.value,
'enabled': enabled.value
}

# Update the instance variables
self.mode = state['mode']
self.polarity = state['polarity']
self.source = state['source']
self.demand = state['demand']
self.pulse_width = state['pulse_width']
self.dwell = state['dwell']
self.recovery = state['recovery']
self.enabled = state['enabled']

# Print the retrieved parameters if verbose is True
if verbose:
print(f"DGD128_Get returned: {result}")
print("Current device parameters:")
for key, value in state.items():
print(f"{key.capitalize()}: {value}")

return state
except Exception as e:
print(f"Error in get_state: {e}")
raise

def run(self, force=False):
"""Change the settings of the DS8R device and trigger an output.
"""
Uploads the parameters and triggers a pulse.
Parameters
---------
force : bool
``True`` allows applying a current greater than 15.0mA,
which can be dangerous. The default value is ``False``.
----------
force : bool, optional
If True, allows applying a current demand exceeding the safe limit (12.4 mA).
Default is False.
Raises
------
ValueError
If the current output is greater than 15.0mA and the 'force' value is ``False``.
If the demand value exceeds the safe limit and force is not True.
This method first uploads the parameters to the device and then triggers a pulse.
It includes safety logic to prevent applying a high current demand unintentionally.
"""
command = ('"{filename}" {mode} {polarity} {source} {demand} '
'{pulse_width} {dwell} {recovery} {enabled}')\
.format(filename=api_path,
mode=self.mode,
polarity=self.polarity,
source=self.source,
demand=self.demand,
pulse_width=self.pulse_width,
dwell=self.dwell,
recovery=self.recovery,
enabled=self.enabled)
if self.demand <= 500:
os.system(command)
elif force:
os.system(command)
safe_limit = 10 # Safe maximum demand without forcing
if self.demand <= safe_limit or force:
self.upload_parameters()
self.trigger_pulse()
else:
raise ValueError(
'You should be careful when applying "demand" values greater than 124 (12.4mA). '
'To apply a current greater than 12.4mA, '
'use "c.run(force=True)".')
f'Demand value {self.demand} exceeds safe limit of {safe_limit} (10.0 mA). '
'To apply a current greater than 10.0 mA, use "run(force=True)".'
)