swayfx-mosaic/tooling/probes/step20c-negative-origin/analyze.py

119 lines
5.4 KiB
Python

"""Summarize retained headless evidence without treating counts as a verdict.
Usage: python analyze.py LAB
Requires Pillow for PNG decoding. Writes analysis.json inside each run directory.
"""
import json
from pathlib import Path
import re
import sys
from PIL import Image
if len(sys.argv) != 2:
raise SystemExit(__doc__)
lab = Path(sys.argv[1]).resolve()
colors = {'A_yellow': (224, 192, 32), 'B_green': (32, 208, 160)}
def open_image(path):
return Image.open(path).convert('RGB')
def local_points(lines, pattern):
points = []
for line in lines:
match = re.search(pattern, line)
if match:
points.append(tuple(map(float, match.groups())))
return points
for run in sorted(lab.glob('run-*')):
if not (run / 'exit-status.json').exists():
continue
client = (run / 'client.log').read_text()
ordinary = (run / 'wev.log').read_text()
compositor = (run / 'compositor.log').read_text()
result = {
'run': run.name,
'exit_status': json.loads((run / 'exit-status.json').read_text()),
'grab_requests': re.findall(
r'(POPUP_[AB]) GRAB_REQUEST origin=wl_pointer.button target=(\w+) serial=(\d+) used=(\d+)',
client),
'popup_parents': re.findall(
r'(POPUP_[AB]) CREATE wl=(\d+) xdg=(\d+) role=(\d+) parent=(\w+) parent_xdg=(\d+)',
client),
'popup_done_order': re.findall(r'(POPUP_[AB]) POPUP_DONE', client),
'popup_destroy_order': re.findall(r'(POPUP_[AB]) DESTROY', client),
'popup_buttons': re.findall(
r'POINTER_BUTTON target=(POPUP_[AB]) serial=(\d+) button=(\d+) state=(\d+)',
client),
'ordinary_buttons': re.findall(
r'button: serial: (\d+); time: \d+; button: (\d+).*state: ([01])',
ordinary),
'scene_clip_logs': re.findall(
r'PROBE_POPUP_CLIP owner=(\S+) local=(-?\d+),(-?\d+) (\d+)x(\d+)',
compositor),
'hidden_owner_logs': re.findall(r'PROBE_POPUP_HIDDEN_OWNER[^\n]*', compositor),
'captures': {},
'open_sample_rgb': {},
}
reference_path = run / 'pointer-validation.json'
if reference_path.exists():
reference = json.loads(reference_path.read_text())
owner_points = local_points(reference['owner_motion_lines'],
r'x=(-?[\d.]+) y=(-?[\d.]+)')
ordinary_points = local_points(reference['ordinary_motion_lines'],
r'x, y: (-?[\d.]+), (-?[\d.]+)')
if len(owner_points) >= 2 and len(ordinary_points) >= 2:
result['pointer_observed'] = [
{'requested_global': list(reference['global_points'][i]),
'observed_output': output, 'observed_surface_local': list(point)}
for i, output, point in zip(range(4),
('HEADLESS-1', 'HEADLESS-2', 'HEADLESS-1', 'HEADLESS-2'),
(owner_points[-2], ordinary_points[-2],
owner_points[-1], ordinary_points[-1]))]
for path in sorted(run.glob('*.png')):
if not path.name.startswith(('left-', 'right-')):
continue
image = open_image(path)
pixels = image.get_flattened_data()
result['captures'][path.name] = {
'size': list(image.size),
'exact_color_pixels': {name: pixels.count(rgb) for name, rgb in colors.items()},
}
measurements_path = run / 'measurements.json'
if measurements_path.exists():
result['measurements'] = json.loads(measurements_path.read_text())
# CONTRACT: the private log's clip is local to the root popup tree.
# Subtracting it from the output viewport origin recovers that applied
# scene-tree origin; XDG role configure offsets locate A and B below it.
if result['scene_clip_logs']:
clip = result['scene_clip_logs'][-1]
left = result['measurements']['verified_outputs']['HEADLESS-1']
root_x, root_y = left[0] - int(clip[1]), left[1] - int(clip[2])
a = result['measurements']['popup_a_configure']
b = result['measurements']['popup_b_configure']
result['applied_scene_from_clip'] = {
'root_popup_tree': [root_x, root_y],
'popup_a': [root_x + a[0], root_y + a[1], a[2], a[3]],
'popup_b': [root_x + a[0] + b[0], root_y + a[1] + b[1], b[2], b[3]],
}
for sample in result['measurements']['sample_points']:
x, y = sample['global']
output = 'left' if x < result['measurements']['verified_outputs']['HEADLESS-2'][0] else 'right'
origin = result['measurements']['verified_outputs'][
'HEADLESS-1' if output == 'left' else 'HEADLESS-2']
path = run / f'{output}-open.png'
if path.exists():
local = (int(x - origin[0]), int(y - origin[1]))
image = open_image(path)
result['open_sample_rgb'][sample['purpose']] = {
'global': [x, y], 'output': output, 'local': list(local),
'rgb': list(image.getpixel(local)) if (
0 <= local[0] < image.width and 0 <= local[1] < image.height) else None,
}
(run / 'analysis.json').write_text(json.dumps(result, indent=2) + '\n')
print(run.name, result['captures'].get('left-open.png', {}).get('exact_color_pixels'),
result['captures'].get('right-open.png', {}).get('exact_color_pixels'))