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.
- lorax/buffer.py +43 -0
- lorax/cache/__init__.py +43 -0
- lorax/cache/csv_tree_graph.py +59 -0
- lorax/cache/disk.py +467 -0
- lorax/cache/file_cache.py +142 -0
- lorax/cache/file_context.py +72 -0
- lorax/cache/lru.py +90 -0
- lorax/cache/tree_graph.py +293 -0
- lorax/cli.py +312 -0
- lorax/cloud/__init__.py +0 -0
- lorax/cloud/gcs_utils.py +205 -0
- lorax/constants.py +66 -0
- lorax/context.py +80 -0
- lorax/csv/__init__.py +7 -0
- lorax/csv/config.py +250 -0
- lorax/csv/layout.py +182 -0
- lorax/csv/newick_tree.py +234 -0
- lorax/handlers.py +998 -0
- lorax/lineage.py +456 -0
- lorax/loaders/__init__.py +0 -0
- lorax/loaders/csv_loader.py +10 -0
- lorax/loaders/loader.py +31 -0
- lorax/loaders/tskit_loader.py +119 -0
- lorax/lorax_app.py +75 -0
- lorax/manager.py +58 -0
- lorax/metadata/__init__.py +0 -0
- lorax/metadata/loader.py +426 -0
- lorax/metadata/mutations.py +146 -0
- lorax/modes.py +190 -0
- lorax/pg.py +183 -0
- lorax/redis_utils.py +30 -0
- lorax/routes.py +137 -0
- lorax/session_manager.py +206 -0
- lorax/sockets/__init__.py +55 -0
- lorax/sockets/connection.py +99 -0
- lorax/sockets/debug.py +47 -0
- lorax/sockets/decorators.py +112 -0
- lorax/sockets/file_ops.py +200 -0
- lorax/sockets/lineage.py +307 -0
- lorax/sockets/metadata.py +232 -0
- lorax/sockets/mutations.py +154 -0
- lorax/sockets/node_search.py +535 -0
- lorax/sockets/tree_layout.py +117 -0
- lorax/sockets/utils.py +10 -0
- lorax/tree_graph/__init__.py +12 -0
- lorax/tree_graph/tree_graph.py +689 -0
- lorax/utils.py +124 -0
- lorax_app/__init__.py +4 -0
- lorax_app/app.py +159 -0
- lorax_app/cli.py +114 -0
- lorax_app/static/X.png +0 -0
- lorax_app/static/assets/index-BCEGlUFi.js +2361 -0
- lorax_app/static/assets/index-iKjzUpA9.css +1 -0
- lorax_app/static/assets/localBackendWorker-BaWwjSV_.js +2 -0
- lorax_app/static/assets/renderDataWorker-BKLdiU7J.js +2 -0
- lorax_app/static/gestures/gesture-flick.ogv +0 -0
- lorax_app/static/gestures/gesture-two-finger-scroll.ogv +0 -0
- lorax_app/static/index.html +14 -0
- lorax_app/static/logo.png +0 -0
- lorax_app/static/lorax-logo.png +0 -0
- lorax_app/static/vite.svg +1 -0
- lorax_arg-0.1.dist-info/METADATA +131 -0
- lorax_arg-0.1.dist-info/RECORD +66 -0
- lorax_arg-0.1.dist-info/WHEEL +5 -0
- lorax_arg-0.1.dist-info/entry_points.txt +4 -0
- lorax_arg-0.1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File operation event handlers for Lorax Socket.IO.
|
|
3
|
+
|
|
4
|
+
Handles load_file, details, and query events.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
import asyncio
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from lorax.context import session_manager, BUCKET_NAME, tree_graph_cache, csv_tree_graph_cache
|
|
13
|
+
from lorax.modes import CURRENT_MODE
|
|
14
|
+
from lorax.constants import (
|
|
15
|
+
UPLOADS_DIR, ERROR_SESSION_NOT_FOUND, ERROR_MISSING_SESSION, ERROR_NO_FILE_LOADED,
|
|
16
|
+
)
|
|
17
|
+
from lorax.cloud.gcs_utils import download_gcs_file
|
|
18
|
+
from lorax.handlers import handle_upload, handle_details
|
|
19
|
+
from lorax.sockets.decorators import require_session
|
|
20
|
+
from lorax.sockets.utils import is_csv_session_file
|
|
21
|
+
|
|
22
|
+
UPLOAD_DIR = Path(UPLOADS_DIR)
|
|
23
|
+
UPLOAD_DIR.mkdir(exist_ok=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def register_file_events(sio):
|
|
27
|
+
"""Register file operation socket events."""
|
|
28
|
+
|
|
29
|
+
async def background_load_file(sid, data):
|
|
30
|
+
try:
|
|
31
|
+
lorax_sid = data.get("lorax_sid")
|
|
32
|
+
share_sid = data.get("share_sid")
|
|
33
|
+
|
|
34
|
+
if not lorax_sid:
|
|
35
|
+
print(f"⚠️ Missing lorax_sid")
|
|
36
|
+
await sio.emit("error", {
|
|
37
|
+
"code": ERROR_MISSING_SESSION,
|
|
38
|
+
"message": "Session ID is missing."
|
|
39
|
+
}, to=sid)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
session = await session_manager.get_session(lorax_sid)
|
|
43
|
+
if not session:
|
|
44
|
+
print(f"⚠️ Unknown sid {lorax_sid}")
|
|
45
|
+
await sio.emit("error", {
|
|
46
|
+
"code": ERROR_SESSION_NOT_FOUND,
|
|
47
|
+
"message": "Session expired. Please refresh the page."
|
|
48
|
+
}, to=sid)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
if share_sid and share_sid != lorax_sid:
|
|
52
|
+
print(f"⚠️ share_sid denied for sid={lorax_sid} target={share_sid}")
|
|
53
|
+
await sio.emit("error", {
|
|
54
|
+
"code": "share_sid_denied",
|
|
55
|
+
"message": "Access denied for shared upload."
|
|
56
|
+
}, to=sid)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
project = str(data.get("project"))
|
|
60
|
+
filename = str(data.get("file"))
|
|
61
|
+
|
|
62
|
+
# Extract genomic coordinates from client if provided
|
|
63
|
+
genomiccoordstart = data.get("genomiccoordstart")
|
|
64
|
+
genomiccoordend = data.get("genomiccoordend")
|
|
65
|
+
print("lorax_sid", lorax_sid, project, filename)
|
|
66
|
+
if not filename:
|
|
67
|
+
print("Missing file param")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if project == 'Uploads':
|
|
71
|
+
target_sid = share_sid if share_sid else lorax_sid
|
|
72
|
+
if CURRENT_MODE == "local":
|
|
73
|
+
file_path = UPLOAD_DIR / project / filename
|
|
74
|
+
blob_path = f"{project}/{filename}"
|
|
75
|
+
else:
|
|
76
|
+
file_path = UPLOAD_DIR / project / target_sid / filename
|
|
77
|
+
blob_path = f"{project}/{target_sid}/{filename}"
|
|
78
|
+
else:
|
|
79
|
+
file_path = UPLOAD_DIR / project / filename
|
|
80
|
+
blob_path = f"{project}/{filename}"
|
|
81
|
+
|
|
82
|
+
if BUCKET_NAME and CURRENT_MODE != "local":
|
|
83
|
+
if file_path.exists():
|
|
84
|
+
print(f"File {file_path} already exists, skipping download.")
|
|
85
|
+
else:
|
|
86
|
+
print(f"Downloading file {file_path} from {BUCKET_NAME}")
|
|
87
|
+
await download_gcs_file(BUCKET_NAME, f"{blob_path}", str(file_path))
|
|
88
|
+
else:
|
|
89
|
+
print("local mode or no bucket; using local uploads only")
|
|
90
|
+
|
|
91
|
+
if not file_path.exists():
|
|
92
|
+
print("File not found")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Clear TreeGraph cache when loading a new file
|
|
96
|
+
await tree_graph_cache.clear_session(lorax_sid)
|
|
97
|
+
await csv_tree_graph_cache.clear_session(lorax_sid)
|
|
98
|
+
|
|
99
|
+
session.file_path = str(file_path)
|
|
100
|
+
await session_manager.save_session(session)
|
|
101
|
+
|
|
102
|
+
print("loading file", file_path, os.getpid())
|
|
103
|
+
ctx = await handle_upload(str(file_path), str(UPLOAD_DIR))
|
|
104
|
+
|
|
105
|
+
await sio.emit("status", {
|
|
106
|
+
"status": "processing-file",
|
|
107
|
+
"message": "Processing file...",
|
|
108
|
+
"filename": filename,
|
|
109
|
+
"project": project
|
|
110
|
+
}, to=sid)
|
|
111
|
+
|
|
112
|
+
# Config is already computed and cached in FileContext
|
|
113
|
+
config = ctx.config
|
|
114
|
+
|
|
115
|
+
if config is None:
|
|
116
|
+
await sio.emit("error", {"message": "Failed to load file configuration"}, to=sid)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Override initial_position if client provided genomic coordinates
|
|
120
|
+
if genomiccoordstart is not None and genomiccoordend is not None:
|
|
121
|
+
try:
|
|
122
|
+
config['initial_position'] = [int(genomiccoordstart), int(genomiccoordend)]
|
|
123
|
+
print(f"Using client-provided coordinates: [{genomiccoordstart}, {genomiccoordend}]")
|
|
124
|
+
except (ValueError, TypeError) as e:
|
|
125
|
+
print(f"Invalid coordinates, using computed: {e}")
|
|
126
|
+
|
|
127
|
+
owner_sid = share_sid if share_sid else lorax_sid
|
|
128
|
+
await sio.emit("load-file-result", {
|
|
129
|
+
"message": "File loaded",
|
|
130
|
+
"sid": sid,
|
|
131
|
+
"filename": filename,
|
|
132
|
+
"config": config,
|
|
133
|
+
"owner_sid": owner_sid
|
|
134
|
+
}, to=sid)
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(f"Load file error: {e}")
|
|
138
|
+
await sio.emit("error", {"message": str(e)}, to=sid)
|
|
139
|
+
|
|
140
|
+
@sio.event
|
|
141
|
+
async def load_file(sid, data):
|
|
142
|
+
asyncio.create_task(background_load_file(sid, data))
|
|
143
|
+
|
|
144
|
+
@sio.event
|
|
145
|
+
async def details(sid, data):
|
|
146
|
+
try:
|
|
147
|
+
lorax_sid = data.get("lorax_sid")
|
|
148
|
+
session = await require_session(lorax_sid, sid, sio)
|
|
149
|
+
if not session:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if not session.file_path:
|
|
153
|
+
print(f"⚠️ No file loaded for session {lorax_sid}")
|
|
154
|
+
await sio.emit("error", {
|
|
155
|
+
"code": ERROR_NO_FILE_LOADED,
|
|
156
|
+
"message": "No file loaded. Please load a file first."
|
|
157
|
+
}, to=sid)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if is_csv_session_file(session.file_path):
|
|
161
|
+
await sio.emit("details-result", {
|
|
162
|
+
"data": {"error": "Details are not supported for CSV yet."}
|
|
163
|
+
}, to=sid)
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
print("fetch details in ", session.sid, os.getpid())
|
|
167
|
+
|
|
168
|
+
result = await handle_details(session.file_path, data)
|
|
169
|
+
await sio.emit("details-result", {"data": json.loads(result)}, to=sid)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f"❌ Details error: {e}")
|
|
172
|
+
await sio.emit("details-result", {"error": str(e)}, to=sid)
|
|
173
|
+
|
|
174
|
+
@sio.event
|
|
175
|
+
async def query(sid, data):
|
|
176
|
+
"""Socket event to query tree nodes."""
|
|
177
|
+
try:
|
|
178
|
+
lorax_sid = data.get("lorax_sid")
|
|
179
|
+
session = await require_session(lorax_sid, sid, sio)
|
|
180
|
+
if not session:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
if not session.file_path:
|
|
184
|
+
print(f"⚠️ No file loaded for session {lorax_sid}")
|
|
185
|
+
await sio.emit("error", {
|
|
186
|
+
"code": ERROR_NO_FILE_LOADED,
|
|
187
|
+
"message": "No file loaded. Please load a file first."
|
|
188
|
+
}, to=sid)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
value = data.get("value")
|
|
192
|
+
local_trees = data.get("localTrees", [])
|
|
193
|
+
|
|
194
|
+
# Acknowledge the query - the actual tree data is processed by the frontend worker
|
|
195
|
+
await sio.emit("query-result", {
|
|
196
|
+
"data": {"value": value, "localTrees": local_trees}
|
|
197
|
+
}, to=sid)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(f"❌ Query error: {e}")
|
|
200
|
+
await sio.emit("query-result", {"error": str(e)}, to=sid)
|
lorax/sockets/lineage.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lineage event handlers for Lorax Socket.IO.
|
|
3
|
+
|
|
4
|
+
Handles ancestry and descendant operations:
|
|
5
|
+
- get_ancestors_event
|
|
6
|
+
- get_descendants_event
|
|
7
|
+
- search_nodes_by_criteria_event
|
|
8
|
+
- get_subtree_event
|
|
9
|
+
- get_mrca_event
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from lorax.context import tree_graph_cache
|
|
13
|
+
from lorax.handlers import get_or_construct_tree_graph
|
|
14
|
+
from lorax.lineage import (
|
|
15
|
+
get_ancestors, get_descendants, search_nodes_by_criteria,
|
|
16
|
+
get_subtree, get_mrca
|
|
17
|
+
)
|
|
18
|
+
from lorax.sockets.decorators import require_session
|
|
19
|
+
from lorax.sockets.utils import is_csv_session_file
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register_lineage_events(sio):
|
|
23
|
+
"""Register lineage-related socket events."""
|
|
24
|
+
|
|
25
|
+
@sio.event
|
|
26
|
+
async def get_ancestors_event(sid, data):
|
|
27
|
+
"""Socket event to get ancestors (path to root) for a node.
|
|
28
|
+
|
|
29
|
+
Requires the tree to be cached first (call cache_trees or process a layout).
|
|
30
|
+
|
|
31
|
+
data: {
|
|
32
|
+
lorax_sid: str,
|
|
33
|
+
tree_index: int,
|
|
34
|
+
node_id: int
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Returns: {
|
|
38
|
+
ancestors: [int], # Node IDs from node to root
|
|
39
|
+
path: [{node_id, time, x, y}], # Path coordinates for visualization
|
|
40
|
+
tree_index: int,
|
|
41
|
+
query_node: int
|
|
42
|
+
}
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
lorax_sid = data.get("lorax_sid")
|
|
46
|
+
session = await require_session(lorax_sid, sid, sio)
|
|
47
|
+
if not session:
|
|
48
|
+
return {"error": "Session not found", "ancestors": [], "path": []}
|
|
49
|
+
|
|
50
|
+
if not session.file_path:
|
|
51
|
+
return {"error": "No file loaded", "ancestors": [], "path": []}
|
|
52
|
+
|
|
53
|
+
if is_csv_session_file(session.file_path):
|
|
54
|
+
return {"error": "Lineage not supported for CSV", "ancestors": [], "path": []}
|
|
55
|
+
|
|
56
|
+
tree_index = data.get("tree_index")
|
|
57
|
+
node_id = data.get("node_id")
|
|
58
|
+
|
|
59
|
+
if tree_index is None or node_id is None:
|
|
60
|
+
return {"error": "Missing tree_index or node_id", "ancestors": [], "path": []}
|
|
61
|
+
|
|
62
|
+
# Ensure tree is cached
|
|
63
|
+
await get_or_construct_tree_graph(
|
|
64
|
+
session.file_path,
|
|
65
|
+
int(tree_index),
|
|
66
|
+
lorax_sid,
|
|
67
|
+
tree_graph_cache
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
result = await get_ancestors(
|
|
71
|
+
tree_graph_cache,
|
|
72
|
+
lorax_sid,
|
|
73
|
+
int(tree_index),
|
|
74
|
+
int(node_id)
|
|
75
|
+
)
|
|
76
|
+
return result
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"❌ Get ancestors error: {e}")
|
|
79
|
+
return {"error": str(e), "ancestors": [], "path": []}
|
|
80
|
+
|
|
81
|
+
@sio.event
|
|
82
|
+
async def get_descendants_event(sid, data):
|
|
83
|
+
"""Socket event to get all descendants of a node.
|
|
84
|
+
|
|
85
|
+
data: {
|
|
86
|
+
lorax_sid: str,
|
|
87
|
+
tree_index: int,
|
|
88
|
+
node_id: int,
|
|
89
|
+
include_tips_only: bool # Optional, default False
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Returns: {
|
|
93
|
+
descendants: [int],
|
|
94
|
+
tips: [int],
|
|
95
|
+
total_descendants: int,
|
|
96
|
+
tree_index: int,
|
|
97
|
+
query_node: int
|
|
98
|
+
}
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
lorax_sid = data.get("lorax_sid")
|
|
102
|
+
session = await require_session(lorax_sid, sid, sio)
|
|
103
|
+
if not session:
|
|
104
|
+
return {"error": "Session not found", "descendants": [], "tips": []}
|
|
105
|
+
|
|
106
|
+
if not session.file_path:
|
|
107
|
+
return {"error": "No file loaded", "descendants": [], "tips": []}
|
|
108
|
+
|
|
109
|
+
if is_csv_session_file(session.file_path):
|
|
110
|
+
return {"error": "Lineage not supported for CSV", "descendants": [], "tips": []}
|
|
111
|
+
|
|
112
|
+
tree_index = data.get("tree_index")
|
|
113
|
+
node_id = data.get("node_id")
|
|
114
|
+
include_tips_only = data.get("include_tips_only", False)
|
|
115
|
+
|
|
116
|
+
if tree_index is None or node_id is None:
|
|
117
|
+
return {"error": "Missing tree_index or node_id", "descendants": [], "tips": []}
|
|
118
|
+
|
|
119
|
+
# Ensure tree is cached
|
|
120
|
+
await get_or_construct_tree_graph(
|
|
121
|
+
session.file_path,
|
|
122
|
+
int(tree_index),
|
|
123
|
+
lorax_sid,
|
|
124
|
+
tree_graph_cache
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
result = await get_descendants(
|
|
128
|
+
tree_graph_cache,
|
|
129
|
+
lorax_sid,
|
|
130
|
+
int(tree_index),
|
|
131
|
+
int(node_id),
|
|
132
|
+
include_tips_only=include_tips_only
|
|
133
|
+
)
|
|
134
|
+
return result
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print(f"❌ Get descendants error: {e}")
|
|
137
|
+
return {"error": str(e), "descendants": [], "tips": []}
|
|
138
|
+
|
|
139
|
+
@sio.event
|
|
140
|
+
async def search_nodes_by_criteria_event(sid, data):
|
|
141
|
+
"""Socket event to search nodes by criteria (time, tip status, etc).
|
|
142
|
+
|
|
143
|
+
data: {
|
|
144
|
+
lorax_sid: str,
|
|
145
|
+
tree_index: int,
|
|
146
|
+
criteria: {
|
|
147
|
+
min_time: float, # Optional
|
|
148
|
+
max_time: float, # Optional
|
|
149
|
+
is_tip: bool, # Optional: True for tips, False for internal
|
|
150
|
+
has_children: bool, # Optional: inverse of is_tip
|
|
151
|
+
node_ids: [int] # Optional: filter to these nodes
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Returns: {
|
|
156
|
+
matches: [int],
|
|
157
|
+
positions: [{node_id, x, y, time}],
|
|
158
|
+
total_matches: int
|
|
159
|
+
}
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
lorax_sid = data.get("lorax_sid")
|
|
163
|
+
session = await require_session(lorax_sid, sid, sio)
|
|
164
|
+
if not session:
|
|
165
|
+
return {"error": "Session not found", "matches": [], "positions": []}
|
|
166
|
+
|
|
167
|
+
if not session.file_path:
|
|
168
|
+
return {"error": "No file loaded", "matches": [], "positions": []}
|
|
169
|
+
|
|
170
|
+
if is_csv_session_file(session.file_path):
|
|
171
|
+
return {"error": "Search not supported for CSV", "matches": [], "positions": []}
|
|
172
|
+
|
|
173
|
+
tree_index = data.get("tree_index")
|
|
174
|
+
criteria = data.get("criteria", {})
|
|
175
|
+
|
|
176
|
+
if tree_index is None:
|
|
177
|
+
return {"error": "Missing tree_index", "matches": [], "positions": []}
|
|
178
|
+
|
|
179
|
+
# Ensure tree is cached
|
|
180
|
+
await get_or_construct_tree_graph(
|
|
181
|
+
session.file_path,
|
|
182
|
+
int(tree_index),
|
|
183
|
+
lorax_sid,
|
|
184
|
+
tree_graph_cache
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
result = await search_nodes_by_criteria(
|
|
188
|
+
tree_graph_cache,
|
|
189
|
+
lorax_sid,
|
|
190
|
+
int(tree_index),
|
|
191
|
+
criteria
|
|
192
|
+
)
|
|
193
|
+
return result
|
|
194
|
+
except Exception as e:
|
|
195
|
+
print(f"❌ Search nodes error: {e}")
|
|
196
|
+
return {"error": str(e), "matches": [], "positions": []}
|
|
197
|
+
|
|
198
|
+
@sio.event
|
|
199
|
+
async def get_subtree_event(sid, data):
|
|
200
|
+
"""Socket event to get the complete subtree rooted at a node.
|
|
201
|
+
|
|
202
|
+
data: {
|
|
203
|
+
lorax_sid: str,
|
|
204
|
+
tree_index: int,
|
|
205
|
+
root_node_id: int
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Returns: {
|
|
209
|
+
nodes: [{node_id, parent_id, x, y, time, is_tip}],
|
|
210
|
+
edges: [{parent, child}],
|
|
211
|
+
total_nodes: int
|
|
212
|
+
}
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
lorax_sid = data.get("lorax_sid")
|
|
216
|
+
session = await require_session(lorax_sid, sid, sio)
|
|
217
|
+
if not session:
|
|
218
|
+
return {"error": "Session not found", "nodes": [], "edges": []}
|
|
219
|
+
|
|
220
|
+
if not session.file_path:
|
|
221
|
+
return {"error": "No file loaded", "nodes": [], "edges": []}
|
|
222
|
+
|
|
223
|
+
if is_csv_session_file(session.file_path):
|
|
224
|
+
return {"error": "Subtree not supported for CSV", "nodes": [], "edges": []}
|
|
225
|
+
|
|
226
|
+
tree_index = data.get("tree_index")
|
|
227
|
+
root_node_id = data.get("root_node_id")
|
|
228
|
+
|
|
229
|
+
if tree_index is None or root_node_id is None:
|
|
230
|
+
return {"error": "Missing tree_index or root_node_id", "nodes": [], "edges": []}
|
|
231
|
+
|
|
232
|
+
# Ensure tree is cached
|
|
233
|
+
await get_or_construct_tree_graph(
|
|
234
|
+
session.file_path,
|
|
235
|
+
int(tree_index),
|
|
236
|
+
lorax_sid,
|
|
237
|
+
tree_graph_cache
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
result = await get_subtree(
|
|
241
|
+
tree_graph_cache,
|
|
242
|
+
lorax_sid,
|
|
243
|
+
int(tree_index),
|
|
244
|
+
int(root_node_id)
|
|
245
|
+
)
|
|
246
|
+
return result
|
|
247
|
+
except Exception as e:
|
|
248
|
+
print(f"❌ Get subtree error: {e}")
|
|
249
|
+
return {"error": str(e), "nodes": [], "edges": []}
|
|
250
|
+
|
|
251
|
+
@sio.event
|
|
252
|
+
async def get_mrca_event(sid, data):
|
|
253
|
+
"""Socket event to find the Most Recent Common Ancestor of multiple nodes.
|
|
254
|
+
|
|
255
|
+
data: {
|
|
256
|
+
lorax_sid: str,
|
|
257
|
+
tree_index: int,
|
|
258
|
+
node_ids: [int] # At least 2 nodes
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
Returns: {
|
|
262
|
+
mrca: int, # Node ID of MRCA
|
|
263
|
+
mrca_time: float,
|
|
264
|
+
mrca_position: {x, y},
|
|
265
|
+
tree_index: int,
|
|
266
|
+
query_nodes: [int]
|
|
267
|
+
}
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
lorax_sid = data.get("lorax_sid")
|
|
271
|
+
session = await require_session(lorax_sid, sid, sio)
|
|
272
|
+
if not session:
|
|
273
|
+
return {"error": "Session not found", "mrca": None}
|
|
274
|
+
|
|
275
|
+
if not session.file_path:
|
|
276
|
+
return {"error": "No file loaded", "mrca": None}
|
|
277
|
+
|
|
278
|
+
if is_csv_session_file(session.file_path):
|
|
279
|
+
return {"error": "MRCA not supported for CSV", "mrca": None}
|
|
280
|
+
|
|
281
|
+
tree_index = data.get("tree_index")
|
|
282
|
+
node_ids = data.get("node_ids", [])
|
|
283
|
+
|
|
284
|
+
if tree_index is None:
|
|
285
|
+
return {"error": "Missing tree_index", "mrca": None}
|
|
286
|
+
|
|
287
|
+
if len(node_ids) < 2:
|
|
288
|
+
return {"error": "Need at least 2 nodes", "mrca": None}
|
|
289
|
+
|
|
290
|
+
# Ensure tree is cached
|
|
291
|
+
await get_or_construct_tree_graph(
|
|
292
|
+
session.file_path,
|
|
293
|
+
int(tree_index),
|
|
294
|
+
lorax_sid,
|
|
295
|
+
tree_graph_cache
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
result = await get_mrca(
|
|
299
|
+
tree_graph_cache,
|
|
300
|
+
lorax_sid,
|
|
301
|
+
int(tree_index),
|
|
302
|
+
[int(n) for n in node_ids]
|
|
303
|
+
)
|
|
304
|
+
return result
|
|
305
|
+
except Exception as e:
|
|
306
|
+
print(f"❌ Get MRCA error: {e}")
|
|
307
|
+
return {"error": str(e), "mrca": None}
|