From 39ab2ad026992cf33529a297cb4d9f340ee7dba9 Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Thu, 23 May 2019 11:01:37 -0400 Subject: [PATCH 1/6] added the lakeshore pvs --- .travis.yml | 1 + nslsii/iocs/lakeshore_control.py | 219 ++++++++++++++++++ nslsii/iocs/lakeshore_ioc_sim.py | 53 +++++ nslsii/iocs/lakeshore_temperature.py | 100 ++++++++ nslsii/tests/lakeshore_test.py | 189 +++++++++++++++ nslsii/tests/temperature_controllers_test.py | 4 +- .../{iocs => }/tests/test_epstwostate_ioc.py | 0 7 files changed, 565 insertions(+), 1 deletion(-) create mode 100644 nslsii/iocs/lakeshore_control.py create mode 100644 nslsii/iocs/lakeshore_ioc_sim.py create mode 100644 nslsii/iocs/lakeshore_temperature.py create mode 100644 nslsii/tests/lakeshore_test.py rename nslsii/{iocs => }/tests/test_epstwostate_ioc.py (100%) diff --git a/.travis.yml b/.travis.yml index 6f4c2e21..402ff29a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ env: - secure: "FhNkkbod0Wc/zUf9cTvwziAYHcjfte2POf+hoVSmC+v/RcYKCNCo+mGGMhF9F4KyC2nzvulfzow7YXoswZqav4+TEEu+mpuPaGlf9aqp8V61eij8MVTwonzQEYmHAy3KatwXxyvvhQpfj3gOuDVolfOg2MtNZi6QERES4E1sjOn714fx2HkVxqH2Y8/PF/FzzGeJaRlVaVci0EdIJ5Ss5c5SjO6JGgxj4hzhTPHjTaLjdLHlVhuB9Yatl80zbhGriljLcDQTHmoSODwBpAh5YLDUZq6B9vomaNB9Hb3e0D5gItjOdj53v6AsHU8LkncZMvsgJgh2sZZqMO6nkpHcYPwJgbPbKd3RtVlk6Kg/tvKQk0rMcxl5fFFeD2i9POnANg/xJsKN6yAEY3kaRwQtajQmlcicSa/wdwv9NhUTtBmA/mnyzxHbQXrB0bEc2P2QVu7U8en6dWaOAqc1VCMrWIhp2ADNWb7JZhYj70TgmExIU3UH8qlMb6dyx50SJUE9waJj3fiiZVkjh+E568ZRSMvL9n+bLlFt4uDT4AysSby6cj+zjfNViKFstTAqjyd5VJEvCoUu73vNzWEiWFtEvKKVL1P3pbLN/G3aSSJMa5fc1o+2lRUwdwNNOOdH6iKBDZGNpE8nGDlTP2b2dhFyEt8nICKJhbgU208jhyyH8Vk=" script: + - export OPHYD_CONTROL_LAYER=caproto - coverage run -m pytest # Run the tests and check for test coverage. - coverage report -m # Generate test coverage report. - codecov # Upload the report to codecov. diff --git a/nslsii/iocs/lakeshore_control.py b/nslsii/iocs/lakeshore_control.py new file mode 100644 index 00000000..b8759d5e --- /dev/null +++ b/nslsii/iocs/lakeshore_control.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +from caproto.server import pvproperty, PVGroup +from caproto import ChannelType + + +class ControlRecord(PVGroup): + + def __init__(self, prefix, *, ioc, **kwargs): + super().__init__(prefix, **kwargs) + self.ioc = ioc + + # PVPositioner required attributes + + _rb_val = 0. + + setpoint = pvproperty(value=_rb_val, + dtype=ChannelType.DOUBLE, + name='}}T-SP') + readback = pvproperty(value=_rb_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}T-RB') + + @readback.getter + async def readback(self, instance): + return self._rb_val + + @setpoint.putter + async def setpoint(self, instance, value): + self._rb_val = value + return value + + _done_val = 0. + + done = pvproperty(value=_done_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Sts:Ramp-Sts') + + @done.getter + async def done(self, instance): + return self._done_val + + # top level attributes + + _heater_range_val = 0. + _heater_status_val = 0. + + heater_range = pvproperty(value=_heater_range_val, + dtype=ChannelType.DOUBLE, + name='}}Val:Range-Sel') + heater_status = pvproperty(value=_heater_status_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Err:Htr-Sts') + + _mode_val = 0. + _enable_val = 0. + _target_channel_val = 0. + + mode = pvproperty(value=_mode_val, + dtype=ChannelType.DOUBLE, + name='}}Mode-Sel') + enable = pvproperty(value=_enable_val, + dtype=ChannelType.DOUBLE, + name='}}Enbl-Sel') + target_channel = pvproperty(value=_target_channel_val, + dtype=ChannelType.DOUBLE, + name='}}Out-Sel') + + # ramp attributes + + _ramp_enable_val = 0. + + ramp_enable = pvproperty(value=_ramp_enable_val, + dtype=ChannelType.DOUBLE, + name='}}Enbl:Ramp-Sel') + + _ramp_rate_val = 0. + + ramp_rate_rb = pvproperty(value=_ramp_rate_val, + dtype=ChannelType.DOUBLE, + name='}}Val:Ramp-RB') + ramp_rate_sp = pvproperty(value=_ramp_rate_val, + dtype=ChannelType.DOUBLE, + name='}}Val:Ramp-Sp') + + @ramp_rate_rb.getter + async def ramp_rate_rb(self, instance): + return self._ramp_rate_val + + @ramp_rate_sp.putter + async def ramp_rate_sp(self, instance, value): + self._ramp_rate_val = value + return value + + # PID loop parameters + + _pid_proportional_val = 0. + + pid_proportional_rb = pvproperty(value=_pid_proportional_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Gain:P-RB') + pid_proportional_sp = pvproperty(value=_pid_proportional_val, + dtype=ChannelType.DOUBLE, + name='}}Gain:P-SP') + + @pid_proportional_rb.getter + async def pid_proportional_rb(self, instance): + return self._pid_proportional_val + + @pid_proportional_sp.putter + async def pid_proportional_sp(self, instance, value): + self._pid_proportional_val = value + return value + + _pid_integral_val = 0. + + pid_integral_rb = pvproperty(value=_pid_integral_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Gain:I-RB') + pid_integral_sp = pvproperty(value=_pid_integral_val, + dtype=ChannelType.DOUBLE, + name='}}Gain:I-SP') + + @pid_integral_rb.getter + async def pid_integral_rb(self, instance): + return self._pid_integral_val + + @pid_integral_sp.putter + async def pid_integral_sp(self, instance, value): + self._pid_integral_val = value + return value + + _pid_derivative_val = 0. + + pid_derivative_rb = pvproperty(value=_pid_derivative_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Gain:D-RB') + pid_derivative_sp = pvproperty(value=_pid_derivative_val, + dtype=ChannelType.DOUBLE, + name='}}Gain:D-SP') + + @pid_derivative_rb.getter + async def pid_derivative_rb(self, instance): + return self._pid_derivative_val + + @pid_derivative_sp.putter + async def pid_derivative_sp(self, instance, value): + self._pid_derivative_val = value + return value + + # output parameters + + _out_current_val = 0. + + out_current = pvproperty(value=_out_current_val, + dtype=ChannelType.DOUBLE, + name='}}Out-I') + + _out_man_current_val = 0. + + out_man_current_rb = pvproperty(value=_out_man_current_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Out:Man-RB') + out_man_current_sp = pvproperty(value=_out_man_current_val, + dtype=ChannelType.DOUBLE, + name='}}Out:Man-SP') + + @out_man_current_rb.getter + async def out_man_current_rb(self, instance): + return self._out_man_current_val + + @out_man_current_sp.putter + async def out_man_current_sp(self, instance, value): + self._out_man_current_val = value + return value + + _out_max_current_val = 0. + + out_max_current_rb = pvproperty(value=_out_max_current_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Out:MaxI-RB') + out_max_current_sp = pvproperty(value=_out_max_current_val, + dtype=ChannelType.DOUBLE, + name='}}Out:MaxI-SP') + + @out_max_current_rb.getter + async def out_max_current_rb(self, instance): + return self._out_max_current_val + + @out_max_current_sp.putter + async def out_max_current_sp(self, instance, value): + self._out_max_current_val = value + return value + + _out_resistance_val = 0. + + out_resistance_rb = pvproperty(value=_out_resistance_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Out:R-RB') + out_resistance_sp = pvproperty(value=_out_resistance_val, + dtype=ChannelType.DOUBLE, + name='}}Out:R-SP') + + @out_resistance_rb.getter + async def out_resistance_rb(self, instance): + return self._out_resistance_val + + @out_resistance_sp.putter + async def out_resistance_sp(self, instance, value): + self._out_resistance_val = value + return value diff --git a/nslsii/iocs/lakeshore_ioc_sim.py b/nslsii/iocs/lakeshore_ioc_sim.py new file mode 100644 index 00000000..067045f2 --- /dev/null +++ b/nslsii/iocs/lakeshore_ioc_sim.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +from caproto.server import PVGroup, ioc_arg_parser, run + +from nslsii.iocs.lakeshore_temperature import TemperatureRecord +from nslsii.iocs.lakeshore_control import ControlRecord + + +class LakeshoreIOC(PVGroup): + """ + Simulates a Lakeshore IOC. + """ + + def __init__(self, prefix, *, groups, **kwargs): + super().__init__(prefix, **kwargs) + self.groups = groups + + +def create_ioc(prefix, temperatures, controls, **ioc_options): + + groups = {} + + ioc = LakeshoreIOC(prefix, groups=groups, **ioc_options) + + for t in temperatures: + t_prefix = f'{prefix}-Chan:{t}' + print('t_prefix:', t_prefix) + groups[t_prefix] = TemperatureRecord(t_prefix, ioc=ioc) + + for c in controls: + c_prefix = f'{prefix}-Out:{c}' + print('c_prefix:', c_prefix) + groups[c_prefix] = ControlRecord(c_prefix, ioc=ioc) + + for prefix, group in groups.items(): + ioc.pvdb.update(**group.pvdb) + + return ioc + + +if __name__ == '__main__': + + ioc_options, run_options = ioc_arg_parser( + default_prefix='test:{{{{', + desc='Lakeshore IOC.') + + temperatures = ['A', 'B', 'C', 'D'] + controls = [1] + + ioc = create_ioc(temperatures=temperatures, + controls=controls, + **ioc_options) + + run(ioc.pvdb, **run_options) diff --git a/nslsii/iocs/lakeshore_temperature.py b/nslsii/iocs/lakeshore_temperature.py new file mode 100644 index 00000000..975ebee4 --- /dev/null +++ b/nslsii/iocs/lakeshore_temperature.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +from caproto.server import pvproperty, PVGroup +from caproto import ChannelType + +from threading import Lock + + +class TemperatureRecord(PVGroup): + + def __init__(self, prefix, *, ioc, **kwargs): + super().__init__(prefix, **kwargs) + self.ioc = ioc + + _T_val = 0. + _TC_val = 0. + _V_val = 0. + _status_val = 0. + _display_name = 'Lakeshore T' + _alarm_high_val = 5.0 + _alarm_low_val = 3.0 + + putter_lock = Lock() + + T = pvproperty(value=_T_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}T-I') + + T_celsius = pvproperty(value=_TC_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}T:C-I') + V = pvproperty(value=_V_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Val:Sens-I') + status = pvproperty(value=_status_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}T-Sts') + display_name_rb = pvproperty(value=_display_name, + read_only=True, + dtype=ChannelType.STRING, + name='}}T:Name-RB') + display_name_sp = pvproperty(value=_display_name, + dtype=ChannelType.STRING, + name='}}T:Name-SP') + alarm_high = pvproperty(value=_alarm_high_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Alrm:High-Sts') + alarm_low = pvproperty(value=_alarm_low_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Alrm:Low-Sts') + + _T_lim_val = 0. + + T_lim_rb = pvproperty(value=_T_lim_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}T:Lim-RB') + T_lim_sp = pvproperty(value=_T_lim_val, + dtype=ChannelType.DOUBLE, + name='}}T:Lim-SP') + + # Methods + + _velocity = 1. + _step_size = 0.1 + + cmd = pvproperty(value='', + dtype=ChannelType.STRING, + name='}}Cmd') + + @cmd.startup + async def cmd(self, instance, async_lib): + instance.ev = async_lib.library.Event() + instance.async_lib = async_lib + + @cmd.putter + async def cmd(self, instance, value): + + if self.putter_lock.locked() is True: + return instance.value + else: + self.putter_lock.acquire() + + p0 = instance.value + dwell = self._step_size/self._velocity + N = max(1, int((value - p0) / self._step_size)) + + for j in range(N): + new_value = p0 + self._step_size*(j+1) + await instance.async_lib.library.sleep(dwell) + await self.T.write(value=new_value) + + self.putter_lock.release() + + return value diff --git a/nslsii/tests/lakeshore_test.py b/nslsii/tests/lakeshore_test.py new file mode 100644 index 00000000..4fe73402 --- /dev/null +++ b/nslsii/tests/lakeshore_test.py @@ -0,0 +1,189 @@ +import os +import pytest +import subprocess +import sys +import time + +from collections import OrderedDict +from ophyd import (Component, DynamicDeviceComponent, Device, + EpicsSignal, EpicsSignalRO, PVPositioner) + +from caproto.sync.client import read + + +def create_device_from_components(name, *, docstring=None, + default_read_attrs=None, + default_configuration_attrs=None, + base_class=Device, class_kwargs=None, + **components): + + if docstring is None: + docstring = f'{name} Device' + + if not isinstance(base_class, tuple): + base_class = (base_class, ) + + if class_kwargs is None: + class_kwargs = {} + + clsdict = OrderedDict( + __doc__=docstring, + _default_read_attrs=default_read_attrs, + _default_configuration_attrs=default_configuration_attrs + ) + + for attr, component in components.items(): + if not isinstance(component, Component): + raise ValueError(f'Attribute {attr} is not a Component. ' + f'It is of type {type(component).__name__}') + + clsdict[attr] = component + + return type(name, base_class, clsdict, **class_kwargs) + + +def lakeshore336(name='Lakeshore336', temperatures=['A', 'B', 'C', 'D'], + controls=[1], docstring=None, + default_read_attrs=None, default_configuration_attrs=None): + + def _set_fields(fields, cls, prefix, field_prefix='', **kwargs): + '''A function that generates the component dictionaries for fields.''' + out_dict = OrderedDict() + for field in fields: + suffix = f'{prefix}{field}' + out_dict[f'{field_prefix}{field}'] = Component(cls, suffix, + **kwargs) + return out_dict + + class _Temperature(Device): + + T = Component(EpicsSignalRO, '}T-I') + T_celsius = Component(EpicsSignalRO, '}T:C-I') + V = Component(EpicsSignalRO, '}Val:Sens-I') + status = Component(EpicsSignalRO, '}T-Sts', kind='config') + display_name = Component(EpicsSignal, '}T:Name-RB', + write_pv='}T:Name-SP', kind='omitted') + + alarm = DynamicDeviceComponent( + {'high': (EpicsSignalRO, '}Alrm:High-Sts', {'kind': 'config'}), + 'low': (EpicsSignalRO, '}Alrm:Low-Sts', {'kind': 'config'})}, + kind='config') + + T_limit = Component(EpicsSignal, '}T:Lim-RB', write_pv='}T:Lim-SP', + kind='omitted') + + class _Control(PVPositioner): + + # PVPositioner required attributes + setpoint = Component(EpicsSignal, '}T-SP') + readback = Component(EpicsSignalRO, '}T-RB') + + done = Component(EpicsSignalRO, '}Sts:Ramp-Sts', kind='omitted') + + # top level attributes + heater_range = Component(EpicsSignal, '}Val:Range-Sel', kind='config') + heater_status = Component(EpicsSignalRO, '}Err:Htr-Sts', + kind='omitted') + mode = Component(EpicsSignal, '}Mode-Sel', kind='config') + enable = Component(EpicsSignal, '}Enbl-Sel', kind='config') + target_channel = Component(EpicsSignal, '}Out-Sel', kind='config') + + # ramp attributes + ramp = DynamicDeviceComponent( + {'enabled': (EpicsSignal, '}Enbl:Ramp-Sel', {'kind': 'config'}), + 'rate': (EpicsSignal, '}Val:Ramp-RB', + {'write_pv': '}Val:Ramp-SP', 'kind': 'config'})}, + kind='config') + + # PID loop parameters + pid = DynamicDeviceComponent( + {'proportional': (EpicsSignal, '}Gain:P-RB', + {'write_pv': '}Gain:P-SP', 'kind': 'config'}), + 'integral': (EpicsSignal, '}Gain:I-RB', + {'write_pv': '}Gain:I-SP', 'kind': 'config'}), + 'derivative': (EpicsSignal, '}Gain:D-RB', + {'write_pv': '}Gain:D-SP', 'kind': 'config'})}, + kind='config') + + # output parameters + output = DynamicDeviceComponent( + {'current': (EpicsSignal, '}Out-I', {}), + 'manual_current': (EpicsSignal, '}Out:Man-RB', + {'write_pv': '}Out:Man-SP'}), + 'max_current': (EpicsSignal, '}Out:MaxI-RB', + {'write_pv': '}Out:MaxI-SP', 'kind': 'config'}), + 'resistance': (EpicsSignal, '}Out:R-RB', + {'write_pv': '}Out:R-SP', 'kind': 'config'})}) + + temp_components = _set_fields(temperatures, _Temperature, '-Chan:') + output_components = _set_fields(controls, _Control, '-Out:', + field_prefix='out') + + components = { + 'temp': Component(create_device_from_components('temp', + **temp_components), + ''), + 'ctrl': Component(create_device_from_components('ctrl', + **output_components), + '')} + + new_class = create_device_from_components( + name, docstring=docstring, default_read_attrs=default_read_attrs, + default_configuration_attrs=default_configuration_attrs, + base_class=Device, **components) + + return new_class + + +@pytest.fixture(scope='class') +def ioc_sim(request): + + # setup code + + stdout = subprocess.PIPE + stdin = None + + ioc_process = subprocess.Popen([sys.executable, '-m', + 'nslsii.iocs.lakeshore_ioc_sim'], + stdout=stdout, stdin=stdin, + env=os.environ) + + print(f'nslsii.iocs.lakeshore_ioc_sim is now running') + + time.sleep(5) + + MyLakeshore336 = lakeshore336() + tc = MyLakeshore336('test:{', name='temp_controller') + + time.sleep(5) + + request.cls.tc = tc + + yield + + # teardown code + + ioc_process.terminate() + + +@pytest.mark.usefixtures('ioc_sim') +class TestIOC: + + def test_caproto_level(self): + + t_rb = read('test:{-Chan:A}T-I') + assert t_rb.data[0] == 0.0 + + c_sp = read('test:{-Out:1}T-SP') + assert c_sp.data[0] == 0.0 + + c_rb = read('test:{-Out:1}T-RB') + assert c_rb.data[0] == 0.0 + + def test_device_level(self): + + res = self.tc.temp.A.T.get() + assert res == 0.0 + + res = self.tc.ctrl.out1.setpoint.get() + assert res == 0.0 diff --git a/nslsii/tests/temperature_controllers_test.py b/nslsii/tests/temperature_controllers_test.py index 33b7978b..21aedfd6 100644 --- a/nslsii/tests/temperature_controllers_test.py +++ b/nslsii/tests/temperature_controllers_test.py @@ -6,6 +6,7 @@ import os import sys import pytest +import time @pytest.fixture @@ -27,13 +28,14 @@ def test_Eurotherm(RE): # Start up an IOC based on the thermo_sim device in caproto.ioc_examples ioc_process = subprocess.Popen([sys.executable, '-m', - 'caproto.tests.example_runner', 'caproto.ioc_examples.thermo_sim'], stdout=stdout, stdin=stdin, env=os.environ) print(f'caproto.ioc_examples.thermo_sim is now running') + time.sleep(5) + # Wrap the rest in a try-except to ensure the ioc is killed before exiting try: euro = Eurotherm('thermo:', name='euro') diff --git a/nslsii/iocs/tests/test_epstwostate_ioc.py b/nslsii/tests/test_epstwostate_ioc.py similarity index 100% rename from nslsii/iocs/tests/test_epstwostate_ioc.py rename to nslsii/tests/test_epstwostate_ioc.py From f30b367a3c1df5629699fa6fd1b11707aa39ba98 Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Thu, 23 May 2019 13:30:21 -0400 Subject: [PATCH 2/6] added the setpoint method --- nslsii/iocs/lakeshore_control.py | 184 +++++++++++++++------------ nslsii/iocs/lakeshore_temperature.py | 28 +++- nslsii/tests/lakeshore_test.py | 19 +++ 3 files changed, 146 insertions(+), 85 deletions(-) diff --git a/nslsii/iocs/lakeshore_control.py b/nslsii/iocs/lakeshore_control.py index b8759d5e..4ed4d21c 100644 --- a/nslsii/iocs/lakeshore_control.py +++ b/nslsii/iocs/lakeshore_control.py @@ -9,6 +9,8 @@ def __init__(self, prefix, *, ioc, **kwargs): super().__init__(prefix, **kwargs) self.ioc = ioc + _false_true_states = ['False', 'True'] + # PVPositioner required attributes _rb_val = 0. @@ -21,26 +23,14 @@ def __init__(self, prefix, *, ioc, **kwargs): dtype=ChannelType.DOUBLE, name='}}T-RB') - @readback.getter - async def readback(self, instance): - return self._rb_val - - @setpoint.putter - async def setpoint(self, instance, value): - self._rb_val = value - return value - _done_val = 0. - done = pvproperty(value=_done_val, - read_only=True, - dtype=ChannelType.DOUBLE, + done = pvproperty(value='False', + read_only=False, + enum_strings=_false_true_states, + dtype=ChannelType.ENUM, name='}}Sts:Ramp-Sts') - @done.getter - async def done(self, instance): - return self._done_val - # top level attributes _heater_range_val = 0. @@ -56,7 +46,7 @@ async def done(self, instance): _mode_val = 0. _enable_val = 0. - _target_channel_val = 0. + _target_channel_val = '' mode = pvproperty(value=_mode_val, dtype=ChannelType.DOUBLE, @@ -65,38 +55,30 @@ async def done(self, instance): dtype=ChannelType.DOUBLE, name='}}Enbl-Sel') target_channel = pvproperty(value=_target_channel_val, - dtype=ChannelType.DOUBLE, + dtype=ChannelType.STRING, name='}}Out-Sel') # ramp attributes _ramp_enable_val = 0. + _ramp_rate_val = 0. ramp_enable = pvproperty(value=_ramp_enable_val, dtype=ChannelType.DOUBLE, name='}}Enbl:Ramp-Sel') - _ramp_rate_val = 0. - ramp_rate_rb = pvproperty(value=_ramp_rate_val, dtype=ChannelType.DOUBLE, name='}}Val:Ramp-RB') ramp_rate_sp = pvproperty(value=_ramp_rate_val, dtype=ChannelType.DOUBLE, - name='}}Val:Ramp-Sp') - - @ramp_rate_rb.getter - async def ramp_rate_rb(self, instance): - return self._ramp_rate_val - - @ramp_rate_sp.putter - async def ramp_rate_sp(self, instance, value): - self._ramp_rate_val = value - return value + name='}}Val:Ramp-SP') # PID loop parameters _pid_proportional_val = 0. + _pid_integral_val = 0. + _pid_derivative_val = 0. pid_proportional_rb = pvproperty(value=_pid_proportional_val, read_only=True, @@ -106,17 +88,6 @@ async def ramp_rate_sp(self, instance, value): dtype=ChannelType.DOUBLE, name='}}Gain:P-SP') - @pid_proportional_rb.getter - async def pid_proportional_rb(self, instance): - return self._pid_proportional_val - - @pid_proportional_sp.putter - async def pid_proportional_sp(self, instance, value): - self._pid_proportional_val = value - return value - - _pid_integral_val = 0. - pid_integral_rb = pvproperty(value=_pid_integral_val, read_only=True, dtype=ChannelType.DOUBLE, @@ -125,17 +96,6 @@ async def pid_proportional_sp(self, instance, value): dtype=ChannelType.DOUBLE, name='}}Gain:I-SP') - @pid_integral_rb.getter - async def pid_integral_rb(self, instance): - return self._pid_integral_val - - @pid_integral_sp.putter - async def pid_integral_sp(self, instance, value): - self._pid_integral_val = value - return value - - _pid_derivative_val = 0. - pid_derivative_rb = pvproperty(value=_pid_derivative_val, read_only=True, dtype=ChannelType.DOUBLE, @@ -144,25 +104,17 @@ async def pid_integral_sp(self, instance, value): dtype=ChannelType.DOUBLE, name='}}Gain:D-SP') - @pid_derivative_rb.getter - async def pid_derivative_rb(self, instance): - return self._pid_derivative_val - - @pid_derivative_sp.putter - async def pid_derivative_sp(self, instance, value): - self._pid_derivative_val = value - return value - # output parameters _out_current_val = 0. + _out_man_current_val = 0. + _out_max_current_val = 0. + _out_resistance_val = 0. out_current = pvproperty(value=_out_current_val, dtype=ChannelType.DOUBLE, name='}}Out-I') - _out_man_current_val = 0. - out_man_current_rb = pvproperty(value=_out_man_current_val, read_only=True, dtype=ChannelType.DOUBLE, @@ -171,6 +123,92 @@ async def pid_derivative_sp(self, instance, value): dtype=ChannelType.DOUBLE, name='}}Out:Man-SP') + out_max_current_rb = pvproperty(value=_out_max_current_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Out:MaxI-RB') + out_max_current_sp = pvproperty(value=_out_max_current_val, + dtype=ChannelType.DOUBLE, + name='}}Out:MaxI-SP') + + out_resistance_rb = pvproperty(value=_out_resistance_val, + read_only=True, + dtype=ChannelType.DOUBLE, + name='}}Out:R-RB') + out_resistance_sp = pvproperty(value=_out_resistance_val, + dtype=ChannelType.DOUBLE, + name='}}Out:R-SP') + + # Putter/Getter Methods + + @setpoint.putter + async def setpoint(self, instance, value): + + # select channel + prefix = self.ioc.prefix.replace('{', '{'*2) + channel = self._target_channel_val + t_k = f'{prefix}-Chan:{channel}' + if t_k in self.ioc.groups: + pass + else: + return instance.value + t_v = self.ioc.groups[t_k] + + # apply cmd + await t_v.cmd.write(value=value) + + self._rb_val = value + return value + + @done.getter + async def done(self, instance): + return self._done_val + + @target_channel.getter + async def target_channel(self, instance): + return self._target_channel_val + + @target_channel.putter + async def target_channel(self, instance, value): + self._target_channel_val = value + return value + + @ramp_rate_rb.getter + async def ramp_rate_rb(self, instance): + return self._ramp_rate_val + + @ramp_rate_sp.putter + async def ramp_rate_sp(self, instance, value): + self._ramp_rate_val = value + return value + + @pid_proportional_rb.getter + async def pid_proportional_rb(self, instance): + return self._pid_proportional_val + + @pid_proportional_sp.putter + async def pid_proportional_sp(self, instance, value): + self._pid_proportional_val = value + return value + + @pid_integral_rb.getter + async def pid_integral_rb(self, instance): + return self._pid_integral_val + + @pid_integral_sp.putter + async def pid_integral_sp(self, instance, value): + self._pid_integral_val = value + return value + + @pid_derivative_rb.getter + async def pid_derivative_rb(self, instance): + return self._pid_derivative_val + + @pid_derivative_sp.putter + async def pid_derivative_sp(self, instance, value): + self._pid_derivative_val = value + return value + @out_man_current_rb.getter async def out_man_current_rb(self, instance): return self._out_man_current_val @@ -180,16 +218,6 @@ async def out_man_current_sp(self, instance, value): self._out_man_current_val = value return value - _out_max_current_val = 0. - - out_max_current_rb = pvproperty(value=_out_max_current_val, - read_only=True, - dtype=ChannelType.DOUBLE, - name='}}Out:MaxI-RB') - out_max_current_sp = pvproperty(value=_out_max_current_val, - dtype=ChannelType.DOUBLE, - name='}}Out:MaxI-SP') - @out_max_current_rb.getter async def out_max_current_rb(self, instance): return self._out_max_current_val @@ -199,16 +227,6 @@ async def out_max_current_sp(self, instance, value): self._out_max_current_val = value return value - _out_resistance_val = 0. - - out_resistance_rb = pvproperty(value=_out_resistance_val, - read_only=True, - dtype=ChannelType.DOUBLE, - name='}}Out:R-RB') - out_resistance_sp = pvproperty(value=_out_resistance_val, - dtype=ChannelType.DOUBLE, - name='}}Out:R-SP') - @out_resistance_rb.getter async def out_resistance_rb(self, instance): return self._out_resistance_val diff --git a/nslsii/iocs/lakeshore_temperature.py b/nslsii/iocs/lakeshore_temperature.py index 975ebee4..86abc13f 100644 --- a/nslsii/iocs/lakeshore_temperature.py +++ b/nslsii/iocs/lakeshore_temperature.py @@ -11,6 +11,8 @@ def __init__(self, prefix, *, ioc, **kwargs): super().__init__(prefix, **kwargs) self.ioc = ioc + _false_true_states = ['False', 'True'] + _T_val = 0. _TC_val = 0. _V_val = 0. @@ -79,13 +81,34 @@ async def cmd(self, instance, async_lib): instance.async_lib = async_lib @cmd.putter - async def cmd(self, instance, value): + async def cmd(self, instance, cmd): + + value = float(cmd) + + # check lock if self.putter_lock.locked() is True: return instance.value else: self.putter_lock.acquire() + # select the lakeshore control + + prefix = self.ioc.prefix.replace('{', '{'*2) + ctrl_suffix = 1 + c_k = f'{prefix}-Out:{ctrl_suffix}' + ctrl = self.ioc.groups[c_k] + + # update the temp and ctrl readbacks + + await ctrl.done.write(value='False') + + await self.T.write(value=value) + await ctrl.readback.write(value=value) + + await ctrl.done.write(value='True') + + ''' p0 = instance.value dwell = self._step_size/self._velocity N = max(1, int((value - p0) / self._step_size)) @@ -95,6 +118,7 @@ async def cmd(self, instance, value): await instance.async_lib.library.sleep(dwell) await self.T.write(value=new_value) - self.putter_lock.release() + ''' + self.putter_lock.release() return value diff --git a/nslsii/tests/lakeshore_test.py b/nslsii/tests/lakeshore_test.py index 4fe73402..fe24ccda 100644 --- a/nslsii/tests/lakeshore_test.py +++ b/nslsii/tests/lakeshore_test.py @@ -187,3 +187,22 @@ def test_device_level(self): res = self.tc.ctrl.out1.setpoint.get() assert res == 0.0 + + res = self.tc.ctrl.out1.target_channel.get() + assert res == '' + + def test_target_channel(self): + + self.tc.ctrl.out1.target_channel.put('A') + res = self.tc.ctrl.out1.target_channel.get() + assert res == 'A' + + def test_ctrl_setpoint(self): + + new_value = 1.0 + + self.tc.ctrl.out1.target_channel.put('A') + self.tc.ctrl.out1.setpoint.put(new_value) + + res = self.tc.temp.A.T.get() + assert res == new_value From 63390d8435d51269b7f20c6245f3ad673b2b8cec Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Fri, 24 May 2019 20:37:45 -0400 Subject: [PATCH 3/6] added ramp --- nslsii/iocs/lakeshore_control.py | 9 ++-- nslsii/iocs/lakeshore_ioc_sim.py | 6 +-- nslsii/iocs/lakeshore_temperature.py | 68 ++++++++++++++++----------- nslsii/tests/lakeshore_test.py | 69 +++++++++++++++++++++++++--- 4 files changed, 113 insertions(+), 39 deletions(-) diff --git a/nslsii/iocs/lakeshore_control.py b/nslsii/iocs/lakeshore_control.py index 4ed4d21c..232fc8ab 100644 --- a/nslsii/iocs/lakeshore_control.py +++ b/nslsii/iocs/lakeshore_control.py @@ -5,8 +5,9 @@ class ControlRecord(PVGroup): - def __init__(self, prefix, *, ioc, **kwargs): + def __init__(self, prefix, *, indx, ioc, **kwargs): super().__init__(prefix, **kwargs) + self._indx = indx self.ioc = ioc _false_true_states = ['False', 'True'] @@ -61,7 +62,7 @@ def __init__(self, prefix, *, ioc, **kwargs): # ramp attributes _ramp_enable_val = 0. - _ramp_rate_val = 0. + _ramp_rate_val = 5. # degree/s ramp_enable = pvproperty(value=_ramp_enable_val, dtype=ChannelType.DOUBLE, @@ -155,7 +156,9 @@ async def setpoint(self, instance, value): t_v = self.ioc.groups[t_k] # apply cmd - await t_v.cmd.write(value=value) + indx = self._indx + cmd = f'{value},{indx}' + await t_v.cmd.write(value=cmd) self._rb_val = value return value diff --git a/nslsii/iocs/lakeshore_ioc_sim.py b/nslsii/iocs/lakeshore_ioc_sim.py index 067045f2..288a6aae 100644 --- a/nslsii/iocs/lakeshore_ioc_sim.py +++ b/nslsii/iocs/lakeshore_ioc_sim.py @@ -24,12 +24,12 @@ def create_ioc(prefix, temperatures, controls, **ioc_options): for t in temperatures: t_prefix = f'{prefix}-Chan:{t}' print('t_prefix:', t_prefix) - groups[t_prefix] = TemperatureRecord(t_prefix, ioc=ioc) + groups[t_prefix] = TemperatureRecord(t_prefix, indx=t, ioc=ioc) for c in controls: c_prefix = f'{prefix}-Out:{c}' print('c_prefix:', c_prefix) - groups[c_prefix] = ControlRecord(c_prefix, ioc=ioc) + groups[c_prefix] = ControlRecord(c_prefix, indx=c, ioc=ioc) for prefix, group in groups.items(): ioc.pvdb.update(**group.pvdb) @@ -44,7 +44,7 @@ def create_ioc(prefix, temperatures, controls, **ioc_options): desc='Lakeshore IOC.') temperatures = ['A', 'B', 'C', 'D'] - controls = [1] + controls = [1, 2] ioc = create_ioc(temperatures=temperatures, controls=controls, diff --git a/nslsii/iocs/lakeshore_temperature.py b/nslsii/iocs/lakeshore_temperature.py index 86abc13f..bc4f0286 100644 --- a/nslsii/iocs/lakeshore_temperature.py +++ b/nslsii/iocs/lakeshore_temperature.py @@ -7,19 +7,20 @@ class TemperatureRecord(PVGroup): - def __init__(self, prefix, *, ioc, **kwargs): + def __init__(self, prefix, *, indx, ioc, **kwargs): super().__init__(prefix, **kwargs) + self._indx = indx self.ioc = ioc _false_true_states = ['False', 'True'] + _step_size = 1 # degree - _T_val = 0. - _TC_val = 0. + _T_val = 273.15 _V_val = 0. _status_val = 0. _display_name = 'Lakeshore T' - _alarm_high_val = 5.0 - _alarm_low_val = 3.0 + _alarm_high_val = 400.0 + _alarm_low_val = 200.0 putter_lock = Lock() @@ -28,7 +29,7 @@ def __init__(self, prefix, *, ioc, **kwargs): dtype=ChannelType.DOUBLE, name='}}T-I') - T_celsius = pvproperty(value=_TC_val, + T_celsius = pvproperty(value=_T_val - 273.15, read_only=True, dtype=ChannelType.DOUBLE, name='}}T:C-I') @@ -47,13 +48,14 @@ def __init__(self, prefix, *, ioc, **kwargs): display_name_sp = pvproperty(value=_display_name, dtype=ChannelType.STRING, name='}}T:Name-SP') - alarm_high = pvproperty(value=_alarm_high_val, - read_only=True, - dtype=ChannelType.DOUBLE, + + alarm_high = pvproperty(value='False', + enum_strings=_false_true_states, + dtype=ChannelType.ENUM, name='}}Alrm:High-Sts') - alarm_low = pvproperty(value=_alarm_low_val, - read_only=True, - dtype=ChannelType.DOUBLE, + alarm_low = pvproperty(value='False', + enum_strings=_false_true_states, + dtype=ChannelType.ENUM, name='}}Alrm:Low-Sts') _T_lim_val = 0. @@ -83,7 +85,25 @@ async def cmd(self, instance, async_lib): @cmd.putter async def cmd(self, instance, cmd): - value = float(cmd) + cmd_list = cmd.split(',') # value, ctrl indx + value = float(cmd_list[0]) + ctrl_indx = int(cmd_list[1]) + + # check alarm high + + if value >= self._alarm_high_val: + await self.alarm_high.write(value=1) + return instance.value + else: + await self.alarm_high.write(value=0) + + # check alarm low + + if value <= self._alarm_low_val: + await self.alarm_low.write(value=1) + return instance.value + else: + await self.alarm_low.write(value=0) # check lock @@ -95,30 +115,26 @@ async def cmd(self, instance, cmd): # select the lakeshore control prefix = self.ioc.prefix.replace('{', '{'*2) - ctrl_suffix = 1 - c_k = f'{prefix}-Out:{ctrl_suffix}' + c_k = f'{prefix}-Out:{ctrl_indx}' ctrl = self.ioc.groups[c_k] # update the temp and ctrl readbacks - await ctrl.done.write(value='False') - - await self.T.write(value=value) - await ctrl.readback.write(value=value) - - await ctrl.done.write(value='True') + await ctrl.done.write(value=0) - ''' - p0 = instance.value - dwell = self._step_size/self._velocity + p0 = self._T_val + dwell = self._step_size/ctrl._ramp_rate_val N = max(1, int((value - p0) / self._step_size)) for j in range(N): new_value = p0 + self._step_size*(j+1) await instance.async_lib.library.sleep(dwell) - await self.T.write(value=new_value) + await self.T.write(value=new_value) + await self.T_celsius.write(value=(new_value - 273.15)) + await ctrl.readback.write(value=new_value) - ''' + self._T_val = value + await ctrl.done.write(value=1) self.putter_lock.release() return value diff --git a/nslsii/tests/lakeshore_test.py b/nslsii/tests/lakeshore_test.py index fe24ccda..b0f133c0 100644 --- a/nslsii/tests/lakeshore_test.py +++ b/nslsii/tests/lakeshore_test.py @@ -43,7 +43,7 @@ def create_device_from_components(name, *, docstring=None, def lakeshore336(name='Lakeshore336', temperatures=['A', 'B', 'C', 'D'], - controls=[1], docstring=None, + controls=[1, 2], docstring=None, default_read_attrs=None, default_configuration_attrs=None): def _set_fields(fields, cls, prefix, field_prefix='', **kwargs): @@ -164,14 +164,13 @@ def ioc_sim(request): # teardown code ioc_process.terminate() - - + @pytest.mark.usefixtures('ioc_sim') class TestIOC: def test_caproto_level(self): - t_rb = read('test:{-Chan:A}T-I') + t_rb = read('test:{-Chan:A}T:C-I') assert t_rb.data[0] == 0.0 c_sp = read('test:{-Out:1}T-SP') @@ -182,7 +181,7 @@ def test_caproto_level(self): def test_device_level(self): - res = self.tc.temp.A.T.get() + res = self.tc.temp.A.T_celsius.get() assert res == 0.0 res = self.tc.ctrl.out1.setpoint.get() @@ -197,12 +196,68 @@ def test_target_channel(self): res = self.tc.ctrl.out1.target_channel.get() assert res == 'A' - def test_ctrl_setpoint(self): + def test_alarm_low(self): + + new_value = 150.0 + + res = self.tc.temp.A.alarm.low.get() + assert res == 0 + + self.tc.ctrl.out1.target_channel.put('A') + self.tc.ctrl.out1.setpoint.put(new_value) + + res = self.tc.temp.A.alarm.low.get() + assert res == 1 + + def test_alarm_high(self): + + new_value = 450.0 + + res = self.tc.temp.A.alarm.high.get() + assert res == 0 + + self.tc.ctrl.out1.target_channel.put('A') + self.tc.ctrl.out1.setpoint.put(new_value) + + res = self.tc.temp.A.alarm.high.get() + assert res == 1 + + def test_1_A(self): - new_value = 1.0 + value = self.tc.temp.A.T.get() + new_value = value + 10 + + rate = self.tc.ctrl.out1.ramp.rate.get() + t = (new_value-value)/rate self.tc.ctrl.out1.target_channel.put('A') self.tc.ctrl.out1.setpoint.put(new_value) + time.sleep(t+1) + + res = self.tc.ctrl.out1.done.get() + assert res == 1 + res = self.tc.temp.A.T.get() assert res == new_value + + def test_2_B(self): + + value = self.tc.temp.B.T.get() + new_value = value + 10 + + rate = self.tc.ctrl.out2.ramp.rate.get() + t = (new_value-value)/rate + + self.tc.ctrl.out2.target_channel.put('B') + self.tc.ctrl.out2.setpoint.put(new_value) + + time.sleep(t+1) + + res = self.tc.ctrl.out2.done.get() + assert res == 1 + + res = self.tc.temp.B.T.get() + assert res == new_value + + From 90c7b5bde0fac44d7857adf11ef11f907e704a93 Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Fri, 24 May 2019 20:41:22 -0400 Subject: [PATCH 4/6] added ramp --- nslsii/iocs/lakeshore_temperature.py | 2 +- nslsii/tests/lakeshore_test.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nslsii/iocs/lakeshore_temperature.py b/nslsii/iocs/lakeshore_temperature.py index bc4f0286..431967b9 100644 --- a/nslsii/iocs/lakeshore_temperature.py +++ b/nslsii/iocs/lakeshore_temperature.py @@ -129,7 +129,7 @@ async def cmd(self, instance, cmd): for j in range(N): new_value = p0 + self._step_size*(j+1) await instance.async_lib.library.sleep(dwell) - await self.T.write(value=new_value) + await self.T.write(value=new_value) await self.T_celsius.write(value=(new_value - 273.15)) await ctrl.readback.write(value=new_value) diff --git a/nslsii/tests/lakeshore_test.py b/nslsii/tests/lakeshore_test.py index b0f133c0..f640fb3b 100644 --- a/nslsii/tests/lakeshore_test.py +++ b/nslsii/tests/lakeshore_test.py @@ -164,7 +164,8 @@ def ioc_sim(request): # teardown code ioc_process.terminate() - + + @pytest.mark.usefixtures('ioc_sim') class TestIOC: @@ -259,5 +260,3 @@ def test_2_B(self): res = self.tc.temp.B.T.get() assert res == new_value - - From c9c4bd3b583a111a4e3fe43a59f26c60a092d4a9 Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Fri, 24 May 2019 20:48:35 -0400 Subject: [PATCH 5/6] renamed temperature_controllers_test --- ...erature_controllers_test.py => temperature_controllers_test.p} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename nslsii/tests/{temperature_controllers_test.py => temperature_controllers_test.p} (100%) diff --git a/nslsii/tests/temperature_controllers_test.py b/nslsii/tests/temperature_controllers_test.p similarity index 100% rename from nslsii/tests/temperature_controllers_test.py rename to nslsii/tests/temperature_controllers_test.p From 7834bcb76650a42c4c0a92a2d079615d0b9455d4 Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Mon, 27 May 2019 21:46:22 -0400 Subject: [PATCH 6/6] added exp/sin function --- nslsii/iocs/lakeshore_temperature.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/nslsii/iocs/lakeshore_temperature.py b/nslsii/iocs/lakeshore_temperature.py index 431967b9..f8f814c0 100644 --- a/nslsii/iocs/lakeshore_temperature.py +++ b/nslsii/iocs/lakeshore_temperature.py @@ -2,6 +2,8 @@ from caproto.server import pvproperty, PVGroup from caproto import ChannelType +import numpy as np + from threading import Lock @@ -77,6 +79,17 @@ def __init__(self, prefix, *, indx, ioc, **kwargs): dtype=ChannelType.STRING, name='}}Cmd') + def exp_sin_func(self, p0, p1, dwell, j, v): + omega = np.pi + setpoint = p1 + Tvar = setpoint*0.1 + K = (p1 - p0)/v/2.5 + dt = dwell*(j+1) + return Tvar*np.exp(-dt / K) * np.sin(omega * dt) + setpoint + + def linear_func(self, p0, dp, j): + return p0 + dp*(j+1) + @cmd.startup async def cmd(self, instance, async_lib): instance.ev = async_lib.library.Event() @@ -123,11 +136,15 @@ async def cmd(self, instance, cmd): await ctrl.done.write(value=0) p0 = self._T_val - dwell = self._step_size/ctrl._ramp_rate_val + dp = self._step_size + v = ctrl._ramp_rate_val + dwell = dp/v N = max(1, int((value - p0) / self._step_size)) for j in range(N): - new_value = p0 + self._step_size*(j+1) + # new_value = p0 + self._step_size*(j+1) + # new_value = self.linear_func(p0, dp, j) + new_value = self.exp_sin_func(p0, value, dwell, j, v) await instance.async_lib.library.sleep(dwell) await self.T.write(value=new_value) await self.T_celsius.write(value=(new_value - 273.15))