Compare commits

...

9 Commits
wip ... paul

Author SHA1 Message Date
4c71e44cd2 changed sign of temperature offset adjustment of d and F in razorbill 2024-07-18 15:41:35 +02:00
5d0c980e22 added razorbill.py frappy file 2024-07-18 15:39:18 +02:00
f1bbdec2b3 post experiment commit, code is operational 2024-07-16 09:49:14 +02:00
2bdf9c6542 ACM1219: add checkHWIdent
waiting 0.5 sec before the first communication helps to start
properly

+ added usb dev names for box
2024-07-03 14:09:13 +02:00
Paul Neves
1fba0fad42 ACM1219: remove wait_before 2024-07-03 11:35:19 +02:00
Paul Neves
0a5de1ebc2 Updated the ACM1219 driver 2024-07-02 15:54:25 +02:00
Paul Neves
92c53ad3ba added GNU license 2024-06-17 20:20:54 +02:00
Paul Neves
802d4e0c99 made displacement and force calibrations writable and updated capacitance channel descriptions 2024-06-17 20:19:33 +02:00
Paul Neves
ee040ce98a added RP100, ACM1219, and dummy classes, and razorbillUC220T config file 2024-06-17 20:11:36 +02:00
4 changed files with 563 additions and 0 deletions

139
cfg/razorbillUC220T_cfg.py Normal file
View File

@ -0,0 +1,139 @@
# call $ bin/frappy-server razorbillUC220T
# in frappy directory, with python with frappy libraries installed.
Node('UC220T.psi.ch',
'A Razorbill UC220T controlled by a RP100 high voltage powersupply and a ACM1219 (AD7746) capacitance meter',
interface='tcp://3000')
Mod('io1',
'frappy_psi.RP100.RP100IO',
'communication',
uri='serial:///dev/ttyACM1?baudrate=9600+bytesize=8+parity=none+stopbits=1',
visibility=2)
Mod('Tension',
'frappy_psi.RP100.VoltageChannel',
'Voltage Channel 1',
temp='T',
io='io1',
target=Param(min=-200, max=200),
max_target=120,
min_target=-20,
slew_rate=5,
channel=1)
Mod('Compression',
'frappy_psi.RP100.VoltageChannel',
'Voltage Channel 2',
temp='T',
io='io1',
target=Param(min=-200, max=200),
max_target=120,
min_target=-20,
slew_rate=5,
channel=2)
Mod('io2',
'frappy_psi.ACM1219.ACM1219IO',
'communication',
uri='serial:///dev/ttyUSB1?baudrate=9600+bytesize=8+parity=none+stopbits=1',
visibility=2)
Mod('C1',
'frappy_psi.ACM1219.OneChannel',
'channel 1',
channel_enabled=True,
channel=1,
io='io2',
group='cap')
Mod('io3',
'frappy_psi.ACM1219.ACM1219IO',
'communication',
uri='serial:///dev/ttyUSB2?baudrate=9600+bytesize=8+parity=none+stopbits=1',
visibility=2)
Mod('C2',
'frappy_psi.ACM1219.OneChannel',
'channel 1',
channel_enabled=True,
channel=1,
io='io3',
group='cap')
# Mod('C1',
# 'frappy_psi.ACM1219.Channel',
# 'channel 1',
# group='cap')
# Mod('C2',
# 'frappy_psi.ACM1219.Channel',
# 'channel 2',
# group='cap')
# Mod('C1C2',
# 'frappy_psi.ACM1219.BothChannels',
# 'Capacitance channels 1 and 2',
# chan1='C1',
# chan2='C2',
# channels_enabled=True,
# io='io2',
# group='cap')
Mod('d',
'frappy_psi.razorbill.Displacement',
'razorbill displacement from capacitance',
cap='C1',
alpha290K=56.710,
d0=95.443,
Cp=0.01883,
d0_curve={'a':4.21,'b':-0.00157,'c':-3.38e-5,'d':5.28e-8,'e':-6.93e-11},
temp='T')
Mod('strain',
'frappy_psi.razorbill.Strain',
'Sample strain from force',
displacement='d',
L=3,
)
Mod('F',
'frappy_psi.razorbill.Force',
'razorbill force from capacitance',
cap='C2',
alpha290K=374.23,
f0=315.63,
Cp=0.0755,
f0_curve={'a':38.9,'b':-0.0147,'c':-0.000346,'d':8.96e-7,'e':-1.58e-9},
temp='T')
Mod('stress',
'frappy_psi.razorbill.Stress',
'Sample stress from force',
force='F',
area=0.1,
)
Mod('YM',
'frappy_psi.razorbill.YoungsModulus',
'Sample youngs modulus from stress and strain',
stress='stress',
strain='strain',
)
Mod('T',
'frappy_psi.razorbill.Temp',
'dummy T written from client',
target=Param(value=300, min=1, max=325),
)
Mod('io4',
'frappy_psi.ls372.StringIO',
'the communication device',
uri='tcp://192.168.3.3:7777',
visibility=2
)
Mod('lsswitcher',
'frappy_psi.ls372.Switcher',
'Switcher control of Lsc controller',
uri='tcp://192.168.3.3:7777',
io='io4',
)
Mod('res',
'frappy_psi.ls372.ResChannel',
'resistivity',
iexc='100uA',
range='63.2mOhm',
channel=1,
switcher='lsswitcher',
)

152
frappy_psi/ACM1219.py Normal file
View File

@ -0,0 +1,152 @@
# *****************************************************************************
# 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:
# Paul M. Neves <pmneves@mit.edu>
# *****************************************************************************
import time
from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, IntRange,\
IDLE, BUSY, WARN, ERROR, Drivable, BoolType, Attached, StructOf
class ACM1219IO(StringIO):
"""communication with ACM1219"""
end_of_line = ('\r\n', '\r') # ('\n', '\r') ('\r\n', '\r')
encoding = 'latin-1' # initial reply might not be ascii for a strange reason
identification = [('*IDN?', r'.*,ACM1219,.*')]
def checkHWIdent(self):
for _ in range(3):
time.sleep(0.5)
try:
self.communicate('*IDN?')
break
except Exception:
pass
super().checkHWIdent()
class Channel(Readable):
value = Parameter('one channel', unit='pF')
class OneChannel(HasIO, Readable):
"""read both capacitance channels in multiplex mode"""
# define the communication class for automatic creation of the IO module
ioClass = ACM1219IO
# modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
value = Parameter('Capacitance of one channel',
FloatRange(0, 21.096, unit='pF'), readonly=True)
channel_enabled = Parameter('channel on or off', BoolType(), readonly=False)
channel = Property('the voltage channel', datatype=IntRange(1,2))
_ch_enabled = False
def read_value(self):
reply = self.communicate('readCVT').split(',')
value = float(reply[self.channel-1])
return value
def read_status(self):
# code = self.communicate(f'readStatus') # returns tons of data
return IDLE, ''
def read_channel_enabled(self):
return self._ch_enabled
def write_channel_enabled(self, channel_enabled):
if channel_enabled:
self.communicate(f'setCIN {self.channel},0,00.0,00.0,0,00.0,00.0')
# self.communicate(f'setCIN 2,0,00.0,00.0,0,00.0,00.0')
self._ch_enabled = True
else:
self.communicate(f'setCIN 0,0,00.0,00.0,0,00.0,00.0')
self._ch_enabled = False
return self.read_channel_enabled()
class BothChannels(HasIO, Readable):
"""read both capacitance channels in multiplex mode"""
# define the communication class for automatic creation of the IO module
ioClass = ACM1219IO
chan1 = Attached(Readable)
chan2 = Attached(Readable)
# modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
value = Parameter('Capacitance 1 and 2, and VT',
StructOf(C1=FloatRange(0, 21.096, unit='pF'), C2=FloatRange(0, 21.096, unit='pF'), VT=FloatRange(-1, 1000, unit='')),
readonly=True)
channels_enabled = Parameter('channels on or off', BoolType(), readonly=False)
_ch_enabled = False
_channel = 1
def Xread_value(self):
# using the inherited HasIO.communicate method to send a command and get the reply
natempt = 0
maxAttempts = 5
while natempt < maxAttempts:
try:
reply = self.communicate(f'readMUC')
# print(reply)
reply = reply.split(',')
C1 = float(reply[0])
C2 = float(reply[1])
VT = float(reply[2])
self.chan1.value = C1
self.chan2.value = C2
return {'C1': C1, 'C2': C2, 'VT': VT}
except:
''
natempt+=1
if natempt >= maxAttempts:
print('Max attempt reached for reading arduino.')
return self.value
def read_value(self):
reply = self.communicate('readCVT').split(',')
value = float(reply[self._channel-1])
previous = dict(self.value)
previous[f'C{self._channel}'] = value
getattr(self, f'chan{self._channel}').value = value
self._channel = 3 - self._channel
self.communicate(f'setCIN {self._channel},0,00.0,00.0,0,00.0,00.0')
return previous
def read_status(self):
# code = self.communicate(f'readStatus') # returns tons of data
return IDLE, ''
def read_channels_enabled(self):
return self._ch_enabled
def write_channels_enabled(self, channels_enabled):
if channels_enabled:
self.communicate(f'setCIN {self._channel},0,00.0,00.0,0,00.0,00.0')
# self.communicate(f'setCIN 2,0,00.0,00.0,0,00.0,00.0')
self._ch_enabled = True
self.communicate(f'setEXC 2,2,3,1')
else:
self.communicate(f'setCIN 0,0,00.0,00.0,0,00.0,00.0')
self._ch_enabled = False
self.communicate(f'setEXC 0,0,3,1')
return self.read_channels_enabled()

130
frappy_psi/RP100.py Normal file
View File

@ -0,0 +1,130 @@
# *****************************************************************************
# 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:
# Paul M. Neves <pmneves@mit.edu>
# *****************************************************************************
from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, IntRange,\
IDLE, BUSY, WARN, ERROR, Drivable, BoolType, Attached
from ast import literal_eval
class RP100IO(StringIO):
"""communication with RP100"""
end_of_line = '\n'
#wait_before = 0.05
identification = [('*IDN?', r'Razorbill,.*')]
class VoltageChannel(HasIO, Drivable):
"""a voltage output with loop"""
temp = Attached()
# define the communication class for automatic creation of the IO module
ioClass = RP100IO
# internal property to configure the channel
channel = Property('the voltage channel', datatype=IntRange(1,2))
# modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
value = Parameter('output voltage', FloatRange(-210, 210, unit='V'),
readonly=True)
target = Parameter('target voltage', FloatRange(-210, 210, unit='V'),
readonly=False)
meas_voltage = Parameter('measured output voltage', FloatRange(-250, 250, unit='V'),
readonly=True)
meas_current = Parameter('measured output current', FloatRange(-0.007, 0.007, unit='A'),
readonly=True)
max_target = Parameter('max. target', FloatRange(0, 210, unit='V'), readonly=False)
min_target = Parameter('max. target', FloatRange(-210, 0, unit='V'), readonly=False)
slew_rate = Parameter('voltage slew rate', FloatRange(0.1e-3, 100e3, unit='V/s'), readonly=False)
output_state = Parameter('output on or off', BoolType(), readonly=False)
def doPoll(self):
super().doPoll()
# calculate temperature dependent voltage limits
temp = self.temp.target
if temp > 250:
self.max_target = 120
self.min_target = -20
elif temp >= 100:
self.max_target = 120
self.min_target = -50 + (temp-100)/5
elif temp >= 10:
self.max_target = 200 - 8*(temp-10)/9
self.min_target = -200 + 5*(temp-10)/3
elif temp < 10:
self.max_target = 200
self.min_target = -200
# if the current voltage exceeds these limits, reduce voltage to max/min
if self.target > self.max_target:
self.write_target(self.max_target)
if self.target < self.min_target:
self.write_target(self.min_target)
def read_value(self):
# using the inherited HasIO.communicate method to send a command and get the reply
reply = self.communicate(f'SOUR{self.channel}:VOLT:NOW?')
return float(reply)
def read_status(self):
while 1:
code, text = literal_eval(self.communicate(f'SYST:ERR?'))
if code == 0:
break
self.log.warning('got error %d %s', code, text)
return IDLE, ''
def read_target(self):
# read back the target value
target = float(self.communicate(f'SOUR{self.channel}:VOLT?'))
return target
def write_target(self, target):
# write here the target to the hardware
if target > self.max_target:
target = self.max_target
self.log.warning('Attempted to set voltage above maximum allowed voltage. Setting to max allowed instead.')
if target < self.min_target:
target = self.min_target
self.log.warning('Attempted to set voltage below minimum allowed voltage. Setting to min allowed instead.')
self.communicate(f'SOUR{self.channel}:VOLT {target};*OPC?')
return self.read_target() # return the read back value
def read_slew_rate(self):
return float(self.communicate(f'SOUR{self.channel}:VOLT:SLEW?'))
def write_slew_rate(self, slew_rate):
self.communicate(f'SOUR{self.channel}:VOLT:SLEW {slew_rate};*OPC?')
return self.read_slew_rate()
def read_output_state(self):
return int(self.communicate(f'OUTP{self.channel}?'))
def write_output_state(self, output_state):
self.communicate(f'OUTP{self.channel} {int(output_state)};*OPC?')
if not output_state:
self.write_target(0)
return self.read_output_state()
def read_meas_voltage(self):
return float(self.communicate(f'MEAS{self.channel}:VOLT?'))
def read_meas_current(self):
return float(self.communicate(f'MEAS{self.channel}:CURR?'))

142
frappy_psi/razorbill.py Normal file
View File

@ -0,0 +1,142 @@
# *****************************************************************************
# 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:
# Paul M. Neves <pmneves@mit.edu>
# *****************************************************************************
from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, IntRange,\
IDLE, BUSY, WARN, ERROR, Drivable, BoolType, Attached, Writable, StructOf
class Temp(Writable):
"""stores sample temperature"""
target = Parameter('target voltage', FloatRange(0, 325, unit='K'),
readonly=False)
def write_target(self, target):
self.value = target
def read_value(self):
return self.value
class Displacement(Readable):
# attached classes for capacitance and temperature
cap = Attached()
temp = Attached()
# modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
value = Parameter('displacement', FloatRange(None, None, unit='um'), readonly=True)
alpha290K = Parameter('capacitor constant at 290 K', FloatRange(None, None, unit='um pF'), readonly=False)
d0 = Parameter('offset displacement', FloatRange(None, None, unit='um'), readonly=False)
Cp = Parameter('parallel capacitance', FloatRange(None, None, unit='pF'), readonly=False)
d0_curve = Parameter('calibration curve for offset displacement',
StructOf(a=FloatRange(None, None, unit='um'),
b=FloatRange(None, None, unit='um/K'),
c=FloatRange(None, None, unit='um/K^2'),
d=FloatRange(None, None, unit='um/K^3'),
e=FloatRange(None, None, unit='um/K^4'),),
readonly=False)
def read_value(self):
# get temperature and capacitance
temp = self.temp.target
cap = self.cap.value
# calculate displacement from temperature and capacitance
d0_T = self.d0_curve['a'] + self.d0_curve['b']*temp + self.d0_curve['c']*temp**2 + self.d0_curve['d']*temp**3 + self.d0_curve['e']*temp**4
disp = self.alpha290K / (cap - self.Cp) - self.d0 - d0_T
return disp
class Force(Readable):
# attached classes for capacitance and temperature
cap = Attached()
temp = Attached()
# modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
value = Parameter('force', FloatRange(None, None, unit='N'), readonly=True)
alpha290K = Parameter('capacitor constant at 290 K', FloatRange(None, None, unit='N pF'), readonly=False)
f0 = Parameter('offset force', FloatRange(None, None, unit='N'), readonly=False)
Cp = Parameter('parallel capacitance', FloatRange(None, None, unit='pF'), readonly=False)
f0_curve = Parameter('calibration curve for offset force',
StructOf(a=FloatRange(None, None, unit='N'),
b=FloatRange(None, None, unit='N/K'),
c=FloatRange(None, None, unit='N/K^2'),
d=FloatRange(None, None, unit='N/K^3'),
e=FloatRange(None, None, unit='N/K^4'),),
readonly=False)
def read_value(self):
# get temperature and capacitance
temp = self.temp.target
cap = self.cap.value
# calculate force from temperature and capacitance
alpha = self.alpha290K * (0.91 + 5e-5*temp + 9e-7*temp**2)
f0_T = self.f0_curve['a'] + self.f0_curve['b']*temp + self.f0_curve['c']*temp**2 + self.f0_curve['d']*temp**3 + self.f0_curve['e']*temp**4
force = alpha / (cap - self.Cp) - self.f0 - f0_T
return force
class Stress(Readable):
# attached classes for displacement
force = Attached()
# modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
value = Parameter('stress', FloatRange(None, None, unit='GPa'), readonly=True)
area = Parameter('cross sectional area of sample in mm^2', FloatRange(None, None, unit='mm^2'), readonly=False)
def read_value(self):
return self.force.value / self.area / 1000
class Strain(Readable):
# attached classes for displacement
displacement = Attached()
# modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
value = Parameter('strain', FloatRange(None, None, unit='m/m'), readonly=True)
L = Parameter('length of sample in mm', FloatRange(None, None, unit='mm'), readonly=False)
def read_value(self):
return self.displacement.value / (1000*self.L)
class YoungsModulus(Readable):
# attached classes for displacement
stress = Attached()
strain = Attached()
# modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
value = Parameter('Young\'s modulus', FloatRange(None, None, unit='GPa'), readonly=True)
def read_value(self):
if self.strain.value:
return self.stress.value / self.strain.value
else:
return 0