Skip to content

Commit 66b33e6

Browse files
committed
Merge branch 'refactor-gui-tests'
2 parents f810452 + 020f3df commit 66b33e6

15 files changed

Lines changed: 3840 additions & 1142 deletions

File tree

ami/client/flowchart.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,11 @@ def __init__(
219219
prometheus_dir=None,
220220
prometheus_port=None,
221221
hutch="",
222+
headless=False,
222223
):
223224

224225
super().__init__()
226+
self.headless = headless
225227

226228
if loop is None:
227229
self.app = QtWidgets.QApplication([])
@@ -358,9 +360,13 @@ def display(self, msg):
358360
if msg.label:
359361
self.updateWindowTitle(msg.label)
360362

361-
self.win.show()
362-
if self.node.viewed:
363-
self.win.activateWindow()
363+
# Skip showing window in headless mode (e.g., for automated tests)
364+
# Window object is still created but not displayed
365+
if not self.headless:
366+
self.win.show()
367+
if self.node.viewed:
368+
self.win.activateWindow()
369+
364370
self.node.viewed = True
365371

366372
def reloadLibrary(self, msg):
@@ -391,6 +397,10 @@ async def send_checkpoint(self, node, event="sigStateChanged"):
391397
def start_prometheus(self):
392398
port = self.prometheus_port
393399

400+
# Skip starting Prometheus if no port is specified
401+
if self.prometheus_dir is None:
402+
return
403+
394404
while True:
395405
try:
396406
pc.start_http_server(port)
@@ -421,13 +431,22 @@ class MessageBroker(object):
421431
"""
422432

423433
def __init__(
424-
self, graphmgr_addr, load, ipcdir=None, prometheus_dir=None, prometheus_port=None, hutch="", save_dir=None
434+
self,
435+
graphmgr_addr,
436+
load,
437+
ipcdir=None,
438+
prometheus_dir=None,
439+
prometheus_port=None,
440+
hutch="",
441+
save_dir=None,
442+
headless=False,
425443
):
426444

427445
if ipcdir is None:
428446
ipcdir = tempfile.mkdtemp()
429447

430448
self.graphmgr_addr = graphmgr_addr
449+
self.headless = headless
431450
self.broker_sub_addr = "ipc://%s/broker_sub" % ipcdir
432451
self.broker_pub_addr = "ipc://%s/broker_pub" % ipcdir
433452

@@ -579,6 +598,7 @@ async def monitor_processes(self):
579598
"prometheus_dir": self.prometheus_dir,
580599
"prometheus_port": self.prometheus_port,
581600
"hutch": self.hutch,
601+
"headless": self.headless,
582602
},
583603
daemon=True,
584604
)
@@ -602,6 +622,7 @@ async def process_messages(self):
602622
"prometheus_dir": self.prometheus_dir,
603623
"prometheus_port": self.prometheus_port,
604624
"hutch": self.hutch,
625+
"headless": self.headless,
605626
},
606627
daemon=True,
607628
)

ami/data.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1659,7 +1659,12 @@ def _random_events(self):
16591659
else:
16601660
event[name] = value
16611661
elif config["dtype"] == "Waveform" or config["dtype"] == "Image":
1662-
event[name] = np.random.normal(config["pedestal"], config["width"], config["shape"])
1662+
if config.get("binary", False):
1663+
# Generate binary array (0 or 1) with 50/50 distribution
1664+
event[name] = np.random.randint(0, 2, config["shape"])
1665+
else:
1666+
# Normal continuous values
1667+
event[name] = np.random.normal(config["pedestal"], config["width"], config["shape"])
16631668
elif config["dtype"] == "List":
16641669
if config.get("type", "integer") == "integer":
16651670
event[name] = list(

ami/fc_to_worker.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate worker configuration for data sources from .fc file.
4+
5+
This script extracts source nodes from a .fc flowchart file and generates
6+
a worker configuration file that can be used with ami-local to test
7+
the flowchart with mock data (static or random).
8+
9+
Usage:
10+
ami-fc-to-source <fc_file> [options]
11+
12+
Examples:
13+
# Generate worker.json from .fc file (random source by default)
14+
ami-fc-to-source tests/graphs/ATM_crix_new.fc
15+
16+
# Specify custom output file and event count
17+
ami-fc-to-source my_graph.fc -o my_worker.json -n 100
18+
19+
# Generate for static source
20+
ami-fc-to-source my_graph.fc --source-type static
21+
22+
# Then use with ami-local
23+
ami-local -n 3 random://worker.json -l my_graph.fc
24+
"""
25+
26+
import argparse
27+
import json
28+
import sys
29+
from pathlib import Path
30+
31+
32+
def extract_sources_from_fc(fc_path):
33+
"""
34+
Parse .fc file and extract source node configurations.
35+
36+
Args:
37+
fc_path: Path to .fc file
38+
39+
Returns:
40+
dict: Source configurations for worker.json
41+
"""
42+
with open(fc_path, "r") as f:
43+
data = json.load(f)
44+
45+
sources = {}
46+
for node in data.get("nodes", []):
47+
if node.get("class") == "SourceNode":
48+
name = node["name"]
49+
terminals = node.get("state", {}).get("terminals", {})
50+
if "Out" in terminals:
51+
ttype = terminals["Out"].get("ttype", "")
52+
sources[name] = map_amitypes_to_config(ttype, source_name=name)
53+
54+
return sources
55+
56+
57+
def map_amitypes_to_config(ttype, source_name=""):
58+
"""
59+
Map amitypes type string to static source config.
60+
61+
Args:
62+
ttype: String like "amitypes.Array2d" or "amitypes.array.Array2d"
63+
source_name: Name of the source node (e.g., "timing:raw:eventcodes")
64+
65+
Returns:
66+
dict: Config for static data source
67+
"""
68+
# Default config
69+
default = {"dtype": "Scalar", "range": [0, 100]}
70+
71+
if not ttype:
72+
return default
73+
74+
# Extract base type (handle both amitypes.Array2d and amitypes.array.Array2d)
75+
if "Array2d" in ttype:
76+
return {"dtype": "Image", "pedestal": 5, "width": 1, "shape": [512, 512]}
77+
elif "Array1d" in ttype:
78+
# Special handling for timing event codes
79+
if source_name == "timing:raw:eventcodes":
80+
return {"dtype": "Waveform", "pedestal": 0, "width": 0, "shape": [300], "binary": True}
81+
else:
82+
return {"dtype": "Waveform", "pedestal": 5, "width": 1, "shape": [1024]}
83+
elif "Array3d" in ttype:
84+
return {"dtype": "Image", "pedestal": 5, "width": 1, "shape": [100, 512, 512]}
85+
elif "int" in ttype.lower():
86+
return {"dtype": "Scalar", "range": [0, 100], "integer": True}
87+
elif "float" in ttype.lower():
88+
return {"dtype": "Scalar", "range": [0.0, 100.0]}
89+
else:
90+
return default
91+
92+
93+
def generate_worker_json(fc_path, num_events=100, repeat=True, interval=0.01, init_time=0.1, source_type="random"):
94+
"""
95+
Generate worker configuration from .fc file.
96+
97+
Args:
98+
fc_path: Path to .fc file
99+
num_events: Number of events to generate (default: 100)
100+
repeat: Whether to loop events (default: True)
101+
interval: Time between events in seconds (default: 0.01)
102+
init_time: Initial wait time in seconds (default: 0.1)
103+
source_type: Type of source - 'static' or 'random' (default: 'random')
104+
105+
Returns:
106+
tuple: (source_type, worker_config_dict)
107+
"""
108+
source_config = extract_sources_from_fc(fc_path)
109+
110+
if not source_config:
111+
print(f"Warning: No source nodes found in {fc_path}", file=sys.stderr)
112+
print("The .fc file may not have any SourceNode entries.", file=sys.stderr)
113+
114+
worker_json = {
115+
"interval": interval,
116+
"init_time": init_time,
117+
"bound": num_events,
118+
"repeat": repeat,
119+
"files": "data.xtc2",
120+
"config": source_config,
121+
}
122+
123+
return source_type, worker_json
124+
125+
126+
def main():
127+
parser = argparse.ArgumentParser(
128+
description="Generate worker configuration for data sources from .fc file",
129+
formatter_class=argparse.RawDescriptionHelpFormatter,
130+
epilog="""
131+
Examples:
132+
# Generate worker.json from .fc file (random source by default)
133+
%(prog)s tests/graphs/ATM_crix_new.fc
134+
135+
# Custom output file and event count
136+
%(prog)s my_graph.fc -o my_worker.json -n 100
137+
138+
# Generate for static source
139+
%(prog)s my_graph.fc --source-type static
140+
141+
# Don't loop events (stop after bound)
142+
%(prog)s my_graph.fc --no-repeat
143+
144+
# Then use with ami-local
145+
ami-local -n 3 random://worker.json -l my_graph.fc
146+
""",
147+
)
148+
149+
parser.add_argument("fc_file", type=str, help="Path to .fc flowchart file")
150+
151+
parser.add_argument(
152+
"-o", "--output", type=str, default="worker.json", help="Output worker.json file (default: worker.json)"
153+
)
154+
155+
parser.add_argument("-n", "--num-events", type=int, default=100, help="Number of events to generate (default: 100)")
156+
157+
parser.add_argument("--no-repeat", action="store_true", help="Do not loop events (stop after bound)")
158+
159+
parser.add_argument("--interval", type=float, default=0.01, help="Time between events in seconds (default: 0.01)")
160+
161+
parser.add_argument("--init-time", type=float, default=0.1, help="Initial wait time in seconds (default: 0.1)")
162+
163+
parser.add_argument("--show-sources", action="store_true", help="Show detected sources and exit")
164+
165+
parser.add_argument(
166+
"--source-type",
167+
type=str,
168+
choices=["static", "random"],
169+
default="random",
170+
help="Type of source to generate for (default: random). "
171+
"static: constant values (all 1s), random: randomized values based on ranges",
172+
)
173+
174+
args = parser.parse_args()
175+
176+
# Check if .fc file exists
177+
fc_path = Path(args.fc_file)
178+
if not fc_path.exists():
179+
print(f"Error: File not found: {args.fc_file}", file=sys.stderr)
180+
sys.exit(1)
181+
182+
# Extract sources
183+
source_config = extract_sources_from_fc(fc_path)
184+
185+
if not source_config:
186+
print(f"Error: No source nodes found in {args.fc_file}", file=sys.stderr)
187+
print("Make sure your .fc file has SourceNode entries.", file=sys.stderr)
188+
sys.exit(1)
189+
190+
# Show sources if requested
191+
if args.show_sources:
192+
print(f"Sources detected in {args.fc_file}:")
193+
for name, config in source_config.items():
194+
dtype = config.get("dtype", "unknown")
195+
print(f" {name:30s} -> {dtype}")
196+
sys.exit(0)
197+
198+
# Generate worker.json
199+
source_type, worker_json = generate_worker_json(
200+
fc_path,
201+
num_events=args.num_events,
202+
repeat=not args.no_repeat,
203+
interval=args.interval,
204+
init_time=args.init_time,
205+
source_type=args.source_type,
206+
)
207+
208+
# Write output
209+
output_path = Path(args.output)
210+
with open(output_path, "w") as f:
211+
json.dump(worker_json, f, indent=2)
212+
213+
# Print summary
214+
print(f"✓ Generated {args.output} (for {source_type} source)")
215+
print(f" Sources detected: {len(source_config)}")
216+
for name, config in source_config.items():
217+
dtype = config.get("dtype", "unknown")
218+
print(f" - {name:30s} ({dtype})")
219+
print(f" Events: {args.num_events}")
220+
print(f" Repeat: {not args.no_repeat}")
221+
print()
222+
print("To use with ami-local:")
223+
print(f" ami-local -n 3 {source_type}://{args.output} -l {args.fc_file}")
224+
225+
226+
if __name__ == "__main__":
227+
main()

ami/flowchart/library/ex_roi.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""Test available types of ROI
2-
"""
1+
"""Test available types of ROI"""
32

43
import os
54
import sys

ami/local.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ami.collector import run_global_collector, run_node_collector
1818
from ami.comm import GraphCommHandler, PlatformAction, Ports
1919
from ami.console import run_console
20+
from ami.fc_to_worker import generate_worker_json
2021
from ami.manager import run_manager
2122
from ami.multiproc import check_mp_start_method
2223
from ami.worker import run_worker
@@ -140,6 +141,15 @@ def build_parser():
140141

141142
parser.add_argument("--cprofile", help="profile with cprofile", action="store_true")
142143

144+
parser.add_argument(
145+
"--source-type",
146+
type=str,
147+
choices=["static", "random"],
148+
default="random",
149+
help="Type of auto-generated source when loading .fc without explicit source "
150+
"(default: random). static: constant values, random: varied data",
151+
)
152+
143153
parser.add_argument("--use-opengl", help="Use opengl for plots.", action="store_true")
144154

145155
parser.add_argument("--use-numba", help="Use numba for plots.", action="store_true")
@@ -245,13 +255,45 @@ def run_ami(args, queue=None):
245255
logger.exception("Problem parsing data source flag %s", flag)
246256

247257
if args.source is not None:
258+
# Explicit source provided - use it
248259
src_url_match = re.match("(?P<prot>.*)://(?P<body>.*)", args.source)
249260
if src_url_match:
250261
src_cfg = src_url_match.groups()
251262
else:
252263
logger.critical("Invalid data source config string: %s", args.source)
253264
return 1
265+
266+
elif args.load is not None:
267+
# No source specified, but loading .fc file - auto-generate
268+
logger.info("No data source specified, auto-generating from %s", args.load)
269+
270+
try:
271+
# Generate worker config directly (returns tuple: source_type, config)
272+
source_type, worker_config = generate_worker_json(
273+
args.load, num_events=1000, repeat=True, interval=0.01, init_time=0.1, source_type=args.source_type
274+
)
275+
276+
if not worker_config.get("config"):
277+
# No sources found in .fc file
278+
logger.warning("No source nodes found in %s", args.load)
279+
logger.info("Continuing without data source. You can add SourceNodes in the GUI.")
280+
src_cfg = None
281+
else:
282+
# Pass the config dict directly to workers (no temp file needed!)
283+
src_cfg = (source_type, worker_config)
284+
num_sources = len(worker_config["config"])
285+
logger.info("Auto-generated %s source with %d sources", source_type, num_sources)
286+
287+
except FileNotFoundError:
288+
logger.error("Flowchart file not found: %s", args.load)
289+
return 1
290+
except Exception as e:
291+
logger.warning("Error during worker config auto-generation: %s", e)
292+
logger.info("Continuing without data source")
293+
src_cfg = None
294+
254295
else:
296+
# No source and no .fc file - continue without source
255297
src_cfg = None
256298

257299
logger.info("Starting ami-local using comm address: %s", comm_addr)

0 commit comments

Comments
 (0)