first try on a fancy upload_custom_dap_script

This commit is contained in:
2025-11-05 18:36:17 +01:00
parent 9fadc3cce9
commit 75e9c0cfe0
2 changed files with 184 additions and 0 deletions
@@ -9,6 +9,7 @@ from .pids import align_pid_left, align_pid_right, aligned_pid_and_n
from .tools import get_current_pulseid
from .poweron import guided_power_on
from .jfstatus import color_bar, header_bar
from .customdap import upload_custom_dap_script
class BrokerClient:
@@ -152,6 +153,10 @@ class BrokerClient:
take_pedestal(self.restapi, self.config, detectors=detectors, rate=rate, pedestalmode=pedestalmode)
def upload_custom_dap_script(self, fname):
upload_custom_dap_script(self.restapi, fname)
@forwards_to(guided_power_on, nfilled=1)
def guided_power_on(self, *args, **kwargs):
guided_power_on(self, *args, **kwargs)
+179
View File
@@ -0,0 +1,179 @@
import sys
import time
import linecache
import importlib.util as ilu
from collections import defaultdict
from pathlib import Path
import numpy as np
import timeit
def upload_custom_dap_script(restapi, fname, *args, name=None, **kwargs):
name = name or Path(fname).stem
code = read_file(fname)
func = load_proc_from_file(fname)
test_run(func)
msg = restapi.upload_custom_dap_script(name, code, *args, **kwargs)
print(msg)
def read_file(fn):
with open(fn) as f:
return f.read()
def load_proc_from_file(fn):
mod = load_module(fn)
proc_func_name = "proc"
mod_name = mod.__name__
func = getattr(mod, proc_func_name, None) or getattr(mod, mod_name, None)
if func is None:
raise AttributeError(f'module "{mod_name}" contains neither "{proc_func_name}" nor "{mod_name}" function')
return func
def load_module(file_path, module_name=None):
module_name = module_name or Path(file_path).stem
spec = ilu.spec_from_file_location(module_name, file_path)
module = ilu.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_run(func, max_time=0.1):
shape = (1024, 512) #TODO: does this have to be the correct JF's size?
image = np.random.random(shape)
mask = image < 0.5
meta = {} #TODO: add some/all possible entries
orig_meta = meta.copy()
orig_image = image.copy()
orig_mask = mask.copy()
with LineProfiler() as lp:
func(meta, image, mask)
name = func.__name__
if meta != orig_meta:
raise RuntimeError(f'function "{name}" modifies the metadata -- this is not allowed, return the result(s) instead')
compare(name, "image", orig_image, image)
compare(name, "mask", orig_mask, mask)
run = lambda: func(meta, image, mask)
mean, _std, msg = timeit_verbose(run)
print("Timing results:", msg)
if mean <= max_time:
return
print("Profiling results:")
lp.print(func.__code__.co_filename)
print()
raise RuntimeError(f'function "{name}" runs for {mean:.3g}s on average -- this is too slow, check the profiling results')
def compare(name, what, before, after):
if not np.array_equal(after, before, equal_nan=True):
print(f'function "{name}" modifies the {what} -- this has no effect outside the function itself')
def timeit_verbose(func, min_time=0.2, target_time=2, min_repeat=3):
timer = timeit.Timer(func)
number, repeat = find_number_and_repeat(timer, min_time, target_time, min_repeat)
times = run_timer(timer, number, repeat)
mean, std = calc_stats(times, number)
msg = f"{fmt_secs(mean)} ± {fmt_secs(std)} per loop (mean ± std. dev. of {repeat:,} runs, {number:,} loops each)"
return mean, std, msg
def find_number_and_repeat(timer, min_time, target_time, min_repeat):
"""
find number so that the total time per repeat >= min_time
pick repeat so that the total time overall ~ target_time, but at least min_repeat
"""
number = 1
total_time = timer.timeit(number)
while total_time < min_time:
number *= 10
total_time = timer.timeit(number)
repeat = int(round(target_time / total_time))
repeat = max(min_repeat, repeat)
return number, repeat
def run_timer(timer, number, repeat):
return [timer.timeit(number) for _ in range(repeat)]
def calc_stats(times, number):
mean = np.mean(times) / number
std = np.std(times) / number
return mean, std
def fmt_secs(time):
UNITS = {
"n": 1e9,
"µ": 1e6,
"m": 1e3,
"": 1
}
for prefix, factor in UNITS.items():
current = time * factor
if current < 1000 or factor == 1:
return f"{current:.3g} {prefix}s"
class LineProfiler:
def __init__(self):
self.timings = defaultdict(lambda: defaultdict(int)) # one per file with timing per lineno/line
self.prev_time = None
self.prev_frame = None
self.prev_lineno = None
def __enter__(self):
sys.settrace(self.tracer)
return self
def __exit__(self, _exc_type, _exc_value, _traceback):
sys.settrace(None)
def tracer(self, frame, event, _arg):
now = time.perf_counter()
if self.prev_time is not None:
filename = self.prev_frame.f_code.co_filename
lineno = self.prev_lineno
line = linecache.getline(filename, lineno).rstrip("\n")
key = (lineno, line)
delta = now - self.prev_time
self.timings[filename][key] += delta
self.prev_time = now
self.prev_frame = frame
self.prev_lineno = frame.f_lineno
return self.tracer
def print(self, fname):
entries = self.timings[fname]
print(f"\nFile: {fname}")
for (lineno, line), timing in sorted(entries.items()):
print(f"{lineno:4} {timing*1e3:8.3f} ms | {line}")
def print_all(self):
for fname in sorted(self.timings):
self.print(fname)