fetched mlz version
- before some chamges in the gerrit pipline Change-Id: I33eb2d75f83345a7039d0fb709e66defefb1c3e0
This commit is contained in:
0
frappy_demo/__init__.py
Normal file
0
frappy_demo/__init__.py
Normal file
361
frappy_demo/cryo.py
Normal file
361
frappy_demo/cryo.py
Normal file
@ -0,0 +1,361 @@
|
||||
# -*- 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:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""playing implementation of a (simple) simulated cryostat"""
|
||||
|
||||
|
||||
import random
|
||||
import time
|
||||
from math import atan
|
||||
|
||||
from frappy.datatypes import BoolType, EnumType, FloatRange, StringType, TupleOf
|
||||
from frappy.lib import clamp, mkthread
|
||||
from frappy.modules import Command, Drivable, Parameter
|
||||
# test custom property (value.test can be changed in config file)
|
||||
from frappy.properties import Property
|
||||
|
||||
|
||||
class TestParameter(Parameter):
|
||||
test = Property('A Property for testing purposes', StringType(), default='', export=True)
|
||||
|
||||
|
||||
class CryoBase(Drivable):
|
||||
is_cryo = Property('private Flag if this is a cryostat', BoolType(), default=True, export=True)
|
||||
|
||||
|
||||
class Cryostat(CryoBase):
|
||||
"""simulated cryostat with:
|
||||
|
||||
- heat capacity of the sample
|
||||
- cooling power
|
||||
- thermal transfer between regulation and samplen
|
||||
"""
|
||||
|
||||
jitter = Parameter("amount of random noise on readout values",
|
||||
datatype=FloatRange(0, 1), unit="K",
|
||||
default=0.1, readonly=False, export=False)
|
||||
T_start = Parameter("starting temperature for simulation",
|
||||
datatype=FloatRange(0), default=10,
|
||||
export=False)
|
||||
looptime = Parameter("timestep for simulation",
|
||||
datatype=FloatRange(0.01, 10), unit="s", default=1,
|
||||
readonly=False, export=False)
|
||||
ramp = Parameter("ramping speed of the setpoint",
|
||||
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
||||
readonly=False)
|
||||
setpoint = Parameter("current setpoint during ramping else target",
|
||||
datatype=FloatRange(), default=1, unit='K')
|
||||
maxpower = Parameter("Maximum heater power",
|
||||
datatype=FloatRange(0), default=1, unit="W",
|
||||
readonly=False,
|
||||
group='heater_settings')
|
||||
heater = Parameter("current heater setting",
|
||||
datatype=FloatRange(0, 100), default=0, unit="%",
|
||||
group='heater_settings')
|
||||
heaterpower = Parameter("current heater power",
|
||||
datatype=FloatRange(0), default=0, unit="W",
|
||||
group='heater_settings')
|
||||
target = Parameter("target temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
readonly=False,)
|
||||
value = TestParameter("regulation temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
test='TEST')
|
||||
pid = Parameter("regulation coefficients",
|
||||
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
|
||||
FloatRange(0, 100)),
|
||||
default=(40, 10, 2), readonly=False,
|
||||
group='pid')
|
||||
# pylint: disable=invalid-name
|
||||
p = Parameter("regulation coefficient 'p'",
|
||||
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
||||
group='pid')
|
||||
i = Parameter("regulation coefficient 'i'",
|
||||
datatype=FloatRange(0, 100), default=10, readonly=False,
|
||||
group='pid')
|
||||
d = Parameter("regulation coefficient 'd'",
|
||||
datatype=FloatRange(0, 100), default=2, readonly=False,
|
||||
group='pid')
|
||||
mode = Parameter("mode of regulation",
|
||||
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
|
||||
default='ramp',
|
||||
readonly=False)
|
||||
pollinterval = Parameter("polling interval",
|
||||
datatype=FloatRange(0), default=5)
|
||||
tolerance = Parameter("temperature range for stability checking",
|
||||
datatype=FloatRange(0, 100), default=0.1, unit='K',
|
||||
readonly=False,
|
||||
group='stability')
|
||||
window = Parameter("time window for stability checking",
|
||||
datatype=FloatRange(1, 900), default=30, unit='s',
|
||||
readonly=False,
|
||||
group='stability')
|
||||
timeout = Parameter("max waiting time for stabilisation check",
|
||||
datatype=FloatRange(1, 36000), default=900, unit='s',
|
||||
readonly=False,
|
||||
group='stability')
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._stopflag = False
|
||||
self._thread = mkthread(self.thread)
|
||||
|
||||
def read_status(self):
|
||||
# instead of asking a 'Hardware' take the value from the simulation
|
||||
return self.status
|
||||
|
||||
def read_value(self):
|
||||
# return regulation value (averaged regulation temp)
|
||||
return self.regulationtemp + \
|
||||
self.jitter * (0.5 - random.random())
|
||||
|
||||
def read_target(self):
|
||||
return self.target
|
||||
|
||||
def write_target(self, value):
|
||||
value = round(value, 2)
|
||||
if value == self.target:
|
||||
# nothing to do
|
||||
return value
|
||||
self.target = value
|
||||
# next read_status will see this status, until the loop updates it
|
||||
self.status = self.Status.BUSY, 'new target set'
|
||||
return value
|
||||
|
||||
def read_maxpower(self):
|
||||
return self.maxpower
|
||||
|
||||
def write_maxpower(self, newpower):
|
||||
# rescale heater setting in % to keep the power
|
||||
heat = max(0, min(100, self.heater * self.maxpower / float(newpower)))
|
||||
self.heater = heat
|
||||
self.maxpower = newpower
|
||||
return newpower
|
||||
|
||||
def write_pid(self, newpid):
|
||||
self.p, self.i, self.d = newpid
|
||||
return (self.p, self.i, self.d)
|
||||
|
||||
def read_pid(self):
|
||||
return (self.p, self.i, self.d)
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
"""Stop ramping the setpoint
|
||||
|
||||
by setting the current setpoint as new target"""
|
||||
# XXX: discussion: take setpoint or current value ???
|
||||
self.write_target(self.setpoint)
|
||||
|
||||
#
|
||||
# calculation helpers
|
||||
#
|
||||
def __coolerPower(self, temp):
|
||||
"""returns cooling power in W at given temperature"""
|
||||
# quadratic up to 42K, is linear from 40W@42K to 100W@600K
|
||||
# return clamp((temp-2)**2 / 32., 0., 40.) + temp * 0.1
|
||||
return clamp(15 * atan(temp * 0.01)**3, 0., 40.) + temp * 0.1 - 0.2
|
||||
|
||||
def __coolerCP(self, temp):
|
||||
"""heat capacity of cooler at given temp"""
|
||||
return 75 * atan(temp / 50)**2 + 1
|
||||
|
||||
def __heatLink(self, coolertemp, sampletemp):
|
||||
"""heatflow from sample to cooler. may be negative..."""
|
||||
flow = (sampletemp - coolertemp) * \
|
||||
((coolertemp + sampletemp) ** 2) / 400.
|
||||
cp = clamp(
|
||||
self.__coolerCP(coolertemp) * self.__sampleCP(sampletemp), 1, 10)
|
||||
return clamp(flow, -cp, cp)
|
||||
|
||||
def __sampleCP(self, temp):
|
||||
return 3 * atan(temp / 30) + \
|
||||
12 * temp / ((temp - 12.)**2 + 10) + 0.5
|
||||
|
||||
def __sampleLeak(self, temp):
|
||||
return 0.02 / temp
|
||||
|
||||
def thread(self):
|
||||
self.sampletemp = self.T_start
|
||||
self.regulationtemp = self.T_start
|
||||
self.status = self.Status.IDLE, ''
|
||||
while not self._stopflag:
|
||||
try:
|
||||
self.__sim()
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
self.status = self.Status.ERROR, str(e)
|
||||
|
||||
def __sim(self):
|
||||
# complex thread handling:
|
||||
# a) simulation of cryo (heat flow, thermal masses,....)
|
||||
# b) optional PID temperature controller with windup control
|
||||
# c) generating status+updated value+ramp
|
||||
# this thread is not supposed to exit!
|
||||
|
||||
self.setpoint = self.target
|
||||
# local state keeping:
|
||||
regulation = self.regulationtemp
|
||||
sample = self.sampletemp
|
||||
# keep history values for stability check
|
||||
window = []
|
||||
timestamp = time.time()
|
||||
heater = 0
|
||||
lastflow = 0
|
||||
last_heaters = (0, 0)
|
||||
delta = 0
|
||||
_I = _D = 0
|
||||
lastD = 0
|
||||
damper = 1
|
||||
lastmode = self.mode
|
||||
while not self._stopflag:
|
||||
t = time.time()
|
||||
h = t - timestamp
|
||||
if h < self.looptime / damper:
|
||||
time.sleep(clamp(self.looptime / damper - h, 0.1, 60))
|
||||
continue
|
||||
# a)
|
||||
sample = self.sampletemp
|
||||
regulation = self.regulationtemp
|
||||
heater = self.heater
|
||||
|
||||
heatflow = self.__heatLink(regulation, sample)
|
||||
self.log.debug('sample = %.5f, regulation = %.5f, heatflow = %.5g',
|
||||
sample, regulation, heatflow)
|
||||
newsample = max(0, sample + (self.__sampleLeak(sample) - heatflow)
|
||||
/ self.__sampleCP(sample) * h)
|
||||
# avoid instabilities due to too small CP
|
||||
newsample = clamp(newsample, sample, regulation)
|
||||
regdelta = (heater * 0.01 * self.maxpower + heatflow -
|
||||
self.__coolerPower(regulation))
|
||||
newregulation = max(
|
||||
0, regulation + regdelta / self.__coolerCP(regulation) * h)
|
||||
# b) see
|
||||
# http://brettbeauregard.com/blog/2011/04/
|
||||
# improving-the-beginners-pid-introduction/
|
||||
if self.mode != self.mode.openloop:
|
||||
# fix artefacts due to too big timesteps
|
||||
# actually i would prefer reducing looptime, but i have no
|
||||
# good idea on when to increase it back again
|
||||
if heatflow * lastflow != -100:
|
||||
if (newregulation - newsample) * (regulation - sample) < 0:
|
||||
# newregulation = (newregulation + regulation) / 2
|
||||
# newsample = (newsample + sample) / 2
|
||||
damper += 1
|
||||
lastflow = heatflow
|
||||
|
||||
error = self.setpoint - newregulation
|
||||
# use a simple filter to smooth delta a little
|
||||
delta = (delta + regulation - newregulation) * 0.5
|
||||
|
||||
kp = self.p * 0.1 # LakeShore P = 10*k_p
|
||||
ki = kp * abs(self.i) / 500. # LakeShore I = 500/T_i
|
||||
kd = kp * abs(self.d) / 2. # LakeShore D = 2*T_d
|
||||
_P = kp * error
|
||||
_I += ki * error * h
|
||||
_D = kd * delta / h
|
||||
|
||||
# avoid reset windup
|
||||
_I = clamp(_I, 0., 100.) # _I is in %
|
||||
|
||||
# avoid jumping heaterpower if switching back to pid mode
|
||||
if lastmode != self.mode:
|
||||
# adjust some values upon switching back on
|
||||
_I = self.heater - _P - _D
|
||||
|
||||
v = _P + _I + _D
|
||||
# in damping mode, use a weighted sum of old + new heaterpower
|
||||
if damper > 1:
|
||||
v = ((damper**2 - 1) * self.heater + v) / damper**2
|
||||
|
||||
# damp oscillations due to D switching signs
|
||||
if _D * lastD < -0.2:
|
||||
v = (v + heater) * 0.5
|
||||
# clamp new heater power to 0..100%
|
||||
heater = clamp(v, 0., 100.)
|
||||
lastD = _D
|
||||
|
||||
self.log.debug('PID: P = %.2f, I = %.2f, D = %.2f, '
|
||||
'heater = %.2f', _P, _I, _D, heater)
|
||||
|
||||
# check for turn-around points to detect oscillations ->
|
||||
# increase damper
|
||||
x, y = last_heaters
|
||||
if (x + 0.1 < y and y > heater + 0.1) or \
|
||||
(x > y + 0.1 and y + 0.1 < heater):
|
||||
damper += 1
|
||||
last_heaters = (y, heater)
|
||||
|
||||
else:
|
||||
# self.heaterpower is set manually, not by pid
|
||||
heater = self.heater
|
||||
last_heaters = (0, 0)
|
||||
|
||||
heater = round(heater, 1)
|
||||
sample = newsample
|
||||
regulation = newregulation
|
||||
lastmode = self.mode
|
||||
# c)
|
||||
if self.setpoint != self.target:
|
||||
if self.ramp == 0 or self.mode == self.mode.enum.pid:
|
||||
maxdelta = 10000
|
||||
else:
|
||||
maxdelta = self.ramp / 60. * h
|
||||
try:
|
||||
self.setpoint = round(self.setpoint + clamp(
|
||||
self.target - self.setpoint, -maxdelta, maxdelta), 3)
|
||||
self.log.debug('setpoint changes to %r (target %r)',
|
||||
self.setpoint, self.target)
|
||||
except (TypeError, ValueError):
|
||||
# self.target might be None
|
||||
pass
|
||||
|
||||
# temperature is stable when all recorded values in the window
|
||||
# differ from setpoint by less than tolerance
|
||||
currenttime = time.time()
|
||||
window.append((currenttime, sample))
|
||||
while window[0][0] < currenttime - self.window:
|
||||
# remove old/stale entries
|
||||
window.pop(0)
|
||||
# obtain min/max
|
||||
deviation = 0
|
||||
for _, _T in window:
|
||||
if abs(_T - self.target) > deviation:
|
||||
deviation = abs(_T - self.target)
|
||||
if (len(window) < 3) or deviation > self.tolerance:
|
||||
self.status = self.Status.BUSY, 'unstable'
|
||||
elif self.setpoint == self.target:
|
||||
self.status = self.Status.IDLE, 'at target'
|
||||
damper -= (damper - 1) * 0.1 # max value for damper is 11
|
||||
else:
|
||||
self.status = self.Status.BUSY, 'ramping setpoint'
|
||||
damper -= (damper - 1) * 0.05
|
||||
self.regulationtemp = round(regulation, 3)
|
||||
self.sampletemp = round(sample, 3)
|
||||
self.heaterpower = round(heater * self.maxpower * 0.01, 3)
|
||||
self.heater = heater
|
||||
timestamp = t
|
||||
self.read_value()
|
||||
|
||||
def shutdown(self):
|
||||
# should be called from server when the server is stopped
|
||||
self._stopflag = True
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join()
|
111
frappy_demo/lakeshore.py
Normal file
111
frappy_demo/lakeshore.py
Normal file
@ -0,0 +1,111 @@
|
||||
#!/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>
|
||||
# *****************************************************************************
|
||||
"""LakeShore demo
|
||||
|
||||
demo example for tutorial
|
||||
"""
|
||||
|
||||
from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType, \
|
||||
IDLE, BUSY, WARN, ERROR, Drivable, IntRange
|
||||
|
||||
|
||||
class LakeshoreIO(StringIO):
|
||||
wait_before = 0.05 # Lakeshore requires a wait time of 50 ms between commands
|
||||
# Lakeshore commands (see manual)
|
||||
# '*IDN?' is sent on connect, and the reply is checked to match the regexp 'LSCI,.*'
|
||||
identification = [('*IDN?', 'LSCI,.*')]
|
||||
default_settings = {'port': 7777, 'baudrate': 57600, 'parity': 'O', 'bytesize': 7}
|
||||
|
||||
|
||||
class TemperatureSensor(HasIO, Readable):
|
||||
"""a temperature sensor (generic for different models)"""
|
||||
# internal property to configure the channel
|
||||
channel = Property('the Lakeshore channel', datatype=StringType())
|
||||
# 0, 1500 is the allowed range by the LakeShore controller
|
||||
# this range should be further restricted in the configuration (see below)
|
||||
value = Parameter(datatype=FloatRange(0, 1500, unit='K'))
|
||||
|
||||
def read_value(self):
|
||||
# the communicate method sends a command and returns the reply
|
||||
reply = self.communicate(f'KRDG?{self.channel}')
|
||||
return float(reply)
|
||||
|
||||
def read_status(self):
|
||||
code = int(self.communicate(f'RDGST?{self.channel}'))
|
||||
if code >= 128:
|
||||
text = 'units overrange'
|
||||
elif code >= 64:
|
||||
text = 'units zero'
|
||||
elif code >= 32:
|
||||
text = 'temperature overrange'
|
||||
elif code >= 16:
|
||||
text = 'temperature underrange'
|
||||
elif code % 2:
|
||||
# ignore 'old reading', as this may happen in normal operation
|
||||
text = 'invalid reading'
|
||||
else:
|
||||
return IDLE, ''
|
||||
return ERROR, text
|
||||
|
||||
|
||||
class TemperatureLoop(TemperatureSensor, Drivable):
|
||||
# lakeshore loop number to be used for this module
|
||||
loop = Property('lakeshore loop', IntRange(1, 2), default=1)
|
||||
target = Parameter(datatype=FloatRange(min=0, max=1500, unit='K'))
|
||||
heater_range = Property('heater power range', IntRange(0, 5)) # max. 3 on LakeShore 336
|
||||
tolerance = Parameter('convergence criterion', FloatRange(0), default=0.1, readonly = False)
|
||||
_driving = False
|
||||
|
||||
def write_target(self, target):
|
||||
# reactivate heater in case it was switched off
|
||||
# the command has to be changed in case of model 340 to f'RANGE {self.heater_range};RANGE?'
|
||||
self.communicate(f'RANGE {self.loop},{self.heater_range};RANGE?{self.loop}')
|
||||
reply = self.communicate(f'SETP {self.loop},{target};SETP? {self.loop}')
|
||||
self._driving = True
|
||||
# Setting the status attribute triggers an update message for the SECoP status
|
||||
# parameter. This has to be done before returning from this method!
|
||||
self.status = BUSY, 'target changed'
|
||||
return float(reply)
|
||||
|
||||
def read_target(self):
|
||||
return float(self.communicate(f'SETP?{self.loop}'))
|
||||
|
||||
def read_status(self):
|
||||
code = int(self.communicate(f'RDGST?{self.channel}'))
|
||||
if code >= 128:
|
||||
text = 'units overrange'
|
||||
elif code >= 64:
|
||||
text = 'units zero'
|
||||
elif code >= 32:
|
||||
text = 'temperature overrange'
|
||||
elif code >= 16:
|
||||
text = 'temperature underrange'
|
||||
elif code % 2:
|
||||
# ignore 'old reading', as this may happen in normal operation
|
||||
text = 'invalid reading'
|
||||
elif abs(self.target - self.value) > self.tolerance:
|
||||
if self._driving:
|
||||
return BUSY, 'approaching setpoint'
|
||||
return WARN, 'temperature out of tolerance'
|
||||
else: # within tolerance: simple convergence criterion
|
||||
self._driving = False
|
||||
return IDLE, ''
|
||||
return ERROR, text
|
132
frappy_demo/lscsim.py
Normal file
132
frappy_demo/lscsim.py
Normal file
@ -0,0 +1,132 @@
|
||||
#!/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>
|
||||
# *****************************************************************************
|
||||
"""a very simple simulator for a LakeShore
|
||||
|
||||
Model 370: parameters are stored, but no cryo simulation
|
||||
Model 336: heat exchanger on channel A with loop 1, sample sensor on channel B
|
||||
"""
|
||||
|
||||
from frappy.modules import Communicator
|
||||
|
||||
|
||||
class Ls370Sim(Communicator):
|
||||
CHANNEL_COMMANDS = [
|
||||
('RDGR?%d', '200.0'),
|
||||
('RDGST?%d', '0'),
|
||||
('RDGRNG?%d', '0,5,5,0,0'),
|
||||
('INSET?%d', '1,5,5,0,0'),
|
||||
('FILTER?%d', '1,5,80'),
|
||||
]
|
||||
OTHER_COMMANDS = [
|
||||
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
|
||||
('SCAN?', '3,1'),
|
||||
('*OPC?', '1'),
|
||||
]
|
||||
pollinterval = 1
|
||||
|
||||
CHANNELS = list(range(1, 17))
|
||||
data = ()
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self.data = dict(self.OTHER_COMMANDS)
|
||||
for fmt, v in self.CHANNEL_COMMANDS:
|
||||
for chan in self.CHANNELS:
|
||||
self.data[fmt % chan] = v
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
self.simulate()
|
||||
|
||||
def simulate(self):
|
||||
# not really a simulation. just for testing RDGST
|
||||
for channel in self.CHANNELS:
|
||||
_, _, _, _, excoff = self.data[f'RDGRNG?{channel}'].split(',')
|
||||
if excoff == '1':
|
||||
self.data[f'RDGST?{channel}'] = '6'
|
||||
else:
|
||||
self.data[f'RDGST?{channel}'] = '0'
|
||||
for chan in self.CHANNELS:
|
||||
prev = float(self.data['RDGR?%d' % chan])
|
||||
# simple simulation: exponential convergence to 100 * channel number
|
||||
# using a weighted average
|
||||
self.data['RDGR?%d' % chan] = '%g' % (0.99 * prev + 0.01 * 100 * chan)
|
||||
|
||||
def communicate(self, command):
|
||||
self.comLog(f'> {command}')
|
||||
|
||||
chunks = command.split(';')
|
||||
reply = []
|
||||
for chunk in chunks:
|
||||
if '?' in chunk:
|
||||
chunk = chunk.replace('? ', '?')
|
||||
reply.append(self.data[chunk])
|
||||
else:
|
||||
for nqarg in (1, 0):
|
||||
if nqarg == 0:
|
||||
qcmd, arg = chunk.split(' ', 1)
|
||||
qcmd += '?'
|
||||
else:
|
||||
qcmd, arg = chunk.split(',', nqarg)
|
||||
qcmd = qcmd.replace(' ', '?', 1)
|
||||
if qcmd in self.data:
|
||||
self.data[qcmd] = arg
|
||||
break
|
||||
reply = ';'.join(reply)
|
||||
self.comLog(f'< {reply}')
|
||||
return reply
|
||||
|
||||
|
||||
class Ls336Sim(Ls370Sim):
|
||||
CHANNEL_COMMANDS = [
|
||||
('KRDG?%s', '295.0'),
|
||||
('RDGST?%s', '0'),
|
||||
]
|
||||
OTHER_COMMANDS = [
|
||||
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
|
||||
('RANGE?1', '0'),
|
||||
('SETP?1', '0'),
|
||||
('CLIMIT?1', ''),
|
||||
('CSET?1', ''),
|
||||
('CMODE?1', ''),
|
||||
('*OPC?', '1'),
|
||||
]
|
||||
|
||||
CHANNELS = 'ABCD'
|
||||
|
||||
vti = 295
|
||||
sample = 295
|
||||
|
||||
def simulate(self):
|
||||
# simple temperature control on channel A:
|
||||
range_ = int(self.data['RANGE?1'])
|
||||
setp = float(self.data['SETP?1'])
|
||||
if range_:
|
||||
# heater on: approach setpoint with 20 sec time constant
|
||||
self.vti = max(self.vti - 0.1, self.vti + (setp - self.vti) * 0.05)
|
||||
else:
|
||||
# heater off 0.1/sec cool down
|
||||
self.vti = max(1.5, self.vti - 0.1)
|
||||
# sample approaching setpoint with 10 sec time constant, but with some
|
||||
# systematic heat loss towards 150 K
|
||||
self.sample = self.sample + (self.vti + (150 - self.vti) * 0.01 - self.sample) * 0.1
|
||||
self.data['KRDG?A'] = str(round(self.vti, 3))
|
||||
self.data['KRDG?B'] = str(round(self.sample, 3))
|
332
frappy_demo/modules.py
Normal file
332
frappy_demo/modules.py
Normal file
@ -0,0 +1,332 @@
|
||||
# -*- 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:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""testing devices"""
|
||||
|
||||
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
|
||||
from frappy.datatypes import ArrayOf, BoolType, EnumType, \
|
||||
FloatRange, IntRange, StringType, StructOf, TupleOf
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy.modules import Drivable
|
||||
from frappy.modules import Parameter as SECoP_Parameter
|
||||
from frappy.modules import Readable
|
||||
from frappy.properties import Property
|
||||
|
||||
|
||||
class Parameter(SECoP_Parameter):
|
||||
test = Property('A property for testing purposes', StringType(), default='',
|
||||
mandatory=False, extname='test')
|
||||
|
||||
|
||||
PERSIST = 101
|
||||
|
||||
|
||||
class Switch(Drivable):
|
||||
"""switch it on or off....
|
||||
"""
|
||||
|
||||
value = Parameter('current state (on or off)',
|
||||
datatype=EnumType(on=1, off=0), default=0,
|
||||
)
|
||||
target = Parameter('wanted state (on or off)',
|
||||
datatype=EnumType(on=1, off=0), default=0,
|
||||
readonly=False,
|
||||
)
|
||||
switch_on_time = Parameter('seconds to wait after activating the switch',
|
||||
datatype=FloatRange(0, 60), unit='s',
|
||||
default=10, export=False,
|
||||
)
|
||||
switch_off_time = Parameter('cool-down time in seconds',
|
||||
datatype=FloatRange(0, 60), unit='s',
|
||||
default=10, export=False,
|
||||
)
|
||||
|
||||
description = Property('The description of the Module', StringType(),
|
||||
default='no description', mandatory=False,
|
||||
extname='description')
|
||||
|
||||
def read_value(self):
|
||||
# could ask HW
|
||||
# we just return the value of the target here.
|
||||
self._update()
|
||||
return self.value
|
||||
|
||||
def write_target(self, value):
|
||||
# could tell HW
|
||||
self.status = (self.Status.BUSY, f'switching {value.name.upper()}')
|
||||
# note: setting self.target to the new value is done after this....
|
||||
|
||||
def read_status(self):
|
||||
self._update()
|
||||
return self.status
|
||||
|
||||
def _update(self):
|
||||
started = self.parameters['target'].timestamp
|
||||
info = ''
|
||||
if self.target > self.value:
|
||||
info = 'waiting for ON'
|
||||
if time.time() > started + self.switch_on_time:
|
||||
info = 'is switched ON'
|
||||
self.value = self.target
|
||||
self.status = self.Status.IDLE, info
|
||||
elif self.target < self.value:
|
||||
info = 'waiting for OFF'
|
||||
if time.time() > started + self.switch_off_time:
|
||||
info = 'is switched OFF'
|
||||
self.value = self.target
|
||||
self.status = self.Status.IDLE, info
|
||||
if info:
|
||||
self.log.info(info)
|
||||
|
||||
|
||||
class MagneticField(Drivable):
|
||||
"""a liquid magnet
|
||||
"""
|
||||
|
||||
value = Parameter('current field in T',
|
||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||
)
|
||||
target = Parameter('target field in T',
|
||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||
readonly=False,
|
||||
)
|
||||
ramp = Parameter('ramping speed',
|
||||
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
||||
readonly=False,
|
||||
)
|
||||
mode = Parameter('what to do after changing field',
|
||||
default=1, datatype=EnumType(persistent=1, hold=0),
|
||||
readonly=False,
|
||||
)
|
||||
heatswitch = Parameter('name of heat switch device',
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
|
||||
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
|
||||
|
||||
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
|
||||
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
_thread.daemon = True
|
||||
_thread.start()
|
||||
|
||||
def read_value(self):
|
||||
return self.value
|
||||
|
||||
def write_target(self, value):
|
||||
self.status = self.Status.BUSY, 'setting target'
|
||||
# could tell HW
|
||||
return round(value, 2)
|
||||
# note: setting self.target to the new value is done after this....
|
||||
# note: we may also return the read-back value from the hw here
|
||||
|
||||
def read_status(self):
|
||||
if self._state == self._state.enum.idle:
|
||||
return (PERSIST, 'at field') if self.value else \
|
||||
(self.Status.IDLE, 'zero field')
|
||||
if self._state == self._state.enum.switch_on:
|
||||
return (self.Status.PREPARE, self._state.name)
|
||||
if self._state == self._state.enum.switch_off:
|
||||
return (self.Status.FINISH, self._state.name)
|
||||
if self._state == self._state.enum.ramp:
|
||||
return (self.Status.RAMPING, self._state.name)
|
||||
return (self.Status.ERROR, self._state.name)
|
||||
|
||||
def _thread(self):
|
||||
loopdelay = 1
|
||||
while True:
|
||||
ts = time.time()
|
||||
if self._state == self._state.enum.idle:
|
||||
if self.target != self.value:
|
||||
self.log.debug('got new target -> switching heater on')
|
||||
self._state = self._state.enum.switch_on
|
||||
self._heatswitch.write_target('on')
|
||||
if self._state == self._state.enum.switch_on:
|
||||
# wait until switch is on
|
||||
if self._heatswitch.read_value() == 'on':
|
||||
self.log.debug('heatswitch is on -> ramp to %.3f',
|
||||
self.target)
|
||||
self._state = self._state.enum.ramp
|
||||
if self._state == self._state.enum.ramp:
|
||||
if self.target == self.value:
|
||||
self.log.debug('at field! mode is %r', self.mode)
|
||||
if self.mode:
|
||||
self.log.debug('at field -> switching heater off')
|
||||
self._state = self._state.enum.switch_off
|
||||
self._heatswitch.write_target('off')
|
||||
else:
|
||||
self.log.debug('at field -> hold')
|
||||
self._state = self._state.enum.idle
|
||||
self.read_status() # push async
|
||||
else:
|
||||
step = self.ramp * loopdelay / 60.
|
||||
step = max(min(self.target - self.value, step), -step)
|
||||
self.value += step
|
||||
if self._state == self._state.enum.switch_off:
|
||||
# wait until switch is off
|
||||
if self._heatswitch.read_value() == 'off':
|
||||
self.log.debug('heatswitch is off at %.3f', self.value)
|
||||
self._state = self._state.enum.idle
|
||||
self.read_status() # update async
|
||||
time.sleep(max(0.01, ts + loopdelay - time.time()))
|
||||
self.log.error(self, 'main thread exited unexpectedly!')
|
||||
|
||||
def stop(self):
|
||||
self.write_target(self.read_value())
|
||||
|
||||
|
||||
class CoilTemp(Readable):
|
||||
"""a coil temperature
|
||||
"""
|
||||
|
||||
value = Parameter('Coil temperatur',
|
||||
unit='K', datatype=FloatRange(), default=0,
|
||||
)
|
||||
sensor = Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
return round(2.3 + random.random(), 3)
|
||||
|
||||
|
||||
class SampleTemp(Drivable):
|
||||
"""a sample temperature
|
||||
"""
|
||||
|
||||
value = Parameter('Sample temperature',
|
||||
unit='K', datatype=FloatRange(), default=10,
|
||||
)
|
||||
sensor = Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
)
|
||||
ramp = Parameter('moving speed in K/min',
|
||||
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
_thread.daemon = True
|
||||
_thread.start()
|
||||
|
||||
def write_target(self, value):
|
||||
# could tell HW
|
||||
return round(value, 2)
|
||||
# note: setting self.target to the new value is done after this....
|
||||
# note: we may also return the read-back value from the hw here
|
||||
|
||||
def _thread(self):
|
||||
loopdelay = 1
|
||||
while True:
|
||||
ts = time.time()
|
||||
if self.value == self.target:
|
||||
if self.status[0] != self.Status.IDLE:
|
||||
self.status = self.Status.IDLE, ''
|
||||
else:
|
||||
self.status = self.Status.BUSY, 'ramping'
|
||||
step = self.ramp * loopdelay / 60.
|
||||
step = max(min(self.target - self.value, step), -step)
|
||||
self.value += step
|
||||
time.sleep(max(0.01, ts + loopdelay - time.time()))
|
||||
self.log.error(self, 'main thread exited unexpectedly!')
|
||||
|
||||
|
||||
class Label(Readable):
|
||||
"""Displays the status of a cryomagnet
|
||||
|
||||
by composing its (stringtype) value from the status/value
|
||||
of several subdevices. used for demoing connections between
|
||||
modules.
|
||||
"""
|
||||
|
||||
system = Parameter("Name of the magnet system",
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
subdev_mf = Parameter("name of subdevice for magnet status",
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
subdev_ts = Parameter("name of subdevice for sample temp",
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
value = Parameter("final value of label string", default='',
|
||||
datatype=StringType(),
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
strings = [self.system]
|
||||
|
||||
dev_ts = self.DISPATCHER.get_module(self.subdev_ts)
|
||||
if dev_ts:
|
||||
strings.append(f"at {dev_ts.read_value():.3f} {dev_ts.parameters['value'].datatype.unit}")
|
||||
else:
|
||||
strings.append('No connection to sample temp!')
|
||||
|
||||
dev_mf = self.DISPATCHER.get_module(self.subdev_mf)
|
||||
if dev_mf:
|
||||
mf_stat = dev_mf.read_status()
|
||||
mf_mode = dev_mf.mode
|
||||
mf_val = dev_mf.value
|
||||
mf_unit = dev_mf.parameters['value'].datatype.unit
|
||||
if mf_stat[0] == self.Status.IDLE:
|
||||
state = 'Persistent' if mf_mode else 'Non-persistent'
|
||||
else:
|
||||
state = mf_stat[1] or 'ramping'
|
||||
strings.append(f'{state} at {mf_val:.1f} {mf_unit}')
|
||||
else:
|
||||
strings.append('No connection to magnetic field!')
|
||||
|
||||
return '; '.join(strings)
|
||||
|
||||
|
||||
class DatatypesTest(Readable):
|
||||
"""for demoing all datatypes
|
||||
"""
|
||||
|
||||
enum = Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
|
||||
readonly=False, default=1)
|
||||
tupleof = Parameter('tuple of int, float and str',
|
||||
datatype=TupleOf(IntRange(), FloatRange(),
|
||||
StringType()),
|
||||
readonly=False, default=(1, 2.3, 'a'))
|
||||
arrayof = Parameter('array: 2..3 times bool',
|
||||
datatype=ArrayOf(BoolType(), 2, 3),
|
||||
readonly=False, default=[1, 0, 1])
|
||||
intrange = Parameter('intrange', datatype=IntRange(2, 9),
|
||||
readonly=False, default=4)
|
||||
floatrange = Parameter('floatrange', datatype=FloatRange(-1, 1),
|
||||
readonly=False, default=0)
|
||||
struct = Parameter('struct(a=str, b=int, c=bool)',
|
||||
datatype=StructOf(a=StringType(), b=IntRange(),
|
||||
c=BoolType()))
|
||||
|
||||
|
||||
class ArrayTest(Readable):
|
||||
x = Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
||||
default=100000 * [0])
|
103
frappy_demo/test.py
Normal file
103
frappy_demo/test.py
Normal file
@ -0,0 +1,103 @@
|
||||
# -*- 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:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""testing devices"""
|
||||
|
||||
|
||||
import random
|
||||
|
||||
from frappy.datatypes import FloatRange, StringType, ValueType
|
||||
from frappy.modules import Communicator, Drivable, Parameter, Property, \
|
||||
Readable
|
||||
from frappy.params import Command
|
||||
|
||||
|
||||
class LN2(Readable):
|
||||
"""Just a readable.
|
||||
|
||||
class name indicates it to be a sensor for LN2,
|
||||
but the implementation may do anything
|
||||
"""
|
||||
|
||||
def read_value(self):
|
||||
return round(100 * random.random(), 1)
|
||||
|
||||
|
||||
class Heater(Drivable):
|
||||
"""Just a driveable.
|
||||
|
||||
class name indicates it to be some heating element,
|
||||
but the implementation may do anything
|
||||
"""
|
||||
|
||||
maxheaterpower = Parameter('maximum allowed heater power',
|
||||
datatype=FloatRange(0, 100), unit='W',
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
return round(100 * random.random(), 1)
|
||||
|
||||
def write_target(self, target):
|
||||
pass
|
||||
|
||||
|
||||
class Temp(Drivable):
|
||||
"""Just a driveable.
|
||||
|
||||
class name indicates it to be some temperature controller,
|
||||
but the implementation may do anything
|
||||
"""
|
||||
|
||||
sensor = Parameter(
|
||||
"Sensor number or calibration id",
|
||||
datatype=StringType(
|
||||
8,
|
||||
16),
|
||||
readonly=True,
|
||||
)
|
||||
target = Parameter(
|
||||
"Target temperature",
|
||||
default=300.0,
|
||||
datatype=FloatRange(0),
|
||||
readonly=False,
|
||||
unit='K',
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
return round(100 * random.random(), 1)
|
||||
|
||||
def write_target(self, target):
|
||||
pass
|
||||
|
||||
|
||||
class Lower(Communicator):
|
||||
"""Communicator returning a lowercase version of the request"""
|
||||
|
||||
@Command(argument=StringType(), result=StringType(), export='communicate')
|
||||
def communicate(self, command):
|
||||
"""lowercase a string"""
|
||||
return str(command).lower()
|
||||
|
||||
class Mapped(Readable):
|
||||
value = Parameter(datatype=StringType())
|
||||
choices = Property('List of choices',
|
||||
datatype=ValueType(list))
|
||||
def read_value(self):
|
||||
return self.choices[random.randrange(len(self.choices))]
|
Reference in New Issue
Block a user