lorax-arg 0.1__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.
Files changed (66) hide show
  1. lorax/buffer.py +43 -0
  2. lorax/cache/__init__.py +43 -0
  3. lorax/cache/csv_tree_graph.py +59 -0
  4. lorax/cache/disk.py +467 -0
  5. lorax/cache/file_cache.py +142 -0
  6. lorax/cache/file_context.py +72 -0
  7. lorax/cache/lru.py +90 -0
  8. lorax/cache/tree_graph.py +293 -0
  9. lorax/cli.py +312 -0
  10. lorax/cloud/__init__.py +0 -0
  11. lorax/cloud/gcs_utils.py +205 -0
  12. lorax/constants.py +66 -0
  13. lorax/context.py +80 -0
  14. lorax/csv/__init__.py +7 -0
  15. lorax/csv/config.py +250 -0
  16. lorax/csv/layout.py +182 -0
  17. lorax/csv/newick_tree.py +234 -0
  18. lorax/handlers.py +998 -0
  19. lorax/lineage.py +456 -0
  20. lorax/loaders/__init__.py +0 -0
  21. lorax/loaders/csv_loader.py +10 -0
  22. lorax/loaders/loader.py +31 -0
  23. lorax/loaders/tskit_loader.py +119 -0
  24. lorax/lorax_app.py +75 -0
  25. lorax/manager.py +58 -0
  26. lorax/metadata/__init__.py +0 -0
  27. lorax/metadata/loader.py +426 -0
  28. lorax/metadata/mutations.py +146 -0
  29. lorax/modes.py +190 -0
  30. lorax/pg.py +183 -0
  31. lorax/redis_utils.py +30 -0
  32. lorax/routes.py +137 -0
  33. lorax/session_manager.py +206 -0
  34. lorax/sockets/__init__.py +55 -0
  35. lorax/sockets/connection.py +99 -0
  36. lorax/sockets/debug.py +47 -0
  37. lorax/sockets/decorators.py +112 -0
  38. lorax/sockets/file_ops.py +200 -0
  39. lorax/sockets/lineage.py +307 -0
  40. lorax/sockets/metadata.py +232 -0
  41. lorax/sockets/mutations.py +154 -0
  42. lorax/sockets/node_search.py +535 -0
  43. lorax/sockets/tree_layout.py +117 -0
  44. lorax/sockets/utils.py +10 -0
  45. lorax/tree_graph/__init__.py +12 -0
  46. lorax/tree_graph/tree_graph.py +689 -0
  47. lorax/utils.py +124 -0
  48. lorax_app/__init__.py +4 -0
  49. lorax_app/app.py +159 -0
  50. lorax_app/cli.py +114 -0
  51. lorax_app/static/X.png +0 -0
  52. lorax_app/static/assets/index-BCEGlUFi.js +2361 -0
  53. lorax_app/static/assets/index-iKjzUpA9.css +1 -0
  54. lorax_app/static/assets/localBackendWorker-BaWwjSV_.js +2 -0
  55. lorax_app/static/assets/renderDataWorker-BKLdiU7J.js +2 -0
  56. lorax_app/static/gestures/gesture-flick.ogv +0 -0
  57. lorax_app/static/gestures/gesture-two-finger-scroll.ogv +0 -0
  58. lorax_app/static/index.html +14 -0
  59. lorax_app/static/logo.png +0 -0
  60. lorax_app/static/lorax-logo.png +0 -0
  61. lorax_app/static/vite.svg +1 -0
  62. lorax_arg-0.1.dist-info/METADATA +131 -0
  63. lorax_arg-0.1.dist-info/RECORD +66 -0
  64. lorax_arg-0.1.dist-info/WHEEL +5 -0
  65. lorax_arg-0.1.dist-info/entry_points.txt +4 -0
  66. lorax_arg-0.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,535 @@
1
+ """
2
+ Node search event handlers for Lorax Socket.IO.
3
+
4
+ Handles search_nodes and get_highlight_positions_event events.
5
+ """
6
+
7
+ import asyncio
8
+
9
+ from lorax.context import tree_graph_cache, csv_tree_graph_cache
10
+ from lorax.constants import ERROR_NO_FILE_LOADED
11
+ from lorax.handlers import (
12
+ should_shift_csv_tips,
13
+ search_nodes_in_trees,
14
+ get_highlight_positions,
15
+ get_multi_value_highlight_positions,
16
+ )
17
+ from lorax.cache import get_file_context
18
+ from lorax.sockets.decorators import require_session
19
+ from lorax.sockets.utils import is_csv_session_file
20
+
21
+
22
+ async def _get_or_parse_csv_tree_graph(ctx, session_id: str, tree_idx: int):
23
+ """
24
+ Return a parsed CSV Newick tree graph, using CsvTreeGraphCache.
25
+
26
+ The cache is populated by the layout pipeline, but node search/highlight
27
+ should also be able to populate it on demand.
28
+ """
29
+ cached = await csv_tree_graph_cache.get(session_id, int(tree_idx))
30
+ if cached is not None:
31
+ return cached
32
+
33
+ # Import lazily to avoid CSV dependencies on tskit paths.
34
+ import pandas as pd
35
+ from lorax.csv.newick_tree import parse_newick_to_tree
36
+
37
+ df = ctx.tree_sequence
38
+ if not isinstance(df, pd.DataFrame):
39
+ return None
40
+
41
+ try:
42
+ newick_str = df.iloc[int(tree_idx)].get("newick")
43
+ except Exception:
44
+ newick_str = None
45
+ if newick_str is None or pd.isna(newick_str):
46
+ return None
47
+
48
+ # Parse with same normalization settings as the layout pipeline.
49
+ times_values = ctx.config.get("times", {}).get("values", [0.0, 1.0])
50
+ max_branch_length = float(times_values[1]) if len(times_values) > 1 else 1.0
51
+ samples_order = ctx.config.get("samples") or []
52
+ shift_tips_to_one = should_shift_csv_tips(ctx.file_path)
53
+ tree_max_branch_length = None
54
+ if "max_branch_length" in df.columns:
55
+ try:
56
+ v = df.iloc[int(tree_idx)].get("max_branch_length")
57
+ if v is not None and not (isinstance(v, float) and pd.isna(v)) and str(v).strip() != "":
58
+ tree_max_branch_length = float(v)
59
+ except Exception:
60
+ tree_max_branch_length = None
61
+
62
+ try:
63
+ graph = await asyncio.to_thread(
64
+ parse_newick_to_tree,
65
+ str(newick_str),
66
+ max_branch_length,
67
+ samples_order=samples_order,
68
+ tree_max_branch_length=tree_max_branch_length,
69
+ shift_tips_to_one=shift_tips_to_one,
70
+ )
71
+ except Exception:
72
+ return None
73
+
74
+ await csv_tree_graph_cache.set(session_id, int(tree_idx), graph)
75
+ return graph
76
+
77
+
78
+ def _find_node_index(graph, node_id: int):
79
+ """Find the array index in a NewickTreeGraph for a given node_id."""
80
+ # node_id is stored as a numpy array; use vectorized equality.
81
+ try:
82
+ idxs = (graph.node_id == int(node_id)).nonzero()[0]
83
+ if idxs.size == 0:
84
+ return None
85
+ return int(idxs[0])
86
+ except Exception:
87
+ return None
88
+
89
+
90
+ def _build_node_id_to_index(graph):
91
+ """Build a node_id -> array index dict for a NewickTreeGraph."""
92
+ try:
93
+ return {int(nid): i for i, nid in enumerate(graph.node_id)}
94
+ except Exception:
95
+ return {}
96
+
97
+
98
+ def _compute_csv_lineage_path(graph, seed_node_id: int, node_id_to_index: dict):
99
+ """
100
+ Compute ancestry path (tip -> root) for a CSV NewickTreeGraph.
101
+
102
+ Returns a list of node_ids starting at seed_node_id, following parent_id until -1.
103
+ Includes a cycle guard to avoid infinite loops on malformed graphs.
104
+ """
105
+ path = []
106
+ current = int(seed_node_id)
107
+ visited = set()
108
+
109
+ while current != -1:
110
+ if current in visited:
111
+ break
112
+ visited.add(current)
113
+ path.append(current)
114
+
115
+ idx = node_id_to_index.get(current)
116
+ if idx is None:
117
+ break
118
+ parent = int(graph.parent_id[idx])
119
+ current = parent
120
+
121
+ return path
122
+
123
+
124
+ def register_node_search_events(sio):
125
+ """Register node search socket events."""
126
+
127
+ @sio.event
128
+ async def search_nodes(sid, data):
129
+ """Socket event to search for nodes matching metadata values in trees.
130
+
131
+ This is used for highlighting nodes when searching/filtering by metadata.
132
+ Returns node_ids for matching samples in each tree, and optionally lineage paths.
133
+ Frontend computes positions using the post-order layout data.
134
+
135
+ data: {
136
+ lorax_sid: str,
137
+ sample_names: [str], # Sample names to search for
138
+ tree_indices: [int], # Tree indices to search in
139
+ show_lineages: bool, # Whether to compute lineage paths
140
+ sample_colors: dict # Optional {sample_name: [r,g,b,a]}
141
+ }
142
+
143
+ Returns: {
144
+ highlights: {tree_idx: [{node_id, name}]},
145
+ lineage: {tree_idx: [{path_node_ids: [int], color}]}
146
+ }
147
+ """
148
+ try:
149
+ lorax_sid = data.get("lorax_sid")
150
+ session = await require_session(lorax_sid, sid, sio)
151
+ if not session:
152
+ return
153
+
154
+ if not session.file_path:
155
+ print(f"⚠️ No file loaded for session {lorax_sid}")
156
+ await sio.emit("error", {
157
+ "code": ERROR_NO_FILE_LOADED,
158
+ "message": "No file loaded. Please load a file first."
159
+ }, to=sid)
160
+ return
161
+
162
+ if is_csv_session_file(session.file_path):
163
+ sample_names = data.get("sample_names", [])
164
+ tree_indices = data.get("tree_indices", [])
165
+ show_lineages = data.get("show_lineages", False)
166
+
167
+ # CSV mode currently supports only tip highlights (no lineage paths).
168
+ if show_lineages:
169
+ show_lineages = False
170
+
171
+ if not sample_names or not tree_indices:
172
+ await sio.emit("search-nodes-result", {"highlights": {}, "lineage": {}}, to=sid)
173
+ return
174
+
175
+ ctx = await get_file_context(session.file_path)
176
+ if ctx is None:
177
+ await sio.emit("search-nodes-result", {"error": "Failed to load CSV"}, to=sid)
178
+ return
179
+
180
+ samples_order = ctx.config.get("samples") or []
181
+ sample_id_map = {str(name): idx for idx, name in enumerate(samples_order)}
182
+
183
+ highlights = {}
184
+
185
+ for tree_idx in tree_indices:
186
+ tree_idx = int(tree_idx)
187
+ graph = await _get_or_parse_csv_tree_graph(ctx, lorax_sid, tree_idx)
188
+ if graph is None:
189
+ continue
190
+
191
+ hits = []
192
+ for name in sample_names:
193
+ node_id = sample_id_map.get(str(name))
194
+ if node_id is None:
195
+ continue
196
+ if _find_node_index(graph, node_id) is None:
197
+ continue
198
+ hits.append({"node_id": int(node_id), "name": str(name)})
199
+
200
+ if hits:
201
+ highlights[tree_idx] = hits
202
+
203
+ await sio.emit("search-nodes-result", {"highlights": highlights, "lineage": {}}, to=sid)
204
+ return
205
+
206
+ sample_names = data.get("sample_names", [])
207
+ tree_indices = data.get("tree_indices", [])
208
+ show_lineages = data.get("show_lineages", False)
209
+ sample_colors = data.get("sample_colors", {})
210
+
211
+ # Check cache and warn if trees not found (they should be cached from layout)
212
+ if tree_indices:
213
+ uncached = []
214
+ for tree_idx in tree_indices:
215
+ cached = await tree_graph_cache.get(lorax_sid, int(tree_idx))
216
+ if cached is None:
217
+ uncached.append(tree_idx)
218
+ if uncached:
219
+ print(f"⚠️ WARNING: Trees {uncached} not in cache for session {lorax_sid[:8]}... "
220
+ f"(expected from layout render)")
221
+
222
+ if not sample_names or not tree_indices:
223
+ await sio.emit("search-nodes-result", {
224
+ "highlights": {},
225
+ "lineage": {}
226
+ }, to=sid)
227
+ return
228
+
229
+ ctx = await get_file_context(session.file_path)
230
+ if ctx is None:
231
+ await sio.emit("search-nodes-result", {
232
+ "error": "Failed to load tree sequence"
233
+ }, to=sid)
234
+ return
235
+
236
+ ts = ctx.tree_sequence
237
+
238
+ result = await asyncio.to_thread(
239
+ search_nodes_in_trees,
240
+ ts,
241
+ sample_names,
242
+ tree_indices,
243
+ show_lineages,
244
+ sample_colors
245
+ )
246
+
247
+ await sio.emit("search-nodes-result", result, to=sid)
248
+ except Exception as e:
249
+ print(f"❌ Search nodes error: {e}")
250
+ await sio.emit("search-nodes-result", {"error": str(e)}, to=sid)
251
+
252
+ @sio.event
253
+ async def get_highlight_positions_event(sid, data):
254
+ """Socket event to get positions for all tip nodes matching a metadata value.
255
+
256
+ Returns positions for ALL matching nodes, ignoring sparsification.
257
+ Used for highlighting nodes that may not be currently rendered.
258
+
259
+ data: {
260
+ lorax_sid: str,
261
+ metadata_key: str, # Metadata key to filter by
262
+ metadata_value: str, # Metadata value to match
263
+ tree_indices: [int] # Tree indices to compute positions for
264
+ }
265
+
266
+ Returns: {
267
+ positions: [{node_id, tree_idx, x, y}, ...]
268
+ }
269
+ """
270
+ try:
271
+ lorax_sid = data.get("lorax_sid")
272
+ session = await require_session(lorax_sid, sid, sio)
273
+ if not session:
274
+ return
275
+
276
+ if not session.file_path:
277
+ print(f"⚠️ No file loaded for session {lorax_sid}")
278
+ await sio.emit("error", {
279
+ "code": ERROR_NO_FILE_LOADED,
280
+ "message": "No file loaded. Please load a file first."
281
+ }, to=sid)
282
+ return
283
+
284
+ if is_csv_session_file(session.file_path):
285
+ metadata_key = data.get("metadata_key")
286
+ metadata_value = data.get("metadata_value")
287
+ tree_indices = data.get("tree_indices", [])
288
+
289
+ if metadata_key != "sample":
290
+ await sio.emit("highlight-positions-result", {"positions": []}, to=sid)
291
+ return
292
+
293
+ if metadata_value is None or not tree_indices:
294
+ await sio.emit("highlight-positions-result", {"positions": []}, to=sid)
295
+ return
296
+
297
+ ctx = await get_file_context(session.file_path)
298
+ if ctx is None:
299
+ await sio.emit("highlight-positions-result", {"error": "Failed to load CSV"}, to=sid)
300
+ return
301
+
302
+ samples_order = ctx.config.get("samples") or []
303
+ sample_id_map = {str(name): idx for idx, name in enumerate(samples_order)}
304
+ target_node_id = sample_id_map.get(str(metadata_value))
305
+ if target_node_id is None:
306
+ await sio.emit("highlight-positions-result", {"positions": []}, to=sid)
307
+ return
308
+
309
+ positions = []
310
+ for tree_idx in tree_indices:
311
+ tree_idx = int(tree_idx)
312
+ graph = await _get_or_parse_csv_tree_graph(ctx, lorax_sid, tree_idx)
313
+ if graph is None:
314
+ continue
315
+
316
+ arr_idx = _find_node_index(graph, target_node_id)
317
+ if arr_idx is None:
318
+ continue
319
+
320
+ # Match the coordinate convention used by CSV layout buffer:
321
+ # build_csv_layout_response swaps (time->x, layout->y).
322
+ positions.append(
323
+ {
324
+ "node_id": int(target_node_id),
325
+ "tree_idx": int(tree_idx),
326
+ # Match tskit highlight convention: x=layout, y=time.
327
+ # (tskit highlight uses TreeGraph internal coords, not the swapped PyArrow coords)
328
+ "x": float(graph.x[arr_idx]),
329
+ "y": float(graph.y[arr_idx]),
330
+ }
331
+ )
332
+
333
+ await sio.emit("highlight-positions-result", {"positions": positions}, to=sid)
334
+ return
335
+
336
+ metadata_key = data.get("metadata_key")
337
+ metadata_value = data.get("metadata_value")
338
+ tree_indices = data.get("tree_indices", [])
339
+
340
+ if not metadata_key or metadata_value is None:
341
+ await sio.emit("highlight-positions-result", {
342
+ "error": "Missing metadata_key or metadata_value"
343
+ }, to=sid)
344
+ return
345
+
346
+ if not tree_indices:
347
+ await sio.emit("highlight-positions-result", {"positions": []}, to=sid)
348
+ return
349
+
350
+ ctx = await get_file_context(session.file_path)
351
+ if ctx is None:
352
+ await sio.emit("highlight-positions-result", {
353
+ "error": "Failed to load tree sequence"
354
+ }, to=sid)
355
+ return
356
+
357
+ ts = ctx.tree_sequence
358
+
359
+ result = await get_highlight_positions(
360
+ ts,
361
+ session.file_path,
362
+ metadata_key,
363
+ metadata_value,
364
+ tree_indices,
365
+ lorax_sid,
366
+ tree_graph_cache
367
+ )
368
+
369
+ await sio.emit("highlight-positions-result", result, to=sid)
370
+ except Exception as e:
371
+ print(f"❌ Get highlight positions error: {e}")
372
+ await sio.emit("highlight-positions-result", {"error": str(e)}, to=sid)
373
+
374
+ @sio.event
375
+ async def search_metadata_multi_event(sid, data):
376
+ """Socket event for multi-value metadata search.
377
+
378
+ Returns positions for tip nodes matching ANY of the metadata values,
379
+ grouped by value for per-value coloring with OR logic.
380
+
381
+ data: {
382
+ lorax_sid: str,
383
+ metadata_key: str, # Metadata key to filter by
384
+ metadata_values: [str], # Array of values (OR logic)
385
+ tree_indices: [int], # Tree indices to compute positions for
386
+ show_lineages: bool # Whether to compute lineage paths
387
+ }
388
+
389
+ Emits: "search-metadata-multi-result" with:
390
+ {
391
+ positions_by_value: {"Africa": [{node_id, tree_idx, x, y}, ...], ...},
392
+ lineages: {"Africa": {tree_idx: [{path_node_ids, color}]}} if show_lineages,
393
+ total_count: int
394
+ }
395
+ """
396
+ try:
397
+ lorax_sid = data.get("lorax_sid")
398
+ session = await require_session(lorax_sid, sid, sio)
399
+ if not session:
400
+ return
401
+
402
+ if not session.file_path:
403
+ print(f"⚠️ No file loaded for session {lorax_sid}")
404
+ await sio.emit("error", {
405
+ "code": ERROR_NO_FILE_LOADED,
406
+ "message": "No file loaded. Please load a file first."
407
+ }, to=sid)
408
+ return
409
+
410
+ if is_csv_session_file(session.file_path):
411
+ metadata_key = data.get("metadata_key")
412
+ metadata_values = data.get("metadata_values", [])
413
+ tree_indices = data.get("tree_indices", [])
414
+ show_lineages = bool(data.get("show_lineages", False))
415
+
416
+ if metadata_key != "sample":
417
+ await sio.emit(
418
+ "search-metadata-multi-result",
419
+ {"positions_by_value": {}, "lineages": {}, "total_count": 0},
420
+ to=sid,
421
+ )
422
+ return
423
+
424
+ if not metadata_values or not tree_indices:
425
+ await sio.emit(
426
+ "search-metadata-multi-result",
427
+ {"positions_by_value": {}, "lineages": {}, "total_count": 0},
428
+ to=sid,
429
+ )
430
+ return
431
+
432
+ ctx = await get_file_context(session.file_path)
433
+ if ctx is None:
434
+ await sio.emit("search-metadata-multi-result", {"error": "Failed to load CSV"}, to=sid)
435
+ return
436
+
437
+ samples_order = ctx.config.get("samples") or []
438
+ sample_id_map = {str(name): idx for idx, name in enumerate(samples_order)}
439
+
440
+ # Deduplicate and stringify values
441
+ unique_values = list({str(v) for v in metadata_values})
442
+ positions_by_value = {v: [] for v in unique_values}
443
+ lineages = {} if show_lineages else {}
444
+ total_count = 0
445
+
446
+ for value in unique_values:
447
+ node_id = sample_id_map.get(value)
448
+ if node_id is None:
449
+ continue
450
+
451
+ value_lineages = {} if show_lineages else None
452
+
453
+ for tree_idx in tree_indices:
454
+ tree_idx = int(tree_idx)
455
+ graph = await _get_or_parse_csv_tree_graph(ctx, lorax_sid, tree_idx)
456
+ if graph is None:
457
+ continue
458
+ node_id_to_index = _build_node_id_to_index(graph) if show_lineages else None
459
+ arr_idx = _find_node_index(graph, node_id)
460
+ if arr_idx is None:
461
+ continue
462
+
463
+ positions_by_value[value].append(
464
+ {
465
+ "node_id": int(node_id),
466
+ "tree_idx": int(tree_idx),
467
+ # Match tskit highlight convention: x=layout, y=time.
468
+ "x": float(graph.x[arr_idx]),
469
+ "y": float(graph.y[arr_idx]),
470
+ }
471
+ )
472
+ total_count += 1
473
+
474
+ if show_lineages:
475
+ path_node_ids = _compute_csv_lineage_path(graph, int(node_id), node_id_to_index)
476
+ if len(path_node_ids) > 1:
477
+ # Emit root -> tip to match frontend L-shape construction.
478
+ path_node_ids = list(reversed(path_node_ids))
479
+ value_lineages.setdefault(tree_idx, []).append(
480
+ {"path_node_ids": path_node_ids, "color": None}
481
+ )
482
+
483
+ if show_lineages and value_lineages:
484
+ lineages[value] = value_lineages
485
+
486
+ await sio.emit(
487
+ "search-metadata-multi-result",
488
+ {"positions_by_value": positions_by_value, "lineages": lineages, "total_count": total_count},
489
+ to=sid,
490
+ )
491
+ return
492
+
493
+ metadata_key = data.get("metadata_key")
494
+ metadata_values = data.get("metadata_values", [])
495
+ tree_indices = data.get("tree_indices", [])
496
+ show_lineages = data.get("show_lineages", False)
497
+
498
+ if not metadata_key:
499
+ await sio.emit("search-metadata-multi-result", {
500
+ "error": "Missing metadata_key"
501
+ }, to=sid)
502
+ return
503
+
504
+ if not metadata_values or not tree_indices:
505
+ await sio.emit("search-metadata-multi-result", {
506
+ "positions_by_value": {},
507
+ "lineages": {},
508
+ "total_count": 0
509
+ }, to=sid)
510
+ return
511
+
512
+ ctx = await get_file_context(session.file_path)
513
+ if ctx is None:
514
+ await sio.emit("search-metadata-multi-result", {
515
+ "error": "Failed to load tree sequence"
516
+ }, to=sid)
517
+ return
518
+
519
+ ts = ctx.tree_sequence
520
+
521
+ result = await get_multi_value_highlight_positions(
522
+ ts,
523
+ session.file_path,
524
+ metadata_key,
525
+ metadata_values,
526
+ tree_indices,
527
+ lorax_sid,
528
+ tree_graph_cache,
529
+ show_lineages
530
+ )
531
+
532
+ await sio.emit("search-metadata-multi-result", result, to=sid)
533
+ except Exception as e:
534
+ print(f"❌ Search metadata multi error: {e}")
535
+ await sio.emit("search-metadata-multi-result", {"error": str(e)}, to=sid)
@@ -0,0 +1,117 @@
1
+ """
2
+ Tree layout event handlers for Lorax Socket.IO.
3
+
4
+ Handles process_postorder_layout and cache_trees events.
5
+ """
6
+
7
+ from lorax.context import tree_graph_cache, csv_tree_graph_cache
8
+ from lorax.handlers import handle_tree_graph_query, ensure_trees_cached
9
+ from lorax.sockets.decorators import require_session
10
+ from lorax.sockets.utils import is_csv_session_file
11
+
12
+
13
+ def register_tree_layout_events(sio):
14
+ """Register tree layout socket events."""
15
+
16
+ @sio.event
17
+ async def process_postorder_layout(sid, data):
18
+ """Socket event to get post-order tree traversal for efficient rendering.
19
+
20
+ Returns PyArrow IPC binary data with post-order node arrays.
21
+ Frontend computes layout using stack-based reconstruction.
22
+
23
+ Uses Socket.IO acknowledgement callback pattern - returns result directly
24
+ instead of emitting to ensure request-response correlation.
25
+ """
26
+ try:
27
+ lorax_sid = data.get("lorax_sid")
28
+ session = await require_session(lorax_sid, sid, sio)
29
+ if not session:
30
+ return {"error": "Session not found", "request_id": data.get("request_id")}
31
+
32
+ if not session.file_path:
33
+ print(f"⚠️ No file loaded for session {lorax_sid}")
34
+ return {"error": "No file loaded for session", "request_id": data.get("request_id")}
35
+
36
+ display_array = data.get("displayArray", [])
37
+ actual_display_array = data.get("actualDisplayArray", display_array)
38
+ sparsification = data.get("sparsification", False)
39
+ request_id = data.get("request_id")
40
+
41
+ # handle_tree_graph_query returns dict with PyArrow buffer (Numba-optimized)
42
+ # Pass session_id and tree_graph_cache for caching TreeGraph objects
43
+ # actual_display_array contains all visible trees for cache eviction
44
+ result = await handle_tree_graph_query(
45
+ session.file_path,
46
+ display_array,
47
+ sparsification=sparsification,
48
+ session_id=lorax_sid,
49
+ tree_graph_cache=tree_graph_cache,
50
+ csv_tree_graph_cache=csv_tree_graph_cache,
51
+ actual_display_array=actual_display_array
52
+ )
53
+
54
+ if "error" in result:
55
+ return {"error": result["error"], "request_id": request_id}
56
+ else:
57
+ # Return result directly - Socket.IO sends as acknowledgement callback
58
+ return {
59
+ "buffer": result["buffer"], # Binary PyArrow IPC data
60
+ "global_min_time": result["global_min_time"],
61
+ "global_max_time": result["global_max_time"],
62
+ "tree_indices": result["tree_indices"],
63
+ "request_id": request_id
64
+ }
65
+ except Exception as e:
66
+ print(f"❌ Postorder layout query error: {e}")
67
+ return {"error": str(e), "request_id": data.get("request_id")}
68
+
69
+ @sio.event
70
+ async def cache_trees(sid, data):
71
+ """Socket event to pre-cache TreeGraph objects for lineage operations.
72
+
73
+ Call this after process_postorder_layout to enable subsequent lineage queries.
74
+
75
+ data: {
76
+ lorax_sid: str,
77
+ tree_indices: [int] # Tree indices to cache
78
+ }
79
+
80
+ Returns: {
81
+ cached_count: int, # Number of trees newly cached
82
+ total_cached: int # Total trees now in cache for session
83
+ }
84
+ """
85
+ try:
86
+ lorax_sid = data.get("lorax_sid")
87
+ session = await require_session(lorax_sid, sid, sio)
88
+ if not session:
89
+ return {"error": "Session not found", "cached_count": 0}
90
+
91
+ if not session.file_path:
92
+ return {"error": "No file loaded", "cached_count": 0}
93
+
94
+ if is_csv_session_file(session.file_path):
95
+ return {"error": "Lineage not supported for CSV", "cached_count": 0}
96
+
97
+ tree_indices = data.get("tree_indices", [])
98
+ if not tree_indices:
99
+ return {"cached_count": 0, "total_cached": 0}
100
+
101
+ newly_cached = await ensure_trees_cached(
102
+ session.file_path,
103
+ tree_indices,
104
+ lorax_sid,
105
+ tree_graph_cache
106
+ )
107
+
108
+ # Get total cached
109
+ all_cached = await tree_graph_cache.get_all_for_session(lorax_sid)
110
+
111
+ return {
112
+ "cached_count": newly_cached,
113
+ "total_cached": len(all_cached)
114
+ }
115
+ except Exception as e:
116
+ print(f"❌ Cache trees error: {e}")
117
+ return {"error": str(e), "cached_count": 0}
lorax/sockets/utils.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Socket utility functions for Lorax.
3
+
4
+ Common helpers shared across socket event handlers.
5
+ """
6
+
7
+
8
+ def is_csv_session_file(file_path: str | None) -> bool:
9
+ """Check if the session file is a CSV file."""
10
+ return bool(file_path) and str(file_path).lower().endswith(".csv")
@@ -0,0 +1,12 @@
1
+ """
2
+ tree_graph - Optimized tree construction with Numba JIT compilation.
3
+
4
+ This module provides fast tree construction from tskit tables with:
5
+ - Numba-compiled post-order traversal (50-100x faster than Python)
6
+ - CSR format for efficient children access
7
+ - PyArrow serialization for frontend rendering
8
+ """
9
+
10
+ from .tree_graph import TreeGraph, construct_tree, construct_trees_batch
11
+
12
+ __all__ = ['TreeGraph', 'construct_tree', 'construct_trees_batch']