-
Notifications
You must be signed in to change notification settings - Fork 5
Fix EM power supply correctness bugs and add safety validation #18
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
base: main
Are you sure you want to change the base?
Changes from all commits
8d96232
9343353
12a91a4
b186f0a
2399fa7
1453ae5
8c02eb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,37 @@ | ||||||||||||||||||||||||||||||
| """Implements functionality unique to the Model 643 and 648 electromagnet power supplies.""" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import serial | ||||||||||||||||||||||||||||||
| from .generic_instrument import GenericInstrument, RegisterBase, _parse_response, InstrumentException | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _validate_number(value, name): | ||||||||||||||||||||||||||||||
| """Validate that a value is a finite number (rejects bool, NaN, and inf). | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||
| value: The value to validate. | ||||||||||||||||||||||||||||||
| name (str): Parameter name for error messages. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||
| ValueError: If the value is not a finite number. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| if isinstance(value, bool): | ||||||||||||||||||||||||||||||
| raise ValueError(f"{name} must be a number, got bool") | ||||||||||||||||||||||||||||||
| if not isinstance(value, (int, float)): | ||||||||||||||||||||||||||||||
| raise ValueError(f"{name} must be a number, got {type(value).__name__}") | ||||||||||||||||||||||||||||||
| if not math.isfinite(value): | ||||||||||||||||||||||||||||||
| raise ValueError(f"{name} must be finite, got {value}") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| class ElectromagnetPowerSupply(GenericInstrument): | ||||||||||||||||||||||||||||||
| """Class object representing a Lake Shore Model 643 or 648 electromagnet power supply.""" | ||||||||||||||||||||||||||||||
| vid_pid = [(0x1FB9, 0x0601), (0x1FB9, 0x0602)] # 643, 648 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| _MAX_CURRENT = 135.1 # Model 648 maximum; Model 643 is 70.1 A | ||||||||||||||||||||||||||||||
| _MIN_RAMP_RATE = 0.0001 # A/s | ||||||||||||||||||||||||||||||
| _MAX_RAMP_RATE = 50.0 # A/s | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def __init__(self, | ||||||||||||||||||||||||||||||
| serial_number=None, | ||||||||||||||||||||||||||||||
| com_port=None, | ||||||||||||||||||||||||||||||
|
|
@@ -251,7 +275,19 @@ def set_limits(self, max_current, max_ramp_rate): | |||||||||||||||||||||||||||||
| A. The Model 648 bounds are 0.0000 - 135.1000 A. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| max_ramp_rate (float): The maximum output current ramp rate setting allowed (0.0001 - 50.000 A/s). | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||
| ValueError: If max_current or max_ramp_rate is not a finite number or out of range. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| _validate_number(max_current, "Max current") | ||||||||||||||||||||||||||||||
| _validate_number(max_ramp_rate, "Max ramp rate") | ||||||||||||||||||||||||||||||
| if max_current < 0 or max_current > self._MAX_CURRENT: | ||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||
| f"Max current {max_current} A is outside valid range (0 - {self._MAX_CURRENT} A)") | ||||||||||||||||||||||||||||||
| if max_ramp_rate < self._MIN_RAMP_RATE or max_ramp_rate > self._MAX_RAMP_RATE: | ||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||
| f"Max ramp rate {max_ramp_rate} A/s is outside valid range " | ||||||||||||||||||||||||||||||
| f"({self._MIN_RAMP_RATE} - {self._MAX_RAMP_RATE} A/s)") | ||||||||||||||||||||||||||||||
| self.command(f"LIMIT {max_current}, {max_ramp_rate}") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def get_limits(self): | ||||||||||||||||||||||||||||||
|
|
@@ -273,7 +309,15 @@ def set_ramp_rate(self, ramp_rate): | |||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||
| ramp_rate (float): The rate at which the current will ramp when a new output current setting is entered | ||||||||||||||||||||||||||||||
| (0.0001 - 50.000 A/s). | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||
| ValueError: If ramp_rate is not a finite number or out of range. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| _validate_number(ramp_rate, "Ramp rate") | ||||||||||||||||||||||||||||||
| if ramp_rate < self._MIN_RAMP_RATE or ramp_rate > self._MAX_RAMP_RATE: | ||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||
| f"Ramp rate {ramp_rate} A/s is outside valid range " | ||||||||||||||||||||||||||||||
| f"({self._MIN_RAMP_RATE} - {self._MAX_RAMP_RATE} A/s)") | ||||||||||||||||||||||||||||||
| self.command(f"RATE {ramp_rate}") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def get_ramp_rate(self): | ||||||||||||||||||||||||||||||
|
|
@@ -294,7 +338,20 @@ def set_ramp_segment(self, segment, current, ramp_rate): | |||||||||||||||||||||||||||||
| current (float): Specifies the upper output current setting that will use this segment. | ||||||||||||||||||||||||||||||
| ramp_rate (float): Specifies the rate at which the current will ramp. (0.0001 - 50.000 A/s). | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||
| ValueError: If segment is not 1-5, or current/ramp_rate is not a finite number or out of range. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| if segment not in range(1, 6): | ||||||||||||||||||||||||||||||
| raise ValueError(f"Segment must be 1-5, got {segment}") | ||||||||||||||||||||||||||||||
|
Comment on lines
+344
to
+345
|
||||||||||||||||||||||||||||||
| _validate_number(current, "Current") | ||||||||||||||||||||||||||||||
| _validate_number(ramp_rate, "Ramp rate") | ||||||||||||||||||||||||||||||
| if abs(current) > self._MAX_CURRENT: | ||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||
| f"Current magnitude {abs(current)} A exceeds maximum safe value of {self._MAX_CURRENT} A") | ||||||||||||||||||||||||||||||
| if ramp_rate < self._MIN_RAMP_RATE or ramp_rate > self._MAX_RAMP_RATE: | ||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||
| f"Ramp rate {ramp_rate} A/s is outside valid range " | ||||||||||||||||||||||||||||||
| f"({self._MIN_RAMP_RATE} - {self._MAX_RAMP_RATE} A/s)") | ||||||||||||||||||||||||||||||
| self.command(f"RSEGS {segment}, {current}, {ramp_rate}") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def get_ramp_segment(self, segment): | ||||||||||||||||||||||||||||||
|
|
@@ -315,7 +372,12 @@ def set_ramp_segments_enable(self, state): | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||
| state (bool): The state of the ramp segments enable. 0=Disabled and 1=Enabled. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||
| ValueError: If state is not a bool or 0/1. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| if state not in (True, False, 0, 1): | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| if state not in (True, False, 0, 1): | |
| # Enforce strict type checking: accept only bool or int 0/1, reject floats and other numerics. | |
| if isinstance(state, bool): | |
| valid = True | |
| elif isinstance(state, int) and state in (0, 1): | |
| valid = True | |
| else: | |
| valid = False | |
| if not valid: |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mode not in (0, 1, 2, 3) will allow bools (True == 1 / False == 0). Passing True currently results in INTWTR True being sent, which is not a valid instrument command. Explicitly reject bool (and/or enforce isinstance(mode, int) and format with int(mode)).
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mode not in (0, 1, 2, 3) will allow bools (True == 1 / False == 0). Passing True will send MAGWTR True, which is not valid for the instrument. Add an explicit bool/type check and/or cast to int when formatting the command.
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
brightness_level not in (0, 1, 2, 3) allows bools (True == 1 / False == 0), and f-string formatting would send DISP True/False. Add an explicit check to reject bool and require an int, and/or format the command with int(brightness_level).
| if brightness_level not in (0, 1, 2, 3): | |
| raise ValueError(f"Brightness level must be 0, 1, 2, or 3, got {brightness_level}") | |
| self.command(f"DISP {brightness_level}") | |
| if isinstance(brightness_level, bool): | |
| raise ValueError("Brightness level must be an int between 0 and 3, got bool") | |
| if not isinstance(brightness_level, int): | |
| raise ValueError( | |
| f"Brightness level must be an int between 0 and 3, got {type(brightness_level).__name__}" | |
| ) | |
| if brightness_level not in (0, 1, 2, 3): | |
| raise ValueError(f"Brightness level must be 0, 1, 2, or 3, got {brightness_level}") | |
| self.command(f"DISP {int(brightness_level)}") |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lock_state not in (0, 1, 2) allows bools (True == 1 / False == 0), which would result in LOCK True,<code> being sent. Add an explicit isinstance(lock_state, bool) rejection (or require int and cast with int(lock_state) when formatting).
| if lock_state not in (0, 1, 2): | |
| raise ValueError(f"Lock state must be 0, 1, or 2, got {lock_state}") | |
| if not isinstance(code, int) or isinstance(code, bool): | |
| raise ValueError(f"Lock code must be an integer, got {type(code).__name__}") | |
| self.command(f"LOCK {lock_state},{code}") | |
| if isinstance(lock_state, bool) or not isinstance(lock_state, int) or lock_state not in (0, 1, 2): | |
| raise ValueError(f"Lock state must be 0, 1, or 2, got {lock_state}") | |
| if not isinstance(code, int) or isinstance(code, bool): | |
| raise ValueError(f"Lock code must be an integer, got {type(code).__name__}") | |
| self.command(f"LOCK {int(lock_state)},{code}") |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mode not in (0, 1, 2) allows bools, and f-string formatting would send XPGM True/False. Add an explicit bool/type check and/or cast to int when formatting the command.
| if mode not in (0, 1, 2): | |
| raise ValueError(f"Programming mode must be 0, 1, or 2, got {mode}") | |
| self.command(f"XPGM {mode}") | |
| if not isinstance(mode, int) or isinstance(mode, bool): | |
| raise ValueError(f"Programming mode must be an integer, got {type(mode).__name__}") | |
| if mode not in (0, 1, 2): | |
| raise ValueError(f"Programming mode must be 0, 1, or 2, got {mode}") | |
| self.command(f"XPGM {int(mode)}") |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
terminator and eoi_enable validation currently allows bools (True == 1 / False == 0). If a caller passes True, the command would become IEEE True,..., which is invalid. Consider explicitly rejecting bool and requiring ints, and/or formatting with int(...) after validating the allowed numeric range.
| if terminator not in (0, 1, 2, 3): | |
| raise ValueError(f"Terminator must be 0, 1, 2, or 3, got {terminator}") | |
| if eoi_enable not in (0, 1): | |
| raise ValueError(f"EOI enable must be 0 or 1, got {eoi_enable}") | |
| if not isinstance(address, int) or isinstance(address, bool) or address < 1 or address > 30: | |
| raise ValueError(f"IEEE address must be an integer from 1-30, got {address}") | |
| self.command(f"IEEE {terminator},{eoi_enable},{address}") | |
| if not isinstance(terminator, int) or isinstance(terminator, bool) or terminator < 0 or terminator > 3: | |
| raise ValueError(f"Terminator must be an integer 0, 1, 2, or 3, got {terminator}") | |
| if not isinstance(eoi_enable, int) or isinstance(eoi_enable, bool) or eoi_enable not in (0, 1): | |
| raise ValueError(f"EOI enable must be an integer 0 or 1, got {eoi_enable}") | |
| if not isinstance(address, int) or isinstance(address, bool) or address < 1 or address > 30: | |
| raise ValueError(f"IEEE address must be an integer from 1-30, got {address}") | |
| self.command(f"IEEE {int(terminator)},{int(eoi_enable)},{int(address)}") |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mode not in (0, 1, 2) allows bools, and the command would be formatted as MODE True/False. Add an explicit bool/type check and/or cast to int when formatting the command.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_MAX_CURRENT is hard-coded to the Model 648 limit (135.1 A) even though this class is used for MODEL643 as well (tests instantiate Model643). This contradicts the docstrings (MODEL643 max 70.1 A) and can allow unsafe currents/limits on a 643. Consider deriving the max current from the connected instrument model (e.g., based on self.model_number) or making it configurable per instance, and update validation accordingly.