pg-sui 0.2.3__py3-none-any.whl → 1.6.16a3__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.
- pg_sui-1.6.16a3.dist-info/METADATA +292 -0
- pg_sui-1.6.16a3.dist-info/RECORD +81 -0
- {pg_sui-0.2.3.dist-info → pg_sui-1.6.16a3.dist-info}/WHEEL +1 -1
- pg_sui-1.6.16a3.dist-info/entry_points.txt +4 -0
- {pg_sui-0.2.3.dist-info → pg_sui-1.6.16a3.dist-info/licenses}/LICENSE +0 -0
- pg_sui-1.6.16a3.dist-info/top_level.txt +1 -0
- pgsui/__init__.py +35 -54
- pgsui/_version.py +34 -0
- pgsui/cli.py +922 -0
- pgsui/data_processing/__init__.py +0 -0
- pgsui/data_processing/config.py +565 -0
- pgsui/data_processing/containers.py +1436 -0
- pgsui/data_processing/transformers.py +557 -907
- pgsui/{example_data/trees → electron/app}/__init__.py +0 -0
- pgsui/electron/app/__main__.py +5 -0
- pgsui/electron/app/extra-resources/.gitkeep +1 -0
- pgsui/electron/app/icons/icons/1024x1024.png +0 -0
- pgsui/electron/app/icons/icons/128x128.png +0 -0
- pgsui/electron/app/icons/icons/16x16.png +0 -0
- pgsui/electron/app/icons/icons/24x24.png +0 -0
- pgsui/electron/app/icons/icons/256x256.png +0 -0
- pgsui/electron/app/icons/icons/32x32.png +0 -0
- pgsui/electron/app/icons/icons/48x48.png +0 -0
- pgsui/electron/app/icons/icons/512x512.png +0 -0
- pgsui/electron/app/icons/icons/64x64.png +0 -0
- pgsui/electron/app/icons/icons/icon.icns +0 -0
- pgsui/electron/app/icons/icons/icon.ico +0 -0
- pgsui/electron/app/main.js +227 -0
- pgsui/electron/app/package-lock.json +6894 -0
- pgsui/electron/app/package.json +51 -0
- pgsui/electron/app/preload.js +15 -0
- pgsui/electron/app/server.py +157 -0
- pgsui/electron/app/ui/logo.png +0 -0
- pgsui/electron/app/ui/renderer.js +131 -0
- pgsui/electron/app/ui/styles.css +59 -0
- pgsui/electron/app/ui/ui_shim.js +72 -0
- pgsui/electron/bootstrap.py +43 -0
- pgsui/electron/launch.py +57 -0
- pgsui/electron/package.json +14 -0
- pgsui/example_data/__init__.py +0 -0
- pgsui/example_data/phylip_files/__init__.py +0 -0
- pgsui/example_data/phylip_files/test.phy +0 -0
- pgsui/example_data/popmaps/__init__.py +0 -0
- pgsui/example_data/popmaps/{test.popmap → phylogen_nomx.popmap} +185 -99
- pgsui/example_data/structure_files/__init__.py +0 -0
- pgsui/example_data/structure_files/test.pops.2row.allsites.str +0 -0
- pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz +0 -0
- pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz.tbi +0 -0
- pgsui/impute/__init__.py +0 -0
- pgsui/impute/deterministic/imputers/allele_freq.py +725 -0
- pgsui/impute/deterministic/imputers/mode.py +844 -0
- pgsui/impute/deterministic/imputers/nmf.py +221 -0
- pgsui/impute/deterministic/imputers/phylo.py +973 -0
- pgsui/impute/deterministic/imputers/ref_allele.py +669 -0
- pgsui/impute/supervised/__init__.py +0 -0
- pgsui/impute/supervised/base.py +343 -0
- pgsui/impute/{unsupervised/models/in_development → supervised/imputers}/__init__.py +0 -0
- pgsui/impute/supervised/imputers/hist_gradient_boosting.py +317 -0
- pgsui/impute/supervised/imputers/random_forest.py +291 -0
- pgsui/impute/unsupervised/__init__.py +0 -0
- pgsui/impute/unsupervised/base.py +1121 -0
- pgsui/impute/unsupervised/callbacks.py +92 -262
- {simulation → pgsui/impute/unsupervised/imputers}/__init__.py +0 -0
- pgsui/impute/unsupervised/imputers/autoencoder.py +1361 -0
- pgsui/impute/unsupervised/imputers/nlpca.py +1666 -0
- pgsui/impute/unsupervised/imputers/ubp.py +1660 -0
- pgsui/impute/unsupervised/imputers/vae.py +1316 -0
- pgsui/impute/unsupervised/loss_functions.py +261 -0
- pgsui/impute/unsupervised/models/__init__.py +0 -0
- pgsui/impute/unsupervised/models/autoencoder_model.py +215 -567
- pgsui/impute/unsupervised/models/nlpca_model.py +155 -394
- pgsui/impute/unsupervised/models/ubp_model.py +180 -1106
- pgsui/impute/unsupervised/models/vae_model.py +269 -630
- pgsui/impute/unsupervised/nn_scorers.py +255 -0
- pgsui/utils/__init__.py +0 -0
- pgsui/utils/classification_viz.py +608 -0
- pgsui/utils/logging_utils.py +22 -0
- pgsui/utils/misc.py +35 -480
- pgsui/utils/plotting.py +996 -829
- pgsui/utils/pretty_metrics.py +290 -0
- pgsui/utils/scorers.py +213 -666
- pg_sui-0.2.3.dist-info/METADATA +0 -322
- pg_sui-0.2.3.dist-info/RECORD +0 -75
- pg_sui-0.2.3.dist-info/top_level.txt +0 -3
- pgsui/example_data/phylip_files/test_n10.phy +0 -118
- pgsui/example_data/phylip_files/test_n100.phy +0 -118
- pgsui/example_data/phylip_files/test_n2.phy +0 -118
- pgsui/example_data/phylip_files/test_n500.phy +0 -118
- pgsui/example_data/structure_files/test.nopops.1row.10sites.str +0 -117
- pgsui/example_data/structure_files/test.nopops.2row.100sites.str +0 -234
- pgsui/example_data/structure_files/test.nopops.2row.10sites.str +0 -234
- pgsui/example_data/structure_files/test.nopops.2row.30sites.str +0 -234
- pgsui/example_data/structure_files/test.nopops.2row.allsites.str +0 -234
- pgsui/example_data/structure_files/test.pops.1row.10sites.str +0 -117
- pgsui/example_data/structure_files/test.pops.2row.10sites.str +0 -234
- pgsui/example_data/trees/test.iqtree +0 -376
- pgsui/example_data/trees/test.qmat +0 -5
- pgsui/example_data/trees/test.rate +0 -2033
- pgsui/example_data/trees/test.tre +0 -1
- pgsui/example_data/trees/test_n10.rate +0 -19
- pgsui/example_data/trees/test_n100.rate +0 -109
- pgsui/example_data/trees/test_n500.rate +0 -509
- pgsui/example_data/trees/test_siterates.txt +0 -2024
- pgsui/example_data/trees/test_siterates_n10.txt +0 -10
- pgsui/example_data/trees/test_siterates_n100.txt +0 -100
- pgsui/example_data/trees/test_siterates_n500.txt +0 -500
- pgsui/example_data/vcf_files/test.vcf +0 -244
- pgsui/example_data/vcf_files/test.vcf.gz +0 -0
- pgsui/example_data/vcf_files/test.vcf.gz.tbi +0 -0
- pgsui/impute/estimators.py +0 -1268
- pgsui/impute/impute.py +0 -1463
- pgsui/impute/simple_imputers.py +0 -1431
- pgsui/impute/supervised/iterative_imputer_fixedparams.py +0 -782
- pgsui/impute/supervised/iterative_imputer_gridsearch.py +0 -1024
- pgsui/impute/unsupervised/keras_classifiers.py +0 -697
- pgsui/impute/unsupervised/models/in_development/cnn_model.py +0 -486
- pgsui/impute/unsupervised/neural_network_imputers.py +0 -1440
- pgsui/impute/unsupervised/neural_network_methods.py +0 -1395
- pgsui/pg_sui.py +0 -261
- pgsui/utils/sequence_tools.py +0 -407
- simulation/sim_benchmarks.py +0 -333
- simulation/sim_treeparams.py +0 -475
- test/__init__.py +0 -0
- test/pg_sui_simtest.py +0 -215
- test/pg_sui_testing.py +0 -523
- test/test.py +0 -151
- test/test_pgsui.py +0 -374
- test/test_tkc.py +0 -185
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pgsui-gui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "PG-SUI GUI wrapper for the Python CLI",
|
|
5
|
+
"author": { "name": "Bradley T. Martin", "email": "evobio721@gmail.com" },
|
|
6
|
+
"homepage": "https://github.com/btmartin721/PG-SUI",
|
|
7
|
+
"main": "main.js",
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "electron .",
|
|
11
|
+
"icons": "electron-icon-builder --input=ui/logo.png --output=icons --flatten",
|
|
12
|
+
"dist": "electron-builder -mwl"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"electron": "^38.4.0",
|
|
16
|
+
"electron-builder": "^24.13.3",
|
|
17
|
+
"electron-icon-builder": "^2.0.1"
|
|
18
|
+
},
|
|
19
|
+
"build": {
|
|
20
|
+
"appId": "io.pgsui.gui",
|
|
21
|
+
"directories": {
|
|
22
|
+
"buildResources": "icons",
|
|
23
|
+
"output": "dist"
|
|
24
|
+
},
|
|
25
|
+
"asar": true,
|
|
26
|
+
"extraResources": [
|
|
27
|
+
{ "from": "../../cli.py", "to": "pgsui/cli.py" },
|
|
28
|
+
{ "from": "extra-resources", "to": "extras" }
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"main.js",
|
|
32
|
+
"preload.js",
|
|
33
|
+
"ui/**/*",
|
|
34
|
+
"icons/**/*"
|
|
35
|
+
],
|
|
36
|
+
"mac": {
|
|
37
|
+
"target": ["dmg", "zip"],
|
|
38
|
+
"category": "public.app-category.science",
|
|
39
|
+
"icon": "icons/icon.icns"
|
|
40
|
+
},
|
|
41
|
+
"linux": {
|
|
42
|
+
"target": ["AppImage", "deb"],
|
|
43
|
+
"category": "Science",
|
|
44
|
+
"icon": "icons"
|
|
45
|
+
},
|
|
46
|
+
"win": {
|
|
47
|
+
"target": ["nsis"],
|
|
48
|
+
"icon": "icons/icon.ico"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('pgsui', {
|
|
4
|
+
detectPgSui: () => ipcRenderer.invoke('detect:pgsui'),
|
|
5
|
+
start: (payload) => ipcRenderer.invoke('pgsui:start', payload),
|
|
6
|
+
stop: () => ipcRenderer.invoke('pgsui:stop'),
|
|
7
|
+
pickFile: (filters) => ipcRenderer.invoke('pick:file', { filters }),
|
|
8
|
+
pickDir: () => ipcRenderer.invoke('pick:dir'),
|
|
9
|
+
pickSave: (defaultPath) => ipcRenderer.invoke('pick:save', { defaultPath }),
|
|
10
|
+
onLog: (fn) => ipcRenderer.on('pgsui:log', (_e, d) => fn(d)),
|
|
11
|
+
onError: (fn) => ipcRenderer.on('pgsui:error', (_e, d) => fn(d)),
|
|
12
|
+
onExit: (fn) => ipcRenderer.on('pgsui:exit', (_e, d) => fn(d)),
|
|
13
|
+
onStarted: (fn) => ipcRenderer.on('pgsui:started', (_e, d) => fn(d)),
|
|
14
|
+
defaultCli: () => ipcRenderer.invoke('default:cli'),
|
|
15
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
|
|
10
|
+
from fastapi.responses import HTMLResponse
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).resolve().parent / "ui"
|
|
14
|
+
WORK = Path("/work")
|
|
15
|
+
app = FastAPI()
|
|
16
|
+
_proc = None
|
|
17
|
+
_log_queue: asyncio.Queue[str] = asyncio.Queue()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_args(p: dict[str, str | int | bool | list[str] | None]) -> list[str]:
|
|
21
|
+
a: list[str] = []
|
|
22
|
+
|
|
23
|
+
def kv(flag, v):
|
|
24
|
+
if v not in (None, "", []):
|
|
25
|
+
a.extend([flag, str(v)])
|
|
26
|
+
|
|
27
|
+
def fl(flag, cond):
|
|
28
|
+
if cond:
|
|
29
|
+
a.append(flag)
|
|
30
|
+
|
|
31
|
+
kv("--input", p.get("inputPath"))
|
|
32
|
+
kv("--format", p.get("format"))
|
|
33
|
+
kv("--popmap", p.get("popmapPath"))
|
|
34
|
+
kv("--prefix", p.get("prefix"))
|
|
35
|
+
kv("--config", p.get("yamlPath"))
|
|
36
|
+
kv("--dump-config", p.get("dumpConfigPath"))
|
|
37
|
+
kv("--preset", p.get("preset") or "fast")
|
|
38
|
+
kv("--sim-strategy", p.get("simStrategy"))
|
|
39
|
+
m = p.get("models") or []
|
|
40
|
+
if m:
|
|
41
|
+
a.extend(["--models", *m])
|
|
42
|
+
inc = p.get("includePops") or []
|
|
43
|
+
if inc:
|
|
44
|
+
a.extend(["--include-pops", *inc])
|
|
45
|
+
fl("--tune", bool(p.get("tune")))
|
|
46
|
+
if p.get("tuneNTrials"):
|
|
47
|
+
kv("--tune-n-trials", p["tuneNTrials"])
|
|
48
|
+
kv("--batch-size", p.get("batchSize") or 64)
|
|
49
|
+
dev = p.get("device")
|
|
50
|
+
if dev in ("cpu", "cuda", "mps"):
|
|
51
|
+
kv("--device", dev)
|
|
52
|
+
if p.get("nJobs"):
|
|
53
|
+
kv("--n-jobs", p["nJobs"])
|
|
54
|
+
pf = p.get("plotFormat")
|
|
55
|
+
if pf in ("png", "pdf", "svg"):
|
|
56
|
+
kv("--plot-format", pf)
|
|
57
|
+
if p.get("seed"):
|
|
58
|
+
kv("--seed", p["seed"])
|
|
59
|
+
fl("--verbose", bool(p.get("verbose")))
|
|
60
|
+
if p.get("logFile"):
|
|
61
|
+
kv("--log-file", p["logFile"])
|
|
62
|
+
fl("--force-popmap", bool(p.get("forcePopmap")))
|
|
63
|
+
fl("--dry-run", bool(p.get("dryRun")))
|
|
64
|
+
for kvp in p.get("setPairs") or []:
|
|
65
|
+
if isinstance(kvp, str) and "=" in kvp:
|
|
66
|
+
a.extend(["--set", kvp])
|
|
67
|
+
return a
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.get("/api/status")
|
|
71
|
+
def status():
|
|
72
|
+
return {"running": _proc is not None}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.get("/api/ls")
|
|
76
|
+
def ls(path: str = Query("/work")):
|
|
77
|
+
p = Path(path).resolve()
|
|
78
|
+
if not str(p).startswith(str(WORK)):
|
|
79
|
+
return {"ok": False, "error": "out_of_root"}
|
|
80
|
+
items = [
|
|
81
|
+
{"name": e.name, "path": str(e), "dir": e.is_dir()} for e in sorted(p.iterdir())
|
|
82
|
+
]
|
|
83
|
+
return {"ok": True, "cwd": str(p), "items": items}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.post("/api/start")
|
|
87
|
+
async def start(payload: dict):
|
|
88
|
+
global _proc
|
|
89
|
+
if _proc:
|
|
90
|
+
return {"ok": False, "error": "already_running"}
|
|
91
|
+
cwd = Path(payload.get("cwd") or "/work").resolve()
|
|
92
|
+
if not str(cwd).startswith(str(WORK)):
|
|
93
|
+
return {"ok": False, "error": "cwd_must_be_under_/work"}
|
|
94
|
+
args = _build_args(payload)
|
|
95
|
+
use_pg = bool(payload.get("usePgSui", True))
|
|
96
|
+
if use_pg:
|
|
97
|
+
cmd = ["pg-sui", *args]
|
|
98
|
+
else:
|
|
99
|
+
py_candidates = [
|
|
100
|
+
payload.get("pythonPath"),
|
|
101
|
+
os.environ.get("PGSUI_PYTHON"),
|
|
102
|
+
"/opt/homebrew/bin/python3.12",
|
|
103
|
+
"/usr/local/bin/python3.12",
|
|
104
|
+
"python3.12",
|
|
105
|
+
"python3",
|
|
106
|
+
"python",
|
|
107
|
+
]
|
|
108
|
+
py = next((c for c in py_candidates if c and shutil.which(c)), "python3")
|
|
109
|
+
cli = payload.get("cliPath")
|
|
110
|
+
if not cli:
|
|
111
|
+
return {"ok": False, "error": "cli_path_required"}
|
|
112
|
+
cmd = [py, cli, *args]
|
|
113
|
+
_proc = await asyncio.create_subprocess_exec(
|
|
114
|
+
*cmd,
|
|
115
|
+
cwd=str(cwd),
|
|
116
|
+
stdout=asyncio.subprocess.PIPE,
|
|
117
|
+
stderr=asyncio.subprocess.PIPE,
|
|
118
|
+
env=os.environ.copy(),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def pump(stream, tag):
|
|
122
|
+
async for line in stream:
|
|
123
|
+
await _log_queue.put(f"{tag}|{line.decode(errors='ignore').rstrip()}")
|
|
124
|
+
|
|
125
|
+
asyncio.create_task(pump(_proc.stdout, "stdout"))
|
|
126
|
+
asyncio.create_task(pump(_proc.stderr, "stderr"))
|
|
127
|
+
return {"ok": True, "argv": cmd, "cwd": str(cwd)}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.post("/api/stop")
|
|
131
|
+
async def stop():
|
|
132
|
+
global _proc
|
|
133
|
+
if not _proc:
|
|
134
|
+
return {"ok": False, "error": "not_running"}
|
|
135
|
+
try:
|
|
136
|
+
_proc.send_signal(signal.SIGTERM)
|
|
137
|
+
try:
|
|
138
|
+
await asyncio.wait_for(_proc.wait(), timeout=5)
|
|
139
|
+
except asyncio.TimeoutError:
|
|
140
|
+
_proc.kill()
|
|
141
|
+
finally:
|
|
142
|
+
_proc = None
|
|
143
|
+
return {"ok": True}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.websocket("/api/logs")
|
|
147
|
+
async def logs(ws: WebSocket):
|
|
148
|
+
await ws.accept()
|
|
149
|
+
try:
|
|
150
|
+
while True:
|
|
151
|
+
msg = await _log_queue.get()
|
|
152
|
+
await ws.send_text(msg)
|
|
153
|
+
except WebSocketDisconnect:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
app.mount("/", StaticFiles(directory=ROOT, html=True), name="ui")
|
|
Binary file
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* ---- helpers (define first) ---- */
|
|
2
|
+
const $ = (id) => document.getElementById(id);
|
|
3
|
+
const logEl = $('log');
|
|
4
|
+
|
|
5
|
+
function appendLog({ stream, line }) {
|
|
6
|
+
const span = document.createElement('span');
|
|
7
|
+
if (stream === 'stderr') span.className = 'err';
|
|
8
|
+
span.textContent = line + '\n';
|
|
9
|
+
logEl.appendChild(span);
|
|
10
|
+
if (logEl.textContent.length > 2_000_000) logEl.textContent = logEl.textContent.slice(-1_000_000);
|
|
11
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* ---- payload ---- */
|
|
15
|
+
function collectPayload() {
|
|
16
|
+
const modelsSel = Array.from($('models').selectedOptions).map(o => o.value);
|
|
17
|
+
const includePops = $('includePops').value.trim().split(/\s+/).filter(Boolean);
|
|
18
|
+
const setPairs = $('setPairs').value.split('\n').map(s => s.trim()).filter(Boolean);
|
|
19
|
+
const bs = $('batchSize').value ? Number($('batchSize').value) : 64;
|
|
20
|
+
return {
|
|
21
|
+
pythonPath: undefined,
|
|
22
|
+
cliPath: undefined,
|
|
23
|
+
cwd: $('cwd').value.trim(),
|
|
24
|
+
inputPath: $('inputPath').value.trim(),
|
|
25
|
+
format: $('format').value,
|
|
26
|
+
popmapPath: $('popmapPath').value.trim() || undefined,
|
|
27
|
+
prefix: $('prefix').value.trim() || undefined,
|
|
28
|
+
yamlPath: $('yamlPath').value.trim() || undefined,
|
|
29
|
+
dumpConfigPath: $('dumpConfigPath').value.trim() || undefined,
|
|
30
|
+
preset: $('preset').value || 'fast',
|
|
31
|
+
simStrategy: $('simStrategy').value || 'random',
|
|
32
|
+
models: modelsSel,
|
|
33
|
+
includePops,
|
|
34
|
+
device: $('device').value || undefined,
|
|
35
|
+
batchSize: bs,
|
|
36
|
+
nJobs: $('nJobs').value ? Number($('nJobs').value) : undefined,
|
|
37
|
+
plotFormat: $('plotFormat').value || undefined,
|
|
38
|
+
seed: $('seed').value.trim() || undefined,
|
|
39
|
+
verbose: $('verbose').checked,
|
|
40
|
+
forcePopmap: $('forcePopmap').checked,
|
|
41
|
+
dryRun: $('dryRun').checked,
|
|
42
|
+
setPairs,
|
|
43
|
+
logFile: $('logFile').value.trim() || undefined,
|
|
44
|
+
tune: $('tune').checked,
|
|
45
|
+
tuneNTrials: $('tune').checked ? (Number($('tuneNTrials').value) || 50) : undefined
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setRunningUI(isRunning) {
|
|
50
|
+
$('start').disabled = isRunning;
|
|
51
|
+
$('stop').disabled = !isRunning;
|
|
52
|
+
document.querySelectorAll('button').forEach(b => { if (b.id !== 'stop') b.disabled = isRunning; });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ---- init small UI bits ---- */
|
|
56
|
+
(() => {
|
|
57
|
+
const tune = $('tune'), tuneOpts = $('tuneOpts');
|
|
58
|
+
if (tune && tuneOpts) tune.addEventListener('change', () => { tuneOpts.style.display = tune.checked ? '' : 'none'; });
|
|
59
|
+
const logoEl = $('logo');
|
|
60
|
+
if (logoEl) logoEl.addEventListener('error', () => { logoEl.style.display = 'none'; });
|
|
61
|
+
})();
|
|
62
|
+
|
|
63
|
+
/* ---- events (wire exactly once) ---- */
|
|
64
|
+
const on = (id, ev, fn) => { const el = $(id); if (el) el.addEventListener(ev, fn); };
|
|
65
|
+
|
|
66
|
+
on('pickCwd', 'click', async () => {
|
|
67
|
+
const d = await window.pgsui.pickDir();
|
|
68
|
+
if (d) $('cwd').value = d;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
on('start', 'click', async () => {
|
|
72
|
+
try {
|
|
73
|
+
if (!window.pgsui) { appendLog({ stream:'stderr', line:'Bridge missing (preload).' }); return; }
|
|
74
|
+
logEl.textContent = '';
|
|
75
|
+
const payload = collectPayload();
|
|
76
|
+
if (!payload.cwd) { appendLog({ stream:'stderr', line:'Working directory is required.' }); return; }
|
|
77
|
+
if (!payload.inputPath) { appendLog({ stream:'stderr', line:'Input file required.' }); return; }
|
|
78
|
+
if (!payload.cliPath) {
|
|
79
|
+
const r = await window.pgsui.defaultCli?.();
|
|
80
|
+
if (r?.ok && r.path) payload.cliPath = r.path;
|
|
81
|
+
}
|
|
82
|
+
if (!payload.cliPath) { appendLog({ stream:'stderr', line:'Could not find <project_root>/pgsui/cli.py. Set PGSUI_CLI_DEFAULT or adjust repo layout.' }); return; }
|
|
83
|
+
const res = await window.pgsui.start(payload);
|
|
84
|
+
if (!res?.ok) appendLog({ stream:'stderr', line:`Start failed: ${res?.error || 'unknown error'}` });
|
|
85
|
+
} catch (e) {
|
|
86
|
+
appendLog({ stream:'stderr', line:`Start exception: ${e?.message || String(e)}` });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
on('stop', 'click', async () => {
|
|
91
|
+
const res = await window.pgsui.stop();
|
|
92
|
+
if (!res.ok) appendLog({ stream:'stderr', line: res.error });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
on('pickInput', 'click', async () => {
|
|
96
|
+
const fmt = $('format').value;
|
|
97
|
+
const filters = {
|
|
98
|
+
vcf: [{ name: 'VCF', extensions: ['vcf','gz'] }],
|
|
99
|
+
phylip: [{ name: 'PHYLIP', extensions: ['phy','phylip'] }],
|
|
100
|
+
structure: [{ name: 'STRUCTURE', extensions: ['str','stru'] }],
|
|
101
|
+
genepop: [{ name: 'GENEPOP', extensions: ['gen','genepop'] }]
|
|
102
|
+
}[fmt] || [{ name: 'All', extensions: ['*'] }];
|
|
103
|
+
const f = await window.pgsui.pickFile(filters);
|
|
104
|
+
if (f) $('inputPath').value = f;
|
|
105
|
+
});
|
|
106
|
+
on('pickPopmap', 'click', async () => {
|
|
107
|
+
const f = await window.pgsui.pickFile();
|
|
108
|
+
if (f) $('popmapPath').value = f;
|
|
109
|
+
});
|
|
110
|
+
on('pickYaml', 'click', async () => {
|
|
111
|
+
const f = await window.pgsui.pickFile([{ name: 'YAML', extensions: ['yml','yaml'] }]);
|
|
112
|
+
if (f) $('yamlPath').value = f;
|
|
113
|
+
});
|
|
114
|
+
on('pickDump', 'click', async () => {
|
|
115
|
+
const f = await window.pgsui.pickSave('effective.yaml');
|
|
116
|
+
if (f) $('dumpConfigPath').value = f;
|
|
117
|
+
});
|
|
118
|
+
on('pickLogFile', 'click', async () => {
|
|
119
|
+
const f = await window.pgsui.pickSave('pgsui-run.log');
|
|
120
|
+
if (f) $('logFile').value = f;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/* ---- streams ---- */
|
|
124
|
+
window.pgsui.onLog(appendLog);
|
|
125
|
+
window.pgsui.onError((e) => appendLog({ stream:'stderr', line: e.message || String(e) }));
|
|
126
|
+
window.pgsui.onExit(({ code }) => { appendLog({ stream:'stdout', line:`Process exited with code ${code}` }); setRunningUI(false); });
|
|
127
|
+
window.pgsui.onStarted(({ argv, cwd }) => {
|
|
128
|
+
appendLog({ stream:'stdout', line:`Started: ${argv?.join(' ') || ''}` });
|
|
129
|
+
appendLog({ stream:'stdout', line:`CWD: ${cwd}` });
|
|
130
|
+
setRunningUI(true);
|
|
131
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
:root{
|
|
2
|
+
--bg:#0b0f14; --panel:#0f1621; --muted:#8aa0b4; --text:#e7eef7;
|
|
3
|
+
--accent:#3aa0ff; --accent-2:#8b5cf6; --danger:#ef4444; --ok:#22c55e;
|
|
4
|
+
--border:#1b2433; --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
5
|
+
--sans: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
6
|
+
}
|
|
7
|
+
*{box-sizing:border-box}
|
|
8
|
+
html,body{height:100%}
|
|
9
|
+
body{
|
|
10
|
+
margin:0; background:
|
|
11
|
+
radial-gradient(1200px 600px at 10% -10%, rgba(58,160,255,.12), transparent 60%),
|
|
12
|
+
radial-gradient(900px 500px at 120% -20%, rgba(139,92,246,.10), transparent 60%),
|
|
13
|
+
var(--bg);
|
|
14
|
+
color:var(--text); font-family:var(--sans);
|
|
15
|
+
}
|
|
16
|
+
.titlebar{height:64px; display:flex; align-items:center; padding:12px 20px 0}
|
|
17
|
+
.logo-row{display:flex; gap:12px; align-items:center}
|
|
18
|
+
.logo{width:40px; height:40px; border-radius:10px; box-shadow:0 6px 18px rgba(0,0,0,.4)}
|
|
19
|
+
h1{margin:0; font-weight:700; letter-spacing:.2px}
|
|
20
|
+
.subtitle{margin:2px 0 0; color:var(--muted); font-size:12px}
|
|
21
|
+
|
|
22
|
+
main{padding:12px 20px; max-width:1200px; margin:0 auto}
|
|
23
|
+
.card{
|
|
24
|
+
background:linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,.00));
|
|
25
|
+
border:1px solid var(--border); border-radius:16px; padding:16px; margin:12px 0;
|
|
26
|
+
box-shadow:0 8px 30px rgba(0,0,0,.25);
|
|
27
|
+
}
|
|
28
|
+
h2{margin:0 0 12px 0; font-size:16px; color:#cfe6ff}
|
|
29
|
+
.muted{color:var(--muted); font-size:12px}
|
|
30
|
+
|
|
31
|
+
.grid{
|
|
32
|
+
display:grid; gap:12px;
|
|
33
|
+
grid-template-columns: repeat(3, minmax(260px, 1fr));
|
|
34
|
+
}
|
|
35
|
+
.grid .colspan{grid-column:1 / -1}
|
|
36
|
+
label{display:block; font-size:12px; color:var(--muted); margin:0 0 6px}
|
|
37
|
+
input,select,textarea{
|
|
38
|
+
width:100%; padding:10px 12px; border-radius:10px; border:1px solid var(--border);
|
|
39
|
+
background:#0d1420; color:var(--text); outline:none;
|
|
40
|
+
}
|
|
41
|
+
input:focus,select:focus,textarea:focus{border-color:var(--accent)}
|
|
42
|
+
textarea{resize:vertical}
|
|
43
|
+
.hstack{display:flex; gap:8px}
|
|
44
|
+
button{
|
|
45
|
+
border:none; border-radius:10px; padding:10px 14px; cursor:pointer; color:#fff;
|
|
46
|
+
background:#1c2433; border:1px solid var(--border);
|
|
47
|
+
}
|
|
48
|
+
button.primary{background:linear-gradient(90deg, var(--accent), var(--accent-2))}
|
|
49
|
+
button.danger{background:linear-gradient(90deg, var(--danger), #f97316)}
|
|
50
|
+
button.ghost{background:transparent; border:1px solid var(--border)}
|
|
51
|
+
button:disabled{opacity:.5; cursor:not-allowed}
|
|
52
|
+
.controls{display:flex; gap:10px; margin:12px 0}
|
|
53
|
+
|
|
54
|
+
.log{
|
|
55
|
+
background:#06090f; color:#e0e8f5; border:1px solid var(--border); border-radius:12px;
|
|
56
|
+
padding:12px; height:340px; overflow:auto; font-family:var(--mono); font-size:12px; line-height:1.35;
|
|
57
|
+
white-space:pre-wrap; word-break:break-word;
|
|
58
|
+
}
|
|
59
|
+
.log .err{color:#ff8a8a}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// ui-shim.js: Browser shim for Electron's window.pgsui API
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
function wsConnect(onLog) {
|
|
5
|
+
const ws = new WebSocket(`ws://${location.host}/api/logs`);
|
|
6
|
+
ws.onmessage = (e) => {
|
|
7
|
+
const s = e.data.indexOf('|');
|
|
8
|
+
if (s > 0) {
|
|
9
|
+
const stream = e.data.slice(0, s);
|
|
10
|
+
const line = e.data.slice(s + 1);
|
|
11
|
+
onLog({ stream, line });
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
ws.onerror = () => {/* no-op */};
|
|
15
|
+
return ws;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const listeners = { log: [], error: [], exit: [], started: [] };
|
|
19
|
+
function emit(type, payload) { listeners[type].forEach(fn => fn(payload)); }
|
|
20
|
+
|
|
21
|
+
let ws = null;
|
|
22
|
+
|
|
23
|
+
const api = {
|
|
24
|
+
// Electron feature parity stubs
|
|
25
|
+
pickFile: async () => null, // Browser cannot provide host filesystem paths
|
|
26
|
+
pickDir: async () => null,
|
|
27
|
+
pickSave: async () => null,
|
|
28
|
+
|
|
29
|
+
// Detect pg-sui presence by calling the API once we start; return optimistic true
|
|
30
|
+
detectPgSui: async () => ({ ok: true }),
|
|
31
|
+
|
|
32
|
+
// Start/stop map to REST
|
|
33
|
+
start: async (payload) => {
|
|
34
|
+
try {
|
|
35
|
+
const r = await fetch('/api/start', {
|
|
36
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
37
|
+
body: JSON.stringify(payload)
|
|
38
|
+
});
|
|
39
|
+
const j = await r.json();
|
|
40
|
+
if (!j.ok) return j;
|
|
41
|
+
emit('started', { argv: j.argv || [], cwd: j.cwd || '' });
|
|
42
|
+
if (!ws) ws = wsConnect((m) => emit('log', m));
|
|
43
|
+
return { ok: true };
|
|
44
|
+
} catch (e) {
|
|
45
|
+
emit('error', { message: String(e) });
|
|
46
|
+
return { ok: false, error: String(e) };
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
stop: async () => {
|
|
50
|
+
try {
|
|
51
|
+
const r = await fetch('/api/stop', { method: 'POST' });
|
|
52
|
+
const j = await r.json();
|
|
53
|
+
if (!j.ok) return j;
|
|
54
|
+
emit('exit', { code: 0 });
|
|
55
|
+
return { ok: true };
|
|
56
|
+
} catch (e) {
|
|
57
|
+
emit('error', { message: String(e) });
|
|
58
|
+
return { ok: false, error: String(e) };
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Event wiring
|
|
63
|
+
onLog: (fn) => { listeners.log.push(fn); },
|
|
64
|
+
onError: (fn) => { listeners.error.push(fn); },
|
|
65
|
+
onExit: (fn) => { listeners.exit.push(fn); },
|
|
66
|
+
onStarted: (fn) => { listeners.started.push(fn); },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Expose only if Electron preload didn't define it
|
|
70
|
+
if (!window.pgsui) window.pgsui = api;
|
|
71
|
+
|
|
72
|
+
})();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
APP_DIR = Path(__file__).resolve().parent / "app"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _which(cmd: str) -> str | None:
|
|
12
|
+
return shutil.which(cmd)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
if not APP_DIR.exists():
|
|
17
|
+
print(f"[pgsui-gui-setup] Missing Electron app at {APP_DIR}", file=sys.stderr)
|
|
18
|
+
return 2
|
|
19
|
+
|
|
20
|
+
node = _which("node")
|
|
21
|
+
npm = _which("npm")
|
|
22
|
+
if not node or not npm:
|
|
23
|
+
print(
|
|
24
|
+
"[pgsui-gui-setup] Node.js and npm are required. Install from https://nodejs.org/",
|
|
25
|
+
file=sys.stderr,
|
|
26
|
+
)
|
|
27
|
+
return 2
|
|
28
|
+
|
|
29
|
+
# Prefer deterministic installs if package-lock.json exists
|
|
30
|
+
lock = APP_DIR / "package-lock.json"
|
|
31
|
+
cmd = ["npm", "ci"] if lock.exists() else ["npm", "install"]
|
|
32
|
+
try:
|
|
33
|
+
print(f"[pgsui-gui-setup] Running: {' '.join(cmd)} in {APP_DIR}")
|
|
34
|
+
subprocess.check_call(cmd, cwd=str(APP_DIR))
|
|
35
|
+
print("[pgsui-gui-setup] Done.")
|
|
36
|
+
return 0
|
|
37
|
+
except subprocess.CalledProcessError as e:
|
|
38
|
+
print(f"[pgsui-gui-setup] Failed: {e}", file=sys.stderr)
|
|
39
|
+
return e.returncode
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
raise SystemExit(main())
|
pgsui/electron/launch.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
APP_DIR = Path(__file__).resolve().parent / "app"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _bin(path: Path, name: str) -> Path:
|
|
13
|
+
# node_modules/.bin on POSIX, node_modules\\.bin on Windows
|
|
14
|
+
d = path / ("node_modules/.bin" if os.name != "nt" else r"node_modules\.bin")
|
|
15
|
+
exe = name + (".cmd" if os.name == "nt" else "")
|
|
16
|
+
return d / exe
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
if not APP_DIR.exists():
|
|
21
|
+
print(f"[pgsui-gui] Missing Electron app at {APP_DIR}", file=sys.stderr)
|
|
22
|
+
return 2
|
|
23
|
+
|
|
24
|
+
# Resolve electron binary: local install first, else global npx fallback
|
|
25
|
+
local_electron = _bin(APP_DIR, "electron")
|
|
26
|
+
npx = shutil.which("npx")
|
|
27
|
+
|
|
28
|
+
env = os.environ.copy()
|
|
29
|
+
env.setdefault("PGSUI_PYTHON", sys.executable)
|
|
30
|
+
env.setdefault("PGSUI_CLI_DEFAULT", str(Path(__file__).resolve().parents[1] / "cli.py"))
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
if local_electron.exists():
|
|
34
|
+
cmd = [str(local_electron), "."]
|
|
35
|
+
proc = subprocess.Popen(cmd, cwd=str(APP_DIR), env=env)
|
|
36
|
+
elif npx:
|
|
37
|
+
# Uses Electron from registry on demand
|
|
38
|
+
cmd = ["npx", "electron", "."]
|
|
39
|
+
proc = subprocess.Popen(cmd, cwd=str(APP_DIR), env=env)
|
|
40
|
+
else:
|
|
41
|
+
print(
|
|
42
|
+
"[pgsui-gui] Electron is not installed. Run: pgsui-gui-setup",
|
|
43
|
+
file=sys.stderr,
|
|
44
|
+
)
|
|
45
|
+
return 2
|
|
46
|
+
|
|
47
|
+
proc.wait()
|
|
48
|
+
return proc.returncode or 0
|
|
49
|
+
except KeyboardInterrupt:
|
|
50
|
+
return 130
|
|
51
|
+
except FileNotFoundError as e:
|
|
52
|
+
print(f"[pgsui-gui] Failed to start Electron: {e}", file=sys.stderr)
|
|
53
|
+
return 2
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pgsui-gui-root",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"start": "electron app",
|
|
6
|
+
"dist": "electron-builder -mwl --projectDir app",
|
|
7
|
+
"icons": "electron-icon-builder --input=app/ui/logo.png --output=app/icons --flatten"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"electron": "^38.4.0",
|
|
11
|
+
"electron-builder": "^24.13.3",
|
|
12
|
+
"electron-icon-builder": "^2.0.1"
|
|
13
|
+
}
|
|
14
|
+
}
|
pgsui/example_data/__init__.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|