molass-gui 0.1.0__tar.gz

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.
@@ -0,0 +1,42 @@
1
+ name: Manual Upload Python Package to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ pypi-publish:
8
+ name: upload release to PyPI
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: write
12
+ id-token: write
13
+
14
+ steps:
15
+ - name: Check out repository
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: '3.x'
22
+
23
+ - name: Install build
24
+ run: pip install build
25
+
26
+ - name: Build package
27
+ run: python -m build
28
+
29
+ - name: Publish package distributions to PyPI
30
+ uses: pypa/gh-action-pypi-publish@release/v1
31
+
32
+ - name: Set up git for pushing
33
+ run: |
34
+ git config --global user.name "github-actions[bot]"
35
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
36
+ git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
37
+
38
+ - name: Create and push version tag
39
+ run: |
40
+ VERSION=$(grep -m1 '^version' pyproject.toml | sed 's/version *= *//;s/"//g')
41
+ git tag "v$VERSION"
42
+ git push origin "v$VERSION"
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: molass_gui
3
+ Version: 0.1.0
4
+ Summary: Tkinter GUI for Molass — initial estimate workflow
5
+ Project-URL: Repository, https://github.com/biosaxs-dev/molass-gui
6
+ Author: Molass Community
7
+ Maintainer: Molass Community
8
+ License: GNU General Public License v3.0
9
+ Keywords: SEC-SAXS
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
12
+ Classifier: Programming Language :: Python
13
+ Requires-Python: <3.15,>=3.9
14
+ Requires-Dist: molass-legacy>=1.6.13
15
+ Requires-Dist: molass>=1.0.5
16
+ Description-Content-Type: text/markdown
17
+
18
+ # molass-gui
19
+
20
+ Tkinter GUI for [Molass](https://github.com/biosaxs-dev/molass-library) — progressive SEC-SAXS analysis workflow.
21
+
22
+ ## Five-phase workflow
23
+
24
+ | Phase | Window | What happens |
25
+ |---|---|---|
26
+ | 1 Setup | Main window | Enter the data folder path |
27
+ | 2 Naive View | `plot_compact` with baseline | Inspect raw data; enter number of components; click **Decompose** |
28
+ | 3 Quick Optimization View | `plot_components` (EGH) | Review EGH decomposition; select column model; click **Upgrade** or **Skip** |
29
+ | 4 Upgraded View | `plot_components` (column model) | Review upgraded decomposition; select method (BH/DE) and subprocess option; click **Rigorous Optimization…** |
30
+ | 5 Rigorous Optimization View | 4-panel live monitor | Rg curve computed → initial score drawn → optimization starts automatically; **Terminate** button available |
31
+
32
+ ## Installation
33
+
34
+ ```
35
+ pip install molass-gui
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```
41
+ molass-gui
42
+ ```
43
+
44
+ Or during development (from the repository root):
45
+
46
+ ```
47
+ py app.py
48
+ ```
49
+
50
+ ## Requirements
51
+
52
+ - Python 3.9–3.14
53
+ - [molass](https://pypi.org/project/molass/) ≥ 1.0.5
54
+ - [molass_legacy](https://pypi.org/project/molass_legacy/) ≥ 1.6.13
@@ -0,0 +1,37 @@
1
+ # molass-gui
2
+
3
+ Tkinter GUI for [Molass](https://github.com/biosaxs-dev/molass-library) — progressive SEC-SAXS analysis workflow.
4
+
5
+ ## Five-phase workflow
6
+
7
+ | Phase | Window | What happens |
8
+ |---|---|---|
9
+ | 1 Setup | Main window | Enter the data folder path |
10
+ | 2 Naive View | `plot_compact` with baseline | Inspect raw data; enter number of components; click **Decompose** |
11
+ | 3 Quick Optimization View | `plot_components` (EGH) | Review EGH decomposition; select column model; click **Upgrade** or **Skip** |
12
+ | 4 Upgraded View | `plot_components` (column model) | Review upgraded decomposition; select method (BH/DE) and subprocess option; click **Rigorous Optimization…** |
13
+ | 5 Rigorous Optimization View | 4-panel live monitor | Rg curve computed → initial score drawn → optimization starts automatically; **Terminate** button available |
14
+
15
+ ## Installation
16
+
17
+ ```
18
+ pip install molass-gui
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```
24
+ molass-gui
25
+ ```
26
+
27
+ Or during development (from the repository root):
28
+
29
+ ```
30
+ py app.py
31
+ ```
32
+
33
+ ## Requirements
34
+
35
+ - Python 3.9–3.14
36
+ - [molass](https://pypi.org/project/molass/) ≥ 1.0.5
37
+ - [molass_legacy](https://pypi.org/project/molass_legacy/) ≥ 1.6.13
@@ -0,0 +1,5 @@
1
+ """Thin launcher — allows `py app.py` during development."""
2
+ from molass_gui.app import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1 @@
1
+ """molass-gui — Tkinter GUI for Molass initial estimate workflow."""
@@ -0,0 +1,82 @@
1
+ """molass-gui — Phase 1: data folder input."""
2
+ import threading
3
+ import tkinter as tk
4
+ from tkinter import ttk, filedialog
5
+
6
+ _DEFAULT_FOLDER = r"C:\Users\takahashi\PyTools\Data\20230705"
7
+
8
+
9
+ class App(tk.Tk):
10
+ def __init__(self):
11
+ super().__init__()
12
+ self.title("Molass")
13
+ self.resizable(False, False)
14
+ self._build_ui()
15
+ self.protocol("WM_DELETE_WINDOW", self._quit)
16
+
17
+ def _quit(self):
18
+ import sys
19
+ self.destroy()
20
+ sys.exit(0)
21
+
22
+ def _build_ui(self):
23
+ f = ttk.Frame(self, padding=16)
24
+ f.pack(fill=tk.BOTH, expand=True)
25
+
26
+ ttk.Label(f, text="Data folder:").grid(row=0, column=0, sticky=tk.W, pady=4)
27
+ self._folder_var = tk.StringVar(value=_DEFAULT_FOLDER)
28
+ ttk.Entry(f, textvariable=self._folder_var, width=52).grid(
29
+ row=0, column=1, padx=6)
30
+ ttk.Button(f, text="Browse…", command=self._browse).grid(row=0, column=2)
31
+
32
+ self._btn = ttk.Button(f, text="Load", command=self._run)
33
+ self._btn.grid(row=1, column=0, columnspan=3, pady=10)
34
+
35
+ self._status_var = tk.StringVar(value="Ready.")
36
+ ttk.Label(f, textvariable=self._status_var, foreground="gray").grid(
37
+ row=2, column=0, columnspan=3, sticky=tk.W)
38
+
39
+ def _browse(self):
40
+ d = filedialog.askdirectory(title="Select data folder")
41
+ if d:
42
+ self._folder_var.set(d)
43
+
44
+ def _run(self):
45
+ folder = self._folder_var.get().strip()
46
+ if not folder:
47
+ self._status_var.set("Please select a data folder first.")
48
+ return
49
+
50
+ self._btn.state(["disabled"])
51
+ self._status_var.set("Loading…")
52
+
53
+ def worker():
54
+ try:
55
+ from molass.DataObjects import SecSaxsData as SSD
56
+ ssd = SSD(folder)
57
+ trimmed = ssd.trimmed_copy()
58
+
59
+ def on_main():
60
+ self._btn.state(["!disabled"])
61
+ self._status_var.set("Ready.")
62
+ from molass_gui.naive_view import NaiveView
63
+ NaiveView(ssd, trimmed, parent=self).show()
64
+
65
+ self.after(0, on_main)
66
+
67
+ except Exception as exc:
68
+ msg = str(exc)
69
+ def on_error(m=msg):
70
+ self._status_var.set(f"Error: {m}")
71
+ self._btn.state(["!disabled"])
72
+ self.after(0, on_error)
73
+
74
+ threading.Thread(target=worker, daemon=True).start()
75
+
76
+
77
+ def main():
78
+ App().mainloop()
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()
@@ -0,0 +1,63 @@
1
+ """NaiveView — Phase 2: plot_compact + num_components → Decompose → QuickView."""
2
+ import threading
3
+ import tkinter as tk
4
+ from tkinter import ttk
5
+
6
+
7
+ class NaiveView:
8
+ def __init__(self, ssd, trimmed, parent=None):
9
+ self._ssd = ssd
10
+ self._trimmed = trimmed
11
+ self._parent = parent
12
+
13
+ def show(self):
14
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
15
+
16
+ win = tk.Toplevel(self._parent)
17
+ win.title("Naive View")
18
+
19
+ # Header: num_components + Decompose button + status
20
+ hdr = ttk.Frame(win, padding=(8, 4))
21
+ hdr.pack(fill=tk.X)
22
+ ttk.Label(hdr, text="Components:").pack(side=tk.LEFT)
23
+ self._nc_var = tk.IntVar(value=3)
24
+ ttk.Spinbox(hdr, from_=1, to=6, textvariable=self._nc_var, width=5).pack(
25
+ side=tk.LEFT, padx=6)
26
+ self._decomp_btn = ttk.Button(hdr, text="Decompose", command=self._decompose)
27
+ self._decomp_btn.pack(side=tk.LEFT, padx=12)
28
+ self._status_var = tk.StringVar(value="")
29
+ ttk.Label(hdr, textvariable=self._status_var, foreground="gray").pack(side=tk.LEFT)
30
+
31
+ # Show trimmed compact plot with baseline overlay
32
+ result = self._trimmed.plot_compact(baseline=True)
33
+ canvas = FigureCanvasTkAgg(result.fig, master=win)
34
+ canvas.draw()
35
+ canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
36
+ NavigationToolbar2Tk(canvas, win).update()
37
+ self._win = win
38
+
39
+ def _decompose(self):
40
+ nc = self._nc_var.get()
41
+ self._decomp_btn.state(["disabled"])
42
+ self._status_var.set("Decomposing\u2026")
43
+
44
+ def worker():
45
+ try:
46
+ corrected = self._trimmed.corrected_copy()
47
+ decomp = corrected.quick_decomposition(num_components=nc)
48
+
49
+ def on_main():
50
+ self._decomp_btn.state(["!disabled"])
51
+ self._status_var.set("")
52
+ from molass_gui.quick_view import QuickView
53
+ QuickView(decomp, self._trimmed, nc, parent=self._win).show()
54
+
55
+ self._win.after(0, on_main)
56
+ except Exception as exc:
57
+ msg = str(exc)
58
+ def on_error(m=msg):
59
+ self._status_var.set(f"Error: {m}")
60
+ self._decomp_btn.state(["!disabled"])
61
+ self._win.after(0, on_error)
62
+
63
+ threading.Thread(target=worker, daemon=True).start()
@@ -0,0 +1,118 @@
1
+ """QuickView — Phase 3: plot_components (EGH) + model selection → Upgrade / Skip → UpgradedView."""
2
+ import threading
3
+ import tkinter as tk
4
+ from tkinter import ttk
5
+
6
+ # (label, recipe model key, pore_dist kwarg or None)
7
+ _MODEL_OPTIONS = [
8
+ ('EGH', 'egh', None),
9
+ ('EGH \u2192 SDM (mono)', 'sdm', None),
10
+ ('EGH \u2192 SDM (lognormal)', 'sdm', 'lognormal'),
11
+ ('EGH \u2192 EDM', 'cedm', None),
12
+ ('EGH \u2192 LKM', 'lkm', None),
13
+ ('EGH \u2192 GRM', 'grm', None),
14
+ ]
15
+ _MODEL_LABELS = [m[0] for m in _MODEL_OPTIONS]
16
+
17
+
18
+ class QuickView:
19
+ def __init__(self, decomp, trimmed, nc, parent=None):
20
+ self._decomp = decomp
21
+ self._trimmed = trimmed
22
+ self._nc = nc
23
+ self._parent = parent
24
+
25
+ def show(self):
26
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
27
+
28
+ win = tk.Toplevel(self._parent)
29
+ win.title("Quick Optimization View")
30
+
31
+ # Header: model selection + PSD σ (conditional) + Upgrade / Skip buttons
32
+ hdr = ttk.Frame(win, padding=(8, 4))
33
+ hdr.pack(fill=tk.X)
34
+
35
+ ttk.Label(hdr, text="Model:").pack(side=tk.LEFT)
36
+ self._model_var = tk.StringVar(value='EGH')
37
+ self._model_cb = ttk.Combobox(hdr, textvariable=self._model_var, values=_MODEL_LABELS,
38
+ state='readonly', width=24)
39
+ self._model_cb.pack(side=tk.LEFT, padx=4)
40
+
41
+ # PSD σ frame — shown only when SDM (lognormal) is selected
42
+ self._psd_frame = ttk.Frame(hdr)
43
+ ttk.Label(self._psd_frame, text="PSD \u03c3:").pack(side=tk.LEFT, padx=(8, 0))
44
+ self._psd_sigma_var = tk.StringVar(value="0.1")
45
+ ttk.Entry(self._psd_frame, textvariable=self._psd_sigma_var, width=6).pack(
46
+ side=tk.LEFT, padx=4)
47
+
48
+ self._action_btn = ttk.Button(hdr, text="Skip", command=self._skip)
49
+ self._action_btn.pack(side=tk.RIGHT, padx=8)
50
+ self._status_var = tk.StringVar(value="")
51
+ ttk.Label(hdr, textvariable=self._status_var, foreground="gray").pack(
52
+ side=tk.LEFT, padx=8)
53
+
54
+ def _on_model_change(*_):
55
+ label = self._model_var.get()
56
+ is_ln = label == 'EGH \u2192 SDM (lognormal)'
57
+ if is_ln:
58
+ self._psd_frame.pack(side=tk.LEFT, after=self._model_cb)
59
+ else:
60
+ self._psd_frame.pack_forget()
61
+ _, key, _ = next(m for m in _MODEL_OPTIONS if m[0] == label)
62
+ if key == 'egh':
63
+ self._action_btn.configure(text='Skip', command=self._skip)
64
+ else:
65
+ self._action_btn.configure(text='Upgrade', command=self._upgrade)
66
+ self._model_var.trace_add('write', _on_model_change)
67
+
68
+ # Show EGH plot_components immediately
69
+ result = self._decomp.plot_components()
70
+ canvas = FigureCanvasTkAgg(result.fig, master=win)
71
+ canvas.draw()
72
+ canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
73
+ NavigationToolbar2Tk(canvas, win).update()
74
+ self._win = win
75
+
76
+ def _skip(self):
77
+ model_info = {'model': 'egh', 'pore_dist': None, 'ln_pore_sigma': None}
78
+ from molass_gui.upgraded_view import UpgradedView
79
+ UpgradedView(self._decomp, self._trimmed, self._nc, model_info,
80
+ parent=self._win).show()
81
+
82
+ def _upgrade(self):
83
+ model_label = self._model_var.get()
84
+ _, model_key, pore_dist = next(m for m in _MODEL_OPTIONS if m[0] == model_label)
85
+ if model_key == 'egh':
86
+ self._skip()
87
+ return
88
+ ln_pore_sigma = float(self._psd_sigma_var.get()) if pore_dist == 'lognormal' else None
89
+ self._action_btn.state(["disabled"])
90
+ self._status_var.set(f"Upgrading to {model_key.upper()}\u2026")
91
+
92
+ def worker():
93
+ try:
94
+ upgrade_kwargs = {}
95
+ if pore_dist is not None:
96
+ upgrade_kwargs['pore_dist'] = pore_dist
97
+ if ln_pore_sigma is not None:
98
+ upgrade_kwargs['model_params'] = {'ln_pore_sigma': ln_pore_sigma}
99
+ upgraded = self._decomp.upgrade(model_key, **upgrade_kwargs)
100
+ model_info = {'model': model_key, 'pore_dist': pore_dist,
101
+ 'ln_pore_sigma': ln_pore_sigma}
102
+
103
+ def on_main():
104
+ self._action_btn.state(["!disabled"])
105
+ self._status_var.set("")
106
+ from molass_gui.upgraded_view import UpgradedView
107
+ UpgradedView(upgraded, self._trimmed, self._nc, model_info,
108
+ parent=self._win).show()
109
+
110
+ self._win.after(0, on_main)
111
+ except Exception as exc:
112
+ msg = str(exc)
113
+ def on_error(m=msg):
114
+ self._status_var.set(f"Error: {m}")
115
+ self._action_btn.state(["!disabled"])
116
+ self._win.after(0, on_error)
117
+
118
+ threading.Thread(target=worker, daemon=True).start()
@@ -0,0 +1,352 @@
1
+ """RigorousView — Phase 5: Rg computation + auto-start optimization + live 4-panel monitor.
2
+
3
+ Receives an already-upgraded Decomposition from UpgradedView (Phase 4).
4
+ Transitions: computing Rg → scoring → running → done.
5
+ """
6
+ import os
7
+ import queue
8
+ import traceback
9
+ import threading
10
+ import time
11
+ import tkinter as tk
12
+ from tkinter import ttk
13
+
14
+
15
+ class RigorousView:
16
+ def __init__(self, decomp, trimmed, est_kwargs, analysis_folder, parent=None):
17
+ """
18
+ Parameters
19
+ ----------
20
+ decomp : Decomposition
21
+ EGH decomposition; Rg curve already cached by QuickView.
22
+ trimmed : SecSaxsData
23
+ Trimmed (uncorrected) SSD, passed to score().
24
+ est_kwargs : dict
25
+ Keys: use_subprocess, pipeline_recipe.
26
+ analysis_folder : str
27
+ Output folder for optimization results.
28
+ parent : tk widget or None
29
+ """
30
+ self._decomp = decomp
31
+ self._trimmed = trimmed
32
+ self._est_kwargs = est_kwargs
33
+ self._analysis_folder = analysis_folder
34
+ self._parent = parent
35
+ self._decomp_for_opt = None
36
+ self._score = None
37
+ self._run_info = None
38
+ self._stopped = False
39
+ self._niter = 0
40
+ self._last_sv_len = 0
41
+ self._redraw_event = threading.Event()
42
+
43
+ def show(self):
44
+ import matplotlib.pyplot as plt
45
+ import matplotlib.gridspec as gridspec
46
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
47
+
48
+ win = tk.Toplevel(self._parent)
49
+ recipe = (self._est_kwargs.get('pipeline_recipe') or {})
50
+ model = recipe.get('model', 'egh').upper()
51
+ method = recipe.get('method', 'bh').upper()
52
+ proc = 'subprocess' if self._est_kwargs.get('use_subprocess') else 'in-process'
53
+ win.title(f"Rigorous Optimization \u2014 {model} | {method} | {proc}")
54
+
55
+ # Prep phase header (hidden after prep completes)
56
+ self._prep_frame = ttk.Frame(win, padding=(8, 2))
57
+ self._prep_frame.pack(fill=tk.X)
58
+ self._prep_var = tk.StringVar(value="Computing Rg curve\u2026")
59
+ ttk.Label(self._prep_frame, textvariable=self._prep_var).pack(side=tk.LEFT)
60
+ n_frames = len(self._decomp.ssd.xr.jv)
61
+ self._pb = ttk.Progressbar(self._prep_frame, maximum=n_frames, length=300)
62
+ self._pb.pack(side=tk.LEFT, padx=8, fill=tk.X, expand=True)
63
+
64
+ # Optimization header (SV, evals, action button)
65
+ hdr = ttk.Frame(win, padding=(8, 4))
66
+ hdr.pack(fill=tk.X)
67
+ self._status_var = tk.StringVar(value="")
68
+ ttk.Label(hdr, textvariable=self._status_var, width=24).pack(side=tk.LEFT)
69
+ self._sv_var = tk.StringVar(value="SV: \u2014")
70
+ ttk.Label(hdr, textvariable=self._sv_var, font=("", 12, "bold")).pack(
71
+ side=tk.LEFT, padx=16)
72
+ self._n_var = tk.StringVar(value="0 evals")
73
+ ttk.Label(hdr, textvariable=self._n_var).pack(side=tk.LEFT)
74
+ self._iter_var = tk.StringVar(value="Iter: \u2014")
75
+ ttk.Label(hdr, textvariable=self._iter_var).pack(side=tk.LEFT, padx=12)
76
+ self._time_var = tk.StringVar(value="")
77
+ ttk.Label(hdr, textvariable=self._time_var, foreground="#555").pack(side=tk.LEFT, padx=4)
78
+ self._action_btn = ttk.Button(hdr, text="Terminate", command=self._stop,
79
+ state='disabled')
80
+ self._action_btn.pack(side=tk.RIGHT, padx=8)
81
+
82
+ # 4-panel figure: top 3 panels + bottom SV strip
83
+ self._fig = plt.figure(figsize=(18, 6.5))
84
+ gs = gridspec.GridSpec(2, 3, figure=self._fig,
85
+ height_ratios=[4, 1.5], hspace=0.45)
86
+ self._ax_uv = self._fig.add_subplot(gs[0, 0])
87
+ self._ax_xr = self._fig.add_subplot(gs[0, 1])
88
+ self._ax_score = self._fig.add_subplot(gs[0, 2])
89
+ self._ax_xr_twin = self._ax_xr.twinx()
90
+ self._ax_xr_twin.grid(False)
91
+ self._ax_sv = self._fig.add_subplot(gs[1, :])
92
+ self._axis_info = (self._fig,
93
+ (self._ax_uv, self._ax_xr, self._ax_score, self._ax_xr_twin))
94
+
95
+ canvas = FigureCanvasTkAgg(self._fig, master=win)
96
+ canvas.draw()
97
+ canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
98
+ NavigationToolbar2Tk(canvas, win).update()
99
+ self._canvas = canvas
100
+ self._win = win
101
+
102
+ self._prep_q = queue.Queue()
103
+ threading.Thread(target=self._prep_worker, daemon=True).start()
104
+ win.after(100, self._prep_poll)
105
+
106
+ # ------------------------------------------------------------------
107
+ # Prep phase
108
+
109
+ def _prep_worker(self):
110
+ try:
111
+ # Upgrade already done in Phase 3; compute Rg then score
112
+ def _rg_cb(rg_buffer, j):
113
+ self._prep_q.put(('rg', j))
114
+ self._decomp.get_rg_curve(progress_cb=_rg_cb)
115
+ self._decomp_for_opt = self._decomp
116
+ score = self._decomp_for_opt.score(trimmed_ssd=self._trimmed)
117
+ self._prep_q.put(('done', score))
118
+ except Exception as exc:
119
+ short = str(exc).split('\n')[0][:120]
120
+ self._prep_q.put(('error', short))
121
+
122
+ def _prep_poll(self):
123
+ while True:
124
+ try:
125
+ item = self._prep_q.get_nowait()
126
+ except queue.Empty:
127
+ break
128
+ if item[0] == 'rg':
129
+ self._pb['value'] = item[1]
130
+ elif item[0] == 'done':
131
+ self._on_prep_done(item[1])
132
+ return
133
+ elif item[0] == 'error':
134
+ self._prep_var.set(f"Prep error: {item[1]}")
135
+ return
136
+ self._win.after(100, self._prep_poll)
137
+
138
+ def _on_prep_done(self, score):
139
+ self._score = score
140
+ self._pb.stop()
141
+ self._prep_frame.pack_forget()
142
+ try:
143
+ opt = score.optimizer
144
+ opt.objective_func(score.init_params, plot=True, axis_info=self._axis_info)
145
+ _retitle_panels(self._ax_uv, self._ax_xr, self._ax_score, score.sv)
146
+ _draw_sv_history(self._ax_sv, [], 0)
147
+ except Exception:
148
+ pass
149
+ self._canvas.draw()
150
+ self._status_var.set(f"Ready \u2014 SV={score.sv:.2f}")
151
+ self._sv_var.set(f"SV: {score.sv:.2f}")
152
+ self._optimize() # auto-start; user can Terminate if needed
153
+
154
+ # ------------------------------------------------------------------
155
+ # Optimization phase
156
+
157
+ def _optimize(self):
158
+ self._status_var.set("Starting\u2026")
159
+ threading.Thread(target=self._launch, daemon=True).start()
160
+
161
+ def _launch(self):
162
+ try:
163
+ use_subprocess = self._est_kwargs.get('use_subprocess', False)
164
+ pipeline_recipe = self._est_kwargs.get('pipeline_recipe', None)
165
+ method = (pipeline_recipe or {}).get('method', 'BH').upper()
166
+
167
+ run_info = self._decomp_for_opt.optimize_rigorously(
168
+ trimmed_ssd=self._trimmed,
169
+ in_process=not use_subprocess,
170
+ async_=True,
171
+ monitor=False,
172
+ method=method,
173
+ analysis_folder=self._analysis_folder,
174
+ pipeline_recipe=pipeline_recipe,
175
+ )
176
+ self._run_info = run_info
177
+ self._win.after(0, self._on_started)
178
+ except Exception as exc:
179
+ tb = traceback.format_exc()
180
+ log_path = os.path.join(self._analysis_folder, 'molass_gui_error.log')
181
+ try:
182
+ os.makedirs(self._analysis_folder, exist_ok=True)
183
+ with open(log_path, 'w') as f:
184
+ f.write(tb)
185
+ except Exception:
186
+ pass
187
+ short = str(exc).split('\n')[0][:120]
188
+ self._win.after(0, lambda m=short: self._status_var.set(
189
+ f"Error: {m} \u2014 see {log_path}"))
190
+
191
+ def _on_started(self):
192
+ self._action_btn.state(['!disabled'])
193
+ self._status_var.set("Running\u2026")
194
+ threading.Thread(target=self._watch_loop, daemon=True).start()
195
+ self._win.after(500, self._ui_poll)
196
+
197
+ def _watch_loop(self):
198
+ """Background: redraw UV/XR/score panels every ~3 s."""
199
+ first = True
200
+ while not self._stopped:
201
+ if first:
202
+ first = False
203
+ else:
204
+ time.sleep(3.0)
205
+ try:
206
+ opt = self._run_info.optimizer
207
+ params = self._run_info.best_params
208
+ if params is None:
209
+ params = self._run_info.init_params
210
+ if params is not None:
211
+ lock = getattr(opt, '_objective_lock', None)
212
+ acquired = lock.acquire(timeout=1.5) if lock else True
213
+ if acquired:
214
+ try:
215
+ for ax in (self._ax_uv, self._ax_xr, self._ax_score,
216
+ self._ax_xr_twin):
217
+ ax.cla()
218
+ self._ax_xr_twin.grid(False)
219
+ opt.objective_func(params, plot=True,
220
+ axis_info=self._axis_info)
221
+ sv_hist = self._run_info.sv_history
222
+ sv_now = sv_hist[-1] if sv_hist else None
223
+ _retitle_panels(self._ax_uv, self._ax_xr, self._ax_score, sv_now)
224
+ finally:
225
+ if lock:
226
+ lock.release()
227
+ self._redraw_event.set()
228
+ except Exception:
229
+ pass
230
+
231
+ def _ui_poll(self):
232
+ """Main-thread: update labels and flush canvas every 500 ms."""
233
+ if self._stopped:
234
+ return
235
+ if self._run_info is None:
236
+ self._win.after(500, self._ui_poll)
237
+ return
238
+ try:
239
+ status = self._run_info.live_status()
240
+ phase = status.get('phase', '?')
241
+ best_sv = status.get('best_sv')
242
+ n_evals = status.get('n_evals', 0)
243
+ n_callbacks = status.get('n_callbacks') or 0
244
+ elapsed_s = status.get('elapsed_s') or 0.0
245
+ manifest = status.get('manifest') or {}
246
+ niter = manifest.get('niter', 0)
247
+
248
+ self._n_var.set(f"{n_evals} evals")
249
+ if best_sv is not None:
250
+ self._sv_var.set(f"SV: {best_sv:.2f}")
251
+ self._iter_var.set(
252
+ f"Iter: {n_callbacks}/{niter}" if niter else f"Iter: {n_callbacks}")
253
+ self._time_var.set(_fmt_time_info(elapsed_s, n_callbacks, niter))
254
+ if niter:
255
+ self._niter = niter
256
+
257
+ if phase == 'done':
258
+ self._status_var.set("Done.")
259
+ self._action_btn.state(["disabled"])
260
+ self._stopped = True
261
+ else:
262
+ p = getattr(self._run_info, '_subprocess_process', None)
263
+ if p is not None and p.poll() is not None and p.poll() != 0 and not n_callbacks:
264
+ stderr_path = os.path.join(
265
+ self._run_info.work_folder or '', 'optimizer_stderr.txt')
266
+ self._status_var.set(
267
+ f"Subprocess error (exit {p.poll()}) \u2014 see {stderr_path}")
268
+ self._action_btn.state(["disabled"])
269
+ self._stopped = True
270
+ else:
271
+ self._status_var.set(f"Phase: {phase}")
272
+
273
+ try:
274
+ sv_hist = self._run_info.sv_history
275
+ sv_len = len(sv_hist) if sv_hist else 0
276
+ sv_changed = sv_len != self._last_sv_len
277
+ if sv_changed:
278
+ _draw_sv_history(self._ax_sv, sv_hist, self._niter)
279
+ self._last_sv_len = sv_len
280
+ except Exception:
281
+ sv_changed = False
282
+ except Exception:
283
+ sv_changed = False
284
+
285
+ if self._redraw_event.is_set() or sv_changed:
286
+ self._redraw_event.clear()
287
+ self._canvas.draw()
288
+ if not self._stopped:
289
+ self._win.after(500, self._ui_poll)
290
+
291
+ def _stop(self):
292
+ self._stopped = True
293
+ if self._run_info is not None:
294
+ try:
295
+ self._run_info.stop()
296
+ except Exception:
297
+ pass
298
+ self._status_var.set("Terminating\u2026")
299
+ self._action_btn.state(["disabled"])
300
+
301
+ def _wait_dead():
302
+ ri = self._run_info
303
+ if ri is not None:
304
+ for _ in range(60):
305
+ time.sleep(0.5)
306
+ if not ri.is_alive:
307
+ break
308
+ self._win.after(0, lambda: self._status_var.set("Terminated."))
309
+ threading.Thread(target=_wait_dead, daemon=True).start()
310
+
311
+
312
+ # ------------------------------------------------------------------
313
+
314
+ def _retitle_panels(ax_uv, ax_xr, ax_score, sv):
315
+ ax_uv.set_title("UV Decomposition", fontsize=16)
316
+ ax_xr.set_title("XR Decomposition", fontsize=16)
317
+ if sv is not None:
318
+ ax_score.set_title(f"Score Breakdown (SV={sv:.1f})", fontsize=16)
319
+ else:
320
+ ax_score.set_title("Score Breakdown", fontsize=16)
321
+
322
+
323
+ def _fmt_duration(seconds):
324
+ h, rem = divmod(int(seconds), 3600)
325
+ m, s = divmod(rem, 60)
326
+ return f"{h}:{m:02d}:{s:02d}" if h else f"{m:02d}:{s:02d}"
327
+
328
+
329
+ def _fmt_time_info(elapsed_s, n_done, n_total):
330
+ parts = [f"Elapsed: {_fmt_duration(elapsed_s)}"]
331
+ if n_done > 0 and n_total > 0 and elapsed_s > 0:
332
+ rate = elapsed_s / n_done
333
+ remaining = (n_total - n_done) * rate
334
+ parts.append(f"ETA: ~{_fmt_duration(remaining)}")
335
+ return " ".join(parts)
336
+
337
+
338
+ def _draw_sv_history(ax, sv_hist, niter=0):
339
+ ax.cla()
340
+ n = len(sv_hist) if sv_hist else 0
341
+ if n > 0:
342
+ xs = range(n)
343
+ ax.step(xs, sv_hist, where='post', lw=1.2, color='steelblue')
344
+ ax.set_ylim(bottom=max(0, min(sv_hist) - 5), top=105)
345
+ if niter > n:
346
+ ax.axvspan(n - 1, niter, alpha=0.06, color='grey')
347
+ ax.axvline(n - 1, color='steelblue', lw=0.8, ls='--', alpha=0.6)
348
+ if niter > 0:
349
+ ax.set_xlim(0, niter)
350
+ ax.set_xlabel("BH hop")
351
+ ax.set_ylabel("SV")
352
+ ax.set_title("SV history")
@@ -0,0 +1,95 @@
1
+ """UpgradedView — Phase 4: upgraded plot_components + method/subprocess → RigorousView."""
2
+ import tkinter as tk
3
+ from tkinter import ttk
4
+
5
+ _METHOD_LABELS = ['BH', 'DE']
6
+
7
+
8
+ class UpgradedView:
9
+ def __init__(self, decomp, trimmed, nc, model_info, parent=None):
10
+ """
11
+ Parameters
12
+ ----------
13
+ decomp : Decomposition
14
+ Upgraded (or EGH) decomposition.
15
+ trimmed : SecSaxsData
16
+ Trimmed SSD for score().
17
+ nc : int
18
+ Number of components (for pipeline_recipe).
19
+ model_info : dict
20
+ Keys: model, pore_dist, ln_pore_sigma.
21
+ parent : tk widget or None
22
+ """
23
+ self._decomp = decomp
24
+ self._trimmed = trimmed
25
+ self._nc = nc
26
+ self._model_info = model_info
27
+ self._parent = parent
28
+
29
+ def show(self):
30
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
31
+
32
+ win = tk.Toplevel(self._parent)
33
+ model = self._model_info['model'].upper()
34
+ win.title(f"Upgraded View \u2014 {model}")
35
+
36
+ # Header: method + subprocess + Rigorous button
37
+ hdr = ttk.Frame(win, padding=(8, 4))
38
+ hdr.pack(fill=tk.X)
39
+
40
+ ttk.Label(hdr, text="Method:").pack(side=tk.LEFT)
41
+ self._method_var = tk.StringVar(value='BH')
42
+ ttk.Combobox(hdr, textvariable=self._method_var, values=_METHOD_LABELS,
43
+ state='readonly', width=8).pack(side=tk.LEFT, padx=4)
44
+
45
+ self._subprocess_var = tk.BooleanVar(value=True)
46
+ ttk.Checkbutton(hdr, text="Use subprocess",
47
+ variable=self._subprocess_var).pack(side=tk.LEFT, padx=12)
48
+
49
+ self._rig_btn = ttk.Button(hdr, text="Rigorous Optimization\u2026",
50
+ command=self._proceed_rigorous)
51
+ self._rig_btn.pack(side=tk.RIGHT, padx=8)
52
+
53
+ # Show plot_components for the current model
54
+ result = self._decomp.plot_components()
55
+ canvas = FigureCanvasTkAgg(result.fig, master=win)
56
+ canvas.draw()
57
+ canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
58
+ NavigationToolbar2Tk(canvas, win).update()
59
+ self._win = win
60
+
61
+ def _proceed_rigorous(self):
62
+ from tkinter import filedialog
63
+ folder = filedialog.askdirectory(
64
+ title="Select output folder for optimization results",
65
+ parent=self._win)
66
+ if not folder:
67
+ return
68
+
69
+ model_key = self._model_info['model']
70
+ pore_dist = self._model_info['pore_dist']
71
+ ln_pore_sigma = self._model_info['ln_pore_sigma']
72
+ method = self._method_var.get().lower()
73
+ use_subprocess = self._subprocess_var.get()
74
+
75
+ pipeline_recipe = {
76
+ 'num_components': self._nc,
77
+ 'model': model_key,
78
+ 'method': method,
79
+ 'decomp_params': {},
80
+ 'trim_params': {},
81
+ 'baseline_params': {},
82
+ }
83
+ if pore_dist is not None:
84
+ pipeline_recipe['pore_dist'] = pore_dist
85
+ if ln_pore_sigma is not None:
86
+ pipeline_recipe['ln_pore_sigma'] = ln_pore_sigma
87
+
88
+ est_kwargs = {
89
+ 'use_subprocess': use_subprocess,
90
+ 'pipeline_recipe': pipeline_recipe,
91
+ }
92
+
93
+ from molass_gui.rigorous_view import RigorousView
94
+ RigorousView(self._decomp, self._trimmed, est_kwargs,
95
+ analysis_folder=folder, parent=self._win).show()
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "molass_gui"
7
+ version = "0.1.0"
8
+
9
+ dependencies = [
10
+ "molass>=1.0.5",
11
+ "molass_legacy>=1.6.13",
12
+ ]
13
+
14
+ requires-python = ">=3.9,<3.15"
15
+
16
+ authors = [
17
+ {name = "Molass Community"},
18
+ ]
19
+
20
+ maintainers = [
21
+ {name = "Molass Community"}
22
+ ]
23
+
24
+ description = "Tkinter GUI for Molass — initial estimate workflow"
25
+ readme = "README.md"
26
+ license = { text = "GNU General Public License v3.0" }
27
+
28
+ keywords = ["SEC-SAXS"]
29
+ classifiers = [
30
+ "Development Status :: 3 - Alpha",
31
+ "Programming Language :: Python",
32
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
33
+ ]
34
+
35
+ [project.scripts]
36
+ molass-gui = "molass_gui.app:main"
37
+
38
+ [project.urls]
39
+ Repository = "https://github.com/biosaxs-dev/molass-gui"