[WIP] ccracks / ccu4: split ccu() into nv(), he() and flow()
Change-Id: I346330a5f350bf03eefe86c8e890b59afaaaa231
This commit is contained in:
parent
44750572d9
commit
d231e9ce06
@ -12,6 +12,6 @@ rack.sensor('Ts', channel='C', calcurve='x186350')
|
|||||||
rack.loop('T', channel='B', calcurve='x174786', output_module='htr', target=10)
|
rack.loop('T', channel='B', calcurve='x174786', output_module='htr', target=10)
|
||||||
rack.heater('htr', 1, '100W', 25)
|
rack.heater('htr', 1, '100W', 25)
|
||||||
|
|
||||||
rack.ccu(he=True, n2=True)
|
rack.he()
|
||||||
|
rack.n2()
|
||||||
rack.hepump()
|
rack.flow(min_open_pulse=0.03)
|
||||||
|
@ -28,6 +28,7 @@ class Rack:
|
|||||||
raise ConfigError(f'no rack found in {instpath}')
|
raise ConfigError(f'no rack found in {instpath}')
|
||||||
self.props = {} # dict (<property>, <method>) of value
|
self.props = {} # dict (<property>, <method>) of value
|
||||||
self.mods = {} # dict (<property>, <method>) of list of <cfg>
|
self.mods = {} # dict (<property>, <method>) of list of <cfg>
|
||||||
|
self.ccu_uri = {}
|
||||||
|
|
||||||
def set_props(self, mod, **kwds):
|
def set_props(self, mod, **kwds):
|
||||||
for prop, method in kwds.items():
|
for prop, method in kwds.items():
|
||||||
@ -88,43 +89,47 @@ class Rack:
|
|||||||
mod = Mod(name, max_heater=max_heater, resistance=resistance, **kwds)
|
mod = Mod(name, max_heater=max_heater, resistance=resistance, **kwds)
|
||||||
self.set_props(mod, io='lakeshore', device='lakeshore', description=f'heater({name})')
|
self.set_props(mod, io='lakeshore', device='lakeshore', description=f'heater({name})')
|
||||||
|
|
||||||
def ccu(self, ccu_uri=None, ccu_io='ccu_io', he=None, n2=None, **kwds):
|
def ccu(self, name=None, ccu_uri=None, ccu_io='ccu_io', args_for_io=None, **kwds):
|
||||||
Mod = self.modfactory
|
if args_for_io is None:
|
||||||
self.ccu_io = ccu_io
|
args_for_io, kwds = kwds, {}
|
||||||
|
prev_uri = self.ccu_uri.get(ccu_io)
|
||||||
ccu_uri = ccu_uri or self.config.get('ccu_uri')
|
ccu_uri = ccu_uri or self.config.get('ccu_uri')
|
||||||
# self.devname = ccu_devname
|
if prev_uri:
|
||||||
Mod(ccu_io, 'frappy_psi.ccu4.IO',
|
if prev_uri == ccu_uri:
|
||||||
'comm. to CCU4', uri=ccu_uri)
|
return kwds # already configured
|
||||||
if he:
|
raise ConfigError(f'rack.{name or "ccu"}: ccu_uri {prev_uri} does not match {ccu_uri}')
|
||||||
if not isinstance(he, str): # e.g. True
|
self.ccu_uri[ccu_io] = ccu_uri
|
||||||
he = 'He_lev'
|
self.modfactory(ccu_io, 'frappy_psi.ccu4.IO', 'comm. to CCU4', uri=ccu_uri, **args_for_io)
|
||||||
Mod(he, cls='frappy_psi.ccu4.HeLevel',
|
return kwds
|
||||||
description='the He Level', io=self.ccu_io)
|
|
||||||
if n2:
|
|
||||||
if isinstance(n2, str):
|
|
||||||
n2 = n2.split(',')
|
|
||||||
else: # e.g. True
|
|
||||||
n2 = []
|
|
||||||
n2, valve, upper, lower = n2 + ['N2_lev', 'N2_valve', 'N2_upper', 'N2_lower'][len(n2):]
|
|
||||||
print(n2, valve, upper, lower)
|
|
||||||
Mod(n2, cls='frappy_psi.ccu4.N2Level',
|
|
||||||
description='the N2 Level', io=ccu_io,
|
|
||||||
valve=valve, upper=upper, lower=lower)
|
|
||||||
Mod(valve, cls='frappy_psi.ccu4.N2FillValve',
|
|
||||||
description='LN2 fill valve', io=ccu_io)
|
|
||||||
Mod(upper, cls='frappy_psi.ccu4.N2TempSensor',
|
|
||||||
description='upper LN2 sensor')
|
|
||||||
Mod(lower, cls='frappy_psi.ccu4.N2TempSensor',
|
|
||||||
description='lower LN2 sensor')
|
|
||||||
|
|
||||||
def hepump(self, hepump_uri=None, hepump_type=None, hepump_io='hepump_io',
|
def he(self, name='He_lev', ccu_io='ccu_io', **kwds):
|
||||||
|
self.ccu('he', ccu_io=ccu_io, args_for_io={}, **kwds)
|
||||||
|
self.modfactory(name, cls='frappy_psi.ccu4.HeLevel',
|
||||||
|
description='the He Level', io=ccu_io, **kwds)
|
||||||
|
|
||||||
|
def n2(self, name='N2_lev', valve='N2_valve', upper='N2_upper', lower='N2_lower', ccu_io='ccu_io', **kwds):
|
||||||
|
self.ccu('n2', ccu_io=ccu_io, args_for_io={}, **kwds)
|
||||||
|
Mod = self.modfactory
|
||||||
|
Mod(name, cls='frappy_psi.ccu4.N2Level',
|
||||||
|
description='the N2 Level', io=ccu_io,
|
||||||
|
valve=valve, upper=upper, lower=lower)
|
||||||
|
Mod(valve, cls='frappy_psi.ccu4.N2FillValve',
|
||||||
|
description='LN2 fill valve', io=ccu_io)
|
||||||
|
Mod(upper, cls='frappy_psi.ccu4.N2TempSensor',
|
||||||
|
description='upper LN2 sensor')
|
||||||
|
Mod(lower, cls='frappy_psi.ccu4.N2TempSensor',
|
||||||
|
description='lower LN2 sensor')
|
||||||
|
|
||||||
|
def flow(self, hepump_uri=None, hepump_type=None, hepump_io='hepump_io',
|
||||||
hepump='hepump', hepump_mot='hepump_mot', hepump_valve='hepump_valve',
|
hepump='hepump', hepump_mot='hepump_mot', hepump_valve='hepump_valve',
|
||||||
flow_sensor='flow_sensor', pump_pressure='pump_pressure', nv='nv',
|
flow_sensor='flow_sensor', pump_pressure='pump_pressure', nv='nv',
|
||||||
ccu_io='ccu_io', **kwds):
|
ccu_io='ccu_io', **kwds):
|
||||||
|
"""creates needle valve and pump access if available"""
|
||||||
|
kwds = self.ccu('flow', ccu_io=ccu_io, args_for_io={}, **kwds)
|
||||||
Mod = self.modfactory
|
Mod = self.modfactory
|
||||||
hepump_type = hepump_type or self.config.get('hepump_type', 'no')
|
hepump_type = hepump_type or self.config.get('hepump_type', 'no')
|
||||||
Mod(nv, 'frappy_psi.ccu4.NeedleValveFlow', 'flow from flow sensor or pump pressure',
|
Mod(nv, 'frappy_psi.ccu4.NeedleValveFlow', 'flow from flow sensor or pump pressure',
|
||||||
flow_sensor=flow_sensor, pressure=pump_pressure, io=ccu_io)
|
flow_sensor=flow_sensor, pressure=pump_pressure, io=ccu_io, **kwds)
|
||||||
Mod(pump_pressure, 'frappy_psi.ccu4.Pressure', 'He pump pressure', io=ccu_io)
|
Mod(pump_pressure, 'frappy_psi.ccu4.Pressure', 'He pump pressure', io=ccu_io)
|
||||||
if hepump_type == 'no':
|
if hepump_type == 'no':
|
||||||
print('no pump, no flow meter - using flow from pressure alone')
|
print('no pump, no flow meter - using flow from pressure alone')
|
||||||
|
@ -22,13 +22,15 @@
|
|||||||
"""drivers for CCU4, the cryostat control unit at SINQ"""
|
"""drivers for CCU4, the cryostat control unit at SINQ"""
|
||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
|
import numpy as np
|
||||||
from frappy.lib.enum import Enum
|
from frappy.lib.enum import Enum
|
||||||
from frappy.lib import clamp
|
from frappy.lib import clamp, formatExtendedTraceback
|
||||||
|
from frappy.lib.interpolation import Interpolation
|
||||||
# the most common Frappy classes can be imported from frappy.core
|
# the most common Frappy classes can be imported from frappy.core
|
||||||
from frappy.core import HasIO, Parameter, Command, Readable, Writable, Drivable, \
|
from frappy.core import HasIO, Parameter, Command, Readable, Writable, Drivable, \
|
||||||
Property, StringIO, BUSY, IDLE, WARN, ERROR, DISABLED, Attached, nopoll
|
Property, StringIO, BUSY, IDLE, WARN, ERROR, DISABLED, Attached, nopoll
|
||||||
from frappy.datatypes import BoolType, EnumType, FloatRange, StructOf, \
|
from frappy.datatypes import BoolType, EnumType, FloatRange, StructOf, \
|
||||||
StatusType, IntRange, StringType, TupleOf
|
StatusType, IntRange, StringType, TupleOf, ArrayOf
|
||||||
from frappy.errors import CommunicationFailedError
|
from frappy.errors import CommunicationFailedError
|
||||||
from frappy.states import HasStates, status_code, Retry
|
from frappy.states import HasStates, status_code, Retry
|
||||||
|
|
||||||
@ -314,6 +316,10 @@ class Pressure(HasFilter, Base, Readable):
|
|||||||
return self.filter(self.filter_time, self.command(f=float)) - self.mbar_offset
|
return self.filter(self.filter_time, self.command(f=float)) - self.mbar_offset
|
||||||
|
|
||||||
|
|
||||||
|
def Table(miny=None, maxy=None):
|
||||||
|
return ArrayOf(TupleOf(FloatRange(), FloatRange(miny, maxy)))
|
||||||
|
|
||||||
|
|
||||||
class NeedleValveFlow(HasStates, Base, Drivable):
|
class NeedleValveFlow(HasStates, Base, Drivable):
|
||||||
flow_sensor = Attached(Readable, mandatory=False)
|
flow_sensor = Attached(Readable, mandatory=False)
|
||||||
pressure = Attached(Pressure, mandatory=False)
|
pressure = Attached(Pressure, mandatory=False)
|
||||||
@ -325,27 +331,53 @@ class NeedleValveFlow(HasStates, Base, Drivable):
|
|||||||
target = Parameter(unit='ln/min')
|
target = Parameter(unit='ln/min')
|
||||||
|
|
||||||
motor_state = Parameter('motor_state', EnumType(M), default=0)
|
motor_state = Parameter('motor_state', EnumType(M), default=0)
|
||||||
tolerance = Parameter('tolerance', FloatRange(0), value=0.25, readonly=False)
|
speed = Parameter('speed moving time / passed time', FloatRange())
|
||||||
tolerance2 = Parameter('tolerance limit above 2 lnm', FloatRange(0), value=0.5, readonly=False)
|
tolerance = Parameter('tolerance', Table(0), value=[(2,0.1),(4,0.4)], readonly=False)
|
||||||
prop = Parameter('proportional term', FloatRange(unit='s/lnm'), readonly=False, default=0.001)
|
prop_open = Parameter('proportional term for opening', Table(0), readonly=False, value=[(1,0.05)])
|
||||||
|
prop_close = Parameter('proportional term for closing', Table(0), readonly=False, value=[(1,0.02)])
|
||||||
deriv = Parameter('min progress time constant', FloatRange(unit='s'),
|
deriv = Parameter('min progress time constant', FloatRange(unit='s'),
|
||||||
default=30, readonly=False)
|
default=30, readonly=False)
|
||||||
settle = Parameter('time within tolerance before getting quiet', FloatRange(unit='s'),
|
|
||||||
default=30, readonly=False)
|
|
||||||
step_factor = Parameter('factor (no progress time) / (min step size)', FloatRange(), default=300, readonly=False)
|
|
||||||
control_active = Parameter('control active flag', BoolType(), readonly=False, default=1)
|
control_active = Parameter('control active flag', BoolType(), readonly=False, default=1)
|
||||||
min_open_step = Parameter('minimal open step', FloatRange(unit='s'), readonly=False, default=0.06)
|
min_open_pulse = Parameter('minimal open step', FloatRange(0, unit='s'), readonly=False, default=0.02)
|
||||||
min_close_step = Parameter('minimal close step', FloatRange(unit='s'), readonly=False, default=0.05)
|
min_close_pulse = Parameter('minimal close step', FloatRange(0, unit='s'), readonly=False, default=0.0)
|
||||||
raw_open_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.12)
|
# raw_open_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.12)
|
||||||
raw_close_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.04)
|
# raw_close_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.04)
|
||||||
pollinterval = Parameter(default=5)
|
pollinterval = Parameter(datatype=FloatRange(1, unit='s'), default=5)
|
||||||
|
_last_dirchange = 0
|
||||||
_ref_time = 0
|
_ref_time = 0
|
||||||
_ref_dif = 0
|
_ref_dif = 0
|
||||||
_dir = 0
|
_dir = 0
|
||||||
_rawdir = 0
|
_rawdir = 0
|
||||||
_step = 0
|
_step = 0
|
||||||
|
_speed_sum = 0
|
||||||
|
_last_era = 0
|
||||||
|
_value = None
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
# poll at least every sec, but update value only
|
||||||
|
# every pollinterval and status when changed
|
||||||
|
if not self.pollInfo.fast_flag:
|
||||||
|
self.pollInfo.interval = min(1, self.pollinterval) # reduce internal poll interval
|
||||||
|
self._value = self.get_value()
|
||||||
|
self._last.append(self._value)
|
||||||
|
del self._last[0:-300]
|
||||||
|
self.read_motor_state()
|
||||||
|
era = time.time() // self.pollinterval
|
||||||
|
if era != self._last_era:
|
||||||
|
self.speed = self._speed_sum / self.pollinterval
|
||||||
|
self._speed_sum = 0
|
||||||
|
self.value = self._value
|
||||||
|
self._last_era = era
|
||||||
|
self.read_status()
|
||||||
|
self.cycle_machine()
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
p = self.pressure.read_value() * self.lnm_per_mbar
|
||||||
|
f = self.flow_sensor.read_value()
|
||||||
|
return p if self.use_pressure else f
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
|
self._last = []
|
||||||
if self.pressure:
|
if self.pressure:
|
||||||
self.pressure.addCallback('value', self.update_from_pressure)
|
self.pressure.addCallback('value', self.update_from_pressure)
|
||||||
if self.flow_sensor:
|
if self.flow_sensor:
|
||||||
@ -354,29 +386,16 @@ class NeedleValveFlow(HasStates, Base, Drivable):
|
|||||||
|
|
||||||
def update_from_flow(self, value):
|
def update_from_flow(self, value):
|
||||||
if not self.use_pressure:
|
if not self.use_pressure:
|
||||||
self.value = value
|
self._value = value
|
||||||
# self.cycle_machine()
|
|
||||||
|
|
||||||
def update_from_pressure(self, value):
|
def update_from_pressure(self, value):
|
||||||
if self.use_pressure:
|
if self.use_pressure:
|
||||||
self.value = value * self.lnm_per_mbar
|
self._value = value * self.lnm_per_mbar
|
||||||
# self.cycle_machine()
|
# self.cycle_machine()
|
||||||
|
|
||||||
# def doPoll(self):
|
|
||||||
# """only the updates should trigger the machine"""
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
p = self.pressure.read_value() * self.lnm_per_mbar
|
self._value = self.get_value()
|
||||||
f = self.flow_sensor.read_value()
|
return self._value
|
||||||
# self.log.info('p %g f %g +- %.2g', p, f, self.flow_sensor.stddev)
|
|
||||||
self.read_motor_state()
|
|
||||||
return p if self.use_pressure else f
|
|
||||||
|
|
||||||
def write_tolerance(self, tolerance):
|
|
||||||
if hasattr(self.pressure, 'tolerance'):
|
|
||||||
self.pressure.tolerance = tolerance / self.lnm_per_mbar
|
|
||||||
if hasattr(self.flow_sensor, 'tolerance'):
|
|
||||||
self.flow_sensor.tolerance = tolerance
|
|
||||||
|
|
||||||
def read_use_pressure(self):
|
def read_use_pressure(self):
|
||||||
if self.pressure:
|
if self.pressure:
|
||||||
@ -386,239 +405,134 @@ class NeedleValveFlow(HasStates, Base, Drivable):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
self.start_machine(self.change_target, fast_poll=1)
|
self.log.info('change target')
|
||||||
|
self.target = value
|
||||||
|
self.start_machine(self.change_target)
|
||||||
|
|
||||||
|
def write_prop_open(self, value):
|
||||||
|
self._prop_open = Interpolation(value)
|
||||||
|
return self._prop_open
|
||||||
|
|
||||||
|
def write_prop_close(self, value):
|
||||||
|
self._prop_close = Interpolation(value)
|
||||||
|
return self._prop_close
|
||||||
|
|
||||||
|
def write_tolerance(self, value):
|
||||||
|
self._tolerance = Interpolation(value)
|
||||||
|
return self._tolerance
|
||||||
|
|
||||||
@status_code(BUSY)
|
@status_code(BUSY)
|
||||||
def change_target(self, state):
|
def change_target(self, sm):
|
||||||
state.in_tol_time = 0
|
sm.last_progress = sm.now
|
||||||
state.last_minstep = {}
|
sm.ref_time = 0
|
||||||
state.last_progress = state.now
|
sm.ref_dif = 0
|
||||||
state.ref_time = 0
|
sm.last_pulse_time = 0
|
||||||
state.ref_dif = 0
|
sm.no_progress_pulse = (0.1, -0.05)
|
||||||
state.prev_dif = 0 # used?
|
self.log.info('target %s value %s', self.target, self._value)
|
||||||
state.last_close_time = 0
|
if abs(self.target - self._value) < self._tolerance(self._value):
|
||||||
state.last_pulse_time = 0
|
self.log.info('go to at_target')
|
||||||
state.raw_fact = 1
|
|
||||||
state.raw_step = 0
|
|
||||||
if abs(self.target - self.value) < self._tolerance():
|
|
||||||
return self.at_target
|
return self.at_target
|
||||||
return self.raw_control
|
self.log.info('go to controlling')
|
||||||
|
|
||||||
def start_direction(self, state):
|
|
||||||
if self.target > self.value:
|
|
||||||
self._dir = 1
|
|
||||||
state.minstep = self.min_open_step
|
|
||||||
else:
|
|
||||||
self._dir = -1
|
|
||||||
state.minstep = self.min_close_step
|
|
||||||
state.prev = []
|
|
||||||
|
|
||||||
def perform_pulse(self, state):
|
|
||||||
tol = self._tolerance()
|
|
||||||
dif = self.target - self.value
|
|
||||||
difdir = dif * self._dir
|
|
||||||
state.last_pulse_time = state.now
|
|
||||||
if difdir > tol:
|
|
||||||
step = state.minstep + (difdir - tol) * self.prop
|
|
||||||
elif difdir > 0:
|
|
||||||
step = state.minstep * difdir / tol
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
self.log.info('MP %g dif=%g tol=%g', step * self._dir, dif, tol)
|
|
||||||
self.command(mp=clamp(-1, step * self._dir, 1))
|
|
||||||
|
|
||||||
@status_code(BUSY)
|
|
||||||
def raw_control(self, state):
|
|
||||||
tol = self._tolerance()
|
|
||||||
if state.init:
|
|
||||||
self.start_direction(state)
|
|
||||||
state.raw_step = self.raw_open_step if self._dir > 0 else -self.raw_close_step
|
|
||||||
state.raw_fact = 1
|
|
||||||
# if self.read_motor_state() == M.closed:
|
|
||||||
# # TODO: also check for flow near lower limit ? but only once after change_target
|
|
||||||
# self.log.info('start with fast opening')
|
|
||||||
# state.raw_step = 1
|
|
||||||
# self._dir = 1
|
|
||||||
difdir = (self.target - self.value) * self._dir
|
|
||||||
state.prev.append(difdir)
|
|
||||||
state.diflim = max(0, difdir - tol * 1)
|
|
||||||
state.success = 0
|
|
||||||
self.command(mp=state.raw_step)
|
|
||||||
self.log.info('first rawstep %g', state.raw_step)
|
|
||||||
state.last_pulse_time = state.now
|
|
||||||
state.raw_pulse_cnt = 0
|
|
||||||
state.cycle_cnt = 0
|
|
||||||
return Retry
|
|
||||||
difdir = (self.target - self.value) * self._dir
|
|
||||||
state.cycle_cnt += 1
|
|
||||||
state.prev.append(difdir)
|
|
||||||
del state.prev[:-5]
|
|
||||||
if state.prev[-1] > max(state.prev[:-1]):
|
|
||||||
# TODO: use the amount of overshoot to reduce the raw_step
|
|
||||||
state.cycle_cnt = 0
|
|
||||||
self.log.info('difference is increasing %s', ' '.join(f'{v:g}' for v in state.prev))
|
|
||||||
return Retry
|
|
||||||
if state.cycle_cnt >= 5:
|
|
||||||
state.cycle_cnt = 0
|
|
||||||
state.diflim = max(tol, min(state.prev) - tol * 0.5)
|
|
||||||
state.raw_pulse_cnt += 1
|
|
||||||
self.command(mp=state.raw_step * state.raw_fact)
|
|
||||||
self.log.info('rawstep %g', state.raw_step)
|
|
||||||
if state.raw_pulse_cnt % 5 == 0 and state.raw_pulse_cnt > 5:
|
|
||||||
state.raw_fact *= 1.25
|
|
||||||
return Retry
|
|
||||||
if difdir >= state.diflim:
|
|
||||||
state.success = max(0, state.success - 1)
|
|
||||||
return Retry
|
|
||||||
state.success += 1
|
|
||||||
if state.success <= 3:
|
|
||||||
return Retry
|
|
||||||
if state.raw_pulse_cnt < 3:
|
|
||||||
state.raw_fact = 1 - (3 - state.raw_pulse_cnt) ** 2 * 0.05
|
|
||||||
if state.raw_fact != 1:
|
|
||||||
if self._dir > 0:
|
|
||||||
self.raw_open_step *= state.raw_fact
|
|
||||||
self.log.info('raw_open_step %g f=%g', self.raw_open_step, state.raw_fact)
|
|
||||||
self.min_open_pulse = min(self.min_open_pulse, self.raw_open_step)
|
|
||||||
else:
|
|
||||||
self.raw_close_step *= state.raw_fact
|
|
||||||
self.log.info('raw_close_step %g f=%g', self.raw_close_step, state.raw_fact)
|
|
||||||
self.min_close_pulse = min(self.min_close_pulse, self.raw_close_step)
|
|
||||||
return self.controlling
|
return self.controlling
|
||||||
|
|
||||||
# @status_code(BUSY)
|
def filtered(self, n=60, m=5, nsigma=2):
|
||||||
# def raw_control(self, state):
|
"""return mean and tolerance, augmented by noise"""
|
||||||
# tol = self._tolerance()
|
# TODO: better idea: use median over last minute and last value and treat them both
|
||||||
# if state.init:
|
n = len(self._last[-n:])
|
||||||
# self.start_direction(state)
|
mean = np.median(self._last[-m:])
|
||||||
# if self._dir != self._rawdir:
|
tol = self._tolerance(mean)
|
||||||
# self._rawdir = self._dir
|
span = 0
|
||||||
# state.first_step = self.first_open_step if self._dir > 0 else -self.first_close_step
|
if len(self._last) >= n + m:
|
||||||
# else:
|
# get span over the last n points
|
||||||
# state.first_step = 0
|
span = max(self._last[-n:]) - min(self._last[-n:])
|
||||||
# state.first_fact = 1
|
slope = mean - np.median(self._last[-n-m:-n])
|
||||||
# # if self.read_motor_state() == M.closed:
|
# in case there is a slope, subtract it
|
||||||
# # # TODO: also check for flow near lower limit ? but only once after change_target
|
tol = math.sqrt(tol ** 2 + max(0, span-abs(slope)) ** 2)
|
||||||
# # self.log.info('start with fast opening')
|
self.log.info('filt %d %d %d %g %g', len(self._last), n, m, self._value, span)
|
||||||
# # state.first_step = 1
|
m = min(m, n)
|
||||||
# # self._dir = 1
|
narr = np.array(self._last[-n:])
|
||||||
# difdir = (self.target - self.value) * self._dir
|
mdif = np.median(np.abs(narr[1:-1] - 0.5 * (narr[:-2] + narr[2:])))
|
||||||
# state.prev = [difdir]
|
return mean, tol
|
||||||
# state.diflim = max(0, difdir - tol * 0.5)
|
|
||||||
# state.success = 0
|
|
||||||
# if state.first_step:
|
|
||||||
# self.command(mp=state.first_step)
|
|
||||||
# else:
|
|
||||||
# self.perform_pulse(state)
|
|
||||||
# self.log.info('firststep %g', state.first_step)
|
|
||||||
# state.last_pulse_time = state.now
|
|
||||||
# state.raw_pulse_cnt = 0
|
|
||||||
# return Retry
|
|
||||||
# difdir = (self.target - self.value) * self._dir
|
|
||||||
# if state.delta(5):
|
|
||||||
# state.diflim = max(0, min(state.prev) - tol * 0.1)
|
|
||||||
# state.prev = [difdir]
|
|
||||||
# state.raw_pulse_cnt += 1
|
|
||||||
# if state.first_step and state.raw_pulse_cnt % 10 == 0:
|
|
||||||
# self.command(mp=state.first_step * state.first_fact)
|
|
||||||
# self.log.info('repeat firststep %g', state.first_step * state.first_fact)
|
|
||||||
# state.first_fact *= 1.25
|
|
||||||
# else:
|
|
||||||
# self.perform_pulse(state)
|
|
||||||
# return Retry
|
|
||||||
# state.prev.append(difdir)
|
|
||||||
# if difdir >= state.diflim:
|
|
||||||
# state.success = max(0, state.success - 1)
|
|
||||||
# return Retry
|
|
||||||
# state.success += 1
|
|
||||||
# if state.success <= 5:
|
|
||||||
# return Retry
|
|
||||||
# if state.first_step:
|
|
||||||
# if state.raw_pulse_cnt < 3:
|
|
||||||
# state.first_fact = 1 - (3 - state.raw_pulse_cnt) ** 2 * 0.04
|
|
||||||
# if state.first_fact != 1:
|
|
||||||
# if self._dir > 0:
|
|
||||||
# self.first_open_step *= state.first_fact
|
|
||||||
# self.log.info('first_open_step %g f=%g', self.first_open_step, state.first_fact)
|
|
||||||
# else:
|
|
||||||
# self.first_close_step *= state.first_fact
|
|
||||||
# self.log.info('first_close_step %g f=%g', self.first_close_step, state.first_fact)
|
|
||||||
# return self.controlling
|
|
||||||
|
|
||||||
@status_code(BUSY)
|
@status_code(BUSY)
|
||||||
def controlling(self, state):
|
def controlling(self, sm):
|
||||||
dif = self.target - self.value
|
tol = self._tolerance(self.target)
|
||||||
if state.init:
|
dif = np.array([self.target - np.median(self._last[-m:]) for m in (1,5,60)])
|
||||||
self.start_direction(state)
|
if sm.init:
|
||||||
state.ref_dif = abs(dif)
|
self.log.info('restart controlling')
|
||||||
state.ref_time = state.now
|
direction = math.copysign(1, dif[1])
|
||||||
state.in_tol_time = 0
|
if direction != self._dir:
|
||||||
|
self.log.info('new dir %g dif=%g', direction, dif[1])
|
||||||
|
self._dir = direction
|
||||||
|
self._last_dirchange = sm.now
|
||||||
|
sm.ref_dif = abs(dif[1])
|
||||||
|
sm.ref_time = sm.now
|
||||||
difdir = dif * self._dir # negative when overshoot happend
|
difdir = dif * self._dir # negative when overshoot happend
|
||||||
# difdif = dif - state.prev_dif
|
# difdif = dif - self._prev_dif
|
||||||
# state.prev_dif = dif
|
# self._prev_dif = dif
|
||||||
expected_dif = state.ref_dif * math.exp((state.ref_time - state.now) / self.deriv)
|
expected_dif = sm.ref_dif * math.exp((sm.ref_time - sm.now) / self.deriv)
|
||||||
|
|
||||||
tol = self._tolerance()
|
if np.all(difdir < tol):
|
||||||
if difdir < tol:
|
if np.all(difdir < -tol):
|
||||||
# prev_minstep = state.last_minstep.pop(self._dir, None)
|
self.log.info('overshoot %r', dif)
|
||||||
# attr = 'min_open_step' if self._dir > 0 else 'min_close_step'
|
return self.controlling
|
||||||
# if prev_minstep is not None:
|
# within tolerance
|
||||||
# # increase minstep
|
self.log.info('at target %r tol %g', dif, tol)
|
||||||
# minstep = getattr(self, attr)
|
return self.at_target
|
||||||
# setattr(self, attr, minstep * 1.1)
|
if np.all(difdir > expected_dif):
|
||||||
# self.log.info('increase %s to %g', attr, minstep)
|
# not enough progress
|
||||||
if difdir > -tol: # within tolerance
|
if sm.now > sm.last_progress + self.deriv:
|
||||||
delta = state.delta()
|
if sm.no_progress_pulse:
|
||||||
state.in_tol_time += delta
|
pulse = abs(sm.no_progress_pulse[self._dir < 0]) * self._dir
|
||||||
if state.in_tol_time > self.settle:
|
self.log.info('not enough progress %g', pulse)
|
||||||
# state.last_minstep.pop(self._dir, None)
|
self.pulse(pulse)
|
||||||
self.log.info('at target %g %g', dif, tol)
|
sm.last_progress = sm.now
|
||||||
return self.at_target
|
if sm.now < sm.last_pulse_time + 2.5:
|
||||||
if difdir < 0:
|
return Retry
|
||||||
return Retry
|
# TODO: check motor state for closed / opened ?
|
||||||
# self.log.info('minstep=0 dif=%g', dif)
|
difd = min(difdir[:2])
|
||||||
else: # overshoot
|
sm.last_pulse_time = sm.now
|
||||||
self.log.info('overshoot %g', dif)
|
if self._dir > 0:
|
||||||
return self.raw_control
|
minstep = self.min_open_pulse
|
||||||
# # overshoot
|
prop = self._prop_open(self._value)
|
||||||
# prev_minstep = state.last_minstep.pop(self._dir, None)
|
else:
|
||||||
# if prev_minstep is None:
|
minstep = self.min_close_pulse
|
||||||
# minstep = getattr(self, attr) * 0.9
|
prop = self._prop_close(self._value)
|
||||||
# self.log.info('decrease %s to %g', attr, minstep)
|
if difd > 0:
|
||||||
# setattr(self, attr, minstep)
|
if prop * tol > minstep:
|
||||||
# self.start_step(state, self.target)
|
# step outside tol is already minstep
|
||||||
# still approaching
|
step = difd * prop
|
||||||
if difdir <= expected_dif:
|
else:
|
||||||
if difdir < expected_dif / 1.25 - tol:
|
if difd > tol:
|
||||||
state.ref_time = state.now
|
step = (minstep + (difd - tol) * prop)
|
||||||
state.ref_dif = (difdir + tol) * 1.25
|
else:
|
||||||
# self.log.info('new ref %g', state.ref_dif)
|
step = minstep * difd / tol
|
||||||
state.last_progress = state.now
|
step *= self._dir
|
||||||
return Retry # progressing: no pulse needed
|
self.log.info('MP %g dif=%g tol=%g', step, difd * self._dir, tol)
|
||||||
if state.now < state.last_pulse_time + 2.5:
|
self.command(mp=step)
|
||||||
|
self._speed_sum += step
|
||||||
return Retry
|
return Retry
|
||||||
# TODO: check motor state for closed / opened ?
|
# still approaching
|
||||||
self.perform_pulse(state)
|
difmax = max(difdir)
|
||||||
return Retry
|
if difmax < expected_dif:
|
||||||
|
sm.ref_time = sm.now
|
||||||
def _tolerance(self):
|
sm.ref_dif = difmax
|
||||||
return min(self.tolerance * min(1, self.value / 2), self.tolerance2)
|
# self.log.info('new ref %g', sm.ref_dif)
|
||||||
|
sm.last_progress = sm.now
|
||||||
|
return Retry # progressing: no pulse needed
|
||||||
|
|
||||||
@status_code(IDLE)
|
@status_code(IDLE)
|
||||||
def at_target(self, state):
|
def at_target(self, sm):
|
||||||
dif = self.target - self.value
|
tol = self._tolerance(self.target)
|
||||||
if abs(dif) > self._tolerance():
|
dif = np.array([self.target - np.median(self._last[-m:]) for m in (1,5,60)])
|
||||||
state.in_tol_time = 0
|
if np.all(dif > tol) or np.all(dif < -tol):
|
||||||
self.log.info('unstable %g', dif)
|
|
||||||
return self.unstable
|
return self.unstable
|
||||||
return Retry
|
return Retry
|
||||||
|
|
||||||
@status_code(IDLE, 'unstable')
|
@status_code(IDLE, 'unstable')
|
||||||
def unstable(self, state):
|
def unstable(self, sm):
|
||||||
difdir = (self.target - self.value) * self._dir
|
sm.no_progress_pulse = None
|
||||||
if difdir < 0 or self._dir == 0:
|
return self.controlling(sm)
|
||||||
return self.raw_control
|
|
||||||
return self.controlling(state)
|
|
||||||
|
|
||||||
def read_motor_state(self):
|
def read_motor_state(self):
|
||||||
return self.command(fm=int)
|
return self.command(fm=int)
|
||||||
@ -628,18 +542,19 @@ class NeedleValveFlow(HasStates, Base, Drivable):
|
|||||||
"""close valve fully"""
|
"""close valve fully"""
|
||||||
self.command(mp=-60)
|
self.command(mp=-60)
|
||||||
self.motor_state = self.command(fm=int)
|
self.motor_state = self.command(fm=int)
|
||||||
self.start_machine(self.closing)
|
self.start_machine(self.closing, fast_poll=0.1)
|
||||||
|
|
||||||
@status_code(BUSY)
|
@status_code(BUSY)
|
||||||
def closing(self, state):
|
def closing(self, sm):
|
||||||
if state.init:
|
if sm.init:
|
||||||
state.start_time = state.now
|
sm.start_time = sm.now
|
||||||
|
self._speed_sum -= sm.delta()
|
||||||
self.read_motor_state()
|
self.read_motor_state()
|
||||||
if self.motor_state == M.closing:
|
if self.motor_state == M.closing:
|
||||||
return Retry
|
return Retry
|
||||||
if self.motor_state == M.closed:
|
if self.motor_state == M.closed:
|
||||||
return self.final_status(IDLE, 'closed')
|
return self.final_status(IDLE, 'closed')
|
||||||
if state.now < state.start_time + 1:
|
if sm.now < sm.start_time + 1:
|
||||||
return Retry
|
return Retry
|
||||||
return self.final_status(IDLE, 'fixed')
|
return self.final_status(IDLE, 'fixed')
|
||||||
|
|
||||||
@ -648,28 +563,162 @@ class NeedleValveFlow(HasStates, Base, Drivable):
|
|||||||
"""open valve fully"""
|
"""open valve fully"""
|
||||||
self.command(mp=60)
|
self.command(mp=60)
|
||||||
self.read_motor_state()
|
self.read_motor_state()
|
||||||
self.start_machine(self.opening)
|
self.start_machine(self.opening, threshold=None)
|
||||||
|
|
||||||
@status_code(BUSY)
|
@status_code(BUSY)
|
||||||
def opening(self, state):
|
def opening(self, sm):
|
||||||
if state.init:
|
if sm.init:
|
||||||
state.start_time = state.now
|
sm.start_time = sm.now
|
||||||
|
self._speed_sum += sm.dleta()
|
||||||
self.read_motor_state()
|
self.read_motor_state()
|
||||||
if self.motor_state == M.opening:
|
if self.motor_state == M.opening:
|
||||||
return Retry
|
return Retry
|
||||||
if self.motor_state == M.opened:
|
if self.motor_state == M.opened:
|
||||||
return self.final_status(IDLE, 'opened')
|
return self.final_status(IDLE, 'opened')
|
||||||
if state.now < state.start_time + 1:
|
if sm.now < sm.start_time + 1:
|
||||||
return Retry
|
return Retry
|
||||||
return self.final_status(IDLE, 'fixed')
|
return self.final_status(IDLE, 'fixed')
|
||||||
|
|
||||||
|
@Command
|
||||||
|
def lim_pulse(self):
|
||||||
|
"""try to open until pressure increases"""
|
||||||
|
p = self.command(f=float)
|
||||||
|
self.start_machine(self.lim_open, threshold=0.5,
|
||||||
|
prev=[p], ref=p, fast_poll=0.1, cnt=0)
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def lim_open(self, sm):
|
||||||
|
self.read_motor_state()
|
||||||
|
if self.motor_state == M.opening:
|
||||||
|
return Retry
|
||||||
|
if self.motor_state == M.opened:
|
||||||
|
return self.final_status(IDLE, 'opened')
|
||||||
|
press, measured = self.command(f=float, mmp=float)
|
||||||
|
sm.prev.append(press)
|
||||||
|
if press > sm.ref + 0.2:
|
||||||
|
sm.cnt += 1
|
||||||
|
if sm.cnt > 5 or press > sm.ref + 0.5:
|
||||||
|
self.log.info('flow increased %g', press)
|
||||||
|
return self.final_status(IDLE, 'flow increased')
|
||||||
|
self.log.info('wait count %g', press)
|
||||||
|
return Retry
|
||||||
|
sm.cnt = 0
|
||||||
|
last5 = sm.prev[-5:]
|
||||||
|
median = sorted(last5)[len(last5) // 2]
|
||||||
|
if press > median:
|
||||||
|
# avoid to pulse again after an even small increase
|
||||||
|
self.log.info('wait %g', press)
|
||||||
|
return Retry
|
||||||
|
sm.ref = min(sm.prev[0], median)
|
||||||
|
if measured:
|
||||||
|
self._speed_sum += measured
|
||||||
|
if measured < 0.1:
|
||||||
|
sm.threshold = round(sm.threshold * 1.1, 2)
|
||||||
|
elif measured > 0.3:
|
||||||
|
sm.threshold = round(sm.threshold * 0.9, 2)
|
||||||
|
self.log.info('measured %g new threshold %g press %g', measured, sm.threshold, press)
|
||||||
|
else:
|
||||||
|
self._speed_sum += 1
|
||||||
|
self.log.info('full pulse')
|
||||||
|
sm.cnt = 0
|
||||||
|
self.command(mft=sm.ref + sm.threshold, mp=1)
|
||||||
|
return Retry
|
||||||
|
|
||||||
@Command(FloatRange())
|
@Command(FloatRange())
|
||||||
def pulse(self, value):
|
def pulse(self, value):
|
||||||
"""perform a motor pulse"""
|
"""perform a motor pulse"""
|
||||||
self.log.info('pulse %g', value)
|
|
||||||
self.command(mp=value)
|
self.command(mp=value)
|
||||||
|
self._speed_sum += value
|
||||||
if value > 0:
|
if value > 0:
|
||||||
self.motor_state = M.opening
|
self.motor_state = M.opening
|
||||||
return self.opening
|
return self.opening
|
||||||
self.motor_state = M.closing
|
self.motor_state = M.closing
|
||||||
return self.closing
|
return self.closing
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def autopar(self):
|
||||||
|
"""adjust automatically needle valve parameters"""
|
||||||
|
self.close()
|
||||||
|
self.start_machine(self.auto_wait, open_pulse=0.1, close_pulse=0.05,
|
||||||
|
minflow=self.read_value(), last=None)
|
||||||
|
return self.auto_wait
|
||||||
|
|
||||||
|
def is_stable(self, sm, n, tol=0.01):
|
||||||
|
"""wait for a stable flow
|
||||||
|
|
||||||
|
n: size of buffer
|
||||||
|
tol: a tolerance
|
||||||
|
"""
|
||||||
|
if sm.last is None:
|
||||||
|
sm.last = []
|
||||||
|
sm.cnt = 0
|
||||||
|
v = self.read_value()
|
||||||
|
sm.last.append(v)
|
||||||
|
del sm.last[:-n]
|
||||||
|
dif = v - sm.last[0]
|
||||||
|
if dif < -tol:
|
||||||
|
sm.cnt -= 1
|
||||||
|
elif dif > tol:
|
||||||
|
sm.cnt += 1
|
||||||
|
else:
|
||||||
|
sm.cnt -= clamp(-1, sm.cnt, 1)
|
||||||
|
if len(sm.last) < n:
|
||||||
|
return False
|
||||||
|
return abs(sm.cnt) < n // 2
|
||||||
|
|
||||||
|
def is_unstable(self, sm, n, tol=0.01):
|
||||||
|
"""wait for a stable flow
|
||||||
|
|
||||||
|
return 0, -1 or 1
|
||||||
|
"""
|
||||||
|
if sm.last is None:
|
||||||
|
sm.last = []
|
||||||
|
sm.cnt = 0
|
||||||
|
v = self.read_value()
|
||||||
|
prevmax = max(sm.last)
|
||||||
|
prevmin = min(sm.last)
|
||||||
|
sm.last.append(v)
|
||||||
|
del sm.last[:-n]
|
||||||
|
self.log.info('unstable %g >? %g <? %g', v, prevmax, prevmin)
|
||||||
|
if v > prevmax + tol:
|
||||||
|
return 1
|
||||||
|
if v < prevmin - tol:
|
||||||
|
return -1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def auto_wait(self, sm):
|
||||||
|
stable = self.is_stable(sm, 5, 0.01)
|
||||||
|
if self._value < sm.minflow:
|
||||||
|
sm.minflow = self._value
|
||||||
|
if self.read_motor_state() == M.closing or not stable:
|
||||||
|
return Retry
|
||||||
|
return self.auto_open
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def auto_open(self, sm):
|
||||||
|
stable = self.is_unstable(sm, 5, 0.1)
|
||||||
|
if stable > 0:
|
||||||
|
sm.start_time = sm.now
|
||||||
|
sm.flow_before = sm.last[-1]
|
||||||
|
self.pulse(sm.open_pulse)
|
||||||
|
return self.auto_close
|
||||||
|
if sm.delta(sm.open_pulse * 2) is not None:
|
||||||
|
self.pulse(sm.open_pulse)
|
||||||
|
return Retry
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def auto_open_stable(self, sm):
|
||||||
|
if self.is_stable(sm, 5, 0.01):
|
||||||
|
return Retry
|
||||||
|
return self.auto_close
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def auto_close(self, sm):
|
||||||
|
if not self.is_stable(sm, 10, 0.01):
|
||||||
|
return Retry
|
||||||
|
self.log.info('before %g pulse %g, flowstep %g', sm.flow_before, sm.open_pulse, sm.last[-1] - sm.flow_before)
|
||||||
|
self.close()
|
||||||
|
return self.final_status(IDLE, '')
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user