quadros 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
quadros/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Reusable Nek5000 snapshot/frame/video renderer."""
2
+
3
+ __version__ = "0.1.0"
quadros/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,517 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import re
7
+ import xml.etree.ElementTree as ET
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ def clean_label(text: str) -> str:
14
+ return re.sub(r"[^A-Za-z0-9_.-]+", "_", str(text)).strip("_") or "item"
15
+
16
+
17
+ def maybe_number(value: str) -> Any:
18
+ text = str(value)
19
+ try:
20
+ number = float(text)
21
+ except ValueError:
22
+ return text
23
+ if number.is_integer() and re.fullmatch(r"[-+]?\d+", text):
24
+ return int(number)
25
+ return number
26
+
27
+
28
+ def to_float_list(values: list[Any]) -> list[float]:
29
+ return [float(value) for value in values]
30
+
31
+
32
+ def first(values: list[Any], default=None):
33
+ return values[0] if values else default
34
+
35
+
36
+ def last_nonempty(values: list[Any], default=None):
37
+ for value in reversed(values):
38
+ if str(value) != "":
39
+ return value
40
+ return default
41
+
42
+
43
+ def rgb_to_hex(values: list[Any]) -> str | None:
44
+ if len(values) < 3:
45
+ return None
46
+ ints = [max(0, min(255, round(float(channel) * 255))) for channel in values[:3]]
47
+ return "#{:02X}{:02X}{:02X}".format(*ints)
48
+
49
+
50
+ def pyvista_camera_position(camera: dict[str, Any]) -> list[list[float]]:
51
+ return [
52
+ list(camera["position"]),
53
+ list(camera["focal_point"]),
54
+ list(camera["view_up"]),
55
+ ]
56
+
57
+
58
+ def is_identity_transform(transform: dict[str, Any]) -> bool:
59
+ position = transform.get("position") or [0.0, 0.0, 0.0]
60
+ scale = transform.get("scale") or [1.0, 1.0, 1.0]
61
+ orientation = transform.get("orientation") or [0.0, 0.0, 0.0]
62
+ origin = transform.get("origin") or [0.0, 0.0, 0.0]
63
+ matrix = transform.get("user_transform") or [
64
+ 1.0, 0.0, 0.0, 0.0,
65
+ 0.0, 1.0, 0.0, 0.0,
66
+ 0.0, 0.0, 1.0, 0.0,
67
+ 0.0, 0.0, 0.0, 1.0,
68
+ ]
69
+ identity_matrix = [
70
+ 1.0, 0.0, 0.0, 0.0,
71
+ 0.0, 1.0, 0.0, 0.0,
72
+ 0.0, 0.0, 1.0, 0.0,
73
+ 0.0, 0.0, 0.0, 1.0,
74
+ ]
75
+ return (
76
+ all(abs(float(value)) < 1e-12 for value in position)
77
+ and all(abs(float(value) - 1.0) < 1e-12 for value in scale)
78
+ and all(abs(float(value)) < 1e-12 for value in orientation)
79
+ and all(abs(float(value)) < 1e-12 for value in origin)
80
+ and all(abs(float(value) - identity_matrix[index]) < 1e-12 for index, value in enumerate(matrix))
81
+ )
82
+
83
+
84
+ class ParaViewState:
85
+ def __init__(self, path: Path):
86
+ self.path = path
87
+ self.tree = ET.parse(path)
88
+ self.root = self.tree.getroot()
89
+ self.state = self.root.find(".//ServerManagerState")
90
+ self.version = None if self.state is None else self.state.attrib.get("version")
91
+ self.proxies = {
92
+ proxy.attrib["id"]: proxy
93
+ for proxy in self.root.findall(".//Proxy")
94
+ if "id" in proxy.attrib
95
+ }
96
+ self.collections: dict[str, list[str]] = {}
97
+ self.item_names: dict[str, list[dict[str, str | None]]] = {}
98
+ self._load_proxy_collection_names()
99
+
100
+ def _load_proxy_collection_names(self) -> None:
101
+ for collection in self.root.findall(".//ProxyCollection"):
102
+ collection_name = collection.attrib.get("name", "")
103
+ for item in collection.findall("Item"):
104
+ proxy_id = item.attrib.get("id")
105
+ if not proxy_id:
106
+ continue
107
+ self.collections.setdefault(collection_name, []).append(proxy_id)
108
+ self.item_names.setdefault(proxy_id, []).append(
109
+ {
110
+ "collection": collection_name,
111
+ "name": item.attrib.get("name"),
112
+ "logname": item.attrib.get("logname"),
113
+ }
114
+ )
115
+
116
+ def proxy_name(self, proxy_id: str) -> str:
117
+ preferred_collections = (
118
+ "sources",
119
+ "views",
120
+ "representations",
121
+ "lookup_tables",
122
+ "scalar_bars",
123
+ "layouts",
124
+ )
125
+ candidates = self.item_names.get(proxy_id, [])
126
+ for collection_name in preferred_collections:
127
+ for item in candidates:
128
+ if item["collection"] == collection_name and item["name"]:
129
+ return str(item["name"])
130
+ for item in candidates:
131
+ if item["name"]:
132
+ return str(item["name"])
133
+ proxy = self.proxies.get(proxy_id)
134
+ if proxy is not None:
135
+ return f"{proxy.attrib.get('type', 'Proxy')}_{proxy_id}"
136
+ return proxy_id
137
+
138
+ def proxy_logname(self, proxy_id: str) -> str | None:
139
+ for item in self.item_names.get(proxy_id, []):
140
+ if item["logname"]:
141
+ return str(item["logname"])
142
+ return None
143
+
144
+ def prop(self, proxy_id: str, name: str):
145
+ proxy = self.proxies[proxy_id]
146
+ for prop in proxy.findall("Property"):
147
+ if prop.attrib.get("name") == name:
148
+ return prop
149
+ return None
150
+
151
+ def values(self, proxy_id: str, name: str) -> list[Any]:
152
+ prop = self.prop(proxy_id, name)
153
+ if prop is None:
154
+ return []
155
+
156
+ elements = []
157
+ for element in prop.findall("Element"):
158
+ if "value" not in element.attrib:
159
+ continue
160
+ index = int(element.attrib.get("index", len(elements)))
161
+ elements.append((index, maybe_number(element.attrib["value"])))
162
+ if elements:
163
+ return [value for _, value in sorted(elements)]
164
+ if "value" in prop.attrib:
165
+ return [maybe_number(prop.attrib["value"])]
166
+ return []
167
+
168
+ def proxy_refs(self, proxy_id: str, name: str) -> list[str]:
169
+ prop = self.prop(proxy_id, name)
170
+ if prop is None:
171
+ return []
172
+ refs = []
173
+ for proxy in prop.findall("Proxy"):
174
+ if "value" in proxy.attrib:
175
+ refs.append(proxy.attrib["value"])
176
+ return refs
177
+
178
+ def proxies_by(self, *, group: str | None = None, proxy_type: str | None = None):
179
+ for proxy_id, proxy in self.proxies.items():
180
+ if group is not None and proxy.attrib.get("group") != group:
181
+ continue
182
+ if proxy_type is not None and proxy.attrib.get("type") != proxy_type:
183
+ continue
184
+ yield proxy_id, proxy
185
+
186
+ def scalar_name(self, proxy_id: str) -> str | None:
187
+ return last_nonempty(
188
+ self.values(proxy_id, "SelectInputScalars")
189
+ or self.values(proxy_id, "InputScalars")
190
+ or self.values(proxy_id, "Scalars")
191
+ )
192
+
193
+ def source_summary(self) -> list[dict[str, Any]]:
194
+ sources = []
195
+ for proxy_id, proxy in self.proxies_by(group="sources"):
196
+ file_names = (
197
+ self.values(proxy_id, "FileName")
198
+ or self.values(proxy_id, "FileNames")
199
+ or self.values(proxy_id, "CaseFileName")
200
+ )
201
+ timestep_values = [
202
+ float(value)
203
+ for value in self.values(proxy_id, "TimestepValues")
204
+ if isinstance(value, (int, float)) and abs(float(value)) > 1e-15
205
+ ]
206
+ if not file_names and not timestep_values:
207
+ continue
208
+ sources.append(
209
+ {
210
+ "id": proxy_id,
211
+ "name": self.proxy_name(proxy_id),
212
+ "logname": self.proxy_logname(proxy_id),
213
+ "type": proxy.attrib.get("type"),
214
+ "file_names": file_names,
215
+ "timestep_values": timestep_values,
216
+ }
217
+ )
218
+ return sources
219
+
220
+ def contour_summary(self) -> list[dict[str, Any]]:
221
+ contours = []
222
+ for proxy_id, proxy in self.proxies_by(group="filters", proxy_type="Contour"):
223
+ input_id = first(self.proxy_refs(proxy_id, "Input"))
224
+ contours.append(
225
+ {
226
+ "id": proxy_id,
227
+ "name": self.proxy_name(proxy_id),
228
+ "logname": self.proxy_logname(proxy_id),
229
+ "type": proxy.attrib.get("type"),
230
+ "input_id": input_id,
231
+ "input_name": self.proxy_name(input_id) if input_id else None,
232
+ "field": self.scalar_name(proxy_id),
233
+ "values": to_float_list(self.values(proxy_id, "ContourValues")),
234
+ "compute_normals": bool(first(self.values(proxy_id, "ComputeNormals"), 0)),
235
+ "compute_gradients": bool(first(self.values(proxy_id, "ComputeGradients"), 0)),
236
+ "compute_scalars": bool(first(self.values(proxy_id, "ComputeScalars"), 0)),
237
+ "generate_triangles": bool(first(self.values(proxy_id, "GenerateTriangles"), 0)),
238
+ }
239
+ )
240
+ return contours
241
+
242
+ def representation_summary(self) -> list[dict[str, Any]]:
243
+ representations = []
244
+ for proxy_id, proxy in self.proxies_by(group="representations", proxy_type="GeometryRepresentation"):
245
+ input_id = first(self.proxy_refs(proxy_id, "Input"))
246
+ diffuse_color = to_float_list(self.values(proxy_id, "DiffuseColor"))
247
+ ambient_color = to_float_list(self.values(proxy_id, "AmbientColor"))
248
+ color_array_values = self.values(proxy_id, "ColorArrayName")
249
+ opacity_array_values = self.values(proxy_id, "OpacityArray")
250
+ transform = {
251
+ "position": to_float_list(self.values(proxy_id, "Position") or [0.0, 0.0, 0.0]),
252
+ "scale": to_float_list(self.values(proxy_id, "Scale") or [1.0, 1.0, 1.0]),
253
+ "orientation": to_float_list(self.values(proxy_id, "Orientation") or [0.0, 0.0, 0.0]),
254
+ "origin": to_float_list(self.values(proxy_id, "Origin") or [0.0, 0.0, 0.0]),
255
+ "user_transform": to_float_list(
256
+ self.values(proxy_id, "UserTransform")
257
+ or [
258
+ 1.0, 0.0, 0.0, 0.0,
259
+ 0.0, 1.0, 0.0, 0.0,
260
+ 0.0, 0.0, 1.0, 0.0,
261
+ 0.0, 0.0, 0.0, 1.0,
262
+ ]
263
+ ),
264
+ }
265
+ representations.append(
266
+ {
267
+ "id": proxy_id,
268
+ "name": self.proxy_name(proxy_id),
269
+ "logname": self.proxy_logname(proxy_id),
270
+ "input_id": input_id,
271
+ "input_name": self.proxy_name(input_id) if input_id else None,
272
+ "visible": bool(first(self.values(proxy_id, "Visibility"), 0)),
273
+ "representation": first(self.values(proxy_id, "Representation")),
274
+ "opacity": float(first(self.values(proxy_id, "Opacity"), 1.0)),
275
+ "diffuse_color": diffuse_color,
276
+ "diffuse_hex": rgb_to_hex(diffuse_color),
277
+ "ambient_color": ambient_color,
278
+ "ambient_hex": rgb_to_hex(ambient_color),
279
+ "color_array": last_nonempty(color_array_values),
280
+ "opacity_array": last_nonempty(opacity_array_values),
281
+ "line_width": first(self.values(proxy_id, "LineWidth")),
282
+ "mesh_visible": bool(first(self.values(proxy_id, "MeshVisibility"), 0)),
283
+ "transform": transform,
284
+ "transform_is_identity": is_identity_transform(transform),
285
+ }
286
+ )
287
+ return representations
288
+
289
+ def camera_summary(self) -> list[dict[str, Any]]:
290
+ views = []
291
+ for proxy_id, proxy in self.proxies_by(group="views", proxy_type="RenderView"):
292
+ camera = {
293
+ "position": to_float_list(self.values(proxy_id, "CameraPosition")),
294
+ "focal_point": to_float_list(self.values(proxy_id, "CameraFocalPoint")),
295
+ "view_up": to_float_list(self.values(proxy_id, "CameraViewUp")),
296
+ "parallel_projection": bool(first(self.values(proxy_id, "CameraParallelProjection"), 0)),
297
+ "parallel_scale": float(first(self.values(proxy_id, "CameraParallelScale"), 0.0)),
298
+ "view_angle": float(first(self.values(proxy_id, "CameraViewAngle"), 30.0)),
299
+ }
300
+ views.append(
301
+ {
302
+ "id": proxy_id,
303
+ "name": self.proxy_name(proxy_id),
304
+ "logname": self.proxy_logname(proxy_id),
305
+ "type": proxy.attrib.get("type"),
306
+ "camera": camera,
307
+ "pyvista_camera_position": pyvista_camera_position(camera),
308
+ "view_size": [int(value) for value in self.values(proxy_id, "ViewSize")],
309
+ "center_of_rotation": to_float_list(self.values(proxy_id, "CenterOfRotation")),
310
+ "representation_ids": self.proxy_refs(proxy_id, "Representations"),
311
+ }
312
+ )
313
+ return views
314
+
315
+ def lookup_table_summary(self) -> list[dict[str, Any]]:
316
+ tables = []
317
+ for proxy_id, proxy in self.proxies_by(group="lookup_tables"):
318
+ rgb_points = self.values(proxy_id, "RGBPoints")
319
+ tables.append(
320
+ {
321
+ "id": proxy_id,
322
+ "name": self.proxy_name(proxy_id),
323
+ "logname": self.proxy_logname(proxy_id),
324
+ "type": proxy.attrib.get("type"),
325
+ "color_space": first(self.values(proxy_id, "ColorSpace")),
326
+ "rgb_points": rgb_points,
327
+ "opacity_mapping": bool(first(self.values(proxy_id, "EnableOpacityMapping"), 0)),
328
+ "nan_opacity": first(self.values(proxy_id, "NanOpacity")),
329
+ }
330
+ )
331
+ return tables
332
+
333
+ def animation_summary(self) -> dict[str, Any]:
334
+ scenes = list(self.proxies_by(group="animation", proxy_type="AnimationScene"))
335
+ if not scenes:
336
+ return {}
337
+ proxy_id, _ = scenes[0]
338
+ return {
339
+ "id": proxy_id,
340
+ "animation_time": first(self.values(proxy_id, "AnimationTime")),
341
+ "start_time": first(self.values(proxy_id, "StartTime")),
342
+ "end_time": first(self.values(proxy_id, "EndTime")),
343
+ "number_of_frames": first(self.values(proxy_id, "NumberOfFrames")),
344
+ "frames_per_timestep": first(self.values(proxy_id, "FramesPerTimestep")),
345
+ "play_mode": first(self.values(proxy_id, "PlayMode")),
346
+ }
347
+
348
+ def summary(self) -> dict[str, Any]:
349
+ contours = self.contour_summary()
350
+ representations = self.representation_summary()
351
+ contour_by_id = {contour["id"]: contour for contour in contours}
352
+ for representation in representations:
353
+ representation["filter"] = contour_by_id.get(representation["input_id"])
354
+ return {
355
+ "source_file": str(self.path),
356
+ "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
357
+ "paraview_version": self.version,
358
+ "animation": self.animation_summary(),
359
+ "sources": self.source_summary(),
360
+ "contours": contours,
361
+ "representations": representations,
362
+ "visible_representations": [rep for rep in representations if rep["visible"]],
363
+ "views": self.camera_summary(),
364
+ "lookup_tables": self.lookup_table_summary(),
365
+ }
366
+
367
+
368
+ def camera_json(summary: dict[str, Any]) -> dict[str, Any]:
369
+ camera_views = {}
370
+ for view in summary["views"]:
371
+ name = clean_label(view["name"])
372
+ camera = dict(view["camera"])
373
+ camera["pyvista_camera_position"] = view["pyvista_camera_position"]
374
+ camera_views[name] = camera
375
+ return {"camera_views": camera_views}
376
+
377
+
378
+ def render_config(summary: dict[str, Any]) -> dict[str, Any]:
379
+ visible_contours = []
380
+ iso_colors: dict[str, str] = {}
381
+ iso_opacities: dict[str, float] = {}
382
+
383
+ for representation in summary["visible_representations"]:
384
+ contour = representation.get("filter")
385
+ if not contour or contour.get("type") != "Contour":
386
+ continue
387
+ field = contour.get("field")
388
+ if not field:
389
+ continue
390
+ for value in contour.get("values", []):
391
+ visible_contours.append((field, float(value)))
392
+ if representation.get("diffuse_hex"):
393
+ iso_colors.setdefault(field, representation["diffuse_hex"])
394
+ iso_opacities.setdefault(field, float(representation.get("opacity", 1.0)))
395
+
396
+ unique_specs = []
397
+ seen = set()
398
+ for field, value in visible_contours:
399
+ key = (field, value)
400
+ if key in seen:
401
+ continue
402
+ seen.add(key)
403
+ unique_specs.append(f"{field}={value:g}")
404
+
405
+ view_names = [clean_label(view["name"]) for view in summary["views"]]
406
+ scene = {
407
+ "name": f"{clean_label(Path(summary['source_file']).stem)}_visible_contours",
408
+ "iso_specs": [],
409
+ "iso_colors": iso_colors,
410
+ "iso_opacities": iso_opacities,
411
+ "compositions": [
412
+ {
413
+ "name": "paraview_visible",
414
+ "iso_specs": unique_specs,
415
+ "views": view_names,
416
+ "resolutions": ["fullhd", "2k", "4k"],
417
+ }
418
+ ],
419
+ "roi": "auto",
420
+ "views": view_names,
421
+ "resolutions": ["fullhd", "2k", "4k"],
422
+ "camera_zoom": 1.0,
423
+ "show_bounds": False,
424
+ }
425
+ return {
426
+ "_generated_from": summary["source_file"],
427
+ "_note": (
428
+ "This maps visible ParaView contour settings into the auto_snp renderer. "
429
+ "If the ParaView state contains multiple data sources, run the renderer once per Nek snapshot."
430
+ ),
431
+ **camera_json(summary),
432
+ "scenes": [scene],
433
+ }
434
+
435
+
436
+ def write_json(path: Path, payload: dict[str, Any], indent: int) -> None:
437
+ path.parent.mkdir(parents=True, exist_ok=True)
438
+ with path.open("w", encoding="utf-8") as handle:
439
+ json.dump(payload, handle, indent=indent)
440
+ handle.write("\n")
441
+
442
+
443
+ def print_report(summary: dict[str, Any], written: list[Path]) -> None:
444
+ print(f"ParaView state: {summary['source_file']}")
445
+ print(f"ParaView version: {summary.get('paraview_version')}")
446
+ print(f"Sources: {len(summary['sources'])}")
447
+ for source in summary["sources"]:
448
+ files = ", ".join(str(item) for item in source.get("file_names", []))
449
+ print(f" - {source['name']} ({source['type']}): {files}")
450
+ print(f"Contours: {len(summary['contours'])}")
451
+ for contour in summary["contours"]:
452
+ values = ", ".join(f"{value:g}" for value in contour["values"])
453
+ print(f" - {contour['name']}: {contour['field']} = {values} from {contour['input_name']}")
454
+ print(f"Visible geometry representations: {len(summary['visible_representations'])}")
455
+ for rep in summary["visible_representations"]:
456
+ contour = rep.get("filter") or {}
457
+ target = contour.get("name", rep.get("input_name"))
458
+ transform_note = ""
459
+ if not rep["transform_is_identity"]:
460
+ transform = rep["transform"]
461
+ transform_note = (
462
+ f", transform position={transform['position']}, "
463
+ f"orientation={transform['orientation']}, scale={transform['scale']}"
464
+ )
465
+ print(
466
+ f" - {rep['name']} -> {target}: "
467
+ f"opacity={rep['opacity']}, color={rep.get('diffuse_hex')}, "
468
+ f"mesh_visible={rep['mesh_visible']}{transform_note}"
469
+ )
470
+ print(f"Views: {len(summary['views'])}")
471
+ for view in summary["views"]:
472
+ camera = view["camera"]
473
+ print(
474
+ f" - {view['name']}: position={camera['position']}, "
475
+ f"focal_point={camera['focal_point']}, parallel_scale={camera['parallel_scale']}"
476
+ )
477
+ print(f" PyVista camera_position={view['pyvista_camera_position']}")
478
+ for path in written:
479
+ print(f"Wrote {path}")
480
+
481
+
482
+ def parse_args() -> argparse.Namespace:
483
+ parser = argparse.ArgumentParser(
484
+ description="Extract cameras, contours, sources, and visible representation settings from a ParaView .pvsm state."
485
+ )
486
+ parser.add_argument("pvsm", type=Path, help="ParaView .pvsm state file")
487
+ parser.add_argument("--summary-json", type=Path, help="Detailed extracted summary JSON")
488
+ parser.add_argument("--camera-json", type=Path, help="Camera JSON usable as PYVISTA_CAMERA_FILE")
489
+ parser.add_argument("--render-config", type=Path, help="auto_snp render config generated from visible contours")
490
+ parser.add_argument("--indent", type=int, default=2, help="JSON indentation")
491
+ parser.add_argument("--no-write", action="store_true", help="Only print the report")
492
+ return parser.parse_args()
493
+
494
+
495
+ def main() -> int:
496
+ args = parse_args()
497
+ state = ParaViewState(args.pvsm)
498
+ summary = state.summary()
499
+
500
+ stem = args.pvsm.with_suffix("")
501
+ summary_path = args.summary_json or stem.with_name(f"{stem.name}_paraview_summary.json")
502
+ camera_path = args.camera_json or stem.with_name(f"{stem.name}_camera_views.json")
503
+ config_path = args.render_config or stem.with_name(f"{stem.name}_render_config.json")
504
+
505
+ written = []
506
+ if not args.no_write:
507
+ write_json(summary_path, summary, args.indent)
508
+ write_json(camera_path, camera_json(summary), args.indent)
509
+ write_json(config_path, render_config(summary), args.indent)
510
+ written.extend([summary_path, camera_path, config_path])
511
+
512
+ print_report(summary, written)
513
+ return 0
514
+
515
+
516
+ if __name__ == "__main__":
517
+ raise SystemExit(main())