Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
6d63c4e0df | |||
98fa19ce3b | |||
7f83f76d38 | |||
0ab849d0cf | |||
8ee49caba5 | |||
b1de9218bd | |||
8eaad86b66 | |||
85400a2777 | |||
dda4afbe5b | |||
9b079ddf4b | |||
898da75b89 | |||
a7a846dfba | |||
6da671df62 | |||
bdb14af4af | |||
e57ad9826e | |||
8775103bf8 | |||
![]() |
5636a76152 | ||
![]() |
745cc69e9e | ||
![]() |
b4c0a827f0 | ||
d57416a73e | |||
![]() |
8dcf6ca658 | ||
bc66a314c4 | |||
6fac63d769 | |||
e41692bf2c | |||
dff3bd2f24 | |||
b67e5a9260 | |||
4815f4e6b4 | |||
e8ec9b415a | |||
5b9e36180e | |||
f1b59e4150 | |||
17070ca732 | |||
![]() |
d618fafe4b | ||
![]() |
dd1dfb3094 | ||
8d6617e288 | |||
![]() |
fdec531c99 | ||
a246584c4a | |||
![]() |
00ef174292 | ||
![]() |
ada66f4851 | ||
![]() |
a9be6475b1 | ||
![]() |
f380289a84 | ||
![]() |
528d80652c | ||
![]() |
7c6df58906 | ||
![]() |
1851c0ac43 | ||
880d472a4a | |||
![]() |
25ff96873b | ||
![]() |
82881049c4 | ||
![]() |
60c9737cfe | ||
![]() |
632db924eb | ||
![]() |
261121297b | ||
![]() |
1bd243f3d2 | ||
![]() |
7c3f9f7196 | ||
9074dfda9d | |||
de32eb09e6 | |||
2e97f0f0ce | |||
0b06acf304 | |||
cc90291358 | |||
03b4604643 |
@ -69,7 +69,7 @@ def main(argv=None):
|
||||
console.setLevel(loglevel)
|
||||
logger.addHandler(console)
|
||||
|
||||
app = QApplication(argv, organizationName='frappy', applicationName='frappy_gui')
|
||||
app = QApplication(argv)
|
||||
|
||||
win = MainWindow(args, logger)
|
||||
app.aboutToQuit.connect(win._onQuit)
|
||||
|
@ -59,23 +59,16 @@ def decode(msg, addr):
|
||||
|
||||
|
||||
def print_answer(answer, *, short=False):
|
||||
try:
|
||||
hostname = socket.gethostbyaddr(answer.address)[0]
|
||||
address = hostname
|
||||
numeric = f' ({answer.address})'
|
||||
except Exception:
|
||||
address = answer.address
|
||||
numeric = ''
|
||||
if short:
|
||||
# NOTE: keep this easily parseable!
|
||||
print(f'{answer.equipment_id} {address}:{answer.port}')
|
||||
print(f'{answer.equipment_id} {answer.address}:{answer.port}')
|
||||
return
|
||||
print(f'Found {answer.equipment_id} at {address}{numeric}:')
|
||||
print(f'Found {answer.equipment_id} at {answer.address}:')
|
||||
print(f' Port: {answer.port}')
|
||||
print(f' Firmware: {answer.firmware}')
|
||||
desc = answer.description.replace('\n', '\n ')
|
||||
print(f' Node description: {desc}')
|
||||
print('-' * 80)
|
||||
print()
|
||||
|
||||
|
||||
def scan(max_wait=1.0):
|
||||
@ -126,14 +119,10 @@ def listen(*, short=False):
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-l', '--listen', action='store_true',
|
||||
help='Keep listening after the broadcast.')
|
||||
parser.add_argument('-s', '--short', action='store_true',
|
||||
help='Print short info (always on when listen).')
|
||||
help='Print short info. '
|
||||
'Keep listening after the broadcast.')
|
||||
args = parser.parse_args(sys.argv[1:])
|
||||
short = args.listen or args.short
|
||||
if not short:
|
||||
print('-' * 80)
|
||||
for answer in scan():
|
||||
print_answer(answer, short=short)
|
||||
print_answer(answer, short=args.listen)
|
||||
if args.listen:
|
||||
listen(short=short)
|
||||
listen(short=args.listen)
|
||||
|
@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
# Add import path for inplace usage
|
||||
sys.path.insert(0, str(Path(__file__).absolute().parents[1]))
|
||||
|
||||
from frappy.client.interactive import Client
|
||||
from frappy_psi.iqplot import Plot
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print('Usage: peus-plot <maxY>')
|
||||
|
||||
|
||||
def get_modules(name):
|
||||
return list(filter(None, (globals().get(name % i) for i in range(10))))
|
||||
|
||||
|
||||
secnode = Client('pc13252:5000')
|
||||
time_size = {'time', 'size'}
|
||||
int_mods = [u] + get_modules('roi%d')
|
||||
t_rois = get_modules('roi%d')
|
||||
i_rois = get_modules('roi%di')
|
||||
q_rois = get_modules('roi%dq')
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
maxy = float(sys.argv[1])
|
||||
else:
|
||||
maxy = 0.02
|
||||
|
||||
|
||||
iqplot = Plot(maxy)
|
||||
|
||||
for i in range(99):
|
||||
pass
|
||||
|
||||
try:
|
||||
while True:
|
||||
curves = np.array(u.get_curves())
|
||||
iqplot.plot(curves,
|
||||
rois=[(r.time - r.size * 0.5, r.time + r.size * 0.5) for r in int_mods],
|
||||
average=([r.time for r in t_rois],
|
||||
[r.value for r in i_rois],
|
||||
[r.value for r in q_rois]))
|
||||
if not iqplot.pause(0.5):
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
iqplot.close()
|
65
bin/us-plot
65
bin/us-plot
@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
# Add import path for inplace usage
|
||||
sys.path.insert(0, str(Path(__file__).absolute().parents[1]))
|
||||
|
||||
from frappy.client.interactive import Client
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from frappy_psi.iqplot import Pause
|
||||
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("""
|
||||
Usage:
|
||||
|
||||
us-plot <end> [<start> [<npoints>]]
|
||||
|
||||
end: end of window [ns]
|
||||
start: start of window [n2], default: 0
|
||||
npoints: number fo points (default 1000)
|
||||
""")
|
||||
sys.exit(0)
|
||||
|
||||
Client('pc13252:5000')
|
||||
|
||||
|
||||
def plot(array, ax, style, xs):
|
||||
xaxis = np.arange(len(array)) * xs
|
||||
return ax.plot(xaxis, array, style)[0]
|
||||
|
||||
|
||||
def update(array, line, xs):
|
||||
xaxis = np.arange(len(array)) * xs
|
||||
line.set_data(np.array([xaxis, array]))
|
||||
|
||||
def on_close(event):
|
||||
sys.exit(0)
|
||||
|
||||
start = 0
|
||||
end = float(sys.argv[1])
|
||||
npoints = 1000
|
||||
if len(sys.argv) > 2:
|
||||
start = float(sys.argv[2])
|
||||
if len(sys.argv) > 3:
|
||||
npoints = float(sys.argv[3])
|
||||
|
||||
fig, ax = plt.subplots(figsize=(15,3))
|
||||
pause = Pause(fig)
|
||||
try:
|
||||
get_signal = iq.get_signal
|
||||
print('plotting RUS signal')
|
||||
except NameError:
|
||||
get_signal = u.get_signal
|
||||
print('plotting PE signal')
|
||||
|
||||
xs, signal = get_signal(start, end, npoints)
|
||||
|
||||
lines = [plot(s, ax, '-', xs) for s in signal]
|
||||
|
||||
while pause(0.5):
|
||||
plt.draw()
|
||||
xs, signal = get_signal(start, end, npoints)
|
||||
for line, sig in zip(lines, signal):
|
||||
update(sig, line, xs)
|
67
cfg/PEUS.py
Normal file
67
cfg/PEUS.py
Normal file
@ -0,0 +1,67 @@
|
||||
Node(equipment_id = 'pe_ultrasound.psi.ch',
|
||||
description = 'pulse echo ultra sound setup',
|
||||
interface = 'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('f',
|
||||
cls = 'frappy_psi.ultrasound.Frequency',
|
||||
description = 'ultrasound frequency and acquisition loop',
|
||||
uri = 'serial:///dev/ttyS1',
|
||||
pars = 'pars',
|
||||
pollinterval = 0.1,
|
||||
time = 900, # start time
|
||||
size = 5000,
|
||||
freq = 1.17568e+06,
|
||||
basefreq = 4.14902e+07,
|
||||
control = False,
|
||||
rusmode = False,
|
||||
amp = 5.0,
|
||||
nr = 1000, #500 #300 #100 #50 #30 #10 #5 #3 #1 #1000 #500 #300 #100 #50 #30 #10 #5 #3 #1 #500
|
||||
sr = 32768, #16384
|
||||
plot = True,
|
||||
maxstep = 100000,
|
||||
bw = 10E6, #butter worth filter bandwidth
|
||||
maxy = 0.7, # y scale for plot
|
||||
curves = 'curves', # module to transmit curves:
|
||||
)
|
||||
|
||||
Mod('curves',
|
||||
cls = 'frappy_psi.ultrasound.Curves',
|
||||
description = 't, I, Q and pulse arrays for plot',
|
||||
)
|
||||
|
||||
Mod('delay',
|
||||
cls = 'frappy__psi.dg645.Delay',
|
||||
description = 'delay line with 2 channels',
|
||||
uri = 'serial:///dev/ttyS2',
|
||||
on1 = 1e-9,
|
||||
on2 = 1E-9,
|
||||
off1 = 400e-9,
|
||||
off2 = 600e-9,
|
||||
)
|
||||
|
||||
Mod('pars',
|
||||
cls = 'frappy_psi.ultrasound.Pars',
|
||||
description = 'SEA parameters',
|
||||
)
|
||||
|
||||
def roi(nr, time=None, size=300):
|
||||
Mod(f'roi{nr}',
|
||||
cls = 'frappy_psi.ultrasound.Roi',
|
||||
description = f'I/Q of region {nr}',
|
||||
main = 'f',
|
||||
time=time or 4000,
|
||||
size=size,
|
||||
enable=time is not None,
|
||||
)
|
||||
|
||||
roi(0, 2450) # you may add size as argument if not default
|
||||
roi(1, 5950)
|
||||
roi(2, 9475)
|
||||
roi(3, 12900)
|
||||
roi(4, 16100)
|
||||
roi(5) # disabled
|
||||
roi(6)
|
||||
roi(7)
|
||||
roi(8)
|
||||
roi(9)
|
@ -1,87 +0,0 @@
|
||||
Node('PEUS.psi.ch',
|
||||
'ultrasound, pulse_echo configuration',
|
||||
interface='5000',
|
||||
)
|
||||
|
||||
Mod('u',
|
||||
'frappy_psi.ultrasound.PulseEcho',
|
||||
'ultrasound acquisition loop',
|
||||
freq='f',
|
||||
# pollinterval=0.1,
|
||||
time=900.0,
|
||||
size=5000.0,
|
||||
nr=500,
|
||||
sr=32768,
|
||||
bw=1e7,
|
||||
)
|
||||
|
||||
Mod('fio',
|
||||
'frappy_psi.ultrasound.FreqStringIO', '',
|
||||
uri='serial:///dev/ttyS1?baudrate=57600',
|
||||
)
|
||||
|
||||
Mod('f',
|
||||
'frappy_psi.ultrasound.Frequency',
|
||||
'writable for frequency',
|
||||
output='R', # L for LF (bnc), R for RF (type N)
|
||||
io='fio',
|
||||
amp=0.5, # VPP
|
||||
)
|
||||
|
||||
Mod('fdif',
|
||||
'frappy_psi.ultrasound.FrequencyDif',
|
||||
'writable for frequency minus base frequency',
|
||||
freq='f',
|
||||
base=41490200.0,
|
||||
)
|
||||
|
||||
# Mod('curves',
|
||||
# 'frappy_psi.ultrasound.Curves',
|
||||
# 't, I, Q and pulse arrays for plot',
|
||||
# )
|
||||
|
||||
def roi(name, time, size, components='iqpa', enable=True, control=False, freq=None, **kwds):
|
||||
description = 'I/Q of region {name}'
|
||||
if freq:
|
||||
kwds.update(cls='frappy_psi.ultrasound.ControlRoi',
|
||||
description=f'{description} as control loop',
|
||||
freq=freq, **kwds)
|
||||
else:
|
||||
kwds.update(cls='frappy_psi.ultrasound.Roi',
|
||||
description=description, **kwds)
|
||||
kwds.update({c: name + c for c in components})
|
||||
Mod(name,
|
||||
main='u',
|
||||
time=time,
|
||||
size=size,
|
||||
enable=enable,
|
||||
**kwds,
|
||||
)
|
||||
for c in components:
|
||||
Mod(name + c,
|
||||
'frappy.modules.Readable',
|
||||
f'{name}{c} component',
|
||||
)
|
||||
|
||||
# control loop
|
||||
roi('roi0', 2450, 300, freq='f', maxstep=100000, minstep=4000)
|
||||
# other rois
|
||||
roi('roi1', 5950, 300)
|
||||
roi('roi2', 9475, 300)
|
||||
roi('roi3', 12900, 300)
|
||||
#roi('roi4', 400, 30, False)
|
||||
#roi('roi5', 400, 30, False)
|
||||
#roi('roi6', 400, 30, False)
|
||||
#roi('roi7', 400, 30, False)
|
||||
#roi('roi8', 400, 30, False)
|
||||
#roi('roi9', 400, 30, False)
|
||||
|
||||
Mod('delay',
|
||||
'frappy_psi.dg645.Delay',
|
||||
'delay line with 2 channels',
|
||||
uri='serial:///dev/ttyS2',
|
||||
on1=1e-09,
|
||||
on2=1e-09,
|
||||
off1=4e-07,
|
||||
off2=6e-07,
|
||||
)
|
62
cfg/RUS.py
Normal file
62
cfg/RUS.py
Normal file
@ -0,0 +1,62 @@
|
||||
Node(equipment_id = 'r_ultrasound.psi.ch',
|
||||
description = 'resonant ultra sound setup',
|
||||
interface = 'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('f',
|
||||
cls = 'frappy_psi.ultrasound.Frequency',
|
||||
description = 'ultrasound frequency and acquisition loop',
|
||||
uri = 'serial:///dev/ttyS1',
|
||||
pars = 'pars',
|
||||
pollinterval = 0.1,
|
||||
time = 900, # start time
|
||||
size = 5000,
|
||||
freq = 1.e+03,
|
||||
basefreq = 1.E+3,
|
||||
control = False,
|
||||
rusmode = False,
|
||||
amp = 2.5,
|
||||
nr = 1, #500 #300 #100 #50 #30 #10 #5 #3 #1 #1000 #500 #300 #100 #50 #30 #10 #5 #3 #1 #500
|
||||
sr = 1E8, #16384
|
||||
plot = True,
|
||||
maxstep = 100000,
|
||||
bw = 10E6, #butter worth filter bandwidth
|
||||
maxy = 0.7, # y scale for plot
|
||||
curves = 'curves', # module to transmit curves:
|
||||
)
|
||||
|
||||
Mod('curves',
|
||||
cls = 'frappy_psi.ultrasound.Curves',
|
||||
description = 't, I, Q and pulse arrays for plot',
|
||||
)
|
||||
|
||||
Mod('roi0',
|
||||
cls = 'frappy_psi.ultrasound.Roi',
|
||||
description = 'I/Q of region in the control loop',
|
||||
time = 300, # this is the center of roi:
|
||||
size = 5000,
|
||||
main = f,
|
||||
)
|
||||
|
||||
Mod('roi1',
|
||||
cls = 'frappy_psi.ultrasound.Roi',
|
||||
description = 'I/Q of region 1',
|
||||
time = 100, # this is the center of roi:
|
||||
size = 300,
|
||||
main = f,
|
||||
)
|
||||
|
||||
Mod('delay',
|
||||
cls = 'frappy__psi.dg645.Delay',
|
||||
description = 'delay line with 2 channels',
|
||||
uri = 'serial:///dev/ttyS2',
|
||||
on1 = 1e-9,
|
||||
on2 = 1E-9,
|
||||
off1 = 400e-9,
|
||||
off2 = 600e-9,
|
||||
)
|
||||
|
||||
Mod('pars',
|
||||
cls = 'frappy_psi.ultrasound.Pars',
|
||||
description = 'SEA parameters',
|
||||
)
|
@ -1,39 +0,0 @@
|
||||
Node(equipment_id = 'r_ultrasound.psi.ch',
|
||||
description = 'resonant ultra sound setup',
|
||||
interface = 'tcp://5000',
|
||||
)
|
||||
|
||||
Mod('iq',
|
||||
cls = 'frappy_psi.ultrasound.RUS',
|
||||
description = 'ultrasound iq mesurement',
|
||||
imod = 'i',
|
||||
qmod = 'q',
|
||||
freq='f',
|
||||
input_range=10, # VPP
|
||||
input_delay = 0,
|
||||
periods = 163,
|
||||
)
|
||||
|
||||
Mod('freqio',
|
||||
'frappy_psi.ultrasound.FreqStringIO',
|
||||
' ',
|
||||
uri = 'serial:///dev/ttyS1?baudrate=57600',
|
||||
)
|
||||
|
||||
Mod('f',
|
||||
cls = 'frappy_psi.ultrasound.Frequency',
|
||||
description = 'ultrasound frequency',
|
||||
io='freqio',
|
||||
output='L', # L for LF (bnc), R for RF (type N)
|
||||
target=10000,
|
||||
)
|
||||
|
||||
Mod('i',
|
||||
cls='frappy.modules.Readable',
|
||||
description='I component',
|
||||
)
|
||||
|
||||
Mod('q',
|
||||
cls='frappy.modules.Readable',
|
||||
description='Q component',
|
||||
)
|
15
cfg/addons/ah2700_cfg.py
Executable file → Normal file
15
cfg/addons/ah2700_cfg.py
Executable file → Normal file
@ -2,21 +2,8 @@ Node('ah2700.frappy.psi.ch',
|
||||
'Andeen Hagerlin 2700 Capacitance Bridge',
|
||||
)
|
||||
|
||||
Mod('cap_io',
|
||||
'frappy_psi.ah2700.Ah2700IO',
|
||||
'',
|
||||
uri='linse-976d-ts:3006',
|
||||
)
|
||||
|
||||
Mod('cap',
|
||||
'frappy_psi.ah2700.Capacitance',
|
||||
'capacitance',
|
||||
io = 'cap_io',
|
||||
)
|
||||
|
||||
Mod('loss',
|
||||
'frappy_psi.parmod.Par',
|
||||
'loss parameter',
|
||||
read='cap.loss',
|
||||
unit='deg',
|
||||
uri='dil4-ts.psi.ch:3008',
|
||||
)
|
||||
|
@ -4,22 +4,33 @@ Node('ls340test.psi.ch',
|
||||
)
|
||||
|
||||
Mod('io',
|
||||
'frappy_psi.lakeshore.IO340',
|
||||
'frappy_psi.lakeshore.Ls340IO',
|
||||
'communication to ls340',
|
||||
uri='tcp://localhost:7777'
|
||||
uri='tcp://ldmprep56-ts:3002'
|
||||
)
|
||||
|
||||
Mod('dev',
|
||||
'frappy_psi.lakeshore.Device340',
|
||||
'device for calcurve',
|
||||
io='io',
|
||||
curve_handling=True,
|
||||
)
|
||||
Mod('T',
|
||||
'frappy_psi.lakeshore.Sensor340',
|
||||
'frappy_psi.lakeshore.TemperatureLoop340',
|
||||
'sample temperature',
|
||||
# output_module='Heater',
|
||||
device='dev',
|
||||
channel='A',
|
||||
calcurve='x29746',
|
||||
output_module='Heater',
|
||||
target=Param(max=470),
|
||||
io='io',
|
||||
channel='B'
|
||||
)
|
||||
|
||||
Mod('T_cold_finger',
|
||||
'frappy_psi.lakeshore.Sensor340',
|
||||
'cold finger temperature',
|
||||
io='io',
|
||||
channel='A'
|
||||
)
|
||||
|
||||
Mod('Heater',
|
||||
'frappy_psi.lakeshore.HeaterOutput',
|
||||
'heater output',
|
||||
channel='B',
|
||||
io='io',
|
||||
resistance=25,
|
||||
max_power=50,
|
||||
current=1
|
||||
)
|
||||
|
@ -1,17 +0,0 @@
|
||||
from frappy_psi.ccracks import Rack
|
||||
|
||||
Node('ori7test.psi.ch',
|
||||
'ORI7 test',
|
||||
'tcp://5000'
|
||||
)
|
||||
|
||||
rack = Rack(Mod)
|
||||
|
||||
rack.lakeshore()
|
||||
rack.sensor('Ts', channel='C', calcurve='x186350')
|
||||
rack.loop('T', channel='B', calcurve='x174786', output_module='htr', target=10)
|
||||
rack.heater('htr', 1, '100W', 25)
|
||||
|
||||
rack.ccu(he=True, n2=True)
|
||||
|
||||
rack.hepump()
|
100
debian/changelog
vendored
100
debian/changelog
vendored
@ -1,4 +1,4 @@
|
||||
frappy-core (0.20.4) stable; urgency=medium
|
||||
frappy-core (0.20.4) jammy; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* remove unused file
|
||||
@ -17,7 +17,7 @@ frappy-core (0.20.4) stable; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 14 Nov 2024 14:43:54 +0100
|
||||
|
||||
frappy-core (0.20.3) stable; urgency=medium
|
||||
frappy-core (0.20.3) jammy; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* fixup test for cfg_editor utils to run from non-checkout, and fix names, and remove example code
|
||||
@ -27,7 +27,7 @@ frappy-core (0.20.3) stable; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 07 Nov 2024 10:57:11 +0100
|
||||
|
||||
frappy-core (0.20.2) stable; urgency=medium
|
||||
frappy-core (0.20.2) jammy; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* pylint: do not try to infer too much
|
||||
@ -73,7 +73,7 @@ frappy-core (0.20.2) stable; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Wed, 06 Nov 2024 10:40:26 +0100
|
||||
|
||||
frappy-core (0.20.1) stable; urgency=medium
|
||||
frappy-core (0.20.1) jammy; urgency=medium
|
||||
|
||||
* gui: do not add a console logger when there is no sys.stdout
|
||||
* remove unused test class
|
||||
@ -83,7 +83,7 @@ frappy-core (0.20.1) stable; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 17 Oct 2024 16:31:27 +0200
|
||||
|
||||
frappy-core (0.20.0) stable; urgency=medium
|
||||
frappy-core (0.20.0) jammy; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* bin: remove make_doc
|
||||
@ -128,7 +128,7 @@ frappy-core (0.20.0) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Thu, 17 Oct 2024 14:24:29 +0200
|
||||
|
||||
frappy-core (0.19.10) stable; urgency=medium
|
||||
frappy-core (0.19.10) jammy; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* debian: let frappy-core replace frappy-demo
|
||||
@ -138,25 +138,25 @@ frappy-core (0.19.10) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 07 Aug 2024 17:00:06 +0200
|
||||
|
||||
frappy-core (0.19.9) stable; urgency=medium
|
||||
frappy-core (0.19.9) jammy; urgency=medium
|
||||
|
||||
* debian: fix missing install dir
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 06 Aug 2024 16:02:50 +0200
|
||||
|
||||
frappy-core (0.19.8) stable; urgency=medium
|
||||
frappy-core (0.19.8) jammy; urgency=medium
|
||||
|
||||
* debian: move demo into core
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 06 Aug 2024 15:58:20 +0200
|
||||
|
||||
frappy-core (0.19.7) stable; urgency=medium
|
||||
frappy-core (0.19.7) jammy; urgency=medium
|
||||
|
||||
* lib: GeneralConfig fix missing keys logic
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 06 Aug 2024 15:04:07 +0200
|
||||
|
||||
frappy-core (0.19.6) stable; urgency=medium
|
||||
frappy-core (0.19.6) jammy; urgency=medium
|
||||
|
||||
[ Jens Krüger ]
|
||||
* SINQ/SEA: Fix import error due to None value
|
||||
@ -170,7 +170,7 @@ frappy-core (0.19.6) stable; urgency=medium
|
||||
|
||||
-- Jens Krüger <jenkins@frm2.tum.de> Tue, 06 Aug 2024 13:56:51 +0200
|
||||
|
||||
frappy-core (0.19.5) stable; urgency=medium
|
||||
frappy-core (0.19.5) jammy; urgency=medium
|
||||
|
||||
* client: fix how to raise error on wrong ident
|
||||
* add missing requirements to setup.py
|
||||
@ -179,13 +179,13 @@ frappy-core (0.19.5) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Mon, 05 Aug 2024 09:30:53 +0200
|
||||
|
||||
frappy-core (0.19.4) stable; urgency=medium
|
||||
frappy-core (0.19.4) jammy; urgency=medium
|
||||
|
||||
* actually exclude cfg-editor
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Fri, 26 Jul 2024 11:46:10 +0200
|
||||
|
||||
frappy-core (0.19.3) stable; urgency=medium
|
||||
frappy-core (0.19.3) jammy; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* frappy_psi.extparams.StructParam: fix doc + simplify
|
||||
@ -205,7 +205,7 @@ frappy-core (0.19.3) stable; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Fri, 26 Jul 2024 08:36:43 +0200
|
||||
|
||||
frappy-core (0.19.2) stable; urgency=medium
|
||||
frappy-core (0.19.2) jammy; urgency=medium
|
||||
|
||||
[ l_samenv ]
|
||||
* fix missing update after error on parameter
|
||||
@ -230,7 +230,7 @@ frappy-core (0.19.2) stable; urgency=medium
|
||||
|
||||
-- l_samenv <jenkins@frm2.tum.de> Tue, 18 Jun 2024 15:21:43 +0200
|
||||
|
||||
frappy-core (0.19.1) stable; urgency=medium
|
||||
frappy-core (0.19.1) jammy; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* SecopClient.online must be True while activating
|
||||
@ -242,7 +242,7 @@ frappy-core (0.19.1) stable; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Fri, 07 Jun 2024 16:50:33 +0200
|
||||
|
||||
frappy-core (0.19.0) stable; urgency=medium
|
||||
frappy-core (0.19.0) jammy; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* simulation: extra_params might be a list
|
||||
@ -298,14 +298,14 @@ frappy-core (0.19.0) stable; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Thu, 16 May 2024 11:31:25 +0200
|
||||
|
||||
frappy-core (0.18.1) stable; urgency=medium
|
||||
frappy-core (0.18.1) focal; urgency=medium
|
||||
|
||||
* mlz: Zapf fix unit handling and small errors
|
||||
* mlz: entangle fix limit check
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 24 Jan 2024 14:59:21 +0100
|
||||
|
||||
frappy-core (0.18.0) stable; urgency=medium
|
||||
frappy-core (0.18.0) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Add shutdownModule function
|
||||
@ -416,7 +416,7 @@ frappy-core (0.18.0) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 17 Jan 2024 12:35:00 +0100
|
||||
|
||||
frappy-core (0.17.13) stable; urgency=medium
|
||||
frappy-core (0.17.13) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* add egg-info to gitignore
|
||||
@ -437,7 +437,7 @@ frappy-core (0.17.13) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 20 Jun 2023 14:38:00 +0200
|
||||
|
||||
frappy-core (0.17.12) stable; urgency=medium
|
||||
frappy-core (0.17.12) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Warn about duplicate module definitions in a file
|
||||
@ -462,7 +462,7 @@ frappy-core (0.17.12) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 13 Jun 2023 06:51:27 +0200
|
||||
|
||||
frappy-core (0.17.11) stable; urgency=medium
|
||||
frappy-core (0.17.11) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Add __format__ to EnumMember
|
||||
@ -535,7 +535,7 @@ frappy-core (0.17.11) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Thu, 25 May 2023 09:38:24 +0200
|
||||
|
||||
frappy-core (0.17.10) stable; urgency=medium
|
||||
frappy-core (0.17.10) focal; urgency=medium
|
||||
|
||||
* Change leftover %-logging calls to lazy
|
||||
* Convert formatting automatically to f-strings
|
||||
@ -547,25 +547,25 @@ frappy-core (0.17.10) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 19 Apr 2023 14:32:52 +0200
|
||||
|
||||
frappy-core (0.17.9) stable; urgency=medium
|
||||
frappy-core (0.17.9) focal; urgency=medium
|
||||
|
||||
* interactive client: avoid messing up the input line
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Tue, 11 Apr 2023 16:09:03 +0200
|
||||
|
||||
frappy-core (0.17.8) stable; urgency=medium
|
||||
frappy-core (0.17.8) focal; urgency=medium
|
||||
|
||||
* Debian: Fix typo
|
||||
|
||||
-- Jens Krüger <jenkins@frm2.tum.de> Wed, 05 Apr 2023 07:20:25 +0200
|
||||
|
||||
frappy-core (0.17.7) stable; urgency=medium
|
||||
frappy-core (0.17.7) focal; urgency=medium
|
||||
|
||||
* Debian: add pyqtgraph dependency
|
||||
|
||||
-- Jens Krüger <jenkins@frm2.tum.de> Wed, 05 Apr 2023 07:07:24 +0200
|
||||
|
||||
frappy-core (0.17.6) stable; urgency=medium
|
||||
frappy-core (0.17.6) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* gui: show parameter properties again
|
||||
@ -585,25 +585,25 @@ frappy-core (0.17.6) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 04 Apr 2023 08:42:26 +0200
|
||||
|
||||
frappy-core (0.17.5) stable; urgency=medium
|
||||
frappy-core (0.17.5) focal; urgency=medium
|
||||
|
||||
* Fix generator
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 22 Mar 2023 12:32:06 +0100
|
||||
|
||||
frappy-core (0.17.4) stable; urgency=medium
|
||||
frappy-core (0.17.4) focal; urgency=medium
|
||||
|
||||
* Fix entangle integration bugs
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Wed, 22 Mar 2023 11:44:34 +0100
|
||||
|
||||
frappy-core (0.17.3) stable; urgency=medium
|
||||
frappy-core (0.17.3) focal; urgency=medium
|
||||
|
||||
* UNRELEASED
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 21 Mar 2023 15:55:09 +0100
|
||||
|
||||
frappy-core (0.17.2) stable; urgency=medium
|
||||
frappy-core (0.17.2) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Fix Simulation and Proxy
|
||||
@ -740,7 +740,7 @@ frappy-core (0.17.2) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 21 Mar 2023 15:49:06 +0100
|
||||
|
||||
frappy-core (0.17.1) stable; urgency=medium
|
||||
frappy-core (0.17.1) focal; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* gitignore: ignore demo PID file
|
||||
@ -759,7 +759,7 @@ frappy-core (0.17.1) stable; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 17:44:56 +0100
|
||||
|
||||
frappy-core (0.17.0) stable; urgency=medium
|
||||
frappy-core (0.17.0) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Rework GUI.
|
||||
@ -770,37 +770,37 @@ frappy-core (0.17.0) stable; urgency=medium
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 21 Feb 2023 13:52:17 +0100
|
||||
|
||||
frappy-core (0.16.1) stable; urgency=medium
|
||||
frappy-core (0.16.1) focal; urgency=medium
|
||||
|
||||
* UNRELEASED
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 08:44:28 +0100
|
||||
|
||||
frappy-core (0.16.4) stable; urgency=medium
|
||||
frappy-core (0.16.4) focal; urgency=medium
|
||||
|
||||
* UNRELEASED
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 08:09:20 +0100
|
||||
|
||||
frappy-core (0.16.3) stable; urgency=medium
|
||||
frappy-core (0.16.3) focal; urgency=medium
|
||||
|
||||
* UNRELEASED
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 08:00:15 +0100
|
||||
|
||||
frappy-core (0.16.2) stable; urgency=medium
|
||||
frappy-core (0.16.2) focal; urgency=medium
|
||||
|
||||
* gui: move icon resources for the cfg editor to its subdirectory
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 21 Feb 2023 07:50:13 +0100
|
||||
|
||||
frappy-core (0.16.1) stable; urgency=medium
|
||||
frappy-core (0.16.1) focal; urgency=medium
|
||||
|
||||
* add frappy-cli to package
|
||||
|
||||
-- Enrico Faulhaber <jenkins@frm2.tum.de> Mon, 20 Feb 2023 17:17:23 +0100
|
||||
|
||||
frappy-core (0.16.0) stable; urgency=medium
|
||||
frappy-core (0.16.0) focal; urgency=medium
|
||||
|
||||
[ Enrico Faulhaber ]
|
||||
* fix sorce package name
|
||||
@ -862,7 +862,7 @@ frappy-core (0.16.0) stable; urgency=medium
|
||||
|
||||
-- Enrico Faulhaber <jenkins@frm2.tum.de> Mon, 20 Feb 2023 16:15:10 +0100
|
||||
|
||||
frappy-core (0.15.0) stable; urgency=medium
|
||||
frappy-core (0.15.0) focal; urgency=medium
|
||||
|
||||
[ Björn Pedersen ]
|
||||
* Remove iohandler left-overs from docs
|
||||
@ -892,7 +892,7 @@ frappy-core (0.15.0) stable; urgency=medium
|
||||
|
||||
-- Björn Pedersen <jenkins@frm2.tum.de> Thu, 10 Nov 2022 14:46:01 +0100
|
||||
|
||||
secop-core (0.14.3) stable; urgency=medium
|
||||
secop-core (0.14.3) focal; urgency=medium
|
||||
|
||||
[ Enrico Faulhaber ]
|
||||
* change repo to secop/frappy
|
||||
@ -908,13 +908,13 @@ secop-core (0.14.3) stable; urgency=medium
|
||||
|
||||
-- Enrico Faulhaber <jenkins@frm2.tum.de> Thu, 03 Nov 2022 13:51:52 +0100
|
||||
|
||||
secop-core (0.14.2) stable; urgency=medium
|
||||
secop-core (0.14.2) focal; urgency=medium
|
||||
|
||||
* systemd generator: adapt to changed config API
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Thu, 20 Oct 2022 15:38:45 +0200
|
||||
|
||||
secop-core (0.14.1) stable; urgency=medium
|
||||
secop-core (0.14.1) focal; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* secop_psi.entangle.AnalogInput: fix main value
|
||||
@ -926,7 +926,7 @@ secop-core (0.14.1) stable; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Thu, 20 Oct 2022 14:04:07 +0200
|
||||
|
||||
secop-core (0.14.0) stable; urgency=medium
|
||||
secop-core (0.14.0) focal; urgency=medium
|
||||
|
||||
* add simple interactive python client
|
||||
* fix undefined status in softcal
|
||||
@ -940,7 +940,7 @@ secop-core (0.14.0) stable; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@frm2.tum.de> Wed, 19 Oct 2022 11:31:50 +0200
|
||||
|
||||
secop-core (0.13.1) stable; urgency=medium
|
||||
secop-core (0.13.1) focal; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* an enum with value 0 should be interpreted as False
|
||||
@ -951,7 +951,7 @@ secop-core (0.13.1) stable; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins02.admin.frm2.tum.de> Tue, 02 Aug 2022 15:31:52 +0200
|
||||
|
||||
secop-core (0.13.0) stable; urgency=medium
|
||||
secop-core (0.13.0) focal; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* debian: fix email addresses in changelog
|
||||
@ -1014,13 +1014,13 @@ secop-core (0.13.0) stable; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 02 Aug 2022 09:47:06 +0200
|
||||
|
||||
secop-core (0.12.4) stable; urgency=medium
|
||||
secop-core (0.12.4) focal; urgency=medium
|
||||
|
||||
* fix command inheritance
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Thu, 11 Nov 2021 16:21:19 +0100
|
||||
|
||||
secop-core (0.12.3) stable; urgency=medium
|
||||
secop-core (0.12.3) focal; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* Makefile: fix docker image
|
||||
@ -1043,7 +1043,7 @@ secop-core (0.12.3) stable; urgency=medium
|
||||
|
||||
-- Georg Brandl <jenkins@jenkins01.admin.frm2.tum.de> Wed, 10 Nov 2021 16:33:19 +0100
|
||||
|
||||
secop-core (0.12.2) stable; urgency=medium
|
||||
secop-core (0.12.2) focal; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* fix issue with new syntax in simulation
|
||||
@ -1055,13 +1055,13 @@ secop-core (0.12.2) stable; urgency=medium
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Tue, 18 May 2021 10:29:17 +0200
|
||||
|
||||
secop-core (0.12.1) stable; urgency=medium
|
||||
secop-core (0.12.1) focal; urgency=medium
|
||||
|
||||
* remove secop-console from debian *.install file
|
||||
|
||||
-- Enrico Faulhaber <jenkins@jenkins02.admin.frm2.tum.de> Tue, 04 May 2021 09:42:53 +0200
|
||||
|
||||
secop-core (0.12.0) stable; urgency=medium
|
||||
secop-core (0.12.0) focal; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* make datatypes immutable
|
||||
|
1
debian/compat
vendored
Normal file
1
debian/compat
vendored
Normal file
@ -0,0 +1 @@
|
||||
11
|
4
debian/control
vendored
4
debian/control
vendored
@ -2,7 +2,7 @@ Source: frappy-core
|
||||
Section: contrib/misc
|
||||
Priority: optional
|
||||
Maintainer: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
Build-Depends: debhelper-compat (= 13),
|
||||
Build-Depends: debhelper (>= 11~),
|
||||
dh-python,
|
||||
python3 (>=3.6),
|
||||
python3-all,
|
||||
@ -20,7 +20,7 @@ Build-Depends: debhelper-compat (= 13),
|
||||
git,
|
||||
markdown,
|
||||
python3-daemon
|
||||
Standards-Version: 4.6.2
|
||||
Standards-Version: 4.1.4
|
||||
X-Python3-Version: >= 3.6
|
||||
|
||||
Package: frappy-core
|
||||
|
@ -498,7 +498,7 @@ class Console(code.InteractiveConsole):
|
||||
history = None
|
||||
if readline:
|
||||
try:
|
||||
history = expanduser(f'~/.config/frappy/{name}-history')
|
||||
history = expanduser(f'~/.frappy-{name}-history')
|
||||
readline.read_history_file(history)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
@ -538,10 +538,10 @@ def init(*nodes):
|
||||
return success
|
||||
|
||||
|
||||
def interact(usage_tail='', appname=None):
|
||||
def interact(usage_tail=''):
|
||||
empty = '_c0' not in clientenv.namespace
|
||||
print(USAGE.format(
|
||||
client_name='cli' if empty else '_c0',
|
||||
client_assign="\ncli = Client('localhost:5000')\n" if empty else '',
|
||||
tail=usage_tail))
|
||||
Console(name=f'cli-{appname}' if appname else 'cli')
|
||||
Console()
|
||||
|
@ -95,9 +95,7 @@ class Collector:
|
||||
self.cls = cls
|
||||
|
||||
def add(self, *args, **kwds):
|
||||
result = self.cls(*args, **kwds)
|
||||
self.list.append(result)
|
||||
return result
|
||||
self.list.append(self.cls(*args, **kwds))
|
||||
|
||||
def append(self, mod):
|
||||
self.list.append(mod)
|
||||
|
@ -27,6 +27,7 @@
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>18</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
|
@ -21,6 +21,7 @@
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>12</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
<underline>true</underline>
|
||||
</font>
|
||||
|
@ -1,50 +0,0 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""handling of prefixes of physical units"""
|
||||
|
||||
import re
|
||||
import prefixed
|
||||
|
||||
prefixed.SI_MAGNITUDE['u'] = 1e-6 # accept 'u' as replacement for 'µ'
|
||||
|
||||
|
||||
class NumberWithUnit:
|
||||
def __init__(self, *units):
|
||||
pfx = "|".join(prefixed.SI_MAGNITUDE)
|
||||
unt = "|".join(units)
|
||||
self.units = units
|
||||
self.pattern = re.compile(rf'\s*([+-]?\d*\.?\d*(?:[eE][+-]?\d+)?\s*(?:{pfx})?)({unt})\s*$')
|
||||
|
||||
def parse(self, value):
|
||||
"""parse and return number and value"""
|
||||
match = self.pattern.match(value)
|
||||
if not match:
|
||||
raise ValueError(f'{value!r} can not be interpreted as a number with unit {",".join(self.units)}')
|
||||
number, unit = match.groups()
|
||||
return prefixed.Float(number), unit
|
||||
|
||||
def getnum(self, value):
|
||||
"""parse and return value only"""
|
||||
return self.parse(value)[0]
|
||||
|
||||
|
||||
def format_with_unit(value, unit='', digits=3):
|
||||
return f'{prefixed.Float(value):.{digits}H}{unit}'
|
@ -60,6 +60,7 @@ class HasAccessibles(HasProperties):
|
||||
(so the dispatcher will get notified of changed values)
|
||||
"""
|
||||
isWrapped = False
|
||||
checkedMethods = set()
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls): # pylint: disable=too-many-branches
|
||||
@ -113,8 +114,8 @@ class HasAccessibles(HasProperties):
|
||||
wrapped_name = '_' + cls.__name__
|
||||
for pname, pobj in accessibles.items():
|
||||
# wrap of reading/writing funcs
|
||||
if not isinstance(pobj, Parameter) or pobj.optional:
|
||||
# nothing to do for Commands and optional parameters
|
||||
if not isinstance(pobj, Parameter):
|
||||
# nothing to do for Commands
|
||||
continue
|
||||
|
||||
rname = 'read_' + pname
|
||||
@ -198,15 +199,16 @@ class HasAccessibles(HasProperties):
|
||||
new_wfunc.__module__ = cls.__module__
|
||||
cls.wrappedAttributes[wname] = new_wfunc
|
||||
|
||||
cls.checkedMethods.update(cls.wrappedAttributes)
|
||||
|
||||
# check for programming errors
|
||||
for attrname, func in cls.__dict__.items():
|
||||
for attrname in dir(cls):
|
||||
prefix, _, pname = attrname.partition('_')
|
||||
if not pname:
|
||||
continue
|
||||
if prefix == 'do':
|
||||
raise ProgrammingError(f'{cls.__name__!r}: old style command {attrname!r} not supported anymore')
|
||||
if (prefix in ('read', 'write') and attrname not in cls.wrappedAttributes
|
||||
and not hasattr(func, 'poll')): # may be a handler, which always has a poll attribute
|
||||
if prefix in ('read', 'write') and attrname not in cls.checkedMethods:
|
||||
raise ProgrammingError(f'{cls.__name__}.{attrname} defined, but {pname!r} is no parameter')
|
||||
|
||||
try:
|
||||
@ -323,7 +325,6 @@ class Module(HasAccessibles):
|
||||
|
||||
pollInfo = None
|
||||
triggerPoll = None # trigger event for polls. used on io modules and modules without io
|
||||
__poller = None # the poller thread, if used
|
||||
|
||||
def __init__(self, name, logger, cfgdict, srv):
|
||||
# remember the secnode for interacting with other modules and the
|
||||
@ -389,8 +390,6 @@ class Module(HasAccessibles):
|
||||
accessibles = self.accessibles
|
||||
self.accessibles = {}
|
||||
for aname, aobj in accessibles.items():
|
||||
if aobj.optional:
|
||||
continue
|
||||
# make a copy of the Parameter/Command object
|
||||
aobj = aobj.copy()
|
||||
acfg = cfgdict.pop(aname, None)
|
||||
@ -451,12 +450,9 @@ class Module(HasAccessibles):
|
||||
self.parameters[name] = accessible
|
||||
if isinstance(accessible, Command):
|
||||
self.commands[name] = accessible
|
||||
if cfg is not None:
|
||||
if cfg:
|
||||
try:
|
||||
for propname, propvalue in cfg.items():
|
||||
if propname in {'value', 'default', 'constant'}:
|
||||
# these properties have ValueType(), but should be checked for datatype
|
||||
accessible.datatype(cfg[propname])
|
||||
accessible.setProperty(propname, propvalue)
|
||||
except KeyError:
|
||||
self.errors.append(f"'{name}' has no property '{propname}'")
|
||||
@ -613,7 +609,7 @@ class Module(HasAccessibles):
|
||||
# we do not need self.errors any longer. should we delete it?
|
||||
# del self.errors
|
||||
if self.polledModules:
|
||||
self.__poller = mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
|
||||
mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
|
||||
self.startModuleDone = True
|
||||
|
||||
def initialReads(self):
|
||||
@ -626,28 +622,8 @@ class Module(HasAccessibles):
|
||||
all parameters are polled once
|
||||
"""
|
||||
|
||||
def stopPollThread(self):
|
||||
"""trigger the poll thread to stop
|
||||
|
||||
this is called on shutdown
|
||||
"""
|
||||
if self.__poller:
|
||||
self.polledModules.clear()
|
||||
self.triggerPoll.set()
|
||||
|
||||
def joinPollThread(self, timeout):
|
||||
"""wait for poll thread to finish
|
||||
|
||||
if the wait time exceeds <timeout> seconds, return and log a warning
|
||||
"""
|
||||
if self.__poller:
|
||||
self.stopPollThread()
|
||||
self.__poller.join(timeout)
|
||||
if self.__poller.is_alive():
|
||||
self.log.warning('can not stop poller')
|
||||
|
||||
def shutdownModule(self):
|
||||
"""called when the server shuts down
|
||||
"""called when the sever shuts down
|
||||
|
||||
any cleanup-work should be performed here, like closing threads and
|
||||
saving data.
|
||||
@ -750,12 +726,11 @@ class Module(HasAccessibles):
|
||||
if not polled_modules: # no polls needed - exit thread
|
||||
return
|
||||
to_poll = ()
|
||||
while modules: # modules will be cleared on shutdown
|
||||
while True:
|
||||
now = time.time()
|
||||
wait_time = 999
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo
|
||||
if pinfo:
|
||||
wait_time = min(pinfo.last_main + pinfo.interval - now, wait_time,
|
||||
pinfo.last_slow + mobj.slowinterval - now)
|
||||
if wait_time > 0 and not to_poll:
|
||||
@ -766,7 +741,7 @@ class Module(HasAccessibles):
|
||||
# call doPoll of all modules where due
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo
|
||||
if pinfo and now > pinfo.last_main + pinfo.interval:
|
||||
if now > pinfo.last_main + pinfo.interval:
|
||||
try:
|
||||
pinfo.last_main = (now // pinfo.interval) * pinfo.interval
|
||||
except ZeroDivisionError:
|
||||
@ -786,7 +761,7 @@ class Module(HasAccessibles):
|
||||
# collect due slow polls
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo
|
||||
if pinfo and now > pinfo.last_slow + mobj.slowinterval:
|
||||
if now > pinfo.last_slow + mobj.slowinterval:
|
||||
to_poll.extend(pinfo.polled_parameters)
|
||||
pinfo.last_slow = (now // mobj.slowinterval) * mobj.slowinterval
|
||||
if to_poll:
|
||||
|
@ -47,7 +47,6 @@ class Accessible(HasProperties):
|
||||
"""
|
||||
|
||||
ownProperties = None
|
||||
optional = False
|
||||
|
||||
def init(self, kwds):
|
||||
# do not use self.propertyValues.update here, as no invalid values should be
|
||||
@ -97,8 +96,6 @@ class Accessible(HasProperties):
|
||||
props = []
|
||||
for k, v in sorted(self.propertyValues.items()):
|
||||
props.append(f'{k}={v!r}')
|
||||
if self.optional:
|
||||
props.append('optional=True')
|
||||
return f"{self.__class__.__name__}({', '.join(props)})"
|
||||
|
||||
def fixExport(self):
|
||||
@ -194,9 +191,8 @@ class Parameter(Accessible):
|
||||
readerror = None
|
||||
omit_unchanged_within = 0
|
||||
|
||||
def __init__(self, description=None, datatype=None, inherit=True, optional=False, **kwds):
|
||||
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
|
||||
super().__init__()
|
||||
self.optional = optional
|
||||
if 'poll' in kwds and generalConfig.tolerate_poll_property:
|
||||
kwds.pop('poll')
|
||||
if datatype is None:
|
||||
@ -230,16 +226,10 @@ class Parameter(Accessible):
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
try:
|
||||
return instance.parameters[self.name].value
|
||||
except KeyError:
|
||||
raise ProgrammingError(f'optional parameter {self.name} it is not implemented') from None
|
||||
|
||||
def __set__(self, obj, value):
|
||||
try:
|
||||
obj.announceUpdate(self.name, value)
|
||||
except KeyError:
|
||||
raise ProgrammingError(f'optional parameter {self.name} it is not implemented') from None
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
@ -376,6 +366,9 @@ class Command(Accessible):
|
||||
* True: exported, name automatic.
|
||||
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||
export=False, default=True)
|
||||
# optional = Property(
|
||||
# '[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
|
||||
# export=False, default=False, settable=False)
|
||||
datatype = Property(
|
||||
"datatype of the command, auto generated from 'argument' and 'result'",
|
||||
DataTypeType(), extname='datainfo', export='always')
|
||||
@ -391,9 +384,8 @@ class Command(Accessible):
|
||||
|
||||
func = None
|
||||
|
||||
def __init__(self, argument=False, *, result=None, inherit=True, optional=False, **kwds):
|
||||
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
||||
super().__init__()
|
||||
self.optional = optional
|
||||
if 'datatype' in kwds:
|
||||
# self.init will complain about invalid keywords except 'datatype', as this is a property
|
||||
raise ProgrammingError("Command() got an invalid keyword 'datatype'")
|
||||
@ -419,8 +411,8 @@ class Command(Accessible):
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
if self.func is None and not self.optional:
|
||||
raise ProgrammingError(f'Command {owner.__name__}.{name} must be optional or used as a method decorator')
|
||||
if self.func is None:
|
||||
raise ProgrammingError(f'Command {owner.__name__}.{name} must be used as a method decorator')
|
||||
|
||||
self.fixExport()
|
||||
self.datatype = CommandType(self.argument, self.result)
|
||||
|
@ -102,6 +102,7 @@ class Handler:
|
||||
"""create the wrapped read_* or write_* methods"""
|
||||
# at this point, this 'method_names' entry is no longer used -> delete
|
||||
self.method_names.discard((self.func.__module__, self.func.__qualname__))
|
||||
owner.checkedMethods.add(name)
|
||||
for key in self.keys:
|
||||
wrapped = self.wrap(key)
|
||||
method_name = self.prefix + key
|
||||
|
@ -19,7 +19,6 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import time
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
|
||||
@ -256,15 +255,6 @@ class SecNode:
|
||||
|
||||
def shutdown_modules(self):
|
||||
"""Call 'shutdownModule' for all modules."""
|
||||
# stop pollers
|
||||
for mod in self.modules.values():
|
||||
mod.stopPollThread()
|
||||
# do not yet join here, as we want to wait in parallel
|
||||
now = time.time()
|
||||
deadline = now + 0.5 # should be long enough for most read functions to finish
|
||||
for mod in self.modules.values():
|
||||
mod.joinPollThread(max(0, deadline - now))
|
||||
now = time.time()
|
||||
for name in self._getSortedModules():
|
||||
self.modules[name].shutdownModule()
|
||||
|
||||
|
@ -142,5 +142,4 @@ class SimDrivable(SimReadable, Drivable):
|
||||
|
||||
@Command
|
||||
def stop(self):
|
||||
"""set target to value"""
|
||||
self.target = self.value
|
||||
|
@ -215,10 +215,7 @@ class HasStates:
|
||||
self.read_status()
|
||||
if fast_poll:
|
||||
sm.reset_fast_poll = True
|
||||
if fast_poll is True:
|
||||
self.setFastPoll(True)
|
||||
else:
|
||||
self.setFastPoll(True, fast_poll)
|
||||
self.pollInfo.trigger(True) # trigger poller
|
||||
|
||||
def stop_machine(self, stopped_status=(IDLE, 'stopped')):
|
||||
|
@ -47,28 +47,6 @@ ADQ_TRANSFER_MODE_NORMAL = 0x00
|
||||
ADQ_CHANNELS_MASK = 0x3
|
||||
|
||||
GHz = 1e9
|
||||
RMS_TO_VPP = 2 * np.sqrt(2)
|
||||
|
||||
|
||||
class Timer:
|
||||
def __init__(self):
|
||||
self.data = [(time.time(), 'start')]
|
||||
|
||||
def __call__(self, text=''):
|
||||
now = time.time()
|
||||
prev = self.data[-1][0]
|
||||
self.data.append((now, text))
|
||||
return now - prev
|
||||
|
||||
def summary(self):
|
||||
return ' '.join(f'{txt} {tim:.3f}' for tim, txt in self.data[1:])
|
||||
|
||||
def show(self):
|
||||
first = prev = self.data[0][0]
|
||||
print('---', first)
|
||||
for tim, txt in self.data[1:]:
|
||||
print(f'{(tim - first) * 1000:9.3f} {(tim - prev) * 1000:9.3f} ms {txt}')
|
||||
prev = tim
|
||||
|
||||
|
||||
class Adq:
|
||||
@ -80,8 +58,12 @@ class Adq:
|
||||
bw_cutoff = 10E6
|
||||
trigger = EXT_TRIG_1
|
||||
adq_num = 1
|
||||
UNDEFINED = -1
|
||||
IDLE = 0
|
||||
BUSY = 1
|
||||
READY = 2
|
||||
status = UNDEFINED
|
||||
data = None
|
||||
busy = False
|
||||
|
||||
def __init__(self):
|
||||
global ADQAPI
|
||||
@ -102,24 +84,23 @@ class Adq:
|
||||
ADQAPI.ADQControlUnit_FindDevices(self.adq_cu)
|
||||
n_of_adq = ADQAPI.ADQControlUnit_NofADQ(self.adq_cu)
|
||||
if n_of_adq != 1:
|
||||
print('number of ADQs must be 1, not %d' % n_of_adq)
|
||||
print('it seems the ADQ was not properly closed')
|
||||
print('please try again or reboot')
|
||||
sys.exit(0)
|
||||
atexit.register(self.deletecu)
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
raise RuntimeError('number of ADQs must be 1, not %d' % n_of_adq)
|
||||
|
||||
rev = ADQAPI.ADQ_GetRevision(self.adq_cu, self.adq_num)
|
||||
revision = ct.cast(rev, ct.POINTER(ct.c_int))
|
||||
out = [f'Connected to ADQ #1, FPGA Revision: {revision[0]}']
|
||||
print('\nConnected to ADQ #1')
|
||||
# Print revision information
|
||||
print('FPGA Revision: {}'.format(revision[0]))
|
||||
if revision[1]:
|
||||
out.append('Local copy')
|
||||
print('Local copy')
|
||||
else:
|
||||
print('SVN Managed')
|
||||
if revision[2]:
|
||||
out.append('SVN Managed - Mixed Revision')
|
||||
print('Mixed Revision')
|
||||
else:
|
||||
out.append('SVN Updated')
|
||||
print(', '.join(out))
|
||||
print('SVN Updated')
|
||||
print('')
|
||||
|
||||
ADQAPI.ADQ_SetClockSource(self.adq_cu, self.adq_num, ADQ_CLOCK_EXT_REF)
|
||||
|
||||
##########################
|
||||
@ -147,11 +128,12 @@ class Adq:
|
||||
raise RuntimeError('ADQ_SetLvlTrigEdge failed.')
|
||||
# if not ADQAPI.ADQ_SetTriggerThresholdVoltage(self.adq_cu, self.adq_num, trigger, ct.c_double(0.2)):
|
||||
# raise RuntimeError('SetTriggerThresholdVoltage failed.')
|
||||
# proabably the folloiwng is wrong.
|
||||
# print("CHANNEL:" + str(ct.c_int(ADQAPI.ADQ_GetLvlTrigChannel(self.adq_cu, self.adq_num))))
|
||||
print("CHANNEL:"+str(ct.c_int(ADQAPI.ADQ_GetLvlTrigChannel(self.adq_cu, self.adq_num))))
|
||||
atexit.register(self.deletecu)
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
|
||||
def init(self, samples_per_record=None, number_of_records=None):
|
||||
"""initialize dimensions and store result object"""
|
||||
"""initialize dimensions"""
|
||||
if samples_per_record:
|
||||
self.samples_per_record = samples_per_record
|
||||
if number_of_records:
|
||||
@ -163,18 +145,13 @@ class Adq:
|
||||
bufp.contents = (ct.c_int16 * self.samples_per_record * self.number_of_records)()
|
||||
|
||||
def deletecu(self):
|
||||
cu = self.__dict__.pop('adq_cu', None)
|
||||
if cu is None:
|
||||
return
|
||||
print('shut down ADQ')
|
||||
# Only disarm trigger after data is collected
|
||||
ADQAPI.ADQ_DisarmTrigger(cu, self.adq_num)
|
||||
ADQAPI.ADQ_MultiRecordClose(cu, self.adq_num)
|
||||
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
|
||||
ADQAPI.ADQ_MultiRecordClose(self.adq_cu, self.adq_num)
|
||||
# Delete ADQControlunit
|
||||
ADQAPI.DeleteADQControlUnit(cu)
|
||||
print('ADQ closed')
|
||||
ADQAPI.DeleteADQControlUnit(self.adq_cu)
|
||||
|
||||
def start(self, data):
|
||||
def start(self):
|
||||
# Start acquisition
|
||||
ADQAPI.ADQ_MultiRecordSetup(self.adq_cu, self.adq_num,
|
||||
self.number_of_records,
|
||||
@ -182,36 +159,35 @@ class Adq:
|
||||
|
||||
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
|
||||
ADQAPI.ADQ_ArmTrigger(self.adq_cu, self.adq_num)
|
||||
self.data = data
|
||||
|
||||
def get_data(self):
|
||||
"""get new data if available"""
|
||||
ready = False
|
||||
data = self.data
|
||||
if not data:
|
||||
self.busy = False
|
||||
return None # no new data
|
||||
self.status = self.BUSY
|
||||
|
||||
def get_status(self):
|
||||
"""check if ADQ card is busy"""
|
||||
if self.status == self.BUSY:
|
||||
if ADQAPI.ADQ_GetAcquiredAll(self.adq_cu, self.adq_num):
|
||||
ready = True
|
||||
data.timer('ready')
|
||||
self.status = self.READY
|
||||
else:
|
||||
if self.trigger == SW_TRIG:
|
||||
ADQAPI.ADQ_SWTrig(self.adq_cu, self.adq_num)
|
||||
if not ready:
|
||||
self.busy = True
|
||||
return None
|
||||
self.data = None
|
||||
t = time.time()
|
||||
return self.status
|
||||
|
||||
def get_data(self, dataclass, **kwds):
|
||||
"""when ready, get raw data from card, else return cached data
|
||||
|
||||
return
|
||||
"""
|
||||
if self.get_status() == self.READY:
|
||||
# Get data from ADQ
|
||||
if not ADQAPI.ADQ_GetData(
|
||||
self.adq_cu, self.adq_num, self.target_buffers,
|
||||
if not ADQAPI.ADQ_GetData(self.adq_cu, self.adq_num, self.target_buffers,
|
||||
self.samples_per_record * self.number_of_records, 2,
|
||||
0, self.number_of_records, ADQ_CHANNELS_MASK,
|
||||
0, self.samples_per_record, ADQ_TRANSFER_MODE_NORMAL):
|
||||
raise RuntimeError('no success from ADQ_GetDATA')
|
||||
data.retrieve(self)
|
||||
return data
|
||||
self.data = dataclass(self, **kwds)
|
||||
self.status = self.IDLE
|
||||
if self.status == self.UNDEFINED:
|
||||
raise RuntimeError('no data available yet')
|
||||
return self.data
|
||||
|
||||
|
||||
class PEdata:
|
||||
@ -219,20 +195,12 @@ class PEdata:
|
||||
self.sample_rate = adq.sample_rate
|
||||
self.samp_freq = self.sample_rate / GHz
|
||||
self.number_of_records = adq.number_of_records
|
||||
self.timer = Timer()
|
||||
|
||||
def retrieve(self, adq):
|
||||
data = []
|
||||
rawsignal = []
|
||||
for ch in range(2):
|
||||
onedim = np.frombuffer(adq.target_buffers[ch].contents, dtype=np.int16)
|
||||
rawsignal.append(onedim[:adq.samples_per_record])
|
||||
# convert 16 bit int to a value in the range -1 .. 1
|
||||
data.append(onedim.reshape(adq.number_of_records, adq.samples_per_record) / float(2 ** 15))
|
||||
data.append(onedim.reshape(adq.number_of_records, adq.samples_per_record) / float(2**14)) # 14 bits ADC
|
||||
# Now this is an array with all records, but the time is artificial
|
||||
self.data = data
|
||||
self.rawsignal = rawsignal
|
||||
self.timer('retrieved')
|
||||
|
||||
def sinW(self, sig, freq, ti, tf):
|
||||
# sig: signal array
|
||||
@ -285,113 +253,61 @@ class PEdata:
|
||||
|
||||
def gates_and_curves(self, freq, pulse, roi, bw_cutoff):
|
||||
"""return iq values of rois and prepare plottable curves for iq"""
|
||||
self.timer('gates')
|
||||
try:
|
||||
self.ndecimate = int(round(self.sample_rate / freq))
|
||||
except TypeError as e:
|
||||
raise TypeError(f'{self.sample_rate}/{freq} {e}')
|
||||
# times = []
|
||||
# times.append(('aviq', time.time()))
|
||||
iq = self.averageiq(self.data, freq / GHz, *pulse)
|
||||
self.timer('aviq')
|
||||
# times.append(('filtro', time.time()))
|
||||
iqf = self.filtro(iq, bw_cutoff)
|
||||
self.timer('filtro')
|
||||
m = max(1, len(iqf[0]) // self.ndecimate)
|
||||
m = len(iqf[0]) // self.ndecimate
|
||||
ll = m * self.ndecimate
|
||||
iqf = [iqfx[0:ll] for iqfx in iqf]
|
||||
self.timer('iqf')
|
||||
# times.append(('iqdec', time.time()))
|
||||
iqd = np.average(np.resize(iqf, (2, m, self.ndecimate)), axis=2)
|
||||
self.timer('avg')
|
||||
t_axis = np.arange(m) * self.ndecimate / self.samp_freq
|
||||
pulsig = np.abs(self.data[0][0])
|
||||
self.timer('pulsig')
|
||||
# times.append(('pulsig', time.time()))
|
||||
pulsig = np.average(np.resize(pulsig, (m, self.ndecimate)), axis=1)
|
||||
result = ([self.box(iqf, *r) for r in roi], # gates
|
||||
(t_axis, iqd[0], iqd[1], pulsig)) # curves
|
||||
self.timer('result')
|
||||
# self.timer.show()
|
||||
# ns = len(self.rawsignal[0]) * self.number_of_records
|
||||
# print(f'{ns} {ns / 2e6} ms')
|
||||
return result
|
||||
|
||||
|
||||
class Namespace:
|
||||
"""holds channel or other data"""
|
||||
def __init__(self, **kwds):
|
||||
self.__dict__.update(**kwds)
|
||||
self.curves = (t_axis, iqd[0], iqd[1], pulsig)
|
||||
# print(times)
|
||||
return [self.box(iqf, *r) for r in roi]
|
||||
|
||||
|
||||
class RUSdata:
|
||||
def __init__(self, adq, freq, periods, delay_samples):
|
||||
def __init__(self, adq, freq, periods):
|
||||
self.sample_rate = adq.sample_rate
|
||||
self.freq = freq
|
||||
self.periods = periods
|
||||
self.delay_samples = delay_samples
|
||||
self.samples_per_record = adq.samples_per_record
|
||||
self.inp = Namespace(idx=0, name='input')
|
||||
self.out = Namespace(idx=1, name='output')
|
||||
self.channels = (self.inp, self.out)
|
||||
self.timer = Timer()
|
||||
input_signal = np.frombuffer(adq.target_buffers[0].contents, dtype=np.int16)
|
||||
output_signal = np.frombuffer(adq.target_buffers[1].contents, dtype=np.int16)
|
||||
complex_sinusoid = np.exp(1j * 2 * np.pi * self.freq / self.sample_rate * np.arange(len(input_signal)))
|
||||
self.input_mixed = input_signal * complex_sinusoid
|
||||
self.output_mixed = output_signal * complex_sinusoid
|
||||
self.input_mean = self.input_mixed.mean()
|
||||
self.output_mean = self.output_mixed.mean()
|
||||
self.iq = self.output_mean / self.input_mean
|
||||
|
||||
def retrieve(self, adq):
|
||||
self.timer('start retrieve')
|
||||
npts = self.samples_per_record - self.delay_samples
|
||||
nbin = max(1, npts // (self.periods * 60)) # for performance reasons, do the binning first
|
||||
nreduced = npts // nbin
|
||||
ft = 2 * np.pi * self.freq * nbin / self.sample_rate * np.arange(nreduced)
|
||||
self.timer('create time axis')
|
||||
# complex_sinusoid = np.exp(1j * ft) # do not use this, below is 33 % faster
|
||||
complex_sinusoid = 1j * np.sin(ft) + np.cos(ft)
|
||||
self.timer('sinusoid')
|
||||
def get_reduced(self, mixed):
|
||||
"""get reduced array and normalize"""
|
||||
nper = self.samples_per_record // self.periods
|
||||
mean = mixed.mean()
|
||||
return mixed[:self.period * nper].reshape((-1, nper)).mean(axis=0) / mean
|
||||
|
||||
rawsignal = [] # for raw plot
|
||||
for chan in self.channels: # looping over input and output
|
||||
# although the ADC is only 14 bit it is represented as unsigend 16 bit numbers,
|
||||
# and due to some calculations (calibration) the last 2 bits are not zero
|
||||
beg = self.delay_samples
|
||||
isignal = np.frombuffer(adq.target_buffers[chan.idx].contents, dtype=np.int16)[beg:beg+nreduced * nbin]
|
||||
self.timer('isignal')
|
||||
reduced = isignal.reshape((-1, nbin)).mean(axis=1) # this converts also int16 to float
|
||||
self.timer('reduce')
|
||||
rawsignal.append(reduced)
|
||||
chan.signal = signal = reduced * 2 ** -16 # in V -> peak to peak 1 V ~ +- 0.5 V
|
||||
self.timer('divide')
|
||||
# calculate RMS * sqrt(2) -> peak sinus amplitude.
|
||||
# may be higher than the input range by a factor 1.4 when heavily clipped
|
||||
chan.amplitude = np.sqrt((signal ** 2).mean()) * RMS_TO_VPP
|
||||
self.timer('amp')
|
||||
chan.mixed = signal * complex_sinusoid
|
||||
self.timer('mix')
|
||||
chan.mean = chan.mixed.mean()
|
||||
self.timer('mean')
|
||||
self.rawsignal = rawsignal
|
||||
if self.inp.mean:
|
||||
self.iq = self.out.mean / self.inp.mean
|
||||
else:
|
||||
self.iq = 0
|
||||
|
||||
def get_quality(self):
|
||||
def calc_quality(self):
|
||||
"""get signal quality info
|
||||
|
||||
quality info (small values indicate good quality):
|
||||
- input_stddev:
|
||||
- input_std and output_std:
|
||||
the imaginary part indicates deviations in phase
|
||||
the real part indicates deviations in amplitude
|
||||
- output_slope:
|
||||
- input_slope and output_slope:
|
||||
the imaginary part indicates a turning phase (rad/sec)
|
||||
the real part indicates changes in amplitude (0.01 ~= 1%/sec)
|
||||
"""
|
||||
self.timer('get_quality')
|
||||
npts = len(self.channels[0].signal)
|
||||
nper = npts // self.periods
|
||||
for chan in self.channels:
|
||||
mean = chan.mixed.mean()
|
||||
chan.reduced = chan.mixed[:self.periods * nper].reshape((-1, nper)).mean(axis=1) / mean
|
||||
reduced = self.get_reduced(self.input_mixed)
|
||||
self.input_stdev = reduced.std()
|
||||
|
||||
timeaxis = np.arange(len(self.out.reduced)) * self.sample_rate / self.freq
|
||||
result = Namespace(
|
||||
input_stddev=self.inp.reduced.std(),
|
||||
output_slope=np.polyfit(timeaxis, self.out.reduced, 1)[0])
|
||||
self.timer('got_quality')
|
||||
self.timer.show()
|
||||
ns = len(self.rawsignal[0])
|
||||
print(f'{ns} {ns / 2e6} ms')
|
||||
return result
|
||||
reduced = self.get_reduced(self.output_mixed)
|
||||
timeaxis = np.arange(len(reduced)) * self.sample_rate / self.freq
|
||||
self.output_slope = np.polyfit(timeaxis, reduced, 1)[0]
|
||||
|
@ -1,131 +0,0 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import time
|
||||
from frappy.core import Attached, Readable, Writable, Parameter, Command, \
|
||||
IDLE, BUSY, DISABLED, ERROR
|
||||
from frappy.datatypes import FloatRange, StatusType, TupleOf, EnumType
|
||||
from frappy.states import HasStates, Retry, status_code
|
||||
|
||||
|
||||
class AutoFill(HasStates, Readable):
|
||||
level = Attached(Readable)
|
||||
valve = Attached(Writable)
|
||||
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
||||
mode = Parameter('auto mode', EnumType(disabled=0, auto=30), readonly=False)
|
||||
|
||||
fill_level = Parameter('low threshold triggering start filling',
|
||||
FloatRange(unit='%'), readonly=False)
|
||||
full_level = Parameter('high threshold triggering stop filling',
|
||||
FloatRange(unit='%'), readonly=False)
|
||||
fill_minutes_range = Parameter('range of possible fill rate',
|
||||
TupleOf(FloatRange(unit='min'), FloatRange(unit='min')),
|
||||
readonly=False)
|
||||
hold_hours_range = Parameter('range of possible consumption rate',
|
||||
TupleOf(FloatRange(unit='h'), FloatRange(unit='h')),
|
||||
readonly=False)
|
||||
fill_delay = Parameter('delay for cooling the transfer line',
|
||||
FloatRange(unit='min'), readonly=False)
|
||||
|
||||
def read_status(self):
|
||||
if self.mode == 'DISABLED':
|
||||
return DISABLED, ''
|
||||
vstatus = self.valve.status
|
||||
if vstatus[0] // 100 != IDLE // 100:
|
||||
self.stop_machine(vstatus)
|
||||
return vstatus
|
||||
status = self.level.read_status(self)
|
||||
if status[0] // 100 == IDLE // 100:
|
||||
return HasStates.read_status(self)
|
||||
self.stop_machine(status)
|
||||
return status
|
||||
|
||||
def write_mode(self, mode):
|
||||
if mode == 'DISABLED':
|
||||
self.stop_machine((DISABLED, ''))
|
||||
elif mode == 'AUTO':
|
||||
self.start_machine(self.watching)
|
||||
return mode
|
||||
|
||||
@status_code(BUSY)
|
||||
def watching(self, state):
|
||||
if state.init:
|
||||
self.valve.write_target(0)
|
||||
delta = state.delta(10)
|
||||
raw = self.level.value
|
||||
if raw > self.value:
|
||||
self.value -= delta / (3600 * self.hold_hours_range[1])
|
||||
elif raw < self.value:
|
||||
self.value -= delta / (3600 * self.hold_hours_range[0])
|
||||
else:
|
||||
self.value = raw
|
||||
if self.value < self.fill_level:
|
||||
return self.precooling
|
||||
return Retry
|
||||
|
||||
@status_code(BUSY)
|
||||
def precooling(self, state):
|
||||
if state.init:
|
||||
state.fillstart = state.now
|
||||
self.valve.write_target(1)
|
||||
delta = state.delta(1)
|
||||
raw = self.level.value
|
||||
if raw > self.value:
|
||||
self.value += delta / (60 * self.fill_minutes_range[0])
|
||||
elif raw < self.value:
|
||||
self.value -= delta / (60 * self.fill_minutes_range[0])
|
||||
else:
|
||||
self.value = raw
|
||||
if self.value > self.full_level:
|
||||
return self.watching
|
||||
if state.now > state.fillstart + self.fill_delay * 60:
|
||||
return self.filling
|
||||
return Retry
|
||||
|
||||
@status_code(BUSY)
|
||||
def filling(self, state):
|
||||
delta = state.delta(1)
|
||||
raw = self.level.value
|
||||
if raw > self.value:
|
||||
self.value += delta / (60 * self.fill_minutes_range[0])
|
||||
elif raw < self.value:
|
||||
self.value += delta / (60 * self.fill_minutes_range[1])
|
||||
else:
|
||||
self.value = raw
|
||||
if self.value > self.full_level:
|
||||
return self.watching
|
||||
return Retry
|
||||
|
||||
def on_cleanup(self, state):
|
||||
try:
|
||||
self.valve.write_target(0)
|
||||
except Exception:
|
||||
pass
|
||||
super().on_cleanup()
|
||||
|
||||
@Command()
|
||||
def fill(self):
|
||||
self.mode = 'AUTO'
|
||||
self.start_machine(self.precooling, fillstart=time.time())
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
self.start_machine(self.watching)
|
@ -1,128 +0,0 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# M. Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from frappy.core import Attached, Command, EnumType, FloatRange, \
|
||||
Drivable, Parameter, BUSY, IDLE, ERROR
|
||||
|
||||
|
||||
class Valve(Drivable):
|
||||
motor = Attached(Drivable) # refers to motor module
|
||||
value = Parameter('valve state',
|
||||
EnumType(closed=0, open=1, undefined=2),
|
||||
default=2)
|
||||
status = Parameter() # inherit properties from Drivable
|
||||
target = Parameter('valve target',
|
||||
EnumType(closed=0, open=1),
|
||||
readonly=False)
|
||||
# TODO: convert to properties after tests
|
||||
open_pos = Parameter('target position for open state', FloatRange(), readonly=False, default=80)
|
||||
mid_pos = Parameter('position for changing speed', FloatRange(), readonly=False, default=5)
|
||||
fast_speed = Parameter('normal speed', FloatRange(), readonly=False, default=40)
|
||||
slow_speed = Parameter('reduced speed', FloatRange(), readonly=False, default=10)
|
||||
__motor_target = None
|
||||
__status = IDLE, ''
|
||||
__value = 'undefined'
|
||||
__drivestate = 0 # 2 when driving to intermediate target or on retry, 1 when driving to final target, 0 when idle
|
||||
|
||||
def doPoll(self):
|
||||
mot = self.motor
|
||||
motpos = mot.read_value()
|
||||
scode, stext = mot.read_status()
|
||||
drivestate = self.__drivestate
|
||||
if scode >= ERROR:
|
||||
if self.__drivestate and self.__remaining_tries > 0:
|
||||
drivestate = 2
|
||||
self.__remaining_tries -= 1
|
||||
mot.reset()
|
||||
mot.write_speed(self.slow_speed)
|
||||
self.__status = BUSY, f'retry {self._action}'
|
||||
else:
|
||||
self.__status = ERROR, f'valve motor: {stext}'
|
||||
elif scode < BUSY:
|
||||
if self.__motor_target is not None and mot.target != self.__motor_target:
|
||||
self.__status = ERROR, 'motor was driven directly'
|
||||
elif drivestate == 2:
|
||||
self.goto(self.target)
|
||||
drivestate = 1
|
||||
else:
|
||||
if -3 < motpos < 3:
|
||||
self.__value = 'closed'
|
||||
self.__status = IDLE, ''
|
||||
elif self.open_pos * 0.5 < motpos < self.open_pos * 1.5:
|
||||
self.__value = 'open'
|
||||
self.__status = IDLE, ''
|
||||
else:
|
||||
self.__status = ERROR, 'undefined'
|
||||
if self.__drivestate and not self.isBusy(self.__status):
|
||||
drivestate = 0
|
||||
self.__motor_target = None
|
||||
self.setFastPoll(False)
|
||||
self.__drivestate = drivestate
|
||||
self.read_status()
|
||||
self.read_value()
|
||||
|
||||
def read_status(self):
|
||||
return self.__status
|
||||
|
||||
def read_value(self):
|
||||
if self.read_status()[0] >= BUSY:
|
||||
return 'undefined'
|
||||
return self.__value
|
||||
|
||||
def goto(self, target):
|
||||
"""go to open, closed or intermediate position
|
||||
|
||||
the intermediate position is targeted when a speed change is needed
|
||||
|
||||
return 2 when a retry is needed, 1 else
|
||||
"""
|
||||
mot = self.motor
|
||||
if target: # 'open'
|
||||
self._action = 'opening'
|
||||
if True or mot.value > self.mid_pos:
|
||||
mot.write_speed(self.fast_speed)
|
||||
self.__motor_target = mot.write_target(self.open_pos)
|
||||
return 1
|
||||
mot.write_speed(self.slow_speed)
|
||||
self.__motor_target = mot.write_target(self.mid_pos)
|
||||
return 2
|
||||
self._action = 'closing'
|
||||
if mot.value > self.mid_pos * 2:
|
||||
mot.write_speed(self.fast_speed)
|
||||
self.__motor_target = mot.write_target(self.mid_pos)
|
||||
return 2
|
||||
mot.write_speed(self.slow_speed)
|
||||
self.__motor_target = mot.write_target(0)
|
||||
return 1
|
||||
|
||||
def write_target(self, target):
|
||||
self.__remaining_tries = 5
|
||||
self.__drivestate = self.goto(target)
|
||||
self.__status = BUSY, self._action
|
||||
self.read_status()
|
||||
self.read_value()
|
||||
self.setFastPoll(True)
|
||||
|
||||
@Command() # python decorator to mark it as a command
|
||||
def stop(self):
|
||||
"""stop the motor -> value might get undefined"""
|
||||
self.__drivestate = 0
|
||||
self.motor.stop()
|
@ -22,7 +22,6 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from os.path import basename, dirname, exists, join
|
||||
|
||||
import numpy as np
|
||||
@ -32,22 +31,13 @@ from scipy.interpolate import PchipInterpolator, CubicSpline, PPoly # pylint: d
|
||||
from frappy.errors import ProgrammingError, RangeError
|
||||
from frappy.lib import clamp
|
||||
|
||||
|
||||
def identity(x):
|
||||
return x
|
||||
|
||||
|
||||
def exp10(x):
|
||||
return 10 ** np.array(x)
|
||||
|
||||
|
||||
to_scale = {
|
||||
'lin': identity,
|
||||
'log': np.log10,
|
||||
'lin': lambda x: x,
|
||||
'log': lambda x: np.log10(x),
|
||||
}
|
||||
from_scale = {
|
||||
'lin': identity,
|
||||
'log': exp10,
|
||||
'lin': lambda x: x,
|
||||
'log': lambda x: 10 ** np.array(x),
|
||||
}
|
||||
TYPES = [ # lakeshore type, inp-type, loglog
|
||||
('DT', 'si', False), # Si diode
|
||||
@ -65,7 +55,7 @@ TYPES = [ # lakeshore type, inp-type, loglog
|
||||
|
||||
OPTION_TYPE = {
|
||||
'loglog': 0, # boolean
|
||||
'extrange': 2, # tuple(min T, max T) for extrapolation
|
||||
'extrange': 2, # tuple(min T, max T for extrapolation
|
||||
'calibrange': 2, # tuple(min T, max T)
|
||||
}
|
||||
|
||||
@ -232,6 +222,14 @@ PARSERS = {
|
||||
}
|
||||
|
||||
|
||||
def check(x, y, islog):
|
||||
# check interpolation error
|
||||
yi = y[:-2] + (x[1:-1] - x[:-2]) * (y[2:] - y[:-2]) / (x[2:] - x[:-2])
|
||||
if islog:
|
||||
return sum((yi - y[1:-1]) ** 2)
|
||||
return sum((np.log10(yi) - np.log10(y[1:-1])) ** 2)
|
||||
|
||||
|
||||
def get_curve(newscale, curves):
|
||||
"""get curve from curve cache (converts not existing ones)
|
||||
|
||||
@ -249,7 +247,6 @@ def get_curve(newscale, curves):
|
||||
class CalCurve(HasOptions):
|
||||
EXTRAPOLATION_AMOUNT = 0.1
|
||||
MAX_EXTRAPOLATION_FACTOR = 2
|
||||
filename = None # calibration file
|
||||
|
||||
def __init__(self, calibspec=None, *, x=None, y=None, cubic_spline=True, **options):
|
||||
"""calibration curve
|
||||
@ -260,7 +257,7 @@ class CalCurve(HasOptions):
|
||||
[<full path> | <name>][,<key>=<value> ...]
|
||||
for <key>/<value> as in parser arguments
|
||||
:param x, y: x and y arrays (given instead of calibspec)
|
||||
:param cubic_spline: set to False for always using Pchip interpolation
|
||||
:param cubic_split: set to False for always using Pchip interpolation
|
||||
:param options: options for parsers
|
||||
"""
|
||||
self.options = options
|
||||
@ -268,31 +265,26 @@ class CalCurve(HasOptions):
|
||||
parser = StdParser()
|
||||
parser.xdata = x
|
||||
parser.ydata = y
|
||||
self.calibname = 'custom'
|
||||
else:
|
||||
if x or y:
|
||||
raise ProgrammingError('can not give both calibspec and x,y ')
|
||||
sensopt = calibspec.split(',')
|
||||
calibname = sensopt.pop(0)
|
||||
self.calibname = basename(calibname)
|
||||
head, dot, ext = self.calibname.rpartition('.')
|
||||
if dot:
|
||||
self.calibname = head
|
||||
_, dot, ext = basename(calibname).rpartition('.')
|
||||
kind = None
|
||||
pathlist = [Path(p.strip()) for p in os.environ.get('FRAPPY_CALIB_PATH', '').split(':')]
|
||||
pathlist.append(Path(dirname(__file__)) / 'calcurves')
|
||||
pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(':')
|
||||
pathlist.append(join(dirname(__file__), 'calcurves'))
|
||||
for path in pathlist:
|
||||
# first try without adding kind
|
||||
filename = path / calibname
|
||||
if filename.exists():
|
||||
filename = join(path.strip(), calibname)
|
||||
if exists(filename):
|
||||
kind = ext if dot else None
|
||||
break
|
||||
# then try adding all kinds as extension
|
||||
for nam in calibname, calibname.upper(), calibname.lower():
|
||||
for kind in PARSERS:
|
||||
filename = path / f'{nam}.{kind}'
|
||||
filename = join(path.strip(), '%s.%s' % (nam, kind))
|
||||
if exists(filename):
|
||||
self.filename = filename
|
||||
break
|
||||
else:
|
||||
continue
|
||||
@ -336,7 +328,6 @@ class CalCurve(HasOptions):
|
||||
not_incr_idx = np.argwhere(x[1:] <= x[:-1])
|
||||
if len(not_incr_idx):
|
||||
raise RangeError('x not monotonic at x=%.4g' % x[not_incr_idx[0]])
|
||||
self.ptc = y[-1] > y[0]
|
||||
|
||||
self.x = {parser.xscale: x}
|
||||
self.y = {parser.yscale: y}
|
||||
@ -353,7 +344,8 @@ class CalCurve(HasOptions):
|
||||
self.convert_x = to_scale[newscale]
|
||||
self.convert_y = from_scale[newscale]
|
||||
self.calibrange = self.options.get('calibrange')
|
||||
self.extra_points = (0, 0)
|
||||
dirty = set()
|
||||
self.extra_points = False
|
||||
self.cutted = False
|
||||
if self.calibrange:
|
||||
self.calibrange = sorted(self.calibrange)
|
||||
@ -379,6 +371,7 @@ class CalCurve(HasOptions):
|
||||
self.y = {newscale: y}
|
||||
ibeg = 0
|
||||
iend = len(x)
|
||||
dirty.add('xy')
|
||||
else:
|
||||
self.extra_points = ibeg, len(x) - iend
|
||||
else:
|
||||
@ -500,48 +493,13 @@ class CalCurve(HasOptions):
|
||||
except IndexError:
|
||||
return defaultx
|
||||
|
||||
def interpolation_error(self, x0, x1, y0, y1, funx, funy, relerror, return_tuple=False):
|
||||
"""calcualte interpoaltion error
|
||||
|
||||
:param x0: start of interval
|
||||
:param x1: end of interval
|
||||
:param y0: y at start of interval
|
||||
:param y1: y at end of interval
|
||||
:param funx: function to convert x from exported scale to internal scale
|
||||
:param funy: function to convert y from internal scale to exported scale
|
||||
:param relerror: True when the exported y scale is linear
|
||||
:param return_tuple: True: return interpolation error as a tuple with two values
|
||||
(without and with 3 additional points)
|
||||
False: return one value without additional points
|
||||
:return: relative deviation
|
||||
"""
|
||||
xspace = np.linspace(x0, x1, 9)
|
||||
x = funx(xspace)
|
||||
yr = self.spline(x)
|
||||
yspline = funy(yr)
|
||||
yinterp = y0 + np.linspace(0.0, y1 - y0, 9)
|
||||
# difference between spline (at m points) and liner interpolation
|
||||
diff = np.abs(yspline - yinterp)
|
||||
# estimate of interpolation error with 4 sections:
|
||||
# difference between spline (at m points) and linear interpolation between neighboring points
|
||||
|
||||
if relerror:
|
||||
fact = 2 / (np.abs(y0) + np.abs(y1)) # division by zero can not happen, as y0 and y1 can not both be zero
|
||||
else:
|
||||
fact = 2.3 # difference is in log10 -> multiply by 1 / log10(e)
|
||||
result = np.max(diff, axis=0) * fact
|
||||
if return_tuple:
|
||||
diff2 = np.abs(0.5 * (yspline[:-2:2] + yspline[2::2]) - funy(yr[1:-1:2]))
|
||||
return result, np.max(diff2, axis=0) * fact
|
||||
return result
|
||||
|
||||
def export(self, logformat=False, nmax=199, yrange=None, extrapolate=True, xlimits=None, nmin=199):
|
||||
def export(self, logformat=False, nmax=199, yrange=None, extrapolate=True, xlimits=None):
|
||||
"""export curve for downloading to hardware
|
||||
|
||||
:param nmax: max number of points. if the number of given points is bigger,
|
||||
the points with the lowest interpolation error are omitted
|
||||
:param logformat: a list with two elements of None, True or False for x and y
|
||||
True: use log, False: use lin, None: use log if self.loglog
|
||||
:param logformat: a list with two elements of None, True or False
|
||||
True: use log, False: use line, None: use log if self.loglog
|
||||
values None are replaced with the effectively used format
|
||||
False / True are replaced by [False, False] / [True, True]
|
||||
default is False
|
||||
@ -549,26 +507,25 @@ class CalCurve(HasOptions):
|
||||
:param extrapolate: a flag indicating whether the curves should be extrapolated
|
||||
to the preset extrapolation range
|
||||
:param xlimits: max x range
|
||||
:param nmin: minimum number of points
|
||||
:return: numpy array with 2 dimensions returning the curve
|
||||
"""
|
||||
|
||||
if logformat in (True, False):
|
||||
logformat = (logformat, logformat)
|
||||
self.logformat = list(logformat)
|
||||
logformat = [logformat, logformat]
|
||||
try:
|
||||
scales = []
|
||||
for idx, logfmt in enumerate(logformat):
|
||||
if logfmt and self.lin_forced[idx]:
|
||||
raise ValueError('%s must contain positive values only' % 'xy'[idx])
|
||||
self.logformat[idx] = linlog = self.loglog if logfmt is None else logfmt
|
||||
logformat[idx] = linlog = self.loglog if logfmt is None else logfmt
|
||||
scales.append('log' if linlog else 'lin')
|
||||
xscale, yscale = scales
|
||||
except (TypeError, AssertionError):
|
||||
raise ValueError('logformat must be a 2 element sequence or a boolean')
|
||||
raise ValueError('logformat must be a 2 element list or a boolean')
|
||||
|
||||
xr = self.spline.x[1:-1] # raw units, excluding extrapolated points
|
||||
x1, x2 = xmin, xmax = xr[0], xr[-1]
|
||||
x = self.spline.x[1:-1] # raw units, excluding extrapolated points
|
||||
x1, x2 = xmin, xmax = x[0], x[-1]
|
||||
y1, y2 = sorted(self.spline([x1, x2]))
|
||||
|
||||
if extrapolate and not yrange:
|
||||
yrange = self.exty
|
||||
@ -578,100 +535,42 @@ class CalCurve(HasOptions):
|
||||
lim = to_scale[self.scale](xlimits)
|
||||
xmin = clamp(xmin, *lim)
|
||||
xmax = clamp(xmax, *lim)
|
||||
# start and end index of calibrated range
|
||||
ibeg, iend = self.extra_points[0], len(xr) - self.extra_points[1]
|
||||
if xmin != x1 or xmax != x2:
|
||||
i, j = np.searchsorted(xr, (xmin, xmax))
|
||||
if abs(xr[i] - xmin) < 0.1 * (xr[i + 1] - xr[i]):
|
||||
ibeg, iend = np.searchsorted(x, (xmin, xmax))
|
||||
if abs(x[ibeg] - xmin) < 0.1 * (x[ibeg + 1] - x[ibeg]):
|
||||
# remove first point, if close
|
||||
i += 1
|
||||
if abs(xr[j - 1] - xmax) < 0.1 * (xr[j - 1] - xr[j - 2]):
|
||||
ibeg += 1
|
||||
if abs(x[iend - 1] - xmax) < 0.1 * (x[iend - 1] - x[iend - 2]):
|
||||
# remove last point, if close
|
||||
j -= 1
|
||||
offset = i - 1
|
||||
xr = np.concatenate(([xmin], xr[i:j], [xmax]))
|
||||
ibeg = max(0, ibeg - offset)
|
||||
iend = min(len(xr), iend - offset)
|
||||
|
||||
yr = self.spline(xr)
|
||||
iend -= 1
|
||||
x = np.concatenate(([xmin], x[ibeg:iend], [xmax]))
|
||||
y = self.spline(x)
|
||||
|
||||
# convert to exported scale
|
||||
if xscale == self.scale:
|
||||
xbwd = identity
|
||||
x = xr
|
||||
else:
|
||||
if self.scale == 'log':
|
||||
xfwd, xbwd = from_scale[self.scale], to_scale[self.scale]
|
||||
else:
|
||||
xfwd, xbwd = to_scale[xscale], from_scale[xscale]
|
||||
x = xfwd(xr)
|
||||
if yscale == self.scale:
|
||||
yfwd = identity
|
||||
y = yr
|
||||
else:
|
||||
if self.scale == 'log':
|
||||
yfwd = from_scale[self.scale]
|
||||
else:
|
||||
yfwd = to_scale[yscale]
|
||||
y = yfwd(yr)
|
||||
if xscale != self.scale:
|
||||
x = to_scale[xscale](from_scale[self.scale](x))
|
||||
if yscale != self.scale:
|
||||
y = to_scale[yscale](from_scale[self.scale](y))
|
||||
|
||||
self.deviation = None
|
||||
nmin = min(nmin, nmax)
|
||||
n = len(x)
|
||||
relerror = yscale == 'lin'
|
||||
if len(x) > nmax:
|
||||
# reduce number of points, if needed
|
||||
n = len(x)
|
||||
i, j = 1, n - 1 # index range for calculating interpolation deviation
|
||||
deviation = np.zeros(n)
|
||||
while True:
|
||||
deviation[i:j] = self.interpolation_error(
|
||||
x[i-1:j-1], x[i+1:j+1], y[i-1:j-1], y[i+1:j+1],
|
||||
xbwd, yfwd, relerror)
|
||||
# calculate interpolation error when a single point is omitted
|
||||
ym = y[i-1:j-1] + (x[i:j] - x[i-1:j-1]) * (y[i+1:j+1] - y[i-1:j-1]) / (x[i+1:j+1] - x[i-1:j-1])
|
||||
if yscale == 'log':
|
||||
deviation[i:j] = np.abs(ym - y[i:j])
|
||||
else:
|
||||
deviation[i:j] = np.abs(ym - y[i:j]) / (np.abs(ym + y[i:j]) + 1e-10)
|
||||
if n <= nmax:
|
||||
break
|
||||
idx = np.argmin(deviation[1:-1]) + 1 # find index of the smallest error
|
||||
y = np.delete(y, idx)
|
||||
x = np.delete(x, idx)
|
||||
deviation = np.delete(deviation, idx)
|
||||
n = len(x)
|
||||
n -= 1
|
||||
# index range to recalculate
|
||||
i, j = max(1, idx - 1), min(n - 1, idx + 1)
|
||||
self.deviation = deviation # for debugging purposes
|
||||
elif n < nmin:
|
||||
if ibeg + 1 < iend:
|
||||
diff1, diff4 = self.interpolation_error(
|
||||
x[ibeg:iend - 1], x[ibeg + 1:iend], y[ibeg:iend - 1], y[ibeg + 1:iend],
|
||||
xbwd, yfwd, relerror, return_tuple=True)
|
||||
dif_target = 1e-4
|
||||
sq4 = np.sqrt(diff4) * 4
|
||||
sq1 = np.sqrt(diff1)
|
||||
offset = 0.49
|
||||
n_mid = nmax - len(x) + iend - ibeg - 1
|
||||
# iteration to find a dif target resulting in no more than nmax points
|
||||
while True:
|
||||
scale = 1 / np.sqrt(dif_target)
|
||||
# estimate number of intermediate points (float!) needed to reach dif_target
|
||||
# number of points estimated from the result of the interpolation error with 4 sections
|
||||
n4 = np.maximum(1, sq4 * scale)
|
||||
# number of points estimated from the result of the interpolation error with 1 section
|
||||
n1 = np.maximum(1, sq1 * scale)
|
||||
# use n4 where n4 > 4, n1, where n1 < 1 and a weighted average in between
|
||||
nn = np.select([n4 > 4, n1 > 1],
|
||||
[n4, (n4 * (n1 - 1) + n1 * (4 - n4)) / (3 + n1 - n4)], n1)
|
||||
n_tot = np.sum(np.rint(nn + offset))
|
||||
extra = n_tot - n_mid
|
||||
if extra <= 0:
|
||||
break
|
||||
dif_target *= (n_tot / n_mid) ** 2
|
||||
|
||||
xnew = [x[:ibeg]]
|
||||
for x0, x1, ni in zip(x[ibeg:iend-1], x[ibeg+1:iend], np.rint(nn + offset)):
|
||||
xnew.append(np.linspace(x0, x1, int(ni) + 1)[:-1])
|
||||
xnew.append(x[iend-1:])
|
||||
x = np.concatenate(xnew)
|
||||
y = yfwd(self.spline(xbwd(x)))
|
||||
# for debugging purposes:
|
||||
self.deviation = self.interpolation_error(x[:-1], x[1:], y[:-1], y[1:], xbwd, yfwd, relerror)
|
||||
|
||||
return np.stack([x, y], axis=1)
|
||||
|
@ -1,139 +0,0 @@
|
||||
import os
|
||||
from glob import glob
|
||||
from pathlib import Path
|
||||
from configparser import ConfigParser
|
||||
from frappy.errors import ConfigError
|
||||
|
||||
|
||||
class Rack:
|
||||
configbase = Path('/home/l_samenv/.config/frappy_instruments')
|
||||
|
||||
def __init__(self, modfactory, **kwds):
|
||||
self.modfactory = modfactory
|
||||
instpath = self.configbase / os.environ['Instrument']
|
||||
sections = {}
|
||||
self.config = {}
|
||||
files = glob(str(instpath / '*.ini'))
|
||||
for filename in files:
|
||||
parser = ConfigParser()
|
||||
parser.optionxform = str
|
||||
parser.read([filename])
|
||||
for section in parser.sections():
|
||||
prev = sections.get(section)
|
||||
if prev:
|
||||
raise ConfigError(f'duplicate {section} section in {filename} and {prev}')
|
||||
sections[section] = filename
|
||||
self.config.update(parser.items(section))
|
||||
if 'rack' not in sections:
|
||||
raise ConfigError(f'no rack found in {instpath}')
|
||||
self.props = {} # dict (<property>, <method>) of value
|
||||
self.mods = {} # dict (<property>, <method>) of list of <cfg>
|
||||
|
||||
def set_props(self, mod, **kwds):
|
||||
for prop, method in kwds.items():
|
||||
value = self.props.get((prop, method))
|
||||
if value is None:
|
||||
# add mod to the list of cfgs to be fixed
|
||||
self.mods.setdefault((prop, method), []).append(mod)
|
||||
else:
|
||||
# set prop in current module
|
||||
if not mod.get(prop): # do not override given and not empty property
|
||||
mod[prop] = value
|
||||
|
||||
def fix_props(self, method, **kwds):
|
||||
for prop, value in kwds.items():
|
||||
if (prop, method) in self.props:
|
||||
raise ConfigError(f'duplicate call to {method}()')
|
||||
self.props[prop, method] = value
|
||||
# set property in modules to be fixed
|
||||
for mod in self.mods.get((prop, method), ()):
|
||||
mod[prop] = value
|
||||
|
||||
def lakeshore(self, ls_uri=None, io='ls_io', dev='ls', model='336', **kwds):
|
||||
Mod = self.modfactory
|
||||
self.fix_props('lakeshore', io=io, device=dev)
|
||||
self.ls_model = model
|
||||
self.ls_dev = dev
|
||||
ls_uri = ls_uri or self.config.get('ls_uri')
|
||||
Mod(io, cls=f'frappy_psi.lakeshore.IO{self.ls_model}',
|
||||
description='comm. to lakeshore in cc rack', uri=ls_uri)
|
||||
self.dev = Mod(dev, cls=f'frappy_psi.lakeshore.Device{self.ls_model}',
|
||||
description='lakeshore in cc rack', io=io, curve_handling=True)
|
||||
|
||||
def sensor(self, name, channel, calcurve, **kwds):
|
||||
Mod = self.modfactory
|
||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.Sensor{self.ls_model}')
|
||||
kwds.setdefault('description', f'T sensor {name}')
|
||||
mod = Mod(name, channel=channel, calcurve=calcurve,
|
||||
device=self.ls_dev, **kwds)
|
||||
self.set_props(mod, io='lakeshore', dev='lakeshore')
|
||||
|
||||
def loop(self, name, channel, calcurve, output_module, **kwds):
|
||||
Mod = self.modfactory
|
||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.Loop{self.ls_model}')
|
||||
kwds.setdefault('description', f'T loop {name}')
|
||||
Mod(name, channel=channel, calcurve=calcurve, output_module=output_module,
|
||||
device=self.ls_dev, **kwds)
|
||||
self.fix_props(f'heater({output_module})', description=f'heater for {name}')
|
||||
|
||||
def heater(self, name, output_no, max_heater, resistance, **kwds):
|
||||
Mod = self.modfactory
|
||||
if output_no == 1:
|
||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.MainOutput{self.ls_model}')
|
||||
elif output_no == 2:
|
||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.SecondaryOutput{self.ls_model}')
|
||||
else:
|
||||
return
|
||||
kwds.setdefault('description', '')
|
||||
mod = Mod(name, max_heater=max_heater, resistance=resistance, **kwds)
|
||||
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):
|
||||
Mod = self.modfactory
|
||||
self.ccu_io = ccu_io
|
||||
ccu_uri = ccu_uri or self.config.get('ccu_uri')
|
||||
# self.devname = ccu_devname
|
||||
Mod(ccu_io, 'frappy_psi.ccu4.IO',
|
||||
'comm. to CCU4', uri=ccu_uri)
|
||||
if he:
|
||||
if not isinstance(he, str): # e.g. True
|
||||
he = 'He_lev'
|
||||
Mod(he, cls='frappy_psi.ccu4.HeLevel',
|
||||
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',
|
||||
hepump='hepump', hepump_mot='hepump_mot', hepump_valve='hepump_valve',
|
||||
flow_sensor='flow_sensor', pump_pressure='pump_pressure', nv='nv',
|
||||
ccu_io='ccu_io', **kwds):
|
||||
Mod = self.modfactory
|
||||
hepump_type = hepump_type or self.config.get('hepump_type', 'no')
|
||||
Mod(nv, 'frappy_psi.ccu4.NeedleValveFlow', 'flow from flow sensor or pump pressure',
|
||||
flow_sensor=flow_sensor, pressure=pump_pressure, io=ccu_io)
|
||||
Mod(pump_pressure, 'frappy_psi.ccu4.Pressure', 'He pump pressure', io=ccu_io)
|
||||
if hepump_type == 'no':
|
||||
print('no pump, no flow meter - using flow from pressure alone')
|
||||
return
|
||||
hepump_uri = hepump_uri or self.config['hepump_uri']
|
||||
Mod(hepump_io, 'frappy.io.BytesIO', 'He pump connection', uri=hepump_uri)
|
||||
Mod(hepump, 'frappy_psi.hepump.HePump', 'He pump', pump_type=hepump_type,
|
||||
valvemotor=hepump_mot, valve=hepump_valve, flow=nv)
|
||||
Mod(hepump_mot, 'frappy_psi.hepump.Motor', 'He pump valve motor', io=hepump_io, maxcurrent=2.8)
|
||||
Mod(hepump_valve, 'frappy_psi.butterflyvalve.Valve', 'He pump valve', motor=hepump_mot)
|
||||
Mod(flow_sensor, 'frappy_psi.sensirion.FlowSensor', 'Flow Sensor', io=hepump_io, nsamples=160)
|
||||
|
@ -23,30 +23,30 @@
|
||||
import time
|
||||
import math
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy.lib import clamp
|
||||
# the most common Frappy classes can be imported from frappy.core
|
||||
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
|
||||
from frappy.datatypes import BoolType, EnumType, FloatRange, StructOf, \
|
||||
StatusType, IntRange, StringType, TupleOf
|
||||
from frappy.dynamic import Pinata
|
||||
from frappy.errors import CommunicationFailedError
|
||||
from frappy.states import HasStates, status_code, Retry
|
||||
|
||||
|
||||
M = Enum(idle=0, opening=1, closing=2, opened=3, closed=4, no_motor=5)
|
||||
M = Enum(idle=0, opening=1, closing=2, opened=3, closed=5, no_motor=6)
|
||||
A = Enum(disabled=0, manual=1, auto=2)
|
||||
|
||||
|
||||
class IO(StringIO):
|
||||
class CCU4IO(StringIO):
|
||||
"""communication with CCU4"""
|
||||
# for completeness: (not needed, as it is the default)
|
||||
end_of_line = '\n'
|
||||
# on connect, we send 'cid' and expect a reply starting with 'CCU4'
|
||||
identification = [('cid', r'cid=CCU4.*')]
|
||||
identification = [('cid', r'CCU4.*')]
|
||||
|
||||
|
||||
class Base(HasIO):
|
||||
ioClass = IO
|
||||
class CCU4Base(HasIO):
|
||||
ioClass = CCU4IO
|
||||
|
||||
def command(self, **kwds):
|
||||
"""send a command and get the response
|
||||
@ -80,7 +80,7 @@ class Base(HasIO):
|
||||
return result
|
||||
|
||||
|
||||
class HeLevel(Base, Readable):
|
||||
class HeLevel(CCU4Base, Readable):
|
||||
"""He Level channel of CCU4"""
|
||||
|
||||
value = Parameter(unit='%')
|
||||
@ -122,10 +122,10 @@ class HeLevel(Base, Readable):
|
||||
return self.command(hfu=value)
|
||||
|
||||
|
||||
class Valve(Base, Writable):
|
||||
class Valve(CCU4Base, Writable):
|
||||
value = Parameter('relay state', BoolType())
|
||||
target = Parameter('relay target', BoolType())
|
||||
ioClass = IO
|
||||
ioClass = CCU4IO
|
||||
STATE_MAP = {0: (0, (IDLE, 'off')),
|
||||
1: (1, (IDLE, 'on')),
|
||||
2: (0, (ERROR, 'no valve')),
|
||||
@ -144,7 +144,7 @@ class Valve(Base, Writable):
|
||||
self.command(**self._close_command)
|
||||
|
||||
def read_status(self):
|
||||
state = int(self.command(**self._query_state))
|
||||
state = self.command(self._query_state)
|
||||
self.value, status = self.STATE_MAP[state]
|
||||
return status
|
||||
|
||||
@ -174,14 +174,14 @@ class N2TempSensor(Readable):
|
||||
value = Parameter('LN2 T sensor', FloatRange(unit='K'), default=0)
|
||||
|
||||
|
||||
class N2Level(Base, Readable):
|
||||
class N2Level(CCU4Base, Pinata, Readable):
|
||||
valve = Attached(Writable, mandatory=False)
|
||||
lower = Attached(Readable, mandatory=False)
|
||||
upper = Attached(Readable, mandatory=False)
|
||||
|
||||
value = Parameter('vessel state', EnumType(empty=0, ok=1, full=2))
|
||||
status = Parameter(datatype=StatusType(Readable, 'DISABLED', 'BUSY'))
|
||||
mode = Parameter('auto mode', EnumType(A), readonly=False, default=A.manual)
|
||||
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
||||
mode = Parameter('auto mode', EnumType(A), readonly=False)
|
||||
|
||||
threshold = Parameter('threshold triggering start/stop filling',
|
||||
FloatRange(unit='K'), readonly=False)
|
||||
@ -206,6 +206,15 @@ class N2Level(Base, Readable):
|
||||
5: (WARN, 'empty'),
|
||||
}
|
||||
|
||||
def scanModules(self):
|
||||
for modname, name in self.names.items():
|
||||
if name:
|
||||
sensor_name = name.replace('$', self.name)
|
||||
self.setProperty(modname, sensor_name)
|
||||
yield sensor_name, {
|
||||
'cls': N2FillValve if modname == 'valve' else N2TempSensor,
|
||||
'description': f'LN2 {modname} T sensor'}
|
||||
|
||||
def initialReads(self):
|
||||
self.command(nav=1) # tell CCU4 to activate LN2 sensor readings
|
||||
super().initialReads()
|
||||
@ -271,335 +280,151 @@ class N2Level(Base, Readable):
|
||||
|
||||
@Command()
|
||||
def fill(self):
|
||||
"""start filling"""
|
||||
self.mode = A.auto
|
||||
self.command(nc=1)
|
||||
self.io.write(nc=1)
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
"""stop filling"""
|
||||
if self.mode == A.auto:
|
||||
# set to watching
|
||||
self.command(nc=3)
|
||||
else:
|
||||
# set to off
|
||||
self.command(nc=0)
|
||||
self.io.write(nc=0)
|
||||
|
||||
|
||||
class HasFilter:
|
||||
__value1 = None
|
||||
__value = None
|
||||
__last = None
|
||||
|
||||
def filter(self, filter_time, value):
|
||||
now = time.time()
|
||||
if self.__value is None:
|
||||
self.__last = now
|
||||
self.__value1 = value
|
||||
self.__value = value
|
||||
weight = (now - self.__last) / filter_time
|
||||
self.__value1 += weight * (value - self.__value)
|
||||
self.__value += weight * (self.__value1 - self.__value)
|
||||
self.__last = now
|
||||
return self.__value
|
||||
|
||||
|
||||
class Pressure(HasFilter, Base, Readable):
|
||||
class FlowPressure(CCU4Base, Readable):
|
||||
value = Parameter(unit='mbar')
|
||||
mbar_offset = Parameter('offset in mbar', FloatRange(unit='mbar'), default=0.8, readonly=False)
|
||||
filter_time = Parameter('filter time', FloatRange(unit='sec'), readonly=False, default=3)
|
||||
mbar_offset = Parameter(unit='mbar', default=0.8, readonly=False)
|
||||
pollinterval = Parameter(default=0.25)
|
||||
|
||||
def read_value(self):
|
||||
return self.filter(self.filter_time, self.command(f=float)) - self.mbar_offset
|
||||
return self.filter(self.command(f=float)) - self.mbar_offset
|
||||
|
||||
|
||||
class NeedleValveFlow(HasStates, Base, Drivable):
|
||||
flow_sensor = Attached(Readable, mandatory=False)
|
||||
pressure = Attached(Pressure, mandatory=False)
|
||||
use_pressure = Parameter('flag (use pressure instead of flow meter)', BoolType(),
|
||||
readonly=False, default=False)
|
||||
lnm_per_mbar = Parameter('scale factor', FloatRange(unit='lnm/mbar'), readonly=False, default=0.6)
|
||||
class NeedleValve(HasStates, CCU4Base, Drivable):
|
||||
flow = Attached(Readable, mandatory=False)
|
||||
flow_pressure = Attached(Readable, mandatory=False)
|
||||
|
||||
value = Parameter(unit='ln/min')
|
||||
target = Parameter(unit='ln/min')
|
||||
|
||||
motor_state = Parameter('motor_state', EnumType(M), default=0)
|
||||
lnm_per_mbar = Parameter(unit='ln/min/mbar', default=0.6, readonly=False)
|
||||
use_pressure = Parameter('use flow from pressure', BoolType(),
|
||||
default=False, readonly=False)
|
||||
motor_state = Parameter('motor_state', EnumType(M))
|
||||
tolerance = Parameter('tolerance', FloatRange(0), value=0.25, readonly=False)
|
||||
tolerance2 = Parameter('tolerance limit above 2 lnm', FloatRange(0), value=0.5, readonly=False)
|
||||
prop = Parameter('proportional term', FloatRange(unit='s/lnm'), readonly=False, default=0.001)
|
||||
prop = Parameter('proportional term', FloatRange(unit='s/lnm'), readonly=False)
|
||||
deriv = Parameter('min progress time constant', FloatRange(unit='s'),
|
||||
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)
|
||||
min_open_step = Parameter('minimal open step', FloatRange(unit='s'), readonly=False, default=0.06)
|
||||
min_close_step = Parameter('minimal close step', FloatRange(unit='s'), readonly=False, default=0.05)
|
||||
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)
|
||||
pollinterval = Parameter(default=5)
|
||||
step_factor = Parameter('factor (no progress time) / (min step size)', FloatRange(), default=300)
|
||||
control_active = Parameter('control active flag', BoolType(), readonly=False)
|
||||
pollinterval = Parameter(default=1)
|
||||
_ref_time = 0
|
||||
_ref_dif = 0
|
||||
_dir = 0
|
||||
_rawdir = 0
|
||||
_last_cycle = 0
|
||||
_last_progress = 0
|
||||
_step = 0
|
||||
|
||||
def initModule(self):
|
||||
if self.pressure:
|
||||
self.pressure.addCallback('value', self.update_from_pressure)
|
||||
if self.flow_sensor:
|
||||
self.flow_sensor.addCallback('value', self.update_from_flow)
|
||||
super().initModule()
|
||||
|
||||
def update_from_flow(self, value):
|
||||
if not self.use_pressure:
|
||||
self.value = value
|
||||
# self.cycle_machine()
|
||||
|
||||
def update_from_pressure(self, value):
|
||||
if self.use_pressure:
|
||||
self.value = value * self.lnm_per_mbar
|
||||
# self.cycle_machine()
|
||||
|
||||
# def doPoll(self):
|
||||
# """only the updates should trigger the machine"""
|
||||
|
||||
def read_value(self):
|
||||
p = self.pressure.read_value() * self.lnm_per_mbar
|
||||
f = self.flow_sensor.read_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
|
||||
if self.flow_pressure:
|
||||
self.flow_pressure.addCallback('value', self.update_flow_pressure)
|
||||
if self.flow:
|
||||
self.flow.addCallback('value', self.update_flow)
|
||||
self.write_tolerance(self.tolerance)
|
||||
|
||||
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
|
||||
if hasattr(self.flow_pressure, 'tolerance'):
|
||||
self.flow_pressure.tolerance = tolerance / self.lnm_per_mbar
|
||||
if hasattr(self.flow, 'tolerance'):
|
||||
self.flow.tolerance = tolerance
|
||||
|
||||
def read_use_pressure(self):
|
||||
if self.pressure:
|
||||
if self.flow_sensor:
|
||||
if self.flow_pressure:
|
||||
if self.flow:
|
||||
return self.use_pressure
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_flow(self, value):
|
||||
if not self.use_pressure:
|
||||
self.value = value
|
||||
self.cycle_machine()
|
||||
|
||||
def update_flow_pressure(self, value):
|
||||
if self.use_pressure:
|
||||
self.value = value * self.lnm_per_mbar
|
||||
self.cycle_machine()
|
||||
|
||||
def write_target(self, value):
|
||||
self.start_machine(self.change_target, fast_poll=1)
|
||||
self.start_machine(self.controlling, in_tol_time=0,
|
||||
ref_time=0, ref_dif=0, prev_dif=0)
|
||||
|
||||
@status_code(BUSY)
|
||||
def change_target(self, state):
|
||||
state.in_tol_time = 0
|
||||
state.last_minstep = {}
|
||||
state.last_progress = state.now
|
||||
state.ref_time = 0
|
||||
state.ref_dif = 0
|
||||
state.prev_dif = 0 # used?
|
||||
state.last_close_time = 0
|
||||
state.last_pulse_time = 0
|
||||
state.raw_fact = 1
|
||||
state.raw_step = 0
|
||||
if abs(self.target - self.value) < self._tolerance():
|
||||
return self.at_target
|
||||
return self.raw_control
|
||||
|
||||
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))
|
||||
def unblock_from_open(self, state):
|
||||
self.motor_state = self.command(fm=int)
|
||||
if self.motor_state == 'opened':
|
||||
self.command(mp=-60)
|
||||
return Retry
|
||||
if self.motor_state == 'closing':
|
||||
return Retry
|
||||
if self.motor_state == 'closed':
|
||||
if self.value > max(1, self.target):
|
||||
return Retry
|
||||
state.flow_before = self.value
|
||||
state.wiggle = 1
|
||||
state.start_wiggle = state.now
|
||||
self.command(mp=60)
|
||||
return self.unblock_open
|
||||
return self.approaching
|
||||
|
||||
@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
|
||||
def unblock_open(self, state):
|
||||
self.motor_state = self.command(fm=int)
|
||||
if self.value < state.flow_before:
|
||||
state.flow_before_open = self.value
|
||||
elif self.value > state.flow_before + 1:
|
||||
state.wiggle = -state.wiggle / 2
|
||||
self.command(mp=state.wiggle)
|
||||
state.start_wiggle = state.now
|
||||
return self.unblock_close
|
||||
if self.motor_state == 'opening':
|
||||
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))
|
||||
if self.motor_state == 'idle':
|
||||
self.command(mp=state.wiggle)
|
||||
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
|
||||
if self.motor_state == 'opened':
|
||||
if state.now < state.start_wiggle + 20:
|
||||
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.final_status(ERROR, 'can not open')
|
||||
return self.controlling
|
||||
|
||||
# @status_code(BUSY)
|
||||
# def raw_control(self, state):
|
||||
# tol = self._tolerance()
|
||||
# if state.init:
|
||||
# self.start_direction(state)
|
||||
# if self._dir != self._rawdir:
|
||||
# self._rawdir = self._dir
|
||||
# state.first_step = self.first_open_step if self._dir > 0 else -self.first_close_step
|
||||
# else:
|
||||
# state.first_step = 0
|
||||
# state.first_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.first_step = 1
|
||||
# # self._dir = 1
|
||||
# difdir = (self.target - self.value) * self._dir
|
||||
# state.prev = [difdir]
|
||||
# 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)
|
||||
def controlling(self, state):
|
||||
dif = self.target - self.value
|
||||
if state.init:
|
||||
self.start_direction(state)
|
||||
state.ref_dif = abs(dif)
|
||||
state.ref_time = state.now
|
||||
state.in_tol_time = 0
|
||||
difdir = dif * self._dir # negative when overshoot happend
|
||||
# difdif = dif - state.prev_dif
|
||||
# state.prev_dif = dif
|
||||
expected_dif = state.ref_dif * math.exp((state.ref_time - state.now) / self.deriv)
|
||||
|
||||
tol = self._tolerance()
|
||||
if difdir < tol:
|
||||
# prev_minstep = state.last_minstep.pop(self._dir, None)
|
||||
# attr = 'min_open_step' if self._dir > 0 else 'min_close_step'
|
||||
# if prev_minstep is not None:
|
||||
# # increase minstep
|
||||
# minstep = getattr(self, attr)
|
||||
# setattr(self, attr, minstep * 1.1)
|
||||
# self.log.info('increase %s to %g', attr, minstep)
|
||||
if difdir > -tol: # within tolerance
|
||||
delta = state.delta()
|
||||
state.in_tol_time += delta
|
||||
if state.in_tol_time > self.settle:
|
||||
# state.last_minstep.pop(self._dir, None)
|
||||
self.log.info('at target %g %g', dif, tol)
|
||||
return self.at_target
|
||||
if difdir < 0:
|
||||
def unblock_close(self, state):
|
||||
self.motor_state = self.command(fm=int)
|
||||
if self.value > state.flow_before:
|
||||
state.flow_before_open = self.value
|
||||
elif self.value < state.flow_before - 1:
|
||||
if state.wiggle < self.prop * 2:
|
||||
return self.final_status(IDLE, '')
|
||||
state.wiggle = -state.wiggle / 2
|
||||
self.command(mp=state.wiggle)
|
||||
state.start_wiggle = state.now
|
||||
return self.unblock_open
|
||||
if self.motor_state == 'closing':
|
||||
return Retry
|
||||
# self.log.info('minstep=0 dif=%g', dif)
|
||||
else: # overshoot
|
||||
self.log.info('overshoot %g', dif)
|
||||
return self.raw_control
|
||||
# # overshoot
|
||||
# prev_minstep = state.last_minstep.pop(self._dir, None)
|
||||
# if prev_minstep is None:
|
||||
# minstep = getattr(self, attr) * 0.9
|
||||
# self.log.info('decrease %s to %g', attr, minstep)
|
||||
# setattr(self, attr, minstep)
|
||||
# self.start_step(state, self.target)
|
||||
# still approaching
|
||||
if difdir <= expected_dif:
|
||||
if difdir < expected_dif / 1.25 - tol:
|
||||
state.ref_time = state.now
|
||||
state.ref_dif = (difdir + tol) * 1.25
|
||||
# self.log.info('new ref %g', state.ref_dif)
|
||||
state.last_progress = state.now
|
||||
return Retry # progressing: no pulse needed
|
||||
if state.now < state.last_pulse_time + 2.5:
|
||||
if self.motor_state == 'idle':
|
||||
self.command(mp=state.wiggle)
|
||||
return Retry
|
||||
# TODO: check motor state for closed / opened ?
|
||||
self.perform_pulse(state)
|
||||
if self.motor_state == 'closed':
|
||||
if state.now < state.start_wiggle + 20:
|
||||
return Retry
|
||||
return self.final_status(ERROR, 'can not close')
|
||||
return self.final_status(WARN, 'unblock interrupted')
|
||||
|
||||
def _tolerance(self):
|
||||
return min(self.tolerance * min(1, self.value / 2), self.tolerance2)
|
||||
@ -609,67 +434,48 @@ class NeedleValveFlow(HasStates, Base, Drivable):
|
||||
dif = self.target - self.value
|
||||
if abs(dif) > self._tolerance():
|
||||
state.in_tol_time = 0
|
||||
self.log.info('unstable %g', dif)
|
||||
return self.unstable
|
||||
return Retry
|
||||
|
||||
@status_code(IDLE, 'unstable')
|
||||
def unstable(self, state):
|
||||
difdir = (self.target - self.value) * self._dir
|
||||
if difdir < 0 or self._dir == 0:
|
||||
return self.raw_control
|
||||
return self.controlling(state)
|
||||
|
||||
def read_motor_state(self):
|
||||
return self.command(fm=int)
|
||||
|
||||
@Command
|
||||
def close(self):
|
||||
"""close valve fully"""
|
||||
self.command(mp=-60)
|
||||
@status_code(BUSY)
|
||||
def controlling(self, state):
|
||||
delta = state.delta(0)
|
||||
dif = self.target - self.value
|
||||
difdif = dif - state.prev_dif
|
||||
state.prev_dif = dif
|
||||
self.motor_state = self.command(fm=int)
|
||||
self.start_machine(self.closing)
|
||||
|
||||
@status_code(BUSY)
|
||||
def closing(self, state):
|
||||
if state.init:
|
||||
state.start_time = state.now
|
||||
self.read_motor_state()
|
||||
if self.motor_state == M.closing:
|
||||
if self.motor_state == 'closed':
|
||||
if dif < 0 or difdif < 0:
|
||||
return Retry
|
||||
if self.motor_state == M.closed:
|
||||
return self.final_status(IDLE, 'closed')
|
||||
if state.now < state.start_time + 1:
|
||||
return self.unblock_from_open
|
||||
elif self.motor_state == 'opened': # trigger also when flow too high?
|
||||
if dif > 0 or difdif > 0:
|
||||
return Retry
|
||||
return self.final_status(IDLE, 'fixed')
|
||||
self.command(mp=-60)
|
||||
return self.unblock_from_open
|
||||
|
||||
@Command
|
||||
def open(self):
|
||||
"""open valve fully"""
|
||||
self.command(mp=60)
|
||||
self.read_motor_state()
|
||||
self.start_machine(self.opening)
|
||||
|
||||
@status_code(BUSY)
|
||||
def opening(self, state):
|
||||
if state.init:
|
||||
state.start_time = state.now
|
||||
self.read_motor_state()
|
||||
if self.motor_state == M.opening:
|
||||
tolerance = self._tolerance()
|
||||
if abs(dif) < tolerance:
|
||||
state.in_tol_time += delta
|
||||
if state.in_tol_time > self.settle:
|
||||
return self.at_target
|
||||
return Retry
|
||||
if self.motor_state == M.opened:
|
||||
return self.final_status(IDLE, 'opened')
|
||||
if state.now < state.start_time + 1:
|
||||
expected_dif = state.ref_dif * math.exp((state.now - state.ref_time) / self.deriv)
|
||||
if abs(dif) < expected_dif:
|
||||
if abs(dif) < expected_dif / 1.25:
|
||||
state.ref_time = state.now
|
||||
state.ref_dif = abs(dif) * 1.25
|
||||
state.last_progress = state.now
|
||||
return Retry # progress is fast enough
|
||||
state.ref_time = state.now
|
||||
state.ref_dif = abs(dif)
|
||||
state.step += dif * delta * self.prop
|
||||
if abs(state.step) < (state.now - state.last_progress) / self.step_factor:
|
||||
# wait until step size is big enough
|
||||
return Retry
|
||||
self.command(mp=state.step)
|
||||
return Retry
|
||||
return self.final_status(IDLE, 'fixed')
|
||||
|
||||
@Command(FloatRange())
|
||||
def pulse(self, value):
|
||||
"""perform a motor pulse"""
|
||||
self.log.info('pulse %g', value)
|
||||
self.command(mp=value)
|
||||
if value > 0:
|
||||
self.motor_state = M.opening
|
||||
return self.opening
|
||||
self.motor_state = M.closing
|
||||
return self.closing
|
||||
|
@ -56,7 +56,7 @@ class Drums(Writable):
|
||||
self._pos = 0
|
||||
for i, action in enumerate(self.pattern[self._pos:]):
|
||||
upper = action.upper()
|
||||
relais = self.actions.get(upper)
|
||||
relais = self.actions.get(action.upper())
|
||||
if relais:
|
||||
relais.write_target(upper == action) # True when capital letter
|
||||
else:
|
||||
|
@ -1,71 +0,0 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from frappy.core import BoolType, FloatRange, Parameter, Readable, Writable, Attached, EnumType, nopoll
|
||||
from frappy_psi.trinamic import Motor
|
||||
from frappy_psi.ccu4 import Pressure, NeedleValveFlow
|
||||
|
||||
|
||||
class ValveMotor(Motor):
|
||||
has_inputs = True
|
||||
|
||||
|
||||
class HePump(Writable):
|
||||
valvemotor = Attached(Motor)
|
||||
flow = Attached(NeedleValveFlow)
|
||||
valve = Attached(Writable)
|
||||
value = Parameter(datatype=BoolType())
|
||||
target = Parameter(datatype=BoolType())
|
||||
pump_type = Parameter('pump type', EnumType(no=0, neodry=1, xds35=2, sv65=3), readonly=False, default=0)
|
||||
eco_mode = Parameter('eco mode', BoolType(), readonly=False)
|
||||
has_feedback = Parameter('feedback works', BoolType(), readonly=False, default=True)
|
||||
|
||||
FLOW_SCALE = {'no': 0, 'neodry': 0.55, 'xds35': 0.6, 'sv65': 0.9}
|
||||
|
||||
def write_target(self, value):
|
||||
self.valvemotor.write_output0(value)
|
||||
|
||||
def read_target(self):
|
||||
return self.valvemotor.read_output0()
|
||||
|
||||
def read_value(self):
|
||||
if self.has_feedback:
|
||||
return not self.valvemotor.read_input3()
|
||||
return self.target
|
||||
|
||||
def write_pump_type(self, value):
|
||||
self.flow.pressure_scale = self.FLOW_SCALE[value.name]
|
||||
|
||||
def read_eco_mode(self):
|
||||
if self.pump_type == 'xds35':
|
||||
return self.valvemotor.read_output1()
|
||||
return False
|
||||
|
||||
def write_eco_mode(self, value):
|
||||
if self.pump_type == 'xds35':
|
||||
return self.valvemotor.write_output1(value)
|
||||
# else silently ignore
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -18,39 +18,16 @@
|
||||
# Jael Celia Lorenzana <jael-celia.lorenzana@psi.ch>
|
||||
# *****************************************************************************
|
||||
|
||||
import os
|
||||
from glob import glob
|
||||
from frappy.core import Readable, Writable, Parameter, BoolType, StringType,\
|
||||
FloatRange, Property, TupleOf, ERROR, IDLE
|
||||
from frappy.errors import ConfigError
|
||||
from math import log
|
||||
|
||||
basepaths = '/sys/class/ionopimax', '/sys/class/ionopi'
|
||||
|
||||
|
||||
class Base:
|
||||
addr = Property('address', StringType())
|
||||
_devpath = None
|
||||
devclass = None
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
# candidates = glob(f'/sys/class/iono*/*/{self.addr}')
|
||||
# if not candidates:
|
||||
# raise ConfigError(f'can not find path for {self.addr}')
|
||||
for basepath in basepaths:
|
||||
for devclass in ([self.devclass] if isinstance(self.devclass, str) else self.devclass):
|
||||
devpath = f'{basepath}/{devclass}'
|
||||
if os.path.exists(devpath):
|
||||
self._devpath = devpath
|
||||
return
|
||||
else:
|
||||
self.log.info('%s does not exist', devpath)
|
||||
else:
|
||||
raise ConfigError(f'device path for {self.devclass} not found {devpath}')
|
||||
|
||||
def read(self, addr, scale=None):
|
||||
with open(f'{self._devpath}/{addr}') as f:
|
||||
with open(f'/sys/class/ionopimax/{self.devclass}/{addr}') as f:
|
||||
result = f.read()
|
||||
if scale:
|
||||
return float(result) / scale
|
||||
@ -58,7 +35,7 @@ class Base:
|
||||
|
||||
def write(self, addr, value, scale=None):
|
||||
value = str(round(value * scale)) if scale else str(value)
|
||||
with open(f'{self._devpath}/{addr}', 'w') as f:
|
||||
with open(f'/sys/class/ionopimax/{self.devclass}/{addr}', 'w') as f:
|
||||
f.write(value)
|
||||
|
||||
|
||||
@ -72,7 +49,7 @@ class DigitalInput(Base, Readable):
|
||||
|
||||
class DigitalOutput(DigitalInput, Writable):
|
||||
target = Parameter('output state', BoolType(), readonly=False)
|
||||
devclass = 'digital_out', 'relay'
|
||||
devclass = 'digital_out'
|
||||
|
||||
def write_target(self, value):
|
||||
self.write(self.addr, value, 1)
|
||||
|
@ -1,65 +1,16 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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:
|
||||
# Damaris Tartarotti Maimone
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""support for ultrasound plot clients"""
|
||||
"""
|
||||
Created on Tue Feb 4 11:07:56 2020
|
||||
|
||||
@author: tartarotti_d-adm
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
# disable the behaviour of raising the window to the front each time it is updated
|
||||
plt.rcParams["figure.raise_window"] = False
|
||||
|
||||
|
||||
NAN = float('nan')
|
||||
|
||||
|
||||
class Pause:
|
||||
"""allows to leave the plot loop when the window is closed
|
||||
|
||||
Usage:
|
||||
pause = Pause(fig)
|
||||
|
||||
# do initial plots
|
||||
plt.show()
|
||||
while pause(0.5):
|
||||
# do plot updates
|
||||
plt.draw()
|
||||
"""
|
||||
def __init__(self, fig):
|
||||
fig.canvas.mpl_connect('close_event', self.on_close)
|
||||
self.running = True
|
||||
|
||||
def on_close(self, event):
|
||||
self.running = False
|
||||
|
||||
def __call__(self, interval):
|
||||
try:
|
||||
plt.pause(interval)
|
||||
except Exception:
|
||||
pass
|
||||
return self.running
|
||||
|
||||
|
||||
def rect(x1, x2, y1, y2):
|
||||
return np.array([[x1,x2,x2,x1,x1],[y1,y1,y2,y2,y1]])
|
||||
|
||||
NAN = float('nan')
|
||||
|
||||
def rects(intervals, y12):
|
||||
result = [rect(*intervals[0], *y12)]
|
||||
@ -68,7 +19,6 @@ def rects(intervals, y12):
|
||||
result.append(rect(*x12, *y12))
|
||||
return np.concatenate(result, axis=1)
|
||||
|
||||
|
||||
class Plot:
|
||||
def __init__(self, maxy):
|
||||
self.lines = {}
|
||||
@ -76,10 +26,6 @@ class Plot:
|
||||
self.first = True
|
||||
self.fig = None
|
||||
|
||||
def pause(self, interval):
|
||||
"""will be overridden when figure is created"""
|
||||
return False
|
||||
|
||||
def set_line(self, iax, name, data, fmt, **kwds):
|
||||
"""
|
||||
plot or update a line
|
||||
@ -122,7 +68,6 @@ class Plot:
|
||||
if self.first:
|
||||
plt.ion()
|
||||
self.fig, axleft = plt.subplots(figsize=(15,7))
|
||||
self.pause = Pause(self.fig)
|
||||
plt.title("I/Q", fontsize=14)
|
||||
axleft.set_xlim(0, curves[0][-1])
|
||||
self.ax = [axleft, axleft.twinx()]
|
||||
@ -152,6 +97,5 @@ class Plot:
|
||||
self.first = False
|
||||
|
||||
plt.draw()
|
||||
# TODO: do not know why this is needed:
|
||||
self.fig.canvas.draw()
|
||||
self.fig.canvas.flush_events()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -278,12 +278,7 @@ class ResChannel(LakeShoreIO, Channel):
|
||||
vexc = 0 if excoff or iscur else exc
|
||||
if (rng, iexc, vexc) != (self.range, self.iexc, self.vexc):
|
||||
self._last_range_change = time.monotonic()
|
||||
try:
|
||||
self.range, self.iexc, self.vexc = rng, iexc, vexc
|
||||
except Exception:
|
||||
# avoid raising errors on disabled channel
|
||||
if self.enabled:
|
||||
raise
|
||||
|
||||
@CommonWriteHandler(rdgrng_params)
|
||||
def write_rdgrng(self, change):
|
||||
|
@ -375,7 +375,6 @@ class HeaterOutput(HasInput, Writable):
|
||||
|
||||
class HeaterUpdate(HeaterOutput):
|
||||
kind = 'HTR,TEMP'
|
||||
target = 0 # switch off loop on startup
|
||||
|
||||
def update_target(self, module, value):
|
||||
self.change(f'DEV::TEMP:LOOP:ENAB', False, off_on)
|
||||
|
@ -166,7 +166,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
||||
def write_target(self, value):
|
||||
self.read_alive_time()
|
||||
if self._blocking_error:
|
||||
self.status = ERROR, 'clear_errors needed after ' + self._blocking_error
|
||||
self.status = ERROR, '<motor>.clear_errors() needed after ' + self._blocking_error
|
||||
raise HardwareError(self.status[1])
|
||||
self.saveParameters()
|
||||
self.start_machine(self.starting, target=value)
|
||||
|
@ -1,96 +0,0 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""sensirion flow sensor,
|
||||
|
||||
connected via an Arduio Nano on a serial connection
|
||||
shared with the trinamic hepump valve motor
|
||||
"""
|
||||
|
||||
import math
|
||||
from frappy.core import Parameter, Readable, IntRange, FloatRange, BoolType, BytesIO, HasIO
|
||||
from frappy.errors import ProgrammingError
|
||||
|
||||
|
||||
class FlowSensor(HasIO, Readable):
|
||||
value = Parameter(unit='ln/min')
|
||||
stddev = Parameter('std dev.', FloatRange(unit='ln/min'))
|
||||
nsamples = Parameter('number of samples for averaging', IntRange(1,1024), default=160)
|
||||
offset = Parameter('offset correction', FloatRange(unit='ln/min'), readonly=False, default=0)
|
||||
scale = Parameter('scale factor', FloatRange(), readonly=False, default=2.3)
|
||||
saved = Parameter('is the current value saved?', BoolType(), readonly=False)
|
||||
pollinterval = Parameter(default=0.2)
|
||||
|
||||
ioClass = BytesIO
|
||||
_saved = None
|
||||
|
||||
def command(self, cmd, nvalues=1):
|
||||
if len(cmd) == 1: # its a query
|
||||
command = f'{cmd}\n'
|
||||
else:
|
||||
if len(cmd) > 7:
|
||||
raise ProgrammingError('number does not fit into 6 characters')
|
||||
command = f'{cmd[0]}{cmd[1:].ljust(6)}\n'
|
||||
reply = self.io.communicate(command.encode('ascii'), max(1, nvalues * 9))
|
||||
if nvalues == 1:
|
||||
return float(reply)
|
||||
if nvalues:
|
||||
return tuple(float(s) for s in reply.split())
|
||||
return None
|
||||
|
||||
def doPoll(self):
|
||||
flow, stddev = self.command('?', nvalues=2)
|
||||
stddev = stddev / math.sqrt(self.nsamples)
|
||||
if (flow, stddev) != (self.value, self.stddev):
|
||||
self.value, self.stddev = flow, stddev
|
||||
# TODO: treat status (e.g. when reading 0 always)
|
||||
|
||||
def read_value(self):
|
||||
self.doPoll()
|
||||
return self.value
|
||||
|
||||
def read_nsamples(self):
|
||||
return self.command('n')
|
||||
|
||||
def write_nsamples(self, nsamples):
|
||||
return self.command(f'n{nsamples}')
|
||||
|
||||
def read_offset(self):
|
||||
return self.command('o')
|
||||
|
||||
def write_offset(self, offset):
|
||||
return self.command(f'o{offset:.2f}')
|
||||
|
||||
def read_scale(self):
|
||||
return self.command('g')
|
||||
|
||||
def write_scale(self, scale):
|
||||
return self.command(f'g{scale:.4f}')
|
||||
|
||||
def read_saved(self):
|
||||
if self._saved is None:
|
||||
self._saved = self.read_scale(), self.read_offset()
|
||||
return True
|
||||
return self._saved == (self.scale, self.offset)
|
||||
|
||||
def write_saved(self, target):
|
||||
if target:
|
||||
self.command('s', nvalues=0)
|
@ -26,8 +26,7 @@ import struct
|
||||
|
||||
from frappy.core import BoolType, Command, EnumType, FloatRange, IntRange, \
|
||||
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done, \
|
||||
IDLE, BUSY, ERROR, Limit, nopoll, ArrayOf
|
||||
from frappy.properties import HasProperties
|
||||
IDLE, BUSY, ERROR, Limit
|
||||
from frappy.io import BytesIO
|
||||
from frappy.errors import CommunicationFailedError, HardwareError, RangeError, IsBusyError
|
||||
from frappy.rwhandler import ReadHandler, WriteHandler
|
||||
@ -120,6 +119,9 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
move_status = Parameter('', EnumType(ok=0, stalled=1, encoder_deviation=2, stalled_and_encoder_deviation=3),
|
||||
group='hwstatus')
|
||||
error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
|
||||
home = Parameter('state of home switch (input 3)', BoolType(), group='more')
|
||||
has_home = Parameter('enable home and activate pullup resistor', BoolType(),
|
||||
default=True, group='more')
|
||||
auto_reset = Parameter('automatic reset after failure', BoolType(), group='more', readonly=False, default=False)
|
||||
free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
|
||||
value=0.1, group='motorparam')
|
||||
@ -130,20 +132,6 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
readonly=False, default=0, visibility=3, group='more')
|
||||
max_retry = Parameter('maximum number of retries', IntRange(0, 99), readonly=False, default=0)
|
||||
stall_thr = Parameter('stallGuard threshold', IntRange(-64, 63), readonly=False, value=0)
|
||||
input_bits = Parameter('input bits', IntRange(0, 255), group='more', export=False)
|
||||
input1 = Parameter('input 1', BoolType(), export=False, group='more')
|
||||
input2 = Parameter('input 2', BoolType(), export=False, group='more')
|
||||
input3 = Parameter('input 3', BoolType(), export=False, group='more')
|
||||
output0 = Parameter('output 0', BoolType(), readonly=False, export=False, group='more', default=0)
|
||||
output1 = Parameter('output 1', BoolType(), readonly=False, export=False, group='more', default=0)
|
||||
home = Parameter('state of home switch (input 3)', BoolType(), group='more', export=False)
|
||||
has_home = Property('enable home and activate pullup resistor', BoolType(), export=False,
|
||||
default=True)
|
||||
has_inputs = Property('inputs are polled', BoolType(), export=False,
|
||||
default=False)
|
||||
with_pullup = Property('activate pullup', BoolType(), export=False,
|
||||
default=True)
|
||||
|
||||
pollinterval = Parameter(group='more')
|
||||
target_min = Limit()
|
||||
target_max = Limit()
|
||||
@ -157,18 +145,6 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
_loading = False # True when loading parameters
|
||||
_drv_try = 0
|
||||
|
||||
def checkProperties(self):
|
||||
super().checkProperties()
|
||||
if self.has_home:
|
||||
self.parameters['home'].export = '_home'
|
||||
self.setProperty('has_inputs', True)
|
||||
if not self.has_inputs:
|
||||
self.setProperty('with_pullup', False)
|
||||
|
||||
def writeInitParams(self):
|
||||
super().writeInitParams()
|
||||
self.comm(SET_IO, 0, self.with_pullup)
|
||||
|
||||
def comm(self, cmd, adr, value=0, bank=0):
|
||||
"""set or get a parameter
|
||||
|
||||
@ -426,6 +402,13 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
def read_steppos(self):
|
||||
return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + self.zero
|
||||
|
||||
def read_home(self):
|
||||
return not self.comm(GET_IO, 255) & 8
|
||||
|
||||
def write_has_home(self, value):
|
||||
"""activate pullup resistor"""
|
||||
return bool(self.comm(SET_IO, 0, value))
|
||||
|
||||
@Command(FloatRange())
|
||||
def set_zero(self, value):
|
||||
"""adapt zero to make current position equal to given value"""
|
||||
@ -476,47 +459,3 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
||||
def set_axis_par(self, adr, value):
|
||||
"""set arbitrary motor parameter"""
|
||||
return self.comm(SET_AXIS_PAR, adr, value)
|
||||
|
||||
def read_input_bits(self):
|
||||
if not self.has_inputs:
|
||||
return 0
|
||||
bits = self.comm(GET_IO, 255)
|
||||
self.input1 = bool(bits & 2)
|
||||
self.input2 = bool(bits & 4)
|
||||
self.input3 = bool(bits & 8)
|
||||
self.home = not self.input3
|
||||
return bits
|
||||
|
||||
@nopoll
|
||||
def read_home(self):
|
||||
self.read_input_bits()
|
||||
return self.home
|
||||
|
||||
@nopoll
|
||||
def read_input1(self):
|
||||
self.read_input_bits()
|
||||
return self.input1
|
||||
|
||||
@nopoll
|
||||
def read_input2(self):
|
||||
self.read_input_bits()
|
||||
return self.input2
|
||||
|
||||
@nopoll
|
||||
def read_input3(self):
|
||||
self.read_input_bits()
|
||||
return self.input3
|
||||
|
||||
def write_output0(self, value):
|
||||
return self.comm(SET_IO, 0, value, bank=2)
|
||||
|
||||
@nopoll
|
||||
def read_output0(self):
|
||||
return self.comm(GET_IO, 0, bank=2)
|
||||
|
||||
def write_output1(self, value):
|
||||
return self.comm(SET_IO, 1, value, bank=2)
|
||||
|
||||
@nopoll
|
||||
def read_output1(self):
|
||||
return self.comm(GET_IO, 1, bank=2)
|
||||
|
@ -25,13 +25,11 @@ import time
|
||||
import numpy as np
|
||||
|
||||
from frappy_psi.adq_mr import Adq, PEdata, RUSdata
|
||||
from frappy.core import Attached, BoolType, Done, FloatRange, HasIO, StatusType, \
|
||||
IntRange, Module, Parameter, Readable, Writable, StatusType, StringIO, StringType, \
|
||||
IDLE, BUSY, DISABLED, WARN, ERROR, TupleOf, ArrayOf, Command, Attached, EnumType ,\
|
||||
Drivable
|
||||
from frappy.core import Attached, BoolType, Done, FloatRange, HasIO, \
|
||||
IntRange, Module, Parameter, Readable, Writable, Drivable, StringIO, StringType, \
|
||||
IDLE, BUSY, DISABLED, ERROR, TupleOf, ArrayOf, Command
|
||||
from frappy.properties import Property
|
||||
from frappy.lib import clamp
|
||||
# from frappy.modules import Collector
|
||||
#from frappy.modules import Collector
|
||||
|
||||
Collector = Readable
|
||||
|
||||
@ -48,12 +46,11 @@ def fname_from_time(t, extension):
|
||||
|
||||
class Roi(Readable):
|
||||
main = Attached()
|
||||
a = Attached(mandatory=False) # amplitude Readable
|
||||
p = Attached(mandatory=False) # phase Readable
|
||||
i = Attached(mandatory=False) # i Readable
|
||||
q = Attached(mandatory=False) # amplitude Readable
|
||||
|
||||
value = Parameter('i, q', TupleOf(FloatRange(), FloatRange()), default=(0, 0))
|
||||
value = Parameter('amplitude', FloatRange(), default=0)
|
||||
phase = Parameter('phase', FloatRange(unit='deg'), default=0)
|
||||
i = Parameter('in phase', FloatRange(), default=0)
|
||||
q = Parameter('out of phase', FloatRange(), default=0)
|
||||
time = Parameter('start time', FloatRange(unit='nsec'), readonly=False)
|
||||
size = Parameter('interval (symmetric around time)', FloatRange(unit='nsec'), readonly=False)
|
||||
enable = Parameter('calculate this roi', BoolType(), readonly=False, default=True)
|
||||
@ -83,44 +80,6 @@ class Roi(Readable):
|
||||
return Done
|
||||
|
||||
|
||||
class ControlRoi(Roi, Readable):
|
||||
freq = Attached()
|
||||
maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||
default=10000)
|
||||
minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||
readonly=False, default=4000)
|
||||
slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||
default=1e6)
|
||||
control_active = Parameter('are we controlling?', BoolType(), readonly=False, default=0)
|
||||
|
||||
_freq_target = None
|
||||
_skipctrl = 2
|
||||
_old = None
|
||||
|
||||
def doPoll(self):
|
||||
inphase = self.value[0]
|
||||
freq = self.freq.target
|
||||
if freq != self._freq_target:
|
||||
self._freq_target = freq
|
||||
# do no control 2 times after changing frequency
|
||||
self._skipctrl = 2
|
||||
if self.control_active:
|
||||
if self._old:
|
||||
newfreq = freq + inphase * self.slope
|
||||
fdif = freq - self._old[0]
|
||||
if abs(fdif) >= self.minstep:
|
||||
idif = inphase - self._old[1]
|
||||
self.slope = - fdif / idif
|
||||
else:
|
||||
# do a 'test' step
|
||||
newfreq = freq + self.minstep
|
||||
self.old = (freq, inphase)
|
||||
if self._skipctrl > 0: # do no control for some time after changing frequency
|
||||
self._skipctrl -= 1
|
||||
elif self.control_active:
|
||||
self._freq_target = self.freq.write_target(clamp(freq - self.maxstep, newfreq, freq + self.maxstep))
|
||||
|
||||
|
||||
class Pars(Module):
|
||||
description = 'relevant parameters from SEA'
|
||||
|
||||
@ -131,75 +90,36 @@ class Pars(Module):
|
||||
|
||||
|
||||
class FreqStringIO(StringIO):
|
||||
end_of_line = '\r\n'
|
||||
end_of_line = '\r'
|
||||
|
||||
|
||||
class Frequency(HasIO, Drivable):
|
||||
class Frequency(HasIO, Writable):
|
||||
value = Parameter('frequency', unit='Hz')
|
||||
amp = Parameter('amplitude (VPP)', FloatRange(unit='V'), readonly=False)
|
||||
output = Parameter('output: L or R', EnumType(L=1, R=0), readonly=False, default='L')
|
||||
amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False)
|
||||
|
||||
last_change = 0
|
||||
ioClass = FreqStringIO
|
||||
dif = None
|
||||
_started = 0
|
||||
_within_write_target = False
|
||||
_nopoll_until = 0
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
if self.isBusy() and time.time() > self._started + 5:
|
||||
self.status = WARN, 'acquisition timeout'
|
||||
|
||||
def register_dif(self, dif):
|
||||
self.dif = dif
|
||||
|
||||
def read_value(self):
|
||||
if time.time() > self._nopoll_until or self.value == 0:
|
||||
self.value = float(self.communicate('FREQ?'))
|
||||
if self.dif:
|
||||
self.dif.read_value()
|
||||
return self.value
|
||||
|
||||
def set_busy(self):
|
||||
"""called by an acquisition module
|
||||
|
||||
from a callback on value within write_target
|
||||
"""
|
||||
if self._within_write_target:
|
||||
self._started = time.time()
|
||||
self.log.info('set busy')
|
||||
self.status = BUSY, 'waiting for acquisition'
|
||||
|
||||
def set_idle(self):
|
||||
if self.isBusy():
|
||||
self.status = IDLE, ''
|
||||
|
||||
def write_target(self, value):
|
||||
self._nopoll_until = time.time() + 10
|
||||
try:
|
||||
self._within_write_target = True
|
||||
# may trigger busy=True from an acquisition module
|
||||
self.value = float(self.communicate('FREQ %.15g;FREQ?' % value))
|
||||
self.communicate('FREQ %.15g;FREQ?' % value)
|
||||
self.last_change = time.time()
|
||||
if self.dif:
|
||||
self.dif.read_value()
|
||||
return self.value
|
||||
finally:
|
||||
self._within_write_target = False
|
||||
|
||||
def write_amp(self, amp):
|
||||
self._nopoll_until = time.time() + 10
|
||||
self.amp = float(self.communicate(f'AMP{self.output.name} {amp} VPP;AMP{self.output.name}? VPP'))
|
||||
return self.amp
|
||||
reply = self.communicate('AMPR %g;AMPR?' % amp)
|
||||
return float(reply)
|
||||
|
||||
def read_amp(self):
|
||||
if time.time() > self._nopoll_until or self.amp == 0:
|
||||
return float(self.communicate(f'AMP{self.output.name}? VPP'))
|
||||
return self.amp
|
||||
reply = self.communicate('AMPR?')
|
||||
return float(reply)
|
||||
|
||||
|
||||
class FrequencyDif(Drivable):
|
||||
class FrequencyDif(Readable):
|
||||
freq = Attached(Frequency)
|
||||
base = Parameter('base frequency', FloatRange(unit='Hz'), default=0)
|
||||
value = Parameter('difference to base frequency', FloatRange(unit='Hz'), default=0)
|
||||
@ -208,114 +128,55 @@ class FrequencyDif(Drivable):
|
||||
super().initModule()
|
||||
self.freq.register_dif(self)
|
||||
|
||||
def write_value(self, target):
|
||||
self.freq.write_target(target + self.base)
|
||||
return self.value # this was updated in Frequency
|
||||
|
||||
def read_value(self):
|
||||
return self.freq.value - self.base
|
||||
return self.freq - self.base
|
||||
|
||||
|
||||
class Base(Collector):
|
||||
freq = Attached()
|
||||
adq = Attached(Adq)
|
||||
sr = Parameter('samples per record', datatype=IntRange(1, 1E9), default=16384)
|
||||
pollinterval = Parameter(datatype=FloatRange(0, 120)) # allow pollinterval = 0
|
||||
_data = None
|
||||
_data_args = None
|
||||
|
||||
def read_status(self):
|
||||
return self.freq.read_status()
|
||||
adqstate = self.adq.get_status()
|
||||
if adqstate == Adq.BUSY:
|
||||
return BUSY, 'acquiring'
|
||||
if adqstate == Adq.UNDEFINED:
|
||||
return ERROR, 'no data yet'
|
||||
if adqstate == Adq.READY:
|
||||
return IDLE, 'new data available'
|
||||
return IDLE, ''
|
||||
|
||||
def get_data(self):
|
||||
data = self.adq.get_data(*self._data_args)
|
||||
if id(data) != id(self._data):
|
||||
self._data = data
|
||||
return data
|
||||
return None
|
||||
|
||||
|
||||
class Base:
|
||||
freq = Attached()
|
||||
sr = Parameter('samples per record', datatype=IntRange(1, 1E9), default=16384)
|
||||
adq = None
|
||||
_rawsignal = None
|
||||
_fast_poll = 0.001
|
||||
|
||||
def shutdownModule(self):
|
||||
if self.adq:
|
||||
self.adq.deletecu()
|
||||
self.adq = None
|
||||
|
||||
@Command(argument=TupleOf(FloatRange(unit='ns'), FloatRange(unit='ns'), IntRange(0,99999)),
|
||||
result=TupleOf(FloatRange(),
|
||||
ArrayOf(ArrayOf(IntRange(-0x7fff, 0x7fff), 0, 99999))))
|
||||
def get_signal(self, start, end, npoints):
|
||||
"""get signal
|
||||
|
||||
:param start: start time (ns)
|
||||
:param end: end time (ns)
|
||||
:param npoints: hint for number of data points
|
||||
:return: (<time-step>, array of array of y)
|
||||
|
||||
for performance reasons the result data is rounded to int16
|
||||
"""
|
||||
# convert ns to samples
|
||||
sr = self.adq.sample_rate * 1e-9
|
||||
istart = round(start * sr)
|
||||
iend = min(self.sr, round(end * sr))
|
||||
nbin = max(1, round((iend - istart) / npoints))
|
||||
iend = iend // nbin * nbin
|
||||
return (nbin / sr,
|
||||
[np.round(ch[istart:iend].reshape((-1, nbin)).mean(axis=1)) for ch in self._rawsignal])
|
||||
|
||||
|
||||
class PulseEcho(Base, Readable):
|
||||
value = Parameter(default=0)
|
||||
class PulseEcho(Base):
|
||||
value = Parameter("t, i, q, pulse curves",
|
||||
TupleOf(*[ArrayOf(FloatRange(), 0, 16283) for _ in range(4)]), default=[[]] * 4)
|
||||
nr = Parameter('number of records', datatype=IntRange(1, 9999), default=500)
|
||||
bw = Parameter('bandwidth lowpassfilter', datatype=FloatRange(unit='Hz'), default=10E6)
|
||||
control = Parameter('control loop on?', BoolType(), readonly=False, default=True)
|
||||
time = Parameter('pulse start time', FloatRange(unit='nsec'),
|
||||
readonly=False)
|
||||
size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
|
||||
readonly=False)
|
||||
pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1)
|
||||
# curves = Attached(mandatory=False)
|
||||
pollinterval = Parameter('poll interval', datatype=FloatRange(0,120))
|
||||
|
||||
_starttime = None
|
||||
starttime = None
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.adq = Adq()
|
||||
self.adq.init(self.sr, self.nr)
|
||||
self.roilist = []
|
||||
self.setFastPoll(True, self._fast_poll)
|
||||
|
||||
def doPoll(self):
|
||||
try:
|
||||
data = self.adq.get_data()
|
||||
except Exception as e:
|
||||
self.status = ERROR, repr(e)
|
||||
return
|
||||
if data is None:
|
||||
if self.adq.busy:
|
||||
return
|
||||
self.adq.start(PEdata(self.adq))
|
||||
self.setFastPoll(True, self._fast_poll)
|
||||
return
|
||||
|
||||
roilist = [r for r in self.roilist if r.enable]
|
||||
freq = self.freq.read_value()
|
||||
if not freq:
|
||||
self.log.info('freq=0')
|
||||
return
|
||||
gates, curves = data.gates_and_curves(
|
||||
freq, (self.time, self.time + self.size),
|
||||
[(r.time, r.time + r.size) for r in roilist], self.bw)
|
||||
for i, roi in enumerate(roilist):
|
||||
a = gates[i][0]
|
||||
b = gates[i][1]
|
||||
roi.value = a, b
|
||||
if roi.i:
|
||||
roi.i.value = a
|
||||
if roi.q:
|
||||
roi.q.value = b
|
||||
if roi.a:
|
||||
roi.a.value = math.sqrt(a ** 2 + b ** 2)
|
||||
if roi.p:
|
||||
roi.p.value = math.atan2(a, b) * 180 / math.pi
|
||||
self._curves = curves
|
||||
self._rawsignal = data.rawsignal
|
||||
|
||||
@Command(result=TupleOf(*[ArrayOf(FloatRange(), 0, 99999)
|
||||
for _ in range(4)]))
|
||||
def get_curves(self):
|
||||
"""retrieve curves"""
|
||||
return self._curves
|
||||
|
||||
def write_nr(self, value):
|
||||
self.adq.init(self.sr, value)
|
||||
@ -329,170 +190,124 @@ class PulseEcho(Base, Readable):
|
||||
def register_roi(self, roi):
|
||||
self.roilist.append(roi)
|
||||
|
||||
def go(self):
|
||||
self.starttime = time.time()
|
||||
self.adq.start()
|
||||
|
||||
CONTINUE = 0
|
||||
GO = 1
|
||||
DONE_GO = 2
|
||||
WAIT_GO = 3
|
||||
def read_value(self):
|
||||
if self.get_rawdata(): # new data available
|
||||
roilist = [r for r in self.roilist if r.enable]
|
||||
freq = self.freq.value
|
||||
gates = self.adq.gates_and_curves(self._data, freq,
|
||||
(self.time, self.time + self.size),
|
||||
[r.interval for r in roilist])
|
||||
for i, roi in enumerate(roilist):
|
||||
roi.i = a = gates[i][0]
|
||||
roi.q = b = gates[i][1]
|
||||
roi.value = math.sqrt(a ** 2 + b ** 2)
|
||||
roi.phase = math.atan2(a, b) * 180 / math.pi
|
||||
return self.adq.curves
|
||||
|
||||
# TODO: CONTROL
|
||||
# inphase = self.roilist[0].i
|
||||
# if self.control:
|
||||
# newfreq = freq + inphase * self.slope - self.basefreq
|
||||
# # step = sorted((-self.maxstep, inphase * self.slope, self.maxstep))[1]
|
||||
# if self.old:
|
||||
# fdif = freq - self.old[0]
|
||||
# idif = inphase - self.old[1]
|
||||
# if abs(fdif) >= self.minstep:
|
||||
# self.slope = - fdif / idif
|
||||
# else:
|
||||
# fdif = 0
|
||||
# idif = 0
|
||||
# newfreq = freq + self.minstep
|
||||
# self.old = (freq, inphase)
|
||||
# if self.skipctrl > 0: # do no control for some time after changing frequency
|
||||
# self.skipctrl -= 1
|
||||
# elif self.control:
|
||||
# self.freq = sorted((self.freq - self.maxstep, newfreq, self.freq + self.maxstep))[1]
|
||||
|
||||
|
||||
class RUS(Base, Collector):
|
||||
freq = Attached()
|
||||
imod = Attached(mandatory=False)
|
||||
qmod = Attached(mandatory=False)
|
||||
class RUS(Base):
|
||||
value = Parameter('averaged (I, Q) tuple', TupleOf(FloatRange(), FloatRange()))
|
||||
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
||||
periods = Parameter('number of periods', IntRange(1, 999999), default=12, readonly=False)
|
||||
input_delay = Parameter('throw away everything before this time',
|
||||
FloatRange(unit='ns'), default=10000, readonly=False)
|
||||
input_range = Parameter('input range (taking into account attenuation)', FloatRange(unit='V'),
|
||||
default=10, readonly=False)
|
||||
output_range = Parameter('output range', FloatRange(unit='V'),
|
||||
default=1, readonly=False)
|
||||
input_amplitude = Parameter('input signal amplitude', FloatRange(unit='V'), default=0)
|
||||
output_amplitude = Parameter('output signal amplitude', FloatRange(unit='V'), default=0)
|
||||
phase = Parameter('phase', FloatRange(unit='deg'), default=0)
|
||||
amp = Parameter('amplitude', FloatRange(), default=0)
|
||||
continuous = Parameter('continuous mode', BoolType(), readonly=False, default=True)
|
||||
pollinterval = Parameter(datatype=FloatRange(0, 120), default=5)
|
||||
periods = Parameter('number of periods', IntRange(1, 9999), default=12)
|
||||
scale = Parameter('scale,taking into account input attenuation', FloatRange(), default=0.1)
|
||||
input_phase_stddev = Parameter('input signal quality', FloatRange(unit='rad'))
|
||||
output_phase_slope = Parameter('output signal phase slope', FloatRange(unit='rad/sec'))
|
||||
output_amp_slope = Parameter('output signal amplitude change', FloatRange(unit='1/sec'))
|
||||
phase = Parameter('phase', FloatRange(unit='deg'))
|
||||
amp = Parameter('amplitude', FloatRange())
|
||||
|
||||
_starttime = None
|
||||
_iq = 0
|
||||
_wait_until = 0 # deadline for returning to continuous mode
|
||||
_action = CONTINUE # one of CONTINUE, GO, DONE_GO, WAIT_GO
|
||||
_status = IDLE, 'no data yet'
|
||||
_busy = False # waiting for end of aquisition (not the same as self.status[0] == BUSY)
|
||||
_requested_freq = None
|
||||
starttime = None
|
||||
_data_args = None
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.adq = Adq()
|
||||
self.freq.addCallback('value', self.update_freq)
|
||||
self.freq.addCallback('target', self.update_freq_target)
|
||||
# self.write_periods(self.periods)
|
||||
|
||||
def update_freq_target(self, value):
|
||||
self.go()
|
||||
|
||||
def update_freq(self, value):
|
||||
self.setFastPoll(True, self._fast_poll)
|
||||
self._requested_freq = value
|
||||
self.freq.set_busy() # is only effective when the update was trigger within freq.write_target
|
||||
|
||||
def get_quality_info(self, data):
|
||||
"""hook for RESqual"""
|
||||
data.timer.show()
|
||||
|
||||
def doPoll(self):
|
||||
try:
|
||||
data = self.adq.get_data()
|
||||
except Exception as e:
|
||||
self.set_status(ERROR, repr(e))
|
||||
self._busy = False
|
||||
self._action = WAIT_GO
|
||||
self.wait_until = time.time() + 2
|
||||
return
|
||||
|
||||
if data: # this is new data
|
||||
self._busy = False
|
||||
self.get_quality_info(data) # hook for RUSqual
|
||||
self._rawsignal = data.rawsignal
|
||||
self.input_amplitude = data.inp.amplitude * self.input_range
|
||||
self.output_amplitude = data.out.amplitude * self.output_range
|
||||
self._iq = iq = data.iq * self.output_range / self.input_range
|
||||
self.phase = np.arctan2(iq.imag, iq.real) * 180 / np.pi
|
||||
self.amp = np.abs(iq)
|
||||
self.read_value()
|
||||
self.set_status(IDLE, '')
|
||||
if self.freq.isBusy():
|
||||
if data.freq == self._requested_freq:
|
||||
self.log.info('set freq idle %.3f', time.time() % 1.0)
|
||||
self.freq.set_idle()
|
||||
else:
|
||||
self.log.warn('freq does not match: requested %.14g, from data: %.14g',
|
||||
self._requested_freq, data.freq)
|
||||
else:
|
||||
self.log.info('freq not busy %.3f', time.time() % 1.0)
|
||||
if self._action == CONTINUE:
|
||||
self.setFastPoll(False)
|
||||
self.log.info('slow')
|
||||
return
|
||||
elif self._busy:
|
||||
if self._action == DONE_GO:
|
||||
self.log.info('busy')
|
||||
self.set_status(BUSY, 'acquiring')
|
||||
else:
|
||||
self.set_status(IDLE, 'acquiring')
|
||||
return
|
||||
if self._action == CONTINUE and self.continuous:
|
||||
print('CONTINUE')
|
||||
self.start_acquisition()
|
||||
self.set_status(IDLE, 'acquiring')
|
||||
return
|
||||
if self._action == GO:
|
||||
print('pending GO')
|
||||
self.start_acquisition()
|
||||
self._action = DONE_GO
|
||||
self.set_status(BUSY, 'acquiring')
|
||||
return
|
||||
if self._action == DONE_GO:
|
||||
self._action = WAIT_GO
|
||||
self._wait_until = time.time() + 2
|
||||
self.set_status(IDLE, 'paused')
|
||||
return
|
||||
if self._action == WAIT_GO:
|
||||
if time.time() > self._wait_until:
|
||||
self._action = CONTINUE
|
||||
self.start_acquisition()
|
||||
self.set_status(IDLE, 'acquiring')
|
||||
|
||||
def set_status(self, *status):
|
||||
self._status = status
|
||||
if self._status != self.status:
|
||||
self.read_status()
|
||||
|
||||
def read_status(self):
|
||||
return self._status
|
||||
|
||||
def read_value(self):
|
||||
if self.imod:
|
||||
self.imod.value = self._iq.real
|
||||
if self.qmod:
|
||||
self.qmod.value = self._iq.imag
|
||||
return self._iq.real, self._iq.imag
|
||||
if self._data_args is None:
|
||||
return self.value # or may we raise as no value is defined yet?
|
||||
data = self.get_data(RUSdata, *self._data_args)
|
||||
if data:
|
||||
# data available
|
||||
data.calc_quality()
|
||||
self.input_phase_stddev = data.input_stddev.imag
|
||||
self.output_phase_slope = data.output_slope.imag
|
||||
self.output_amp_slope = data.output_slope.real
|
||||
|
||||
iq = data.iq * self.scale
|
||||
self.phase = np.arctan2(iq.imag, iq.real) * 180 / np.pi
|
||||
self.amp = np.abs(iq.imag, iq.real)
|
||||
return iq.real, iq.imag
|
||||
return self.value
|
||||
|
||||
@Command
|
||||
def go(self):
|
||||
"""start acquisition"""
|
||||
self.log.info('go %.3f', time.time() % 1.0)
|
||||
if self._busy:
|
||||
self._action = GO
|
||||
else:
|
||||
self._action = DONE_GO
|
||||
self.start_acquisition()
|
||||
self._status = BUSY, 'acquiring'
|
||||
self.starttime = time.time()
|
||||
freq = self.freq.value
|
||||
self._data_args = (RUSdata, freq, self.periods)
|
||||
self.sr = round(self.periods * self.adq.sample_rate / freq)
|
||||
self.adq.init(self.sr, 1)
|
||||
self.adq.start()
|
||||
self.read_status()
|
||||
|
||||
def start_acquisition(self):
|
||||
self.log.info('start %.3f', time.time() % 1.0)
|
||||
freq = self.freq.read_value()
|
||||
self.sr = round(self.periods * self.adq.sample_rate / freq)
|
||||
delay_samples = round(self.input_delay * self.adq.sample_rate * 1e-9)
|
||||
self.adq.init(self.sr + delay_samples, 1)
|
||||
self.adq.start(RUSdata(self.adq, freq, self.periods, delay_samples))
|
||||
self._busy = True
|
||||
self.setFastPoll(True, self._fast_poll)
|
||||
|
||||
class ControlLoop:
|
||||
maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||
default=10000)
|
||||
minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||
readonly=False, default=4000)
|
||||
slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||
default=1e6)
|
||||
|
||||
|
||||
class RUSqual(RUS):
|
||||
"""version with additional info about quality of input and output signal"""
|
||||
|
||||
input_phase_stddev = Parameter('input signal quality', FloatRange(unit='rad'), default=0)
|
||||
output_phase_slope = Parameter('output signal phase slope', FloatRange(unit='rad/sec'), default=0)
|
||||
output_amp_slope = Parameter('output signal amplitude change', FloatRange(unit='1/sec'), default=0)
|
||||
|
||||
def get_quality_info(self, data):
|
||||
qual = data.get_quality()
|
||||
self.input_phase_stddev = qual.input_stddev.imag
|
||||
self.output_phase_slope = qual.output_slope.imag
|
||||
self.output_amp_slope = qual.output_slope.real
|
||||
# class Frequency(HasIO, Readable):
|
||||
# pars = Attached()
|
||||
# curves = Attached(mandatory=False)
|
||||
# maxy = Property('plot y scale', datatype=FloatRange(), default=0.5)
|
||||
#
|
||||
# value = Parameter('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0)
|
||||
# basefreq = Parameter('base frequency', FloatRange(unit='Hz'), readonly=False)
|
||||
# nr = Parameter('number of records', datatype=IntRange(1,10000), default=500)
|
||||
# sr = Parameter('samples per record', datatype=IntRange(1,1E9), default=16384)
|
||||
# freq = Parameter('target frequency', FloatRange(unit='Hz'), readonly=False)
|
||||
# bw = Parameter('bandwidth lowpassfilter', datatype=FloatRange(unit='Hz'),default=10E6)
|
||||
# amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False)
|
||||
# control = Parameter('control loop on?', BoolType(), readonly=False, default=True)
|
||||
# rusmode = Parameter('RUS mode on?', BoolType(), readonly=False, default=False)
|
||||
# time = Parameter('pulse start time', FloatRange(unit='nsec'),
|
||||
# readonly=False)
|
||||
# size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
|
||||
# readonly=False)
|
||||
# pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1)
|
||||
# maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||
# default=10000)
|
||||
# minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||
# readonly=False, default=4000)
|
||||
# slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||
# default=1e6)
|
||||
# plot = Parameter('create plot images', BoolType(), readonly=False, default=True)
|
||||
# save = Parameter('save data', BoolType(), readonly=False, default=True)
|
||||
# pollinterval = Parameter(datatype=FloatRange(0,120))
|
||||
|
@ -96,7 +96,7 @@ def print_commit(line):
|
||||
print(' '.join(output), title)
|
||||
cnt[0] += 1
|
||||
if cnt[0] % 50 == 0:
|
||||
if input(f' {br0:11s} {br1:11s}--- press any letter to continue, return to stop ---') == '':
|
||||
if input(f' {br0:11s} {br1:11s}'):
|
||||
raise StopIteration()
|
||||
|
||||
|
||||
|
@ -1,76 +0,0 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""tests for probable implementation errors."""
|
||||
|
||||
import sys
|
||||
import importlib
|
||||
from glob import glob
|
||||
import pytest
|
||||
from frappy.core import Module, Drivable
|
||||
from frappy.errors import ProgrammingError
|
||||
|
||||
|
||||
all_drivables = set()
|
||||
for pyfile in glob('frappy_*/*.py') + glob('frappy/*.py'):
|
||||
module = pyfile[:-3].replace('/', '.')
|
||||
try:
|
||||
importlib.import_module(module)
|
||||
except Exception as e:
|
||||
print(module, e)
|
||||
continue
|
||||
for obj_ in sys.modules[module].__dict__.values():
|
||||
if isinstance(obj_, type) and issubclass(obj_, Drivable):
|
||||
all_drivables.add(obj_)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('modcls', all_drivables)
|
||||
def test_stop_doc(modcls):
|
||||
# make sure that implemented stop methods have a doc string
|
||||
if (modcls.stop.description == Drivable.stop.description
|
||||
and modcls.stop.func != Drivable.stop.func):
|
||||
assert modcls.stop.func.__doc__ # stop method needs a doc string
|
||||
|
||||
|
||||
def test_bad_method_test():
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod1(Module): # pylint: disable=unused-variable
|
||||
def read_param(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod2(Module): # pylint: disable=unused-variable
|
||||
def write_param(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod3(Module): # pylint: disable=unused-variable
|
||||
def do_cmd(self):
|
||||
pass
|
||||
|
||||
# no complain in this case
|
||||
# checking this would make code to check much more complicated.
|
||||
# in the rare cases used it might even be intentional
|
||||
class Mixin:
|
||||
def read_param(self):
|
||||
pass
|
||||
|
||||
class ModTest(Mixin, Module): # pylint: disable=unused-variable
|
||||
pass
|
@ -23,6 +23,8 @@
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import importlib
|
||||
from glob import glob
|
||||
import pytest
|
||||
|
||||
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
||||
@ -920,6 +922,27 @@ def test_interface_classes(bases, iface_classes):
|
||||
assert m.interface_classes == iface_classes
|
||||
|
||||
|
||||
all_drivables = set()
|
||||
for pyfile in glob('frappy_*/*.py'):
|
||||
module = pyfile[:-3].replace('/', '.')
|
||||
try:
|
||||
importlib.import_module(module)
|
||||
except Exception as e:
|
||||
print(module, e)
|
||||
continue
|
||||
for obj_ in sys.modules[module].__dict__.values():
|
||||
if isinstance(obj_, type) and issubclass(obj_, Drivable):
|
||||
all_drivables.add(obj_)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('modcls', all_drivables)
|
||||
def test_stop_doc(modcls):
|
||||
# make sure that implemented stop methods have a doc string
|
||||
if (modcls.stop.description == Drivable.stop.description
|
||||
and modcls.stop.func != Drivable.stop.func):
|
||||
assert modcls.stop.func.__doc__ # stop method needs a doc string
|
||||
|
||||
|
||||
def test_write_error():
|
||||
updates = {}
|
||||
srv = ServerStub(updates)
|
||||
|
@ -26,7 +26,7 @@ import pytest
|
||||
|
||||
from frappy.datatypes import BoolType, FloatRange, IntRange, StructOf
|
||||
from frappy.errors import ProgrammingError
|
||||
from frappy.modulebase import HasAccessibles, Module
|
||||
from frappy.modulebase import HasAccessibles
|
||||
from frappy.params import Command, Parameter
|
||||
|
||||
|
||||
@ -149,105 +149,3 @@ def test_update_unchanged_ok(arg, value):
|
||||
def test_update_unchanged_fail(arg):
|
||||
with pytest.raises(ProgrammingError):
|
||||
Parameter('', datatype=FloatRange(), default=0, update_unchanged=arg)
|
||||
|
||||
|
||||
def make_module(cls):
|
||||
class DispatcherStub:
|
||||
def announce_update(self, moduleobj, pobj):
|
||||
pass
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
|
||||
class ServerStub:
|
||||
dispatcher = DispatcherStub()
|
||||
secnode = None
|
||||
|
||||
return cls('test', LoggerStub(), {'description': 'test'}, ServerStub())
|
||||
|
||||
|
||||
def test_optional_parameters():
|
||||
class Base(Module):
|
||||
p1 = Parameter('overridden', datatype=FloatRange(),
|
||||
default=1, readonly=False, optional=True)
|
||||
p2 = Parameter('not overridden', datatype=FloatRange(),
|
||||
default=2, readonly=False, optional=True)
|
||||
|
||||
class Mod(Base):
|
||||
p1 = Parameter()
|
||||
|
||||
def read_p1(self):
|
||||
return self.p1
|
||||
|
||||
def write_p1(self, value):
|
||||
return value
|
||||
|
||||
assert Base.accessibles['p2'].optional
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod2(Base): # pylint: disable=unused-variable
|
||||
def read_p2(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod3(Base): # pylint: disable=unused-variable
|
||||
def write_p2(self):
|
||||
pass
|
||||
|
||||
base = make_module(Base)
|
||||
mod = make_module(Mod)
|
||||
|
||||
assert 'p1' not in base.accessibles
|
||||
assert 'p1' not in base.parameters
|
||||
assert 'p2' not in base.accessibles
|
||||
assert 'p2' not in base.parameters
|
||||
|
||||
assert 'p1' in mod.accessibles
|
||||
assert 'p1' in mod.parameters
|
||||
assert 'p2' not in mod.accessibles
|
||||
assert 'p2' not in mod.parameters
|
||||
|
||||
assert mod.p1 == 1
|
||||
assert mod.read_p1() == 1
|
||||
mod.p1 = 11
|
||||
assert mod.read_p1() == 11
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
assert mod.p2
|
||||
with pytest.raises(AttributeError):
|
||||
mod.read_p2()
|
||||
with pytest.raises(ProgrammingError):
|
||||
mod.p2 = 2
|
||||
with pytest.raises(AttributeError):
|
||||
mod.write_p2(2)
|
||||
|
||||
|
||||
def test_optional_commands():
|
||||
class Base(Module):
|
||||
c1 = Command(FloatRange(1), result=FloatRange(2), description='overridden', optional=True)
|
||||
c2 = Command(description='not overridden', optional=True)
|
||||
|
||||
class Mod(Base):
|
||||
def c1(self, value):
|
||||
return value + 1
|
||||
|
||||
base = make_module(Base)
|
||||
mod = make_module(Mod)
|
||||
|
||||
assert 'c1' not in base.accessibles
|
||||
assert 'c1' not in base.commands
|
||||
assert 'c2' not in base.accessibles
|
||||
assert 'c2' not in base.commands
|
||||
|
||||
assert 'c1' in mod.accessibles
|
||||
assert 'c1' in mod.commands
|
||||
assert 'c2' not in mod.accessibles
|
||||
assert 'c2' not in mod.commands
|
||||
|
||||
assert mod.c1(7) == 8
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
mod.c2()
|
||||
|
Loading…
x
Reference in New Issue
Block a user