Skip to content

Commit ecdaa67

Browse files
authored
Merge pull request #26 from cmgriffin/plz1004wh-pulse-sequence-feature
Plz1004wh pulse sequence feature
2 parents 43ffd43 + 7c102ce commit ecdaa67

File tree

3 files changed

+259
-1
lines changed

3 files changed

+259
-1
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pythonequipmentdrivers"
7-
version = "2.8.1"
7+
version = "2.9.0"
88
authors = [
99
{ name="Anna Giasson", email="AnnaGraceGiasson@GMail.com" },
1010
]

src/pythonequipmentdrivers/sink/_kikusui_plz1004wh.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
from typing import Union
2+
from dataclasses import dataclass
3+
import itertools
24

35
from ..core import VisaResource
46

57

8+
@dataclass
9+
class SequenceStep:
10+
current: float
11+
trigger: bool = False
12+
13+
614
class Kikusui_PLZ1004WH(VisaResource): # 1 kW
715
"""
816
Kikusui_PLZ1004WH(address)
@@ -17,6 +25,8 @@ class Kikusui_PLZ1004WH(VisaResource): # 1 kW
1725
# logic
1826
# documenation
1927

28+
SequenceStep = SequenceStep
29+
2030
def set_state(self, state: bool) -> None:
2131
"""
2232
set_state(state)
@@ -376,3 +386,149 @@ def measure_power(self) -> float:
376386

377387
response = self.query_resource("MEAS:POW?")
378388
return float(response)
389+
390+
def configure_sequence(
391+
self,
392+
steps: list["Kikusui_PLZ1004WH.SequenceStep"],
393+
current_range: str = "HIGH",
394+
step_size: float = 1e-3,
395+
initialize: bool = True,
396+
) -> None:
397+
"""
398+
Configure the load fast sequence consisting of a series of steps. Each step
399+
includes a current value and whether or not a trigger pulse should be emitted.
400+
401+
Args:
402+
steps (list[Kikusui_PLZ1004WH.SequenceStep]): A list of SequenceSteps
403+
describing the sequence to be executed. Each step has a load setting and
404+
the option to emit a trigger pulse. A maximum of 1024 steps can be used
405+
but note that transmitting a large number of steps takes significant
406+
time.
407+
current_range (str, optional): Range setting to use (LOW, MED, HIGH). Refer
408+
to manual for the maximum current that each range is capable of.
409+
Typically LOW = 1.32A, MED = 13.2A, and HIGH = 132A. Defaults to "HIGH".
410+
step_size (float, optional): Size (duration) of each step. Valid range is
411+
100us to 100ms. Defaults to 1ms.
412+
initialize (bool, optional): Send the initialization commands to set up the
413+
load in addition to sending the sequence steps. Setting to false can
414+
save some time if no settings need to be changed. Defaults to True.
415+
"""
416+
MIN_STEP_SIZE = 100e-6
417+
MAX_STEP_SIZE = 100e-3
418+
VALID_RANGES = {"LOW", "MEDIUM", "MED", "HIGH"}
419+
MAX_SEQ_LENGTH = 1024
420+
421+
sequence_len = len(steps)
422+
423+
# validate the inputs
424+
if sequence_len > MAX_SEQ_LENGTH:
425+
raise ValueError(f"sequence length is {sequence_len} > {MAX_SEQ_LENGTH=}")
426+
if not current_range.upper() in VALID_RANGES:
427+
raise ValueError(f"{current_range=} is not in {VALID_RANGES=}")
428+
if step_size > MAX_STEP_SIZE or step_size < MIN_STEP_SIZE:
429+
raise ValueError(f"step_size must be <{MAX_STEP_SIZE} and >{MIN_STEP_SIZE}")
430+
431+
if initialize:
432+
# fast sequence requires 11, select it for editing
433+
self.write_resource("prog:name 11")
434+
# set sequence to fast CC mode
435+
self.write_resource("prog:mode fcc")
436+
# run the sequence just once
437+
self.write_resource("prog:loop 1")
438+
# set the step length
439+
self.write_resource(f"prog:fsp:time {step_size}")
440+
# set the current range for the sequence
441+
self.write_resource(f"prog:cran {current_range}")
442+
# set the sequence length
443+
self.write_resource(f"prog:fsp:end {sequence_len}")
444+
445+
# Write the sequence of currents in chunks of 8 using prog:fsp:edit:wave
446+
# to reduce number of transactions. Write individual steps only when
447+
# a trigger pulse is needed
448+
for steps_chunk, first_step_idx in zip(
449+
itertools.batched(steps, 8),
450+
itertools.count(start=1, step=8),
451+
):
452+
currents = [step.current for step in steps_chunk]
453+
# pad the chunk of currents up to 8 with 0s
454+
currents += (8 - len(currents)) * [0]
455+
# write the chunk of currents
456+
self.write_resource(
457+
f"prog:fsp:edit:wave {first_step_idx},"
458+
+ ",".join(str(c) for c in currents)
459+
)
460+
461+
# set the steps that have triggers
462+
for offset, step in enumerate(steps_chunk):
463+
if not step.trigger:
464+
continue
465+
step_idx = first_step_idx + offset
466+
# store the current of the step
467+
curr = self.query_resource(f"prog:fsp:edit? {step_idx}").split(",")[0]
468+
# write the step with a trigger
469+
self.write_resource(f"prog:fsp:edit {step_idx},{curr},1")
470+
471+
def run_sequence(self) -> None:
472+
"""
473+
Run the current sequence.
474+
"""
475+
self.write_resource("prog:stat run")
476+
477+
def configure_pulse_seqeunce(
478+
self,
479+
pulse_current: float,
480+
pulse_width: float,
481+
trig_delay: float,
482+
step_size: float = 1e-3,
483+
initial_idle_time: float = 10e-3,
484+
idle_current: float = 0.0,
485+
current_range: str = "HIGH",
486+
keep_load_on: bool = False,
487+
) -> None:
488+
"""
489+
Configure the load to produce a single pulse sequence consisting of an initial
490+
low current period for the load to "warm up" followed by a high current period
491+
at the specified current and a ending low current period fixed at 1ms. A trigger
492+
pulse is emmited during the pulse with the defined delay.
493+
494+
Args:
495+
pulse_current (float): Current to set during the pulse
496+
pulse_width (float): width of the pulse in seconds
497+
trig_delay (float): delay from the beginning of the pulse to when the
498+
trigger pulse is emmited in seconds
499+
step_size (float, optional): Size of the steps which the sequence will be
500+
broken up into. A smaller step results in more resolution of the
501+
timings. Defaults to 1e-3.
502+
initial_idle_time (float, optional): Time to set the load to the initial
503+
value before starting the pulse. This is needed since the load cannot
504+
immediately produce a high load at the beginning of a sequence. 10ms was
505+
found to be effective. Defaults to 10e-3.
506+
idle_current (float, optional): Current to set during idle times. 0A
507+
typically works fine for this purpose. Defaults to 0.0.
508+
current_range (str, optional): Range setting to use (LOW, MED, HIGH). Refer
509+
to manual for the maximum current that each range is capable of.
510+
Typically LOW = 1.32A, MED = 13.2A, and HIGH = 132A. Defaults to "HIGH".
511+
keep_load_on (bool, optional): Keep the load at the specified idle_current
512+
after the sequence completes. Defaults to False.
513+
"""
514+
END_IDLE_TIME = 1e-3
515+
seq_len = initial_idle_time + pulse_width + END_IDLE_TIME
516+
if trig_delay + initial_idle_time > seq_len:
517+
ValueError(f"{trig_delay=} not valid for {seq_len=}")
518+
steps = list(
519+
itertools.chain(
520+
521+
(SequenceStep(idle_current) for _ in range(round(initial_idle_time / step_size))),
522+
523+
(SequenceStep(pulse_current) for _ in range(round(pulse_width / step_size))),
524+
525+
(SequenceStep(idle_current) for _ in range(round(END_IDLE_TIME / step_size))),
526+
)
527+
)
528+
# +1 since trigger occurs at the beginning of a step
529+
trigger_idx = round((initial_idle_time + trig_delay) / step_size + 1)
530+
steps[trigger_idx].trigger = True
531+
self.configure_sequence(steps, current_range, step_size)
532+
if keep_load_on:
533+
self.write_resource(f"prog:linp {1 if idle_current else 0}")
534+
self.write_resource(f"prog:lval {idle_current}")

tests/sink_test.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import unittest
2+
from unittest.mock import MagicMock, patch, call
3+
import functools
4+
import itertools
5+
6+
7+
import pythonequipmentdrivers as ped
8+
9+
10+
class Test_Kikusui_PLZ1004WH(unittest.TestCase):
11+
def setUp(self) -> None:
12+
# there is a lot that happens is VisaResource.__init__ so it is easier to just
13+
# mock it
14+
with patch.object(
15+
ped.core.VisaResource, "__init__", lambda address, *args, **kwargs: None
16+
):
17+
self.inst = ped.sink.Kikusui_PLZ1004WH("12345")
18+
self.inst.write_resource = MagicMock(spec=self.inst.write_resource)
19+
self.inst.query_resource = MagicMock(spec=self.inst.query_resource)
20+
return super().setUp()
21+
22+
def test_set_state(self):
23+
self.inst.set_state(True)
24+
self.inst.write_resource.assert_called_with("OUTP 1")
25+
26+
@staticmethod
27+
def _query_configure_sequence_effect(steps: list, cmd: str):
28+
idx = int(cmd.split()[-1])
29+
selected_step = steps[idx - 1]
30+
return f"{selected_step.current},{int(selected_step.trigger)}"
31+
32+
def test_configure_sequence(self):
33+
34+
steps = [self.inst.SequenceStep(n, False) for n in range(10)]
35+
steps[4].trigger = True
36+
self.inst.query_resource.side_effect = functools.partial(
37+
self._query_configure_sequence_effect, steps
38+
)
39+
self.inst.configure_sequence(
40+
steps, current_range="HIGH", step_size=0.001, initialize=False
41+
)
42+
expected_write_calls = [
43+
call("prog:fsp:edit:wave 1,0,1,2,3,4,5,6,7"),
44+
call("prog:fsp:edit 5,4,1"),
45+
call("prog:fsp:edit:wave 9,8,9,0,0,0,0,0,0"),
46+
]
47+
expected_query_calls = [
48+
call("prog:fsp:edit? 5"),
49+
]
50+
51+
self.inst.write_resource.assert_has_calls(expected_write_calls)
52+
self.inst.query_resource.assert_has_calls(expected_query_calls)
53+
54+
def test_configure_sequence_init(self):
55+
steps = [self.inst.SequenceStep(n, False) for n in range(10)]
56+
steps[4].trigger = True
57+
self.inst.query_resource.side_effect = functools.partial(
58+
self._query_configure_sequence_effect, steps
59+
)
60+
self.inst.configure_sequence(
61+
steps, current_range="HIGH", step_size=0.001, initialize=True
62+
)
63+
expected_calls = [
64+
call("prog:name 11"),
65+
call("prog:mode fcc"),
66+
call("prog:loop 1"),
67+
call(f"prog:fsp:time {0.001}"),
68+
call("prog:cran HIGH"),
69+
call("prog:fsp:end 10"),
70+
call("prog:fsp:edit:wave 1,0,1,2,3,4,5,6,7"),
71+
call("prog:fsp:edit 5,4,1"),
72+
call("prog:fsp:edit:wave 9,8,9,0,0,0,0,0,0"),
73+
]
74+
expected_query_calls = [
75+
call("prog:fsp:edit? 5"),
76+
]
77+
78+
self.inst.write_resource.assert_has_calls(expected_calls)
79+
self.inst.query_resource.assert_has_calls(expected_query_calls)
80+
81+
@patch.object(ped.sink.Kikusui_PLZ1004WH, "configure_sequence")
82+
def test_configure_pulse_sequence(self, configure_seq_mock: MagicMock):
83+
self.inst.configure_pulse_seqeunce(
84+
pulse_current=10,
85+
pulse_width=10e-3,
86+
trig_delay=2e-3,
87+
step_size=1e-3,
88+
initial_idle_time=0.01,
89+
idle_current=0,
90+
current_range="HIGH",
91+
)
92+
93+
steps = list(
94+
itertools.chain(
95+
(self.inst.SequenceStep(0) for _ in range(10)),
96+
(self.inst.SequenceStep(10) for _ in range(10)),
97+
(self.inst.SequenceStep(0) for _ in range(1)),
98+
)
99+
)
100+
steps[13].trigger = True
101+
102+
configure_seq_mock.assert_called_once_with(steps, "HIGH", 1e-3)

0 commit comments

Comments
 (0)