fetched mlz version

- before some chamges in the gerrit pipline

Change-Id: I33eb2d75f83345a7039d0fb709e66defefb1c3e0
This commit is contained in:
2023-05-02 11:31:30 +02:00
parent b19a8c2e5c
commit da15df076a
765 changed files with 35890 additions and 59302 deletions

0
frappy_demo/__init__.py Normal file
View File

361
frappy_demo/cryo.py Normal file
View 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
View 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
View 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
View 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
View 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))]