Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 077a182b4d | |||
| 3e7e53135c | |||
| 57b567f453 | |||
|
|
2b42e3fa0a | ||
|
|
5b0da3ba98 | ||
|
|
c80b4ac5fb | ||
|
|
8cb9154bb5 | ||
|
|
813d1b76ef | ||
|
|
183709b7ce | ||
| 2cdf1fc58e | |||
| ffaa9c83bd | |||
| f9a0fdf7e4 | |||
| 7dfb2ff4e3 | |||
| 84c0017c03 | |||
| 2126956160 | |||
| 4cdd3b0709 | |||
|
|
15d38d7cc1 | ||
|
|
9904d31f0b | ||
|
|
b07d2ae8a3 | ||
|
|
7d7cb02f17 | ||
|
|
1017925ca0 | ||
| bb14d02884 | |||
| 4c499cf048 | |||
| e403396941 | |||
| 5b42df4a5e | |||
| 841ef224f6 | |||
| 8142ba746d | |||
|
|
5358412b7a | ||
| 010f0747e1 | |||
|
|
047c52b5a5 | ||
|
|
f846c5cb31 | ||
|
|
0e4a427bc3 | ||
|
|
2d8b609a3c | ||
|
|
6e3865b345 | ||
| 0004dc7620 | |||
| 158477792f | |||
| fd0e762d18 | |||
|
|
a16ec6cc91 | ||
|
|
777a2cb6a9 | ||
| cb3e98f86d | |||
| a8bafde64e | |||
| 36c512d50b | |||
|
|
17b7a01ce1 | ||
| be66faa591 | |||
| e27b4f72b5 | |||
|
|
bc7922f5c8 | ||
|
|
99a58933ec | ||
| 9e000528d2 | |||
| 4a2ce62dd8 | |||
| 9e6699dd1e | |||
|
|
416cdd5a88 | ||
|
|
1bd188e326 | ||
|
|
f7b29ee959 | ||
|
|
f6a0ccb38b | ||
|
|
b93a0cd87b | ||
| be6ba73c89 | |||
| c075738584 | |||
| 0fa2e8332d | |||
| afb49199a1 | |||
| 416fe6ddc0 | |||
| e3cb5d2e60 | |||
|
|
998367a727 | ||
|
|
ab918a33ae | ||
|
|
397ec2efbd | ||
| 67032ff59b | |||
| 03c356590b | |||
| 06bec41ed3 | |||
| 4cd6929d4b | |||
| a89f7a3c44 | |||
| a4330081b7 | |||
| 3b997d7d86 | |||
| 612295d360 | |||
| 9e39a43193 | |||
| 6adfafaa27 | |||
| f6c4090b96 | |||
| ecef2b8974 | |||
|
|
96a7e2109b | ||
|
|
2f3c68a5c5 | ||
|
|
e9a195d61e | ||
|
|
6ac3938b78 | ||
|
|
b4cfdcfc1a | ||
|
|
d32fb647a6 | ||
|
|
abf7859fd6 | ||
|
|
55ea2b8cc4 | ||
| 27600e3ddf | |||
|
|
6b4244f071 | ||
| 1d81fc6fcd | |||
| dfce0bdfbc | |||
| c39aef10aa | |||
| 45dd87060b | |||
| 8019b359c4 | |||
| 4c5109e5a3 | |||
| bf4b3e5683 | |||
| af34fef1e1 | |||
| 5e1c22ba28 | |||
| 0bc4a63aa7 | |||
| cb2c10655c | |||
| 6c49abea74 | |||
| dee8f8929e | |||
| 2e143963df | |||
| 4bc82c2896 | |||
| 833a68db51 | |||
| b9f046a665 | |||
| 9d9b5b2694 | |||
| 255adbf8d9 | |||
|
|
bc0133f55a | ||
|
|
09e59b93d8 | ||
| 2474dc5e72 | |||
|
|
9dab41441f | ||
|
|
4af46a0ea2 | ||
|
|
b844b83352 | ||
|
|
3b63e32395 | ||
|
|
5168e0133d | ||
|
|
9ea6082ed8 | ||
|
|
f205cf76aa | ||
| db9ce02028 | |||
| c4a39306e4 | |||
| 024de0bd32 | |||
| d2d63c47e1 | |||
| 565e8e6fd3 | |||
| 89bc7f6dfe | |||
| c69fe1571a | |||
| c40033a816 | |||
| da37175cbb | |||
| 2020928289 | |||
| 9df6794678 | |||
| 41f3b7526e | |||
|
|
f80624b48d | ||
|
|
9e2e6074c8 | ||
| 5a13888498 | |||
| 5a8a6b88ff | |||
| b84b7964e3 | |||
|
|
6c5dddc449 | ||
| 78fa49ef74 | |||
| d92b154292 | |||
| 073fe1a08b | |||
| f80c793cd9 | |||
| 519e9e2ed7 | |||
| 14036160f7 | |||
| 131dc60807 | |||
| 49722a858f | |||
| c61b674382 | |||
| 091543be56 | |||
| d2885bdd72 | |||
| 975593dd6b | |||
|
|
4fe28363d3 | ||
| 28b19dbf57 | |||
| 05189d094a | |||
| 47da14eef9 | |||
|
|
7904f243cb | ||
| a2fed8df03 | |||
| 19f965bced | |||
| 3e4ea2515e | |||
| 714c820115 | |||
| a8e1d0e1e8 | |||
|
|
d7a1604bd5 | ||
| b92095974b | |||
|
|
8dc9c57e9d | ||
|
|
7c95f1f8ee | ||
| 3786d2f209 | |||
| 138b84e84c | |||
| 997e8e26e9 | |||
| 644d005dad | |||
| 36dfe968e8 | |||
| 0932228596 | |||
|
|
dff0c819de | ||
|
|
fd917724d8 | ||
| bf43858031 | |||
| f354b19cf0 | |||
| f304ac019e | |||
| 9a9a22588f | |||
| 3e26dd49d0 | |||
| 5a456a82b0 | |||
| f6868da3b9 | |||
| ee31f8fb45 | |||
| a6a3f80e30 | |||
| ad36ab1067 | |||
| f2d795cfba | |||
| c04337c3a4 | |||
| 57d5298c92 | |||
| 9a6421a54f | |||
| c5d429346d |
22
calibtest.py
Normal file
22
calibtest.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import sys
|
||||
import os
|
||||
from glob import glob
|
||||
from frappy_psi.calcurve import CalCurve
|
||||
|
||||
os.chdir('/Users/zolliker/gitpsi/calcurves')
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
calib = sys.argv[1]
|
||||
c = CalCurve(calib)
|
||||
else:
|
||||
for file in sorted(glob('*.*')):
|
||||
if file.endswith('.md') or file.endswith('.std'):
|
||||
continue
|
||||
try:
|
||||
c = CalCurve(file)
|
||||
xy = c.export()
|
||||
print('%9.4g %12.7g %9.4g %9.4g %s' % (tuple(c.extx) + tuple(c.exty) + (file,)))
|
||||
except Exception as e:
|
||||
print(file, e)
|
||||
calib = file
|
||||
|
||||
@@ -6,7 +6,7 @@ Node('QnwTC1test.psi.ch',
|
||||
Mod('io',
|
||||
'frappy_psi.qnw.QnwIO',
|
||||
'connection for Quantum northwest',
|
||||
uri='tcp://ldm-fi-ts:3001',
|
||||
uri='tcp://ldmcc01-ts:3004',
|
||||
)
|
||||
|
||||
Mod('T',
|
||||
|
||||
@@ -6,7 +6,7 @@ Node('TFA10.psi.ch',
|
||||
Mod('io',
|
||||
'frappy_psi.thermofisher.ThermFishIO',
|
||||
'connection for ThermoFisher A10',
|
||||
uri='tcp://ldm-fi-ts:3002',
|
||||
uri='tcp://ldmse-d910-ts:3001',
|
||||
)
|
||||
|
||||
Mod('T',
|
||||
|
||||
@@ -24,6 +24,7 @@ Mod('ts_low',
|
||||
minrange=13,
|
||||
range=22,
|
||||
tolerance = 0.1,
|
||||
vexc = 3,
|
||||
htrrng=4,
|
||||
)
|
||||
|
||||
@@ -32,7 +33,8 @@ Mod('ts_high',
|
||||
'sample Cernox',
|
||||
channel = 1,
|
||||
switcher = 'lsc_channel',
|
||||
minrange=9,
|
||||
minrange=11,
|
||||
vexc = 5,
|
||||
range=22,
|
||||
tolerance = 0.1,
|
||||
htrrng=5,
|
||||
@@ -45,6 +47,8 @@ Mod('ts',
|
||||
value=Param(unit='K'),
|
||||
low='ts_low',
|
||||
high='ts_high',
|
||||
#min_high=0.6035,
|
||||
#max_low=1.6965,
|
||||
min_high=0.6,
|
||||
max_low=1.7,
|
||||
tolerance=0.1,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
Node('lockin830test.psi.ch',
|
||||
'lockin830 test',
|
||||
'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('io',
|
||||
'frappy_psi.SR830.SR830_IO',
|
||||
'lockin communication',
|
||||
uri='tcp://linse-976d-ts:3002',
|
||||
)
|
||||
|
||||
Mod('XY',
|
||||
'frappy_psi.SR830.XY',
|
||||
'XY channels',
|
||||
io='io',
|
||||
)
|
||||
@@ -1,12 +1,14 @@
|
||||
Node('flamemag.psi.ch',
|
||||
'flame magnet',
|
||||
interface='tcp://5000'
|
||||
interface='tcp://5000',
|
||||
)
|
||||
|
||||
sea_cfg = 'flamemag.config'
|
||||
|
||||
Mod('cio',
|
||||
'frappy_psi.cryoltd.IO',
|
||||
'IO to cryo ltd software',
|
||||
uri='tcp://flamedil:3128',
|
||||
uri='tcp://flamemag:3128',
|
||||
)
|
||||
|
||||
Mod('main',
|
||||
@@ -24,7 +26,7 @@ Mod('B',
|
||||
target=Param(
|
||||
max=35000.0,
|
||||
),
|
||||
mode='PERSISTENT',
|
||||
#mode='PERSISTENT',
|
||||
hw_units='T',
|
||||
A_to_G=285.73,
|
||||
ramp=Param(
|
||||
@@ -32,7 +34,7 @@ Mod('B',
|
||||
),
|
||||
overshoot={'o': 1.0, 't': 180.0},
|
||||
degauss={'s': 500.0, 'd': 30.0, 'f': 5.0, 't': 120.0},
|
||||
tolerance=5.0,
|
||||
tolerance=50.0,
|
||||
wait_switch_on = 30,
|
||||
wait_switch_off = 30,
|
||||
wait_stable_field=180.0,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
Node('multimetertest.psi.ch',
|
||||
'multimeter test',
|
||||
'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('io',
|
||||
'frappy_psi.HP.HP_IO',
|
||||
'multimeter communication',
|
||||
uri='/dev/cu.usbserial-21410',
|
||||
)
|
||||
|
||||
Mod('Voltage',
|
||||
'frappy_psi.HP.Voltage',
|
||||
'voltage',
|
||||
io='io',
|
||||
)
|
||||
|
||||
Mod('Current',
|
||||
'frappy_psi.HP.Current',
|
||||
'current',
|
||||
io='io',
|
||||
)
|
||||
|
||||
Mod('Resistance',
|
||||
'frappy_psi.HP.Resistance',
|
||||
'resistivity',
|
||||
io='io',
|
||||
)
|
||||
|
||||
Mod('Frequency',
|
||||
'frappy_psi.HP.Frequency',
|
||||
'resistivity',
|
||||
io='io',
|
||||
)
|
||||
@@ -1,67 +0,0 @@
|
||||
Node('bridge.psi.ch',
|
||||
'ac resistance bridge',
|
||||
'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('io',
|
||||
'frappy_psi.bridge.BridgeIO',
|
||||
'communication to sim900',
|
||||
uri='serial:///dev/cu.usbserial-14340',
|
||||
)
|
||||
|
||||
Mod('res1',
|
||||
'frappy_psi.bridge.Resistance',
|
||||
'module communication',
|
||||
io='io',
|
||||
port=1,
|
||||
)
|
||||
|
||||
Mod('res2',
|
||||
'frappy_psi.bridge.Resistance',
|
||||
'module communication',
|
||||
io='io',
|
||||
port=3,
|
||||
)
|
||||
|
||||
Mod('res3',
|
||||
'frappy_psi.bridge.Resistance',
|
||||
'module communication',
|
||||
io='io',
|
||||
port=5,
|
||||
)
|
||||
|
||||
Mod('phase1',
|
||||
'frappy_psi.bridge.Phase',
|
||||
'module communication',
|
||||
resistance='res1',
|
||||
)
|
||||
|
||||
Mod('phase2',
|
||||
'frappy_psi.bridge.Phase',
|
||||
'module communication',
|
||||
resistance='res2',
|
||||
)
|
||||
|
||||
Mod('phase3',
|
||||
'frappy_psi.bridge.Phase',
|
||||
'module communication',
|
||||
resistance='res3',
|
||||
)
|
||||
|
||||
Mod('dev1',
|
||||
'frappy_psi.bridge.Deviation',
|
||||
'module communication',
|
||||
resistance='res1',
|
||||
)
|
||||
|
||||
Mod('dev2',
|
||||
'frappy_psi.bridge.Deviation',
|
||||
'module communication',
|
||||
resistance='res1',
|
||||
)
|
||||
|
||||
Mod('dev3',
|
||||
'frappy_psi.bridge.Deviation',
|
||||
'module communication',
|
||||
resistance='res3',
|
||||
)
|
||||
@@ -138,13 +138,6 @@ Mod('T_one_K',
|
||||
io='itc',
|
||||
)
|
||||
|
||||
Mod('htr_one_K',
|
||||
'frappy_psi.mercury.HeaterOutput',
|
||||
'1 K plate warmup heater',
|
||||
slot='DB3.H1',
|
||||
io='itc',
|
||||
)
|
||||
|
||||
Mod('T_mix_wup',
|
||||
'frappy_psi.mercury.TemperatureLoop',
|
||||
'mix. chamber warmup temperature',
|
||||
|
||||
@@ -36,12 +36,12 @@ from time import sleep, time as currenttime
|
||||
import PyTango
|
||||
|
||||
from frappy.datatypes import ArrayOf, EnumType, FloatRange, IntRange, \
|
||||
LimitsType, StringType, TupleOf, ValueType
|
||||
LimitsType, StatusType, StringType, TupleOf, ValueType
|
||||
from frappy.errors import CommunicationFailedError, ConfigError, \
|
||||
HardwareError, ProgrammingError, WrongTypeError
|
||||
from frappy.lib import lazy_property
|
||||
from frappy.modules import Command, Drivable, Module, Parameter, Readable, \
|
||||
StatusType, Writable, Property
|
||||
from frappy.modules import Command, Drivable, Module, Parameter, Property, \
|
||||
Readable, Writable
|
||||
|
||||
#####
|
||||
|
||||
|
||||
@@ -194,19 +194,19 @@ class Nmr(Readable):
|
||||
x = val['xval'][:len(val['yval'])]
|
||||
return (x, val['yval'])
|
||||
|
||||
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
||||
@Command(IntRange(1), result=TupleOf(ArrayOf(string, maxlen=100),
|
||||
ArrayOf(floating, maxlen=100)))
|
||||
def get_amplitude(self):
|
||||
"""Last 20 amplitude datapoints."""
|
||||
rv = self.cell.cell.nmr_paramlog_get('amplitude', 20)
|
||||
def get_amplitude(self, count):
|
||||
"""Last <count> amplitude datapoints."""
|
||||
rv = self.cell.cell.nmr_paramlog_get('amplitude', count)
|
||||
x = [ str(timestamp) for timestamp in rv['xval']]
|
||||
return (x,rv['yval'])
|
||||
|
||||
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
||||
@Command(IntRange(1), result=TupleOf(ArrayOf(string, maxlen=100),
|
||||
ArrayOf(floating, maxlen=100)))
|
||||
def get_phase(self):
|
||||
"""Last 20 phase datapoints."""
|
||||
val = self.cell.cell.nmr_paramlog_get('phase', 20)
|
||||
def get_phase(self, count):
|
||||
"""Last <count> phase datapoints."""
|
||||
val = self.cell.cell.nmr_paramlog_get('phase', count)
|
||||
return ([str(timestamp) for timestamp in val['xval']], val['yval'])
|
||||
|
||||
|
||||
|
||||
229
frappy_psi/HP.py
229
frappy_psi/HP.py
@@ -1,229 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors: Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Hewlett-Packard HP34401A Multimeter (not finished)"""
|
||||
|
||||
import re
|
||||
from frappy.core import HasIO, Readable, Parameter, FloatRange, EnumType, StatusType, IDLE, ERROR, WARN
|
||||
|
||||
|
||||
def string_to_value(value):
|
||||
"""
|
||||
Converting the value to float, removing the units, converting the prefix into the number.
|
||||
:param value: value
|
||||
:return: float value without units
|
||||
"""
|
||||
value_with_unit = re.compile(r'(\d+)([pnumkMG]?)')
|
||||
value, pfx = value_with_unit.match(value).groups()
|
||||
pfx_dict = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, 'G': 1e9}
|
||||
if pfx in pfx_dict:
|
||||
value = round(float(value) * pfx_dict[pfx], 12)
|
||||
return float(value)
|
||||
|
||||
|
||||
class HP_IO(HasIO):
|
||||
end_of_line = b'\n'
|
||||
identification = [('*IDN?', r'HEWLETT-PACKARD,34401A,0,.*')]
|
||||
|
||||
|
||||
class HP34401A(HP_IO):
|
||||
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
||||
autorange = Parameter('autorange_on', EnumType('autorange', off=0, on=1), readonly=False, default=0)
|
||||
|
||||
def comm(self, cmd): # read until \n
|
||||
string = f'{cmd}\n'
|
||||
n_string = string.encode()
|
||||
response = self.communicate(n_string)
|
||||
|
||||
if response:
|
||||
return response
|
||||
|
||||
response = self.communicate(n_string)
|
||||
return response if response else None
|
||||
|
||||
def read_range(self, function):
|
||||
return self.comm(f'{function}:range?')
|
||||
|
||||
def write_range(self, function, range):
|
||||
return self.comm(f'{function}:range {range}')
|
||||
|
||||
def write_autorange(self, function):
|
||||
cmd = f'{function}:range:auto {"on" if self.autorange == 0 else "off"}'
|
||||
self.comm(cmd)
|
||||
return self.comm(f'{function}:range:auto?')
|
||||
|
||||
def read_resolution(self, function):
|
||||
return self.comm(f'{function}:resolution?')
|
||||
|
||||
def write_resolution(self, function, resolution):
|
||||
self.comm(f'{function}:resolution {resolution}')
|
||||
return self.comm(f'{function}:resolution?')
|
||||
|
||||
def read_status(self):
|
||||
stb = int(self.comm('*STB?'))
|
||||
esr = int(self.comm('*ESR?'))
|
||||
|
||||
if esr & (1 << 3):
|
||||
return ERROR, 'self-test/calibration/reading failed'
|
||||
if esr & (1 << 4):
|
||||
return ERROR, 'execution error'
|
||||
if esr & (1 << 5):
|
||||
return ERROR, 'syntax error'
|
||||
if esr & (1 << 2):
|
||||
return ERROR, 'query error'
|
||||
if stb & (1 << 3):
|
||||
return WARN, 'questionable data'
|
||||
if stb & (1 << 5):
|
||||
return WARN, 'standard event register is not empty'
|
||||
if stb & (1 << 6):
|
||||
return WARN, 'requested service'
|
||||
|
||||
if any(stb & (1 << i) for i in range(3) or stb & (1 << 7)):
|
||||
return IDLE, ''
|
||||
if esr & (1 << 6):
|
||||
return IDLE, ''
|
||||
if esr & (1 << 7):
|
||||
return IDLE, ''
|
||||
if stb & (1 << 4):
|
||||
return IDLE, 'message available'
|
||||
if esr & (1 << 0):
|
||||
return IDLE, 'operation complete'
|
||||
if esr & (1 << 1):
|
||||
return IDLE, 'not used'
|
||||
|
||||
|
||||
class Voltage(HP34401A, Readable):
|
||||
value = Parameter('voltage', datatype=FloatRange(0.1, 1000), unit='V')
|
||||
range = Parameter('voltage sensitivity value', FloatRange(), unit='V', default=1, readonly=False)
|
||||
resolution = Parameter('resolution')
|
||||
mode = Parameter('measurement mode: ac/dc', readonly=False)
|
||||
|
||||
ioClass = HP_IO
|
||||
|
||||
MODE_NAMES = {0: 'dc', 1: 'ac'}
|
||||
VOLT_RANGE = ['100mV', '1V', '10V', '100V', '1000V']
|
||||
v_range = Parameter('voltage range', EnumType('voltage index range',
|
||||
{name: idx for idx, name in enumerate(VOLT_RANGE)}), readonly=False)
|
||||
|
||||
acdc = None
|
||||
|
||||
def write_mode(self, mode):
|
||||
"""
|
||||
Set the mode - AC or DC
|
||||
:param mode: AC/DC
|
||||
:return:
|
||||
"""
|
||||
if mode == 1:
|
||||
self.comm(f'configure:voltage:AC {self.range}, {self.resolution}')
|
||||
else:
|
||||
self.comm(f'configure:voltage:DC {self.range}, {self.resolution}')
|
||||
self.acdc = self.MODE_NAMES[mode]
|
||||
return self.comm(f'function?')
|
||||
|
||||
def read_value(self):
|
||||
"""
|
||||
Makes a AC/DC voltage measurement.
|
||||
:return: AC/DC value
|
||||
"""
|
||||
return self.comm(f'measure:voltage:{self.acdc}?')
|
||||
|
||||
def write_autorange_acdc(self, function):
|
||||
full_function = f'{function}:{self.acdc}'
|
||||
return self.write_autorange(full_function)
|
||||
|
||||
def read_range_voltage(self):
|
||||
return self.read_range(f'voltage:{self.acdc}')
|
||||
|
||||
def write_range_voltage(self, range):
|
||||
return self.write_range(f'voltage:{self.acdc}', range)
|
||||
|
||||
def write_autorange_voltage(self):
|
||||
return self.write_autorange_acdc('voltage')
|
||||
|
||||
def read_resolution_voltage(self):
|
||||
return self.read_resolution(f'voltage:{self.acdc}')
|
||||
|
||||
def write_resolution_voltage(self, resolution):
|
||||
return self.write_resolution(f'voltage:{self.acdc}', resolution)
|
||||
|
||||
|
||||
class Current(HP34401A, Readable, Voltage):
|
||||
value = Parameter('current', FloatRange, unit='A')
|
||||
range = Parameter('current range', FloatRange)
|
||||
CURR_RANGE_AC = ['10mA', '100mA', '1A', '3A']
|
||||
CURR_RANGE_DC = ['1A', '3A']
|
||||
|
||||
def read_range_current(self):
|
||||
return self.read_range(f'current:{self.acdc}')
|
||||
|
||||
def write_autorange_current(self):
|
||||
return self.write_autorange_acdc('current')
|
||||
|
||||
def write_range_current(self, range):
|
||||
return self.write_range(f'current:{self.acdc}', range)
|
||||
|
||||
def read_resolution_current(self):
|
||||
return self.read_resolution(f'current:{self.acdc}')
|
||||
|
||||
def write_resolution_current(self, resolution):
|
||||
return self.write_resolution(f'current:{self.acdc}', resolution)
|
||||
|
||||
|
||||
class Resistance(HP34401A, Readable):
|
||||
value = Parameter('resistance')
|
||||
mode = Parameter('measurement mode: 2-/4-wire ohms', EnumType(two_wire=2, four_wire=4), readonly=False)
|
||||
resolution = Parameter('resistance measurement resolution')
|
||||
range = Parameter('resistance measurement range')
|
||||
RESIST_RANGE = ['100Om', '1kOm', '10kOm', '100kOm', '1MOm', '10MOm', '100MOm']
|
||||
FUNCTION_MAP = {2: 'resistance', 4: 'fresistance'}
|
||||
|
||||
def write_range_resistance(self, range):
|
||||
return self.write_range(f'{self.FUNCTION_MAP[self.mode]}', range)
|
||||
|
||||
def read_range_resistance(self):
|
||||
return self.read_range(f'{self.FUNCTION_MAP[self.mode]}')
|
||||
|
||||
def write_mode(self, mode):
|
||||
if mode == 2:
|
||||
self.comm(f'configure:resistance {self.range},{self.resolution}')
|
||||
elif mode == 4:
|
||||
self.comm(f'configure:fresistance {self.range}, {self.resolution}')
|
||||
return self.comm('configure?')
|
||||
|
||||
def write_autorange_resistance(self):
|
||||
return self.write_autorange(self.FUNCTION_MAP[self.mode])
|
||||
|
||||
def read_resolution_resistance(self):
|
||||
return self.read_resolution(f'{self.FUNCTION_MAP[self.mode]}')
|
||||
|
||||
def write_resolution_resistance(self, resolution):
|
||||
return self.write_resolution(f'{self.FUNCTION_MAP[self.mode]}', resolution)
|
||||
|
||||
|
||||
class Frequency(HP34401A, Readable):
|
||||
value = Parameter('frequency', FloatRange(3, 300e3), unit='Hz')
|
||||
|
||||
def write_autorange_frequency(self):
|
||||
return self.write_autorange('frequency')
|
||||
|
||||
def read_resolution_frequency(self):
|
||||
return self.read_resolution('frequency')
|
||||
|
||||
def write_resolution_frequency(self, resolution):
|
||||
return self.write_resolution('frequency', resolution)
|
||||
@@ -15,7 +15,6 @@
|
||||
#
|
||||
# Module authors: Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Stanford Research Systems SR830 DS Lock-in Amplifier"""
|
||||
|
||||
import re
|
||||
import time
|
||||
@@ -26,11 +25,6 @@ from frappy.errors import IsBusyError
|
||||
|
||||
|
||||
def string_to_value(value):
|
||||
"""
|
||||
Converting the value to float, removing the units, converting the prefix into the number.
|
||||
:param value: value
|
||||
:return: float value without units
|
||||
"""
|
||||
value_with_unit = re.compile(r'(\d+)([pnumkMG]?)')
|
||||
value, pfx = value_with_unit.match(value).groups()
|
||||
pfx_dict = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, 'G': 1e9}
|
||||
@@ -46,13 +40,6 @@ class SR830_IO(StringIO):
|
||||
|
||||
class StanfRes(HasIO, Readable):
|
||||
def set_par(self, cmd, *args):
|
||||
"""
|
||||
Set parameter.
|
||||
Query commands are the same as setting commands, but they have a question mark.
|
||||
:param cmd: command
|
||||
:param args: value(s)
|
||||
:return: reply
|
||||
"""
|
||||
head = ','.join([cmd] + [a if isinstance(a, str) else f'{a:g}' for a in args])
|
||||
tail = cmd.replace(' ', '? ')
|
||||
new_tail = re.sub(r'[0-9.]+', '', tail)
|
||||
@@ -162,10 +149,6 @@ class XY(StanfRes):
|
||||
return IDLE, ''
|
||||
|
||||
def read_value(self):
|
||||
"""
|
||||
Read XY. The manual autorange implemented.
|
||||
:return:
|
||||
"""
|
||||
if self.read_status()[0] == BUSY:
|
||||
raise IsBusyError('changing gain')
|
||||
reply = self.get_par('SNAP? 1, 2')
|
||||
@@ -183,13 +166,11 @@ class XY(StanfRes):
|
||||
return int(self.get_par('SENS?'))
|
||||
|
||||
def read_range(self):
|
||||
"""Sensitivity range value"""
|
||||
idx = self.read_irange()
|
||||
name = self.SEN_RANGE[idx]
|
||||
return string_to_value(name)
|
||||
|
||||
def write_irange(self, irange):
|
||||
"""Index of sensitivity from the range"""
|
||||
value = int(irange)
|
||||
self.set_par(f'SENS {value}')
|
||||
self._autogain_started = time.time()
|
||||
@@ -197,12 +178,6 @@ class XY(StanfRes):
|
||||
return value
|
||||
|
||||
def write_range(self, target):
|
||||
"""
|
||||
Setting the sensitivity range.
|
||||
cl_idx/cl_value is the closest index/value from the range to the target
|
||||
:param target:
|
||||
:return: closest value of the sensitivity range
|
||||
"""
|
||||
target = float(target)
|
||||
cl_idx = None
|
||||
cl_value = float('inf')
|
||||
@@ -219,7 +194,6 @@ class XY(StanfRes):
|
||||
return cl_value
|
||||
|
||||
def read_itc(self):
|
||||
"""Time constant index from the range"""
|
||||
return int(self.get_par(f'OFLT?'))
|
||||
|
||||
def write_itc(self, itc):
|
||||
@@ -227,18 +201,11 @@ class XY(StanfRes):
|
||||
return self.set_par(f'OFLT {value}')
|
||||
|
||||
def read_tc(self):
|
||||
"""Read time constant value from the range"""
|
||||
idx = self.read_itc()
|
||||
name = self.TIME_CONST[idx]
|
||||
return string_to_value(name)
|
||||
|
||||
def write_tc(self, target):
|
||||
"""
|
||||
Setting the time constant from the range.
|
||||
cl_idx/cl_value is the closest index/value from the range to the target
|
||||
:param target: time constant
|
||||
:return: closest time constant value
|
||||
"""
|
||||
target = float(target)
|
||||
cl_idx = None
|
||||
cl_value = float('inf')
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors: Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Stanford Research Systems SIM900 Mainframe"""
|
||||
|
||||
import re
|
||||
from frappy.core import StringIO, HasIO, Readable, \
|
||||
Parameter, FloatRange, IntRange, EnumType, \
|
||||
Property, Attached, IDLE, ERROR, WARN
|
||||
|
||||
|
||||
def string_to_value(value):
|
||||
"""
|
||||
Converting the value to float, removing the units, converting the prefix into the number.
|
||||
:param value: value
|
||||
:return: float value without units
|
||||
"""
|
||||
value_with_unit = re.compile(r'(\d+)([pnumkMG]?)')
|
||||
value, pfx = value_with_unit.match(value).groups()
|
||||
pfx_dict = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, 'G': 1e9}
|
||||
if pfx in pfx_dict:
|
||||
value = round(float(value) * pfx_dict[pfx], 12)
|
||||
return float(value)
|
||||
|
||||
|
||||
def find_idx(list_of_values, target):
|
||||
"""
|
||||
Search for the nearest value and index from the given range for the given target.
|
||||
:param list_of_values: range of values
|
||||
:param target: target
|
||||
:return: closest index and closest value
|
||||
"""
|
||||
target = float(target)
|
||||
cl_idx = None
|
||||
cl_value = float('inf')
|
||||
|
||||
for idx, value in enumerate(list_of_values):
|
||||
if value >= target:
|
||||
diff = value - target
|
||||
|
||||
if diff < cl_value:
|
||||
cl_value = value
|
||||
cl_idx = idx
|
||||
|
||||
return cl_idx, cl_value
|
||||
|
||||
|
||||
class BridgeIO(StringIO):
|
||||
"""_\n is placed at the beginning of each command to distinguish
|
||||
the previous response with a possible asynchronous response from the actual value returned by the method. """
|
||||
|
||||
end_of_line = '\n'
|
||||
identification = [('_\n*IDN?', r'Stanford_Research_Systems,.*')]
|
||||
|
||||
|
||||
class Base(HasIO):
|
||||
port = Property('modules port', IntRange(0, 15))
|
||||
|
||||
def communicate(self, command):
|
||||
"""
|
||||
Connection to the particular module x.
|
||||
:param command: command
|
||||
:return: return
|
||||
"""
|
||||
return self.io.communicate(f'_\nconn {self.port:x},"_\n"\n{command}')
|
||||
|
||||
def query(self, command):
|
||||
"""converting to float"""
|
||||
return float(self.communicate(command))
|
||||
|
||||
|
||||
class Resistance(Base, Readable):
|
||||
value = Parameter('resistance', datatype=FloatRange, unit='ohm')
|
||||
output_offset = Parameter('resistance deviation', datatype=FloatRange, unit='Ohm', readonly=False)
|
||||
phase_hold = Parameter('phase hold', EnumType('phase hold', off=0, on=1))
|
||||
|
||||
RES_RANGE = ['20mOhm', '200mOhm', '2Ohm', '20Ohm', '200Ohm', '2kOhm', '20kOhm', '200kOhm',
|
||||
'2MOhm', '20MOhm']
|
||||
irange = Parameter('resistance range index', EnumType('resistance range index',
|
||||
{name: idx for idx, name in enumerate(RES_RANGE)}),
|
||||
readonly=False)
|
||||
range = Parameter('resistance range value', FloatRange(2e-2, 2e7), unit='Om', readonly=False)
|
||||
|
||||
TIME_CONST = ['0.3s', '1s', '3s', '10s', '30s', '100s', '300s']
|
||||
itc = Parameter('time constant index',
|
||||
EnumType('time const. index range',
|
||||
{name: value for value, name in enumerate(TIME_CONST)}), readonly=False)
|
||||
tc = Parameter('time constant value', FloatRange(1e-1, 3e2), unit='s', readonly=False)
|
||||
|
||||
EXCT_RANGE = ['0', '3uV', '10uV', '30uV', '100uV', '300uV', '1mV', '3mV', '10mV', '30mV']
|
||||
iexct = Parameter('excitation index',
|
||||
EnumType('excitation index range', {name: idx for idx, name in enumerate(EXCT_RANGE, start=-1)}),
|
||||
readonly=False)
|
||||
exct = Parameter('excitation value', FloatRange(0, 3e-2), unit='s', default=300, readonly=False)
|
||||
|
||||
autorange = Parameter('autorange_on', EnumType('autorange', off=0, on=1),
|
||||
readonly=False, default=0)
|
||||
|
||||
RES_RANGE_values = [string_to_value(value) for value in RES_RANGE]
|
||||
TIME_CONST_values = [string_to_value(value) for value in TIME_CONST]
|
||||
EXCT_RANGE_values = [string_to_value(value) for value in EXCT_RANGE]
|
||||
|
||||
ioClass = BridgeIO
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
max_res = abs(self.value)
|
||||
if self.autorange == 1:
|
||||
if max_res >= 0.9 * self.range and self.irange < 9:
|
||||
self.write_irange(self.irange + 1)
|
||||
elif max_res <= 0.3 * self.range and self.irange > 0:
|
||||
self.write_irange(self.irange - 1)
|
||||
|
||||
def read_status(self):
|
||||
"""
|
||||
Both the mainframe (SR900) and the module (SR921) have the same commands for the status,
|
||||
here implemented commands are made for module status, not the frame!
|
||||
|
||||
:return: status type and message
|
||||
"""
|
||||
esr = int(self.communicate('*esr?')) # standart event status byte
|
||||
ovsr = int(self.communicate('ovsr?')) # overload status
|
||||
cesr = int(self.communicate('cesr?')) # communication error status
|
||||
|
||||
if esr & (1 << 1):
|
||||
return ERROR, 'input error, cleared'
|
||||
if esr & (1 << 2):
|
||||
return ERROR, 'query error'
|
||||
if esr & (1 << 4):
|
||||
return ERROR, 'execution error'
|
||||
if esr & (1 << 5):
|
||||
return ERROR, 'command error'
|
||||
if cesr & (1 << 0):
|
||||
return ERROR, 'parity error'
|
||||
if cesr & (1 << 2):
|
||||
return ERROR, 'noise error'
|
||||
if cesr & (1 << 4):
|
||||
return ERROR, 'input overflow, cleared'
|
||||
if cesr & (1 << 3):
|
||||
return ERROR, 'hardware overflow'
|
||||
if ovsr & (1 << 0):
|
||||
return ERROR, 'output overload'
|
||||
if cesr & (1 << 7):
|
||||
return WARN, 'device clear'
|
||||
if ovsr & (1 << 2):
|
||||
return WARN, 'current saturation'
|
||||
if ovsr & (1 << 3):
|
||||
return WARN, 'under servo'
|
||||
if ovsr & (1 << 4):
|
||||
return WARN, 'over servo'
|
||||
return IDLE, ''
|
||||
|
||||
def read_value(self):
|
||||
return self.query('rval?')
|
||||
|
||||
def read_irange(self):
|
||||
"""index of the resistance value according to the range"""
|
||||
return self.query('rang?')
|
||||
|
||||
def write_irange(self, idx):
|
||||
value = int(idx)
|
||||
self.query(f'rang {value}; rang?')
|
||||
self.read_range()
|
||||
return value
|
||||
|
||||
def read_range(self):
|
||||
"""value of the resistance range"""
|
||||
idx = self.read_irange()
|
||||
name = self.RES_RANGE[idx]
|
||||
return string_to_value(name)
|
||||
|
||||
def write_range(self, target):
|
||||
cl_idx, cl_value = find_idx(self.RES_RANGE_values, target)
|
||||
self.query(f'rang {cl_idx}; rang?')
|
||||
return cl_value
|
||||
|
||||
def read_output_offset(self):
|
||||
"""Output offset, can be set by user. This is the value subtracted from the measured value"""
|
||||
return self.query('rset?')
|
||||
|
||||
def write_output_offset(self, output_offset):
|
||||
self.query(f'rset {output_offset};rset?')
|
||||
|
||||
def read_itc(self):
|
||||
"""index of the temperature constant value according to the range"""
|
||||
return self.query('tcon?')
|
||||
|
||||
def write_itc(self, itc):
|
||||
self.read_itc()
|
||||
value = int(itc)
|
||||
return self.query(f'tcon {value}; tcon?')
|
||||
|
||||
def read_tc(self):
|
||||
idx = self.read_itc()
|
||||
name = self.TIME_CONST[idx]
|
||||
return string_to_value(name)
|
||||
|
||||
def write_tc(self, target):
|
||||
cl_idx, cl_value = find_idx(self.TIME_CONST_values, target)
|
||||
self.query(f'tcon {cl_idx};tcon?')
|
||||
return cl_value
|
||||
|
||||
def read_autorange(self):
|
||||
return self.autorange
|
||||
|
||||
def write_autorange(self, value):
|
||||
self.query(f'agai {value:d};agai?')
|
||||
return value
|
||||
|
||||
def read_iexct(self):
|
||||
"""index of the excitation value according to the range"""
|
||||
return int(self.query('exci?'))
|
||||
|
||||
def write_iexct(self, iexct):
|
||||
value = int(iexct)
|
||||
return self.query(f'exci {value};exci?')
|
||||
|
||||
def write_exct(self, target):
|
||||
target = float(target)
|
||||
cl_idx = None
|
||||
cl_value = float('inf')
|
||||
min_diff = float('inf')
|
||||
|
||||
for idx, value in enumerate(self.EXCT_RANGE_values):
|
||||
diff = abs(value - target)
|
||||
|
||||
if diff < min_diff:
|
||||
min_diff = diff
|
||||
cl_value = value
|
||||
cl_idx = idx
|
||||
|
||||
self.write_iexct(cl_idx)
|
||||
return cl_value
|
||||
|
||||
def read_exct(self):
|
||||
idx = int(self.read_iexct())
|
||||
name = self.EXCT_RANGE[idx + 1]
|
||||
return string_to_value(name)
|
||||
|
||||
def read_phase_hold(self):
|
||||
"""
|
||||
Set the phase hold mode (if on - phase is assumed to be zero).
|
||||
:return: 0 - off, 1 - on
|
||||
"""
|
||||
return int(self.communicate('phld?'))
|
||||
|
||||
def write_phase_hold(self, phase_hold):
|
||||
self.communicate(f'phld {phase_hold}')
|
||||
return self.read_phase_hold()
|
||||
|
||||
|
||||
class Phase(Readable):
|
||||
resistance = Attached()
|
||||
value = Parameter('phase', FloatRange, default=0, unit='deg')
|
||||
|
||||
def read_value(self):
|
||||
return self.resistance.query('phas?')
|
||||
|
||||
|
||||
class Deviation(Readable):
|
||||
resistance = Attached()
|
||||
value = Parameter('resistance deviation', FloatRange(), unit='Ohm')
|
||||
|
||||
def read_value(self):
|
||||
return self.resistance.query('rdev?')
|
||||
576
frappy_psi/calcurve.py
Normal file
576
frappy_psi/calcurve.py
Normal file
@@ -0,0 +1,576 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Software calibration"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from os.path import basename, dirname, exists, join
|
||||
|
||||
import numpy as np
|
||||
from scipy.interpolate import PchipInterpolator, CubicSpline, PPoly # pylint: disable=import-error
|
||||
|
||||
|
||||
from frappy.errors import ProgrammingError, RangeError
|
||||
from frappy.lib import clamp
|
||||
|
||||
to_scale = {
|
||||
'lin': lambda x: x,
|
||||
'log': lambda x: np.log10(x),
|
||||
}
|
||||
from_scale = {
|
||||
'lin': lambda x: x,
|
||||
'log': lambda x: 10 ** np.array(x),
|
||||
}
|
||||
TYPES = [ # lakeshore type, inp-type, loglog
|
||||
('DT', 'si', False), # Si diode
|
||||
('TG', 'gaalas', False), # GaAlAs diode
|
||||
('PT', 'pt250', False), # platinum, 250 Ohm range
|
||||
('PT', 'pt500', False), # platinum, 500 Ohm range
|
||||
('PT', 'pt2500', False), # platinum, 2500 Ohm range
|
||||
('RF', 'rhfe', False), # rhodium iron
|
||||
('CC', 'c', True), # carbon, LakeShore acronym unknown
|
||||
('CX', 'cernox', True), # Cernox
|
||||
('RX', 'ruox', True), # rutheniumm oxide
|
||||
('GE', 'ge', True), # germanium, LakeShore acronym unknown
|
||||
]
|
||||
|
||||
|
||||
OPTION_TYPE = {
|
||||
'loglog': 0, # boolean
|
||||
'extrange': 2, # tuple(min T, max T for extrapolation
|
||||
'calibrange': 2, # tuple(min T, max T)
|
||||
}
|
||||
|
||||
|
||||
class HasOptions:
|
||||
def insert_option(self, key, value):
|
||||
key = key.strip()
|
||||
argtype = OPTION_TYPE.get(key, 1)
|
||||
if argtype == 1: # one number or string
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
value = value.strip()
|
||||
elif argtype == 0:
|
||||
if value.strip().lower() in ('false', '0'):
|
||||
value = False
|
||||
else:
|
||||
value = True
|
||||
else:
|
||||
value = [float(f) for f in value.split(',')]
|
||||
self.options[key] = value
|
||||
|
||||
|
||||
class StdParser(HasOptions):
|
||||
"""parser used for reading columns"""
|
||||
def __init__(self, **options):
|
||||
"""keys of options may be either 'x' or 'logx' and either 'y' or 'logy'
|
||||
|
||||
default is x=0, y=1
|
||||
"""
|
||||
if 'logx' in options:
|
||||
self.xscale = 'log'
|
||||
self.xcol = options.pop('logx')
|
||||
else:
|
||||
self.xscale = 'lin'
|
||||
self.xcol = options.pop('x', 0)
|
||||
if 'logy' in options:
|
||||
self.yscale = 'log'
|
||||
self.ycol = options.pop('logy')
|
||||
else:
|
||||
self.yscale = 'lin'
|
||||
self.ycol = options.pop('y', 1)
|
||||
self.xdata, self.ydata = [], []
|
||||
self.options = options
|
||||
self.invalid_lines = []
|
||||
|
||||
def parse(self, line):
|
||||
"""get numbers from a line and put them to self.xdata / self.ydata"""
|
||||
row = line.split()
|
||||
try:
|
||||
self.xdata.append(float(row[self.xcol]))
|
||||
self.ydata.append(float(row[self.ycol]))
|
||||
except (IndexError, ValueError):
|
||||
self.invalid_lines.append(line)
|
||||
return
|
||||
|
||||
def finish(self):
|
||||
pass
|
||||
|
||||
|
||||
class InpParser(StdParser):
|
||||
"""M. Zollikers *.inp calcurve format"""
|
||||
HEADERLINE = re.compile(r'#?(?:(\w+)\s*=\s*([^!# \t\n]*)|curv.*)')
|
||||
INP_TYPES = {ityp: (ltyp, loglog) for ltyp, ityp, loglog in TYPES}
|
||||
|
||||
def __init__(self, **options):
|
||||
options.update(x=0, y=1)
|
||||
super().__init__(**options)
|
||||
self.header = True
|
||||
|
||||
def parse(self, line):
|
||||
"""scan header"""
|
||||
if self.header:
|
||||
match = self.HEADERLINE.match(line)
|
||||
if match:
|
||||
key, value = match.groups()
|
||||
if key is None:
|
||||
self.header = False
|
||||
else:
|
||||
key = key.lower()
|
||||
value = value.strip()
|
||||
if key == 'type':
|
||||
type_, loglog = self.INP_TYPES.get(value.lower(), (None, None))
|
||||
if type_ is not None:
|
||||
self.options['type'] = type_
|
||||
if loglog is not None:
|
||||
self.options['loglog'] = loglog
|
||||
else:
|
||||
self.insert_option(key, value)
|
||||
return
|
||||
elif line.startswith('!'):
|
||||
return
|
||||
super().parse(line)
|
||||
|
||||
|
||||
class Parser340(StdParser):
|
||||
"""parser for LakeShore *.340 files"""
|
||||
HEADERLINE = re.compile(r'([^:]*):\s*([^(]*)')
|
||||
CALIBHIGH = dict(L=325, M=420, H=500, B=40)
|
||||
|
||||
def __init__(self, **options):
|
||||
options.update(x=1, y=2)
|
||||
super().__init__(**options)
|
||||
self.header = True
|
||||
|
||||
def parse(self, line):
|
||||
"""scan header"""
|
||||
if self.header:
|
||||
match = self.HEADERLINE.match(line)
|
||||
if match:
|
||||
key, value = match.groups()
|
||||
key = ''.join(key.split()).lower()
|
||||
value = value.strip()
|
||||
if key == 'dataformat':
|
||||
if value[0:1] == '4':
|
||||
self.xscale, self.yscale = 'log', 'lin' # logOhm
|
||||
self.options['loglog'] = True
|
||||
elif value[0:1] == '5':
|
||||
self.xscale, self.yscale = 'log', 'log' # logOhm, logK
|
||||
self.options['loglog'] = True
|
||||
elif value[0:1] in ('1', '2', '3'):
|
||||
self.options['loglog'] = False
|
||||
else:
|
||||
raise ValueError('invalid Data Format')
|
||||
self.options['scale'] = self.xscale + self.yscale
|
||||
self.insert_option(key, value)
|
||||
return
|
||||
if 'No.' in line:
|
||||
self.header = False
|
||||
return
|
||||
if len(line.split()) != 3:
|
||||
return
|
||||
super().parse(line)
|
||||
if self.header and self.xdata:
|
||||
# valid line detected
|
||||
self.header = False
|
||||
|
||||
def finish(self):
|
||||
model = self.options.get('sensormodel', '').split('-')
|
||||
if model[0]:
|
||||
self.options['type'] = model[0]
|
||||
if 'calibrange' not in self.options:
|
||||
if len(model) > 2:
|
||||
try: # e.g. model[-1] == 1.4M -> calibrange = 1.4, 420
|
||||
self.options['calibrange'] = float(model[-1][:-1]), self.CALIBHIGH[model[-1][-1]]
|
||||
return
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
|
||||
class CaldatParser(StdParser):
|
||||
"""parser for format from sea/tcl/startup/calib_ext.tcl"""
|
||||
|
||||
def __init__(self, options):
|
||||
options.update(x=1, y=2)
|
||||
super().__init__(options)
|
||||
|
||||
|
||||
PARSERS = {
|
||||
"340": Parser340,
|
||||
"inp": InpParser,
|
||||
"caldat": CaldatParser,
|
||||
"dat": StdParser, # lakeshore raw data *.dat format
|
||||
}
|
||||
|
||||
|
||||
def check(x, y, islog):
|
||||
# check interpolation error
|
||||
yi = y[:-2] + (x[1:-1] - x[:-2]) * (y[2:] - y[:-2]) / (x[2:] - x[:-2])
|
||||
if islog:
|
||||
return sum((yi - y[1:-1]) ** 2)
|
||||
return sum((np.log10(yi) - np.log10(y[1:-1])) ** 2)
|
||||
|
||||
|
||||
def get_curve(newscale, curves):
|
||||
"""get curve from curve cache (converts not existing ones)
|
||||
|
||||
:param newscale: the new scale to get
|
||||
:param curves: a dict <scale> of <array> storing available scales
|
||||
:return: the retrieved or converted curve
|
||||
"""
|
||||
if newscale in curves:
|
||||
return curves[newscale]
|
||||
for scale, array in curves.items():
|
||||
curves[newscale] = curve = to_scale[newscale](from_scale[scale](array))
|
||||
return curve
|
||||
|
||||
|
||||
class CalCurve(HasOptions):
|
||||
EXTRAPOLATION_AMOUNT = 0.1
|
||||
MAX_EXTRAPOLATION_FACTOR = 2
|
||||
|
||||
def __init__(self, calibspec=None, *, x=None, y=None, cubic_spline=True, **options):
|
||||
"""calibration curve
|
||||
|
||||
:param calibspec: a string with name or filename, options
|
||||
lookup path for files in env. variable FRAPPY_CALIB_PATH
|
||||
calibspec format:
|
||||
[<full path> | <name>][,<key>=<value> ...]
|
||||
for <key>/<value> as in parser arguments
|
||||
:param x, y: x and y arrays (given instead of calibspec)
|
||||
:param cubic_split: set to False for always using Pchip interpolation
|
||||
:param options: options for parsers
|
||||
"""
|
||||
self.options = options
|
||||
if calibspec is None:
|
||||
parser = StdParser()
|
||||
parser.xdata = x
|
||||
parser.ydata = y
|
||||
else:
|
||||
if x or y:
|
||||
raise ProgrammingError('can not give both calibspec and x,y ')
|
||||
sensopt = calibspec.split(',')
|
||||
calibname = sensopt.pop(0)
|
||||
_, dot, ext = basename(calibname).rpartition('.')
|
||||
kind = None
|
||||
pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(':')
|
||||
pathlist.append(join(dirname(__file__), 'calcurves'))
|
||||
for path in pathlist:
|
||||
# first try without adding kind
|
||||
filename = join(path.strip(), calibname)
|
||||
if exists(filename):
|
||||
kind = ext if dot else None
|
||||
break
|
||||
# then try adding all kinds as extension
|
||||
for nam in calibname, calibname.upper(), calibname.lower():
|
||||
for kind in PARSERS:
|
||||
filename = join(path.strip(), '%s.%s' % (nam, kind))
|
||||
if exists(filename):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError(calibname)
|
||||
sensopt = iter(sensopt)
|
||||
for opt in sensopt:
|
||||
key, _, value = opt.lower().partition('=')
|
||||
if OPTION_TYPE.get(key) == 2:
|
||||
self.options[key] = float(value), float(next(sensopt))
|
||||
else:
|
||||
self.insert_option(key, value)
|
||||
kind = self.options.pop('kind', kind)
|
||||
cls = PARSERS.get(kind, StdParser)
|
||||
try:
|
||||
parser = cls(**self.options)
|
||||
with open(filename, encoding='utf-8') as f:
|
||||
for line in f:
|
||||
parser.parse(line)
|
||||
parser.finish()
|
||||
except Exception as e:
|
||||
raise ValueError('error parsing calib curve %s %r' % (calibspec, e)) from e
|
||||
# take defaults from parser options
|
||||
self.options = dict(parser.options, **self.options)
|
||||
|
||||
x = np.asarray(parser.xdata)
|
||||
y = np.asarray(parser.ydata)
|
||||
if len(x) < 2:
|
||||
raise ValueError('calib file %s has less than 2 points' % calibspec)
|
||||
|
||||
if x[0] > x[-1]:
|
||||
x = np.flip(np.array(x))
|
||||
y = np.flip(np.array(y))
|
||||
else:
|
||||
x = np.array(x)
|
||||
y = np.array(y)
|
||||
not_incr_idx = np.argwhere(x[1:] <= x[:-1])
|
||||
if len(not_incr_idx):
|
||||
raise RangeError('x not monotonic at x=%.4g' % x[not_incr_idx[0]])
|
||||
|
||||
self.x = {parser.xscale: x}
|
||||
self.y = {parser.yscale: y}
|
||||
self.lin_forced = [parser.yscale == 'lin' and (y[0] <= 0 or y[-1] <= 0),
|
||||
parser.xscale == 'lin' and x[0] <= 0]
|
||||
if sum(self.lin_forced):
|
||||
self.loglog = False
|
||||
else:
|
||||
self.loglog = self.options.get('loglog', y[0] > y[-1]) # loglog defaults to True for NTC
|
||||
newscale = 'log' if self.loglog else 'lin'
|
||||
self.scale = newscale
|
||||
x = get_curve(newscale, self.x)
|
||||
y = get_curve(newscale, self.y)
|
||||
self.convert_x = to_scale[newscale]
|
||||
self.convert_y = from_scale[newscale]
|
||||
self.calibrange = self.options.get('calibrange')
|
||||
dirty = set()
|
||||
self.extra_points = False
|
||||
self.cutted = False
|
||||
if self.calibrange:
|
||||
self.calibrange = sorted(self.calibrange)
|
||||
# determine indices (ibeg, iend) of first and last well calibrated point
|
||||
ylin = get_curve('lin', self.y)
|
||||
beg, end = self.calibrange
|
||||
if y[0] > y[-1]:
|
||||
ylin = -ylin
|
||||
beg, end = -end, -beg
|
||||
|
||||
ibeg, iend = np.searchsorted(ylin, (beg, end))
|
||||
if ibeg > 0 and abs(ylin[ibeg-1] - beg) < 0.1 * (ylin[ibeg] - ylin[ibeg-1]):
|
||||
# add previous point, if close
|
||||
ibeg -= 1
|
||||
if iend < len(ylin) and abs(ylin[iend] - end) < 0.1 * (ylin[iend] - ylin[iend-1]):
|
||||
# add next point, if close
|
||||
iend += 1
|
||||
if self.options.get('cut', False):
|
||||
self.cutted = True
|
||||
x = x[ibeg:iend]
|
||||
y = y[ibeg:iend]
|
||||
self.x = {newscale: x}
|
||||
self.y = {newscale: y}
|
||||
ibeg = 0
|
||||
iend = len(x)
|
||||
dirty.add('xy')
|
||||
else:
|
||||
self.extra_points = ibeg, len(x) - iend
|
||||
else:
|
||||
ibeg = 0
|
||||
iend = len(x)
|
||||
ylin = get_curve('lin', self.y)
|
||||
self.calibrange = tuple(sorted([ylin[0], ylin[-1]]))
|
||||
|
||||
if cubic_spline:
|
||||
# fit well calibrated part with spline
|
||||
# determine slopes of calibrated part with CubicSpline
|
||||
spline = CubicSpline(x[ibeg:iend], y[ibeg:iend])
|
||||
roots = spline.derivative().roots(extrapolate=False)
|
||||
if len(roots):
|
||||
cubic_spline = False
|
||||
|
||||
self.cubic_spline = cubic_spline
|
||||
if cubic_spline:
|
||||
coeff = spline.c
|
||||
if self.extra_points:
|
||||
p = PchipInterpolator(x, y).c
|
||||
# use Pchip outside left and right of calibrange
|
||||
# remark: first derivative at end of calibrange is not continuous
|
||||
coeff = np.concatenate((p[:, :ibeg], coeff, p[:, iend-1:]), axis=1)
|
||||
else:
|
||||
spline = PchipInterpolator(x, y)
|
||||
coeff = spline.c
|
||||
# extrapolation extension
|
||||
# linear extrapolation is more robust than spline extrapolation
|
||||
x1, x2 = x[0], x[-1]
|
||||
# take slope at end of calibrated range for extrapolation
|
||||
slopes = spline([x[ibeg], x[iend-1]], 1)
|
||||
for i, j in enumerate([ibeg, iend-2]):
|
||||
# slope of last interval in calibrange
|
||||
si = (y[j+1] - y[j])/(x[j+1] - x[j])
|
||||
# make sure slope is not more than a factor 2 different
|
||||
# from the slope calculated at the outermost calibrated intervals
|
||||
slopes[i] = clamp(slopes[i], 2*si, 0.5 * si)
|
||||
dx = 0.1 if self.loglog else (x2 - x1) * 0.1
|
||||
xe = np.concatenate(([x1 - dx], x, [x2 + dx]))
|
||||
# x3 = np.append(x, x2 + dx)
|
||||
# y3 = np.append(y, y[-1] + slope * dx)
|
||||
y0 = y[0] - slopes[0] * dx
|
||||
coeff = np.concatenate(([[0], [0], [slopes[0]], [y0]], coeff, [[0], [0], [slopes[1]], [y[-1]]]), axis=1)
|
||||
self.spline = PPoly(coeff, xe)
|
||||
# ranges without extrapolation:
|
||||
self.xrange = get_curve('lin', self.x)[[0, -1]]
|
||||
self.yrange = sorted(get_curve('lin', self.y)[[0, -1]])
|
||||
self.calibrange = [max(self.calibrange[0], self.yrange[0]),
|
||||
min(self.calibrange[1], self.yrange[1])]
|
||||
self.set_extrapolation()
|
||||
|
||||
# check
|
||||
# ys = self.spline(xe)
|
||||
# ye = np.concatenate(([y0], y, [y[-1] + slope2 * dx]))
|
||||
# assert np.all(np.abs(ys - ye) < 1e-5 * (0.1 + np.abs(ys + ye)))
|
||||
|
||||
def set_extrapolation(self, extleft=None, extright=None):
|
||||
"""set default extrapolation range for export method
|
||||
|
||||
:param extleft: y value for the lower end of the extrapolation
|
||||
:param extright: y value for the upper end of the extrapolation
|
||||
|
||||
if arguments omitted or None are replaced by a default extrapolation scheme
|
||||
|
||||
on return self.extx and self.exty are set to the extrapolated ranges
|
||||
"""
|
||||
yc1, yc2 = self.calibrange
|
||||
y1, y2 = to_scale[self.scale]([yc1, yc2])
|
||||
d = (y2 - y1) * self.EXTRAPOLATION_AMOUNT
|
||||
yex1, yex2 = tuple(from_scale[self.scale]([y1 - d, y2 + d]))
|
||||
t1, t2 = tuple(from_scale[self.scale]([y1, y2]))
|
||||
|
||||
# raw units, excluding extrapolation points at end
|
||||
xrng = self.spline.x[1], self.spline.x[-2]
|
||||
# first and last point
|
||||
yp1, yp2 = sorted(from_scale[self.scale](self.spline(xrng)))
|
||||
xrng = from_scale[self.scale](xrng)
|
||||
|
||||
# limit by maximal factor
|
||||
f = self.MAX_EXTRAPOLATION_FACTOR
|
||||
# but ext range should be at least to the points in curve
|
||||
self.exty = [min(yp1, max(yex1, min(t1 / f, t1 * f))),
|
||||
max(yp2, min(yex2, max(t2 * f, t2 / f)))]
|
||||
if extleft is not None:
|
||||
self.exty[0] = min(extleft, yp1)
|
||||
if extright is not None:
|
||||
self.exty[1] = max(extright, yp2)
|
||||
self.extx = sorted(self.invert(*yd) for yd in zip(self.exty, xrng))
|
||||
# check that sensor range is not extended by more than a factor f
|
||||
extnew = [max(self.extx[0], min(xrng[0] / f, xrng[0] * f)),
|
||||
min(self.extx[1], max(xrng[1] / f, xrng[1] * f))]
|
||||
if extnew != self.extx:
|
||||
# need further reduction
|
||||
self.extx = extnew
|
||||
self.exty = sorted(self(extnew))
|
||||
|
||||
def convert(self, value):
|
||||
"""convert a single value
|
||||
|
||||
return a tuple (converted value, boolean: was it clamped?)
|
||||
"""
|
||||
x = clamp(value, *self.extx)
|
||||
return self(x), x == value
|
||||
|
||||
def __call__(self, value):
|
||||
"""convert value or numpy array without checking extrapolation range"""
|
||||
return self.convert_y(self.spline(self.convert_x(value)))
|
||||
|
||||
def invert(self, y, defaultx=None, xscale=True, yscale=True):
|
||||
"""invert y, return defaultx if no solution is found"""
|
||||
if yscale:
|
||||
y = to_scale[self.scale](y)
|
||||
r = self.spline.solve(y)
|
||||
try:
|
||||
if xscale:
|
||||
return from_scale[self.scale](r[0])
|
||||
return r[0]
|
||||
except IndexError:
|
||||
return defaultx
|
||||
|
||||
def export(self, logformat=False, nmax=199, yrange=None, extrapolate=True, xlimits=None):
|
||||
"""export curve for downloading to hardware
|
||||
|
||||
:param nmax: max number of points. if the number of given points is bigger,
|
||||
the points with the lowest interpolation error are omitted
|
||||
:param logformat: a list with two elements of None, True or False
|
||||
True: use log, False: use line, None: use log if self.loglog
|
||||
values None are replaced with the effectively used format
|
||||
False / True are replaced by [False, False] / [True, True]
|
||||
default is False
|
||||
:param yrange: to reduce or extrapolate to this interval (extrapolate is ignored when given)
|
||||
:param extrapolate: a flag indicating whether the curves should be extrapolated
|
||||
to the preset extrapolation range
|
||||
:param xlimits: max x range
|
||||
:return: numpy array with 2 dimensions returning the curve
|
||||
"""
|
||||
|
||||
if logformat in (True, False):
|
||||
logformat = [logformat, logformat]
|
||||
try:
|
||||
scales = []
|
||||
for idx, logfmt in enumerate(logformat):
|
||||
if logfmt and self.lin_forced[idx]:
|
||||
raise ValueError('%s must contain positive values only' % 'xy'[idx])
|
||||
logformat[idx] = linlog = self.loglog if logfmt is None else logfmt
|
||||
scales.append('log' if linlog else 'lin')
|
||||
xscale, yscale = scales
|
||||
except (TypeError, AssertionError):
|
||||
raise ValueError('logformat must be a 2 element list or a boolean')
|
||||
|
||||
x = self.spline.x[1:-1] # raw units, excluding extrapolated points
|
||||
x1, x2 = xmin, xmax = x[0], x[-1]
|
||||
y1, y2 = sorted(self.spline([x1, x2]))
|
||||
|
||||
if extrapolate and not yrange:
|
||||
yrange = self.exty
|
||||
if yrange is not None:
|
||||
xmin, xmax = sorted(self.invert(*yd, xscale=False) for yd in zip(yrange, [x1, x2]))
|
||||
if xlimits is not None:
|
||||
lim = to_scale[self.scale](xlimits)
|
||||
xmin = clamp(xmin, *lim)
|
||||
xmax = clamp(xmax, *lim)
|
||||
if xmin != x1 or xmax != x2:
|
||||
ibeg, iend = np.searchsorted(x, (xmin, xmax))
|
||||
if abs(x[ibeg] - xmin) < 0.1 * (x[ibeg + 1] - x[ibeg]):
|
||||
# remove first point, if close
|
||||
ibeg += 1
|
||||
if abs(x[iend - 1] - xmax) < 0.1 * (x[iend - 1] - x[iend - 2]):
|
||||
# remove last point, if close
|
||||
iend -= 1
|
||||
x = np.concatenate(([xmin], x[ibeg:iend], [xmax]))
|
||||
y = self.spline(x)
|
||||
|
||||
# convert to exported scale
|
||||
if xscale != self.scale:
|
||||
x = to_scale[xscale](from_scale[self.scale](x))
|
||||
if yscale != self.scale:
|
||||
y = to_scale[yscale](from_scale[self.scale](y))
|
||||
|
||||
# reduce number of points, if needed
|
||||
n = len(x)
|
||||
i, j = 1, n - 1 # index range for calculating interpolation deviation
|
||||
deviation = np.zeros(n)
|
||||
while True:
|
||||
# calculate interpolation error when a single point is omitted
|
||||
ym = y[i-1:j-1] + (x[i:j] - x[i-1:j-1]) * (y[i+1:j+1] - y[i-1:j-1]) / (x[i+1:j+1] - x[i-1:j-1])
|
||||
if yscale == 'log':
|
||||
deviation[i:j] = np.abs(ym - y[i:j])
|
||||
else:
|
||||
deviation[i:j] = np.abs(ym - y[i:j]) / (np.abs(ym + y[i:j]) + 1e-10)
|
||||
if n <= nmax:
|
||||
break
|
||||
idx = np.argmin(deviation[1:-1]) + 1 # find index of the smallest error
|
||||
y = np.delete(y, idx)
|
||||
x = np.delete(x, idx)
|
||||
deviation = np.delete(deviation, idx)
|
||||
n -= 1
|
||||
# index range to recalculate
|
||||
i, j = max(1, idx - 1), min(n - 1, idx + 1)
|
||||
self.deviation = deviation # for debugging purposes
|
||||
return np.stack([x, y], axis=1)
|
||||
@@ -29,7 +29,8 @@ import re
|
||||
import time
|
||||
from math import copysign
|
||||
from frappy.core import HasIO, StringIO, Readable, Drivable, Parameter, Command, \
|
||||
Module, Property, Attached, Enum, IDLE, BUSY, ERROR
|
||||
Module, Property, Attached, Enum, IDLE, BUSY, ERROR, nopoll, PersistentParam, \
|
||||
PersistentMixin
|
||||
from frappy.errors import ConfigError, BadValueError, HardwareError
|
||||
from frappy.datatypes import FloatRange, StringType, EnumType, StructOf
|
||||
from frappy.states import HasStates, status_code, Retry
|
||||
@@ -41,7 +42,10 @@ VALUE_UNIT = re.compile(r'([-0-9.E]*\d|inf)([A-Za-z/%]*)$')
|
||||
|
||||
def as_float(value):
|
||||
"""converts string (with unit) to float"""
|
||||
return float(VALUE_UNIT.match(value).group(1))
|
||||
try:
|
||||
return float(VALUE_UNIT.match(value).group(1))
|
||||
except Exception:
|
||||
raise ValueError(f'can not convert {value!r} to float with unit')
|
||||
|
||||
|
||||
BOOL_MAP = {'TRUE': True, 'FALSE': False}
|
||||
@@ -113,6 +117,8 @@ class Main(HasIO, Module):
|
||||
# ignore multiline values
|
||||
# if needed, we may collect here and treat with a special key
|
||||
continue
|
||||
if not value:
|
||||
continue # silently ignore empty values
|
||||
obj, pname, cvt = self.params_map.get(key, missing)
|
||||
if obj:
|
||||
if not hasattr(obj, pname):
|
||||
@@ -268,7 +274,7 @@ class BaseMagfield(HasStates, Channel):
|
||||
def cvt_error(self, text):
|
||||
if text != self._last_error:
|
||||
self._last_error = text
|
||||
self.log.error(text)
|
||||
self.log.error(f'{self.channel}_Error: {text}')
|
||||
return text
|
||||
return self._error_text
|
||||
|
||||
@@ -287,6 +293,11 @@ class BaseMagfield(HasStates, Channel):
|
||||
ramp = Parameter()
|
||||
target = Parameter()
|
||||
|
||||
@nopoll
|
||||
def read_setpoint(self):
|
||||
self.main.doPoll()
|
||||
return self.setpoint
|
||||
|
||||
def write_ramp(self, ramp):
|
||||
if self._rate_units != 'A/s':
|
||||
self.sendcmd('Set:<CH>:ChangeRateUnits A/s')
|
||||
@@ -323,9 +334,18 @@ class BaseMagfield(HasStates, Channel):
|
||||
return super().start_field_change
|
||||
|
||||
def start_ramp_to_target(self, sm):
|
||||
self.start_sweep(sm.target)
|
||||
try:
|
||||
self.start_sweep(sm.target)
|
||||
self.log.info('start_ramp_to_target: start_sweep done')
|
||||
except Exception as e:
|
||||
self.log.error('start_ramp_to_target: start_sweep failed with %r', e)
|
||||
raise
|
||||
return self.ramp_to_target # -> stabilize_field
|
||||
|
||||
# def start_ramp_to_target(self, sm):
|
||||
# self.start_sweep(sm.target)
|
||||
# return self.ramp_to_target # -> stabilize_field
|
||||
|
||||
def stabilize_field(self, sm):
|
||||
if self._ready_text == 'FALSE':
|
||||
# wait for overshoot/degauss/cycle
|
||||
@@ -432,7 +452,7 @@ class BaseMagfield(HasStates, Channel):
|
||||
self._error_text = ''
|
||||
|
||||
|
||||
class MainField(BaseMagfield, magfield.Magfield):
|
||||
class MainField(PersistentMixin, BaseMagfield, magfield.Magfield):
|
||||
checked_modules = None
|
||||
|
||||
def earlyInit(self):
|
||||
@@ -456,12 +476,13 @@ class MainField(BaseMagfield, magfield.Magfield):
|
||||
super().check_limits(value)
|
||||
self.check_combined(None, 0, value)
|
||||
|
||||
mode = Parameter(datatype=EnumType(PersistencyMode))
|
||||
mode = PersistentParam(datatype=EnumType(PersistencyMode))
|
||||
|
||||
def write_mode(self, mode):
|
||||
self.reset_error()
|
||||
super().write_mode(mode) # updates mode
|
||||
return mode
|
||||
self.mode = mode
|
||||
self.saveParameters()
|
||||
|
||||
@status_code('PREPARING')
|
||||
def start_ramp_to_field(self, sm):
|
||||
|
||||
123
frappy_psi/frozenparam.py
Normal file
123
frappy_psi/frozenparam.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
|
||||
import time
|
||||
import math
|
||||
from frappy.core import Parameter, FloatRange, IntRange, Property
|
||||
from frappy.errors import ProgrammingError
|
||||
|
||||
|
||||
class FrozenParam(Parameter):
|
||||
"""workaround for lazy hardware
|
||||
|
||||
Some hardware does not react nicely: when a parameter is changed,
|
||||
and read back immediately, still the old value is returned.
|
||||
This special parameter helps fixing this problem.
|
||||
|
||||
Mechanism:
|
||||
|
||||
- after a call to write_<param> for a short time (<n_polls> * <interval>)
|
||||
the hardware is polled until the readback changes before the 'changed'
|
||||
message is replied to the client
|
||||
- if there is no change yet within short time, the 'changed' message is
|
||||
set with the given value and further calls to read_<param> return also
|
||||
this given value until the readback value has changed or until
|
||||
<timeout> sec have passed.
|
||||
|
||||
For float parameters, the behaviour for small changes is improved
|
||||
when the write_<param> method tries to return the (may be rounded) value,
|
||||
as if it would be returned by the hardware. If this behaviour is not
|
||||
known, or the programmer is too lazy to implement it, write_<param>
|
||||
should return None or the given value.
|
||||
Also it will help to adjust the datatype properties
|
||||
'absolute_resolution' and 'relative_resolution' to reasonable values.
|
||||
"""
|
||||
timeout = Property('timeout for freezing readback value',
|
||||
FloatRange(0, unit='s'), default=30)
|
||||
n_polls = Property("""number polls within write method""",
|
||||
IntRange(0), default=1)
|
||||
interval = Property("""interval for polls within write method
|
||||
|
||||
the product n_polls * interval should not be more than a fraction of a second
|
||||
in order not to block the connection for too long
|
||||
""",
|
||||
FloatRange(0, unit='s'), default=0.05)
|
||||
new_value = None
|
||||
previous_value = None
|
||||
expire = 0
|
||||
is_float = True # assume float. will be fixed later
|
||||
|
||||
def isclose(self, v1, v2):
|
||||
if v1 == v2:
|
||||
return True
|
||||
if self.is_float:
|
||||
dt = self.datatype
|
||||
try:
|
||||
return math.isclose(v1, v2, abs_tol=dt.absolute_tolerance,
|
||||
rel_tol=dt.relative_tolerance)
|
||||
except AttributeError:
|
||||
# fix once for ever when datatype is not a float
|
||||
self.is_float = False
|
||||
return False
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
try:
|
||||
rfunc = getattr(owner, f'read_{name}')
|
||||
wfunc = getattr(owner, f'write_{name}')
|
||||
except AttributeError:
|
||||
raise ProgrammingError(f'FrozenParam: methods read_{name} and write_{name} must exist') from None
|
||||
|
||||
super().__set_name__(owner, name)
|
||||
|
||||
def read_wrapper(self, pname=name, rfunc=rfunc):
|
||||
pobj = self.parameters[pname]
|
||||
value = rfunc(self)
|
||||
if pobj.new_value is None:
|
||||
return value
|
||||
if not pobj.isclose(value, pobj.new_value):
|
||||
if value == pobj.previous_value:
|
||||
if time.time() < pobj.expire:
|
||||
return pobj.new_value
|
||||
self.log.warning('%s readback did not change within %g sec',
|
||||
pname, pobj.timeout)
|
||||
else:
|
||||
# value has changed, but is not matching new value
|
||||
self.log.warning('%s readback changed from %r to %r but %r was given',
|
||||
pname, pobj.previous_value, value, pobj.new_value)
|
||||
# readback value has changed or returned value is roughly equal to the new value
|
||||
pobj.new_value = None
|
||||
return value
|
||||
|
||||
def write_wrapper(self, value, wfunc=wfunc, rfunc=rfunc, read_wrapper=read_wrapper, pname=name):
|
||||
pobj = self.parameters[pname]
|
||||
pobj.previous_value = rfunc(self)
|
||||
pobj.new_value = wfunc(self, value)
|
||||
if pobj.new_value is None: # as wfunc is the unwrapped write_* method, the return value may be None
|
||||
pobj.new_value = value
|
||||
pobj.expire = time.time() + pobj.timeout
|
||||
for cnt in range(pobj.n_polls):
|
||||
if cnt: # we may be lucky, and the readback value has already changed
|
||||
time.sleep(pobj.interval)
|
||||
value = read_wrapper(self)
|
||||
if pobj.new_value is None:
|
||||
return value
|
||||
return pobj.new_value
|
||||
|
||||
setattr(owner, f'read_{name}', read_wrapper)
|
||||
setattr(owner, f'write_{name}', write_wrapper)
|
||||
@@ -15,8 +15,6 @@
|
||||
#
|
||||
# Module authors: Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Thermo Haake Phoenix P1 Bath Circulator"""
|
||||
|
||||
import re
|
||||
import time
|
||||
from frappy.core import StringIO, HasIO, Parameter, FloatRange, BoolType, \
|
||||
@@ -24,13 +22,7 @@ from frappy.core import StringIO, HasIO, Parameter, FloatRange, BoolType, \
|
||||
from frappy_psi.convergence import HasConvergence
|
||||
from frappy.errors import CommunicationFailedError
|
||||
|
||||
|
||||
def convert(string):
|
||||
"""
|
||||
Converts reply to a number
|
||||
:param string: reply from the command
|
||||
:return: number
|
||||
"""
|
||||
number = re.sub(r'[^0-9.-]', '', string)
|
||||
return float(number)
|
||||
|
||||
@@ -63,21 +55,11 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
||||
]
|
||||
|
||||
def get_values_status(self):
|
||||
"""
|
||||
Supplementary command for the operating status method.
|
||||
Removes the extra symbol and converts each status value into integer.
|
||||
|
||||
:return: array of integers
|
||||
"""
|
||||
reply = self.communicate('B')
|
||||
string = reply.rstrip('$')
|
||||
return [int(val) for val in string]
|
||||
|
||||
def read_status(self): # control_active update
|
||||
"""
|
||||
Operating status.
|
||||
:return: statu type and message
|
||||
"""
|
||||
values_str = self.get_values_status()
|
||||
self.read_control_active()
|
||||
|
||||
@@ -90,10 +72,6 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
||||
return IDLE, ''
|
||||
|
||||
def read_value(self):
|
||||
"""
|
||||
F1 - internal temperature, F2 - external temperature
|
||||
:return: float temperature value
|
||||
"""
|
||||
if self.mode == 1:
|
||||
value = self.communicate('F1')
|
||||
else:
|
||||
@@ -101,11 +79,6 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
||||
return convert(value)
|
||||
|
||||
def write_control_active(self, value):
|
||||
"""
|
||||
Turning on/off the heating, pump and regulation
|
||||
:param value: 0 is OFF, 1 is ON
|
||||
:return:
|
||||
"""
|
||||
if value is True:
|
||||
self.communicate('GO') # heating and pump run
|
||||
self.communicate('W SR') # regulation
|
||||
@@ -126,11 +99,6 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
||||
return convert(string)
|
||||
|
||||
def write_target(self, target):
|
||||
"""
|
||||
Selecting Celsius, setting the target
|
||||
:param target: target
|
||||
:return: target
|
||||
"""
|
||||
self.write_control_active(True)
|
||||
self.read_status()
|
||||
self.communicate('W TE C')
|
||||
@@ -138,11 +106,6 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
||||
return target
|
||||
|
||||
def write_mode(self, mode):
|
||||
"""
|
||||
Switching to internal or external control
|
||||
:param mode: internal/external
|
||||
:return: selected mode
|
||||
"""
|
||||
if mode == 1:
|
||||
self.communicate('W IN')
|
||||
self.communicate('W EX')
|
||||
@@ -150,7 +113,7 @@ class TemperatureLoop(HasIO, HasConvergence, Drivable):
|
||||
|
||||
@Command
|
||||
def clear_errors(self):
|
||||
""" Reset after error. Otherwise the status will not be updated"""
|
||||
""" Reset after error"""
|
||||
if self.read_status()[0] == ERROR:
|
||||
try:
|
||||
self.communicate('ER')
|
||||
|
||||
@@ -231,14 +231,17 @@ class ResChannel(Channel):
|
||||
def _read_value(self):
|
||||
"""read value, without update"""
|
||||
now = time.monotonic()
|
||||
if now + 0.5 < max(self._last_range_change, self.switcher._start_switch) + self.pause:
|
||||
if now - 0.5 < max(self._last_range_change, self.switcher._start_switch) + self.pause:
|
||||
return None
|
||||
result = float(self.communicate('RDGR?%d' % self.channel))
|
||||
if result == 0:
|
||||
if self.autorange:
|
||||
rng = int(max(self.minrange, self.range)) # convert from enum to int
|
||||
self.write_range(min(self.MAX_RNG, rng + 1))
|
||||
return None
|
||||
if self.autorange:
|
||||
self.fix_autorange()
|
||||
if now + 0.5 > self._last_range_change + self.pause:
|
||||
if now - 0.5 > self._last_range_change + self.pause:
|
||||
rng = int(max(self.minrange, self.range)) # convert from enum to int
|
||||
if self.status[0] < self.Status.ERROR:
|
||||
if abs(result) > self.RES_SCALE[rng]:
|
||||
@@ -251,8 +254,10 @@ class ResChannel(Channel):
|
||||
lim -= 0.05 # not more than 4 steps at once
|
||||
# effectively: <0.16 %: 4 steps, <1%: 3 steps, <5%: 2 steps, <20%: 1 step
|
||||
elif rng < self.MAX_RNG:
|
||||
self.log.debug('increase range due to error %d', rng)
|
||||
rng = min(self.MAX_RNG, rng + 1)
|
||||
if rng != self.range:
|
||||
self.log.debug('range change to %d', rng)
|
||||
self.write_range(rng)
|
||||
self._last_range_change = now
|
||||
return result
|
||||
@@ -381,6 +386,10 @@ class TemperatureLoop(HasConvergence, TemperatureChannel, Drivable):
|
||||
htrrng = Parameter('', EnumType(HTRRNG), readonly=False)
|
||||
_control_active = False
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
self.set_htrrng()
|
||||
|
||||
@Command
|
||||
def control_off(self):
|
||||
"""switch control off"""
|
||||
|
||||
@@ -112,7 +112,7 @@ class SimpleMagfield(HasStates, Drivable):
|
||||
last = self._last_target
|
||||
if last is None:
|
||||
try:
|
||||
last = self.setpoint # get read back from HW, if available
|
||||
last = self.read_setpoint() # get read back from HW, if available
|
||||
except Exception:
|
||||
pass
|
||||
if last is None or abs(last - self.value) > self.tolerance:
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"""modules to access parameters"""
|
||||
|
||||
from frappy.core import Drivable, EnumType, IDLE, Attached, StringType, Property, \
|
||||
Parameter, FloatRange, Readable, ERROR
|
||||
Parameter, FloatRange, BoolType, Readable, ERROR
|
||||
from frappy.errors import ConfigError
|
||||
from frappy_psi.convergence import HasConvergence
|
||||
from frappy_psi.mixins import HasRamp
|
||||
@@ -148,51 +148,61 @@ class SwitchDriv(HasConvergence, Drivable):
|
||||
max_low = Parameter('maximum low target', FloatRange(unit='$'), readonly=False)
|
||||
# disable_other = Parameter('whether to disable unused channel', BoolType(), readonly=False)
|
||||
selected = Parameter('selected module', EnumType(low=LOW, high=HIGH), readonly=False, default=0)
|
||||
_switch_target = None # if not None, switch to selection mhen mid range is reached
|
||||
autoswitch = Parameter('switch sensor automatically', BoolType(), readonly=False, default=True)
|
||||
_switch_target = None # if not None, switch to selection when mid range is reached
|
||||
|
||||
# TODO: copy units from attached module
|
||||
# TODO: callbacks for updates
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
if self.isBusy():
|
||||
if self._switch_target is not None:
|
||||
mid = (self.min_high + self.max_low) * 0.5
|
||||
if self._switch_target == HIGH:
|
||||
low = get_value(self.low, mid) # returns mid when low is invalid
|
||||
if low > mid:
|
||||
self.value = self.low.value
|
||||
self._switch_target = None
|
||||
self.write_target(self.target)
|
||||
return
|
||||
else:
|
||||
high = get_value(self.high, mid) # return mid then high is invalid
|
||||
if high < mid:
|
||||
self.value = self.high.value
|
||||
self._switch_target = None
|
||||
self.write_target(self.target)
|
||||
return
|
||||
else:
|
||||
if self._switch_target is not None:
|
||||
mid = (self.min_high + self.max_low) * 0.5
|
||||
if self._switch_target == HIGH:
|
||||
low = get_value(self.low, mid) # returns mid when low is invalid
|
||||
if low > mid:
|
||||
self.value = self.low.value
|
||||
self._switch_target = None
|
||||
self.write_target(self.target)
|
||||
return
|
||||
else:
|
||||
high = get_value(self.high, mid) # return mid when high is invalid
|
||||
if high < mid: # change to self.max_low
|
||||
self.value = self.high.value
|
||||
self._switch_target = None
|
||||
self.write_target(self.target)
|
||||
return
|
||||
if not self.isBusy() and self.autoswitch:
|
||||
low = get_value(self.low, self.max_low)
|
||||
high = get_value(self.high, self.min_high)
|
||||
low_valid = low < self.max_low
|
||||
high_valid = high > self.min_high
|
||||
if high_valid and high > self.max_low:
|
||||
if not low_valid:
|
||||
if not low_valid and not self.low.control_active:
|
||||
set_enabled(self.low, False)
|
||||
return
|
||||
if low_valid and low < self.min_high:
|
||||
if not high_valid:
|
||||
if not high_valid and not self.high.control_active:
|
||||
set_enabled(self.high, False)
|
||||
return
|
||||
set_enabled(self.low, True)
|
||||
set_enabled(self.high, True)
|
||||
# keep only one channel on
|
||||
#set_enabled(self.low, True)
|
||||
#set_enabled(self.high, True)
|
||||
|
||||
def get_selected(self):
|
||||
low = get_value(self.low, self.max_low)
|
||||
high = get_value(self.high, self.min_high)
|
||||
if low < self.min_high:
|
||||
return 0
|
||||
if high > self.max_low:
|
||||
return 1
|
||||
return self.selected
|
||||
|
||||
def read_value(self):
|
||||
return self.low.value if self.selected == LOW else self.high.value
|
||||
return self.low.value if self.get_selected() == LOW else self.high.value
|
||||
|
||||
def read_status(self):
|
||||
status = self.low.status if self.selected == LOW else self.high.status
|
||||
status = self.low.status if self.get_selected() == LOW else self.high.status
|
||||
if status[0] >= ERROR:
|
||||
return status
|
||||
return super().read_status() # convergence status
|
||||
@@ -202,22 +212,23 @@ class SwitchDriv(HasConvergence, Drivable):
|
||||
selected = self.selected
|
||||
target1 = target
|
||||
self._switch_target = None
|
||||
if target > self.max_low:
|
||||
if target > self.max_low * 0.75 + self.min_high * 0.25:
|
||||
if self.value < self.min_high:
|
||||
target1 = self.max_low
|
||||
target1 = min(target, self.max_low)
|
||||
self._switch_target = HIGH
|
||||
selected = LOW
|
||||
else:
|
||||
this, other = other, this
|
||||
selected = HIGH
|
||||
elif target < self.min_high:
|
||||
if self.value > self.max_low:
|
||||
target1 = self.min_high
|
||||
self._switch_target = LOW
|
||||
this, other = other, this
|
||||
selected = HIGH
|
||||
else:
|
||||
selected = LOW
|
||||
elif target < self.min_high * 0.75 + self.max_low * 0.25:
|
||||
# reinstall this with higher threshold (e.g. 4 K)?
|
||||
#if self.value > self.max_low:
|
||||
# target1 = max(self.min_high, target)
|
||||
# self._switch_target = LOW
|
||||
# this, other = other, this
|
||||
# selected = HIGH
|
||||
#else:
|
||||
selected = LOW
|
||||
elif self.selected == HIGH:
|
||||
this, other = other, this
|
||||
if hasattr(other, 'control_off'):
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors: Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Keithley Instruments 2601B-PULSE System (not finished)"""
|
||||
|
||||
from frappy.core import StringIO, HasIO, Readable, Writable, \
|
||||
Parameter, FloatRange, EnumType
|
||||
|
||||
|
||||
class PulseIO(StringIO):
|
||||
end_of_line = '\n'
|
||||
identification = [('*IDN?', 'Keithley Instruments, Model 2601B-PULSE,.*')]
|
||||
|
||||
|
||||
class Base(HasIO):
|
||||
|
||||
def set_source(self):
|
||||
"""
|
||||
Set the source -always current
|
||||
:return:
|
||||
"""
|
||||
return self.communicate(f'smua.source.func = smua.OUTPUT_DCAMPS')
|
||||
|
||||
def get_par(self, cmd):
|
||||
return self.communicate(f'reading = smua.measure.{cmd}()')
|
||||
|
||||
def auto_onof(self, val):
|
||||
if val == 1:
|
||||
return f'smua.AUTORANGE_ON'
|
||||
if val == 0:
|
||||
return f'smua.AUTORANGE_OFF'
|
||||
|
||||
def set_measure(self, cmd, val):
|
||||
return self.communicate(f'smua.measure.{cmd} = {val}')
|
||||
|
||||
|
||||
class Create_Pulse(Base, Readable, Writable):
|
||||
target = Parameter('source target', FloatRange, unit='A', readonly=False)
|
||||
width = Parameter('pulse width', FloatRange, unit="s", readonly=False)
|
||||
resistance = Parameter('resistance', FloatRange)
|
||||
|
||||
SOURCE_RANGE = ['100nA', '1uA', '10uA', '100uA', '1mA', '10mA', '100mA', '1A', '3A']
|
||||
range = Parameter('source range', EnumType('source range',
|
||||
{name: idx for idx, name in enumerate(SOURCE_RANGE)}), readonly=False)
|
||||
|
||||
def read_range(self):
|
||||
return self.range
|
||||
|
||||
def write_range(self, range):
|
||||
self.communicate(f'smua.source.rangei = {range}')
|
||||
return self.range
|
||||
|
||||
def write_target(self, target):
|
||||
return self.communicate(f'smua.source.leveli = {target}')
|
||||
|
||||
def read_resistance(self):
|
||||
return self.communicate('reading = smua.measure.r()')
|
||||
|
||||
|
||||
class Script(Create_Pulse):
|
||||
@@ -16,7 +16,7 @@
|
||||
# Module authors:
|
||||
# Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Temperature Controller TC1 Quantum NorthWest"""
|
||||
|
||||
|
||||
from frappy.core import Readable, Parameter, FloatRange, IDLE, ERROR, BoolType,\
|
||||
StringIO, HasIO, Property, WARN, Drivable, BUSY, StringType, Done
|
||||
@@ -31,17 +31,10 @@ class QnwIO(StringIO):
|
||||
|
||||
class SensorTC1(HasIO, Readable):
|
||||
ioClass = QnwIO
|
||||
value = Parameter(unit='degC', min=-55, max=150)
|
||||
value = Parameter(unit='degC', min=-15, max=120)
|
||||
channel = Property('channel name', StringType())
|
||||
|
||||
def set_param(self, adr, value=None):
|
||||
"""
|
||||
Set parameter.
|
||||
Every command starts with "[F1", and the end of line is "]".
|
||||
:param adr: second part of the command
|
||||
:param value: value to set
|
||||
:return: value converted to float
|
||||
"""
|
||||
short = adr.split()[0]
|
||||
# try 3 times in case we got an asynchronous message
|
||||
for _ in range(3):
|
||||
@@ -73,7 +66,7 @@ class SensorTC1(HasIO, Readable):
|
||||
|
||||
class TemperatureLoopTC1(SensorTC1, Drivable):
|
||||
value = Parameter('temperature', unit='degC')
|
||||
target = Parameter('setpoint', unit='degC', min=-55, max=150)
|
||||
target = Parameter('setpoint', unit='degC', min=-5, max=110)
|
||||
control = Parameter('temperature control flag', BoolType(), readonly=False)
|
||||
ramp = Parameter('ramping value', FloatRange, unit='degC/min', readonly=False)
|
||||
ramp_used = Parameter('ramping status', BoolType(), default=False, readonly=False)
|
||||
@@ -87,16 +80,6 @@ class TemperatureLoopTC1(SensorTC1, Drivable):
|
||||
return self.get_param('MT')
|
||||
|
||||
def read_status(self):
|
||||
"""
|
||||
the device returns 4 symbols according to the current status. These symbols are:
|
||||
”0” or “1” - number of unreported errors
|
||||
”+” or “-” - stirrer is on/off
|
||||
”+” or ”-” - temperature control is on/off
|
||||
”S” or “C” - current sample holder tempeerature is stable/changing
|
||||
There could be the fifth status symbol:
|
||||
”+” or “-” or “W” - rampping is on/off/waiting
|
||||
:return: status messages
|
||||
"""
|
||||
status = super().read_status()
|
||||
if status[0] == ERROR:
|
||||
return status
|
||||
|
||||
@@ -26,6 +26,7 @@ from frappy.datatypes import EnumType, FloatRange, StringType
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput
|
||||
from frappy_psi import mercury
|
||||
from frappy_psi.frozenparam import FrozenParam
|
||||
|
||||
actions = Enum(none=0, condense=1, circulate=2, collect=3)
|
||||
open_close = Mapped(CLOSE=0, OPEN=1)
|
||||
@@ -39,22 +40,23 @@ class Action(MercuryChannel, Writable):
|
||||
mix_channel = Property('mix channel', StringType(), 'T5')
|
||||
still_channel = Property('cool down channel', StringType(), 'T4')
|
||||
value = Parameter('running action', EnumType(actions))
|
||||
target = Parameter('action to do', EnumType(none=0, condense=1, collect=3), readonly=False)
|
||||
target = FrozenParam('action to do', EnumType(none=0, condense=1, collect=3), readonly=False)
|
||||
_target = 0
|
||||
|
||||
def read_value(self):
|
||||
return self.query('SYS:DR:ACTN', actions_map)
|
||||
|
||||
def read_target(self):
|
||||
return self._target
|
||||
# as target is a FrozenParam, value might be still lag behind target
|
||||
# but will be updated when changed from an other source
|
||||
read_target = read_value
|
||||
|
||||
def write_target(self, value):
|
||||
self._target = value
|
||||
self.change('SYS:DR:CHAN:COOL', self.cooldown_channel, str)
|
||||
self.change('SYS:DR:CHAN:STIL', self.still_channel, str)
|
||||
self.change('SYS:DR:CHAN:MC', self.mix_channel, str)
|
||||
self.change('DEV:T5:TEMP:MEAS:ENAB', 'ON', str)
|
||||
return self.change('SYS:DR:ACTN', value, actions_map)
|
||||
self.change('SYS:DR:ACTN', value, actions_map)
|
||||
return value
|
||||
|
||||
# actions:
|
||||
# NONE (no action)
|
||||
@@ -74,7 +76,7 @@ class Action(MercuryChannel, Writable):
|
||||
class Valve(MercuryChannel, Drivable):
|
||||
kind = 'VALV'
|
||||
value = Parameter('valve state', EnumType(closed=0, opened=1))
|
||||
target = Parameter('valve target', EnumType(close=0, open=1))
|
||||
target = FrozenParam('valve target', EnumType(close=0, open=1))
|
||||
|
||||
_try_count = None
|
||||
|
||||
@@ -108,6 +110,10 @@ class Valve(MercuryChannel, Drivable):
|
||||
self.change('DEV::VALV:SIG:STATE', self.target, open_close)
|
||||
return BUSY, 'waiting'
|
||||
|
||||
# as target is a FrozenParam, value might be still lag behind target
|
||||
# but will be updated when changed from an other source
|
||||
read_target = read_value
|
||||
|
||||
def write_target(self, value):
|
||||
if value != self.read_value():
|
||||
self._try_count = 0
|
||||
@@ -120,13 +126,18 @@ class Valve(MercuryChannel, Drivable):
|
||||
class Pump(MercuryChannel, Writable):
|
||||
kind = 'PUMP'
|
||||
value = Parameter('pump state', EnumType(off=0, on=1))
|
||||
target = Parameter('pump target', EnumType(off=0, on=1))
|
||||
target = FrozenParam('pump target', EnumType(off=0, on=1))
|
||||
|
||||
def read_value(self):
|
||||
return self.query('DEV::PUMP:SIG:STATE', off_on)
|
||||
|
||||
# as target is a FrozenParam, value might be still lag behind target
|
||||
# but will be updated when changed from an other source
|
||||
read_target = read_value
|
||||
|
||||
def write_target(self, value):
|
||||
return self.change('DEV::PUMP:SIG:STATE', value, off_on)
|
||||
self.change('DEV::PUMP:SIG:STATE', value, off_on)
|
||||
return value
|
||||
|
||||
def read_status(self):
|
||||
return IDLE, ''
|
||||
|
||||
Reference in New Issue
Block a user