pycompound 0.1.0__py3-none-any.whl → 0.1.2__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.
app.py CHANGED
@@ -4,6 +4,8 @@ from pycompound.spec_lib_matching import run_spec_lib_matching_on_HRMS_data
4
4
  from pycompound.spec_lib_matching import run_spec_lib_matching_on_NRMS_data
5
5
  from pycompound.spec_lib_matching import tune_params_on_HRMS_data
6
6
  from pycompound.spec_lib_matching import tune_params_on_NRMS_data
7
+ from pycompound.spec_lib_matching import tune_params_on_HRMS_data_shiny
8
+ from pycompound.spec_lib_matching import tune_params_on_NRMS_data_shiny
7
9
  from pycompound.plot_spectra import generate_plots_on_HRMS_data
8
10
  from pycompound.plot_spectra import generate_plots_on_NRMS_data
9
11
  from pathlib import Path
@@ -18,8 +20,45 @@ import matplotlib.pyplot as plt
18
20
  import pandas as pd
19
21
  import numpy as np
20
22
  import netCDF4 as nc
21
- from pyteomics import mgf
22
- from pyteomics import mzml
23
+ from pyteomics import mgf, mzml
24
+ import ast
25
+ from numbers import Real
26
+
27
+
28
+
29
+ _LOG_QUEUE: asyncio.Queue[str] = asyncio.Queue()
30
+
31
+ def _run_with_redirects(fn, writer, *args, **kwargs):
32
+ with redirect_stdout(writer), redirect_stderr(writer):
33
+ return fn(*args, **kwargs)
34
+
35
+
36
+ def strip_text(s):
37
+ return [x.strip() for x in s.strip('[]').split(',') if x.strip()]
38
+
39
+
40
+ def strip_numeric(s):
41
+ return [float(x.strip()) for x in s.strip('[]').split(',') if x.strip()]
42
+
43
+
44
+ def strip_weights(s):
45
+ obj = ast.literal_eval(s) if isinstance(s, (str, bytes)) else s
46
+ keys = ['Cosine', 'Shannon', 'Renyi', 'Tsallis']
47
+
48
+ if isinstance(obj, (list, tuple)):
49
+ if len(obj) == 4 and all(isinstance(x, Real) for x in obj):
50
+ tuples = [obj]
51
+ else:
52
+ tuples = list(obj)
53
+ else:
54
+ raise ValueError(f"Expected a 4-tuple or a sequence of 4-tuples, got {type(obj).__name__}")
55
+
56
+ out = []
57
+ for t in tuples:
58
+ if not (isinstance(t, (list, tuple)) and len(t) == 4):
59
+ raise ValueError(f"Each item must be a 4-tuple, got: {t!r}")
60
+ out.append(dict(zip(keys, t)))
61
+ return out
23
62
 
24
63
 
25
64
  def build_library(input_path=None, output_path=None):
@@ -152,40 +191,45 @@ def extract_first_column_ids(file_path: str, max_ids: int = 20000):
152
191
  return []
153
192
 
154
193
 
194
+ def _open_plot_window(session, png_bytes: bytes, title: str = "plot.png"):
195
+ """Send PNG bytes to browser and open in a new window as a data URL."""
196
+ b64 = base64.b64encode(png_bytes).decode("ascii")
197
+ data_url = f"data:image/png;base64,{b64}"
198
+ session.send_custom_message("open-plot-window", {"png": data_url, "title": title})
199
+
200
+
155
201
  def plot_spectra_ui(platform: str):
156
- # Base inputs common to all platforms
157
202
  base_inputs = [
158
203
  ui.input_file("query_data", "Upload query dataset (mgf, mzML, cdf, msp, or csv):"),
159
204
  ui.input_file("reference_data", "Upload reference dataset (mgf, mzML, cdf, msp, or csv):"),
160
- ui.input_selectize(
161
- "spectrum_ID1",
162
- "Select spectrum ID 1:",
163
- choices=[],
164
- multiple=False,
165
- options={"placeholder": "Upload a query file to load IDs..."},
166
- ),
167
- ui.input_selectize(
168
- "spectrum_ID2",
169
- "Select spectrum ID 2 (optional):",
170
- choices=[],
171
- multiple=False,
172
- options={"placeholder": "Upload a reference file to load IDs..."},
173
- ),
205
+ ui.input_selectize(
206
+ "spectrum_ID1",
207
+ "Select spectrum ID 1 (default is the first spectrum in the library):",
208
+ choices=[],
209
+ multiple=False,
210
+ options={"placeholder": "Upload a library..."},
211
+ ),
212
+ ui.input_selectize(
213
+ "spectrum_ID2",
214
+ "Select spectrum ID 2 (default is the first spectrum in the library):",
215
+ choices=[],
216
+ multiple=False,
217
+ options={"placeholder": "Upload a library..."},
218
+ ),
174
219
  ui.input_select("similarity_measure", "Select similarity measure:", ["cosine","shannon","renyi","tsallis","mixture","jaccard","dice","3w_jaccard","sokal_sneath","binary_cosine","mountford","mcconnaughey","driver_kroeber","simpson","braun_banquet","fager_mcgowan","kulczynski","intersection","hamming","hellinger"]),
220
+ ui.input_text('weights', 'Weights for mixture similarity measure (cosine, shannon, renyi, tsallis):', '0.25, 0.25, 0.25, 0.25'),
175
221
  ui.input_select(
176
222
  "high_quality_reference_library",
177
- "Indicate whether the reference library is considered high quality. "
178
- "If True, filtering and noise removal are only applied to the query spectra.",
223
+ "Indicate whether the reference library is considered high quality. If True, filtering and noise removal are only applied to the query spectra.",
179
224
  [False, True],
180
225
  ),
181
226
  ]
182
227
 
183
- # Extra inputs depending on platform
184
228
  if platform == "HRMS":
185
229
  extra_inputs = [
186
230
  ui.input_text(
187
231
  "spectrum_preprocessing_order",
188
- "Sequence of characters for preprocessing order (C, F, M, N, L, W). M must be included, C before M if used.",
232
+ "Sequence of characters for preprocessing order (C (centroiding), F (filtering), M (matching), N (noise removal), L (low-entropy transformation), W (weight factor transformation)). M must be included, C before M if used.",
189
233
  "FCNMWL",
190
234
  ),
191
235
  ui.input_numeric("window_size_centroiding", "Centroiding window-size:", 0.5),
@@ -195,12 +239,11 @@ def plot_spectra_ui(platform: str):
195
239
  extra_inputs = [
196
240
  ui.input_text(
197
241
  "spectrum_preprocessing_order",
198
- "Sequence of characters for preprocessing order (F, N, L, W).",
242
+ "Sequence of characters for preprocessing order (F (filtering), N (noise removal), L (low-entropy transformation), W (weight factor transformation)).",
199
243
  "FNLW",
200
244
  )
201
245
  ]
202
246
 
203
- # Numeric inputs
204
247
  numeric_inputs = [
205
248
  ui.input_numeric("mz_min", "Minimum m/z for filtering:", 0),
206
249
  ui.input_numeric("mz_max", "Maximum m/z for filtering:", 99999999),
@@ -213,68 +256,77 @@ def plot_spectra_ui(platform: str):
213
256
  ui.input_numeric("entropy_dimension", "Entropy dimension (Renyi/Tsallis only):", 1.1),
214
257
  ]
215
258
 
216
- # Y-axis transformation select input
217
259
  select_input = ui.input_select(
218
260
  "y_axis_transformation",
219
261
  "Transformation to apply to intensity axis:",
220
262
  ["normalized", "none", "log10", "sqrt"],
221
263
  )
222
264
 
223
- # Run and Back buttons
224
265
  run_button_plot_spectra = ui.download_button("run_btn_plot_spectra", "Run", style="font-size:16px; padding:15px 30px; width:200px; height:80px")
225
266
  back_button = ui.input_action_button("back", "Back to main menu", style="font-size:16px; padding:15px 30px; width:200px; height:80px")
226
267
 
227
- # Layout base_inputs and extra_inputs in columns
228
268
  if platform == "HRMS":
229
269
  inputs_columns = ui.layout_columns(
230
- ui.div(base_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
231
- ui.div([base_inputs[5:6], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
270
+ ui.div(base_inputs[0:6], style="display:flex; flex-direction:column; gap:10px;"),
271
+ ui.div([base_inputs[6:7], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
232
272
  ui.div(numeric_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
233
273
  ui.div([numeric_inputs[5:10], select_input], style="display:flex; flex-direction:column; gap:10px;"),
234
- col_widths=(3, 3, 3, 3),
274
+ col_widths=(3,3,3,3),
235
275
  )
236
276
  elif platform == "NRMS":
237
277
  inputs_columns = ui.layout_columns(
238
- ui.div(base_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
239
- ui.div([base_inputs[5:6], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
278
+ ui.div(base_inputs[0:6], style="display:flex; flex-direction:column; gap:10px;"),
279
+ ui.div([base_inputs[6:7], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
240
280
  ui.div(numeric_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
241
281
  ui.div([numeric_inputs[5:10], select_input], style="display:flex; flex-direction:column; gap:10px;"),
242
- col_widths=(3, 3, 3, 3),
282
+ col_widths=(3,3,3,3),
243
283
  )
244
284
 
245
- # Combine everything
246
285
  return ui.div(
247
286
  ui.TagList(
248
287
  ui.h2("Plot Spectra"),
249
288
  inputs_columns,
250
289
  run_button_plot_spectra,
251
290
  back_button,
252
- ui.div(ui.output_text("plot_query_status"), style="margin-top:8px; font-size:14px")
291
+ ui.div(ui.output_text("plot_query_status"), style="margin-top:8px; font-size:14px"),
292
+ ui.div(ui.output_text("plot_reference_status"), style="margin-top:8px; font-size:14px")
253
293
  ),
254
294
  )
255
295
 
256
296
 
257
297
 
258
298
  def run_spec_lib_matching_ui(platform: str):
259
- # Base inputs common to all platforms
260
299
  base_inputs = [
261
300
  ui.input_file("query_data", "Upload query dataset (mgf, mzML, cdf, msp, or csv):"),
262
301
  ui.input_file("reference_data", "Upload reference dataset (mgf, mzML, cdf, msp, or csv):"),
263
302
  ui.input_select("similarity_measure", "Select similarity measure:", ["cosine","shannon","renyi","tsallis","mixture","jaccard","dice","3w_jaccard","sokal_sneath","binary_cosine","mountford","mcconnaughey","driver_kroeber","simpson","braun_banquet","fager_mcgowan","kulczynski","intersection","hamming","hellinger"]),
303
+ ui.input_text('weights', 'Weights for mixture similarity measure (cosine, shannon, renyi, tsallis):', '0.25, 0.25, 0.25, 0.25'),
304
+ ui.input_selectize(
305
+ "spectrum_ID1",
306
+ "Select spectrum ID 1 (only applicable for plotting; default is the first spectrum in the query library):",
307
+ choices=[],
308
+ multiple=False,
309
+ options={"placeholder": "Upload a library..."},
310
+ ),
311
+ ui.input_selectize(
312
+ "spectrum_ID2",
313
+ "Select spectrum ID 2 (only applicable for plotting; default is the first spectrum in the reference library):",
314
+ choices=[],
315
+ multiple=False,
316
+ options={"placeholder": "Upload a library..."},
317
+ ),
264
318
  ui.input_select(
265
319
  "high_quality_reference_library",
266
- "Indicate whether the reference library is considered high quality. "
267
- "If True, filtering and noise removal are only applied to the query spectra.",
320
+ "Indicate whether the reference library is considered high quality. If True, filtering and noise removal are only applied to the query spectra.",
268
321
  [False, True],
269
- ),
322
+ )
270
323
  ]
271
324
 
272
- # Extra inputs depending on platform
273
325
  if platform == "HRMS":
274
326
  extra_inputs = [
275
327
  ui.input_text(
276
328
  "spectrum_preprocessing_order",
277
- "Sequence of characters for preprocessing order (C, F, M, N, L, W). M must be included, C before M if used.",
329
+ "Sequence of characters for preprocessing order (C (centroiding), F (filtering), M (matching), N (noise removal), L (low-entropy transformation), W (weight factor transformation)). M must be included, C before M if used.",
278
330
  "FCNMWL",
279
331
  ),
280
332
  ui.input_numeric("window_size_centroiding", "Centroiding window-size:", 0.5),
@@ -284,12 +336,11 @@ def run_spec_lib_matching_ui(platform: str):
284
336
  extra_inputs = [
285
337
  ui.input_text(
286
338
  "spectrum_preprocessing_order",
287
- "Sequence of characters for preprocessing order (F, N, L, W).",
339
+ "Sequence of characters for preprocessing order (F (filtering), N (noise removal), L (low-entropy transformation), W (weight factor transformation)).",
288
340
  "FNLW",
289
341
  )
290
342
  ]
291
343
 
292
- # Numeric inputs
293
344
  numeric_inputs = [
294
345
  ui.input_numeric("mz_min", "Minimum m/z for filtering:", 0),
295
346
  ui.input_numeric("mz_max", "Maximum m/z for filtering:", 99999999),
@@ -300,30 +351,29 @@ def run_spec_lib_matching_ui(platform: str):
300
351
  ui.input_numeric("wf_int", "Intensity weight factor:", 1.0),
301
352
  ui.input_numeric("LET_threshold", "Low-entropy threshold:", 0.0),
302
353
  ui.input_numeric("entropy_dimension", "Entropy dimension (Renyi/Tsallis only):", 1.1),
303
- ui.input_numeric("n_top_matches_to_save", "Number of top matches to save:", 1),
354
+ ui.input_numeric("n_top_matches_to_save", "Number of top matches to save:", 3),
304
355
  ]
305
356
 
306
357
 
307
- # Run and Back buttons
308
- run_button_spec_lib_matching = ui.download_button("run_btn_spec_lib_matching", "Run", style="font-size:16px; padding:15px 30px; width:200px; height:80px")
358
+ run_button_spec_lib_matching = ui.download_button("run_btn_spec_lib_matching", "Run Spectral Library Matching", style="font-size:16px; padding:15px 30px; width:200px; height:80px")
359
+ run_button_plot_spectra_within_spec_lib_matching = ui.download_button("run_btn_plot_spectra_within_spec_lib_matching", "Plot Spectra", style="font-size:16px; padding:15px 30px; width:200px; height:80px")
309
360
  back_button = ui.input_action_button("back", "Back to main menu", style="font-size:16px; padding:15px 30px; width:200px; height:80px")
310
361
 
311
- # Layout base_inputs and extra_inputs in columns
312
362
  if platform == "HRMS":
313
363
  inputs_columns = ui.layout_columns(
314
- ui.div(base_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
315
- ui.div([base_inputs[5:6], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
364
+ ui.div(base_inputs[0:6], style="display:flex; flex-direction:column; gap:10px;"),
365
+ ui.div([base_inputs[6:7], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
316
366
  ui.div(numeric_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
317
367
  ui.div(numeric_inputs[5:10], style="display:flex; flex-direction:column; gap:10px;"),
318
- col_widths=(3, 3, 3, 3),
368
+ col_widths=(3,3,3,3)
319
369
  )
320
370
  elif platform == "NRMS":
321
371
  inputs_columns = ui.layout_columns(
322
- ui.div(base_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
323
- ui.div([base_inputs[5:6], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
372
+ ui.div(base_inputs[0:6], style="display:flex; flex-direction:column; gap:10px;"),
373
+ ui.div([base_inputs[6:7], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
324
374
  ui.div(numeric_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
325
375
  ui.div(numeric_inputs[5:10], style="display:flex; flex-direction:column; gap:10px;"),
326
- col_widths=(3, 3, 3, 3),
376
+ col_widths=(3,3,3,3)
327
377
  )
328
378
 
329
379
  log_panel = ui.card(
@@ -332,19 +382,99 @@ def run_spec_lib_matching_ui(platform: str):
332
382
  style="max-height:300px; overflow:auto"
333
383
  )
334
384
 
335
- # Combine everything
336
385
  return ui.div(
337
386
  ui.TagList(
338
387
  ui.h2("Run Spectral Library Matching"),
339
388
  inputs_columns,
340
389
  run_button_spec_lib_matching,
390
+ run_button_plot_spectra_within_spec_lib_matching,
341
391
  back_button,
342
- log_panel,
392
+ log_panel
343
393
  ),
344
394
  )
345
395
 
346
396
 
347
397
 
398
+ def run_parameter_tuning_ui(platform: str):
399
+ base_inputs = [
400
+ ui.input_file("query_data", "Upload query dataset (mgf, mzML, cdf, msp, or csv):"),
401
+ ui.input_file("reference_data", "Upload reference dataset (mgf, mzML, cdf, msp, or csv):"),
402
+ ui.input_selectize("similarity_measure", "Select similarity measure(s):", ["cosine","shannon","renyi","tsallis","mixture","jaccard","dice","3w_jaccard","sokal_sneath","binary_cosine","mountford","mcconnaughey","driver_kroeber","simpson","braun_banquet","fager_mcgowan","kulczynski","intersection","hamming","hellinger"], multiple=True, selected='cosine'),
403
+ ui.input_text('weights', 'Weights for mixture similarity measure (cosine, shannon, renyi, tsallis):', '((0.25, 0.25, 0.25, 0.25))'),
404
+ ui.input_text("high_quality_reference_library", "Indicate whether the reference library is considered high quality. If True, filtering and noise removal are only applied to the query spectra.", '[True]')
405
+ ]
406
+
407
+ if platform == "HRMS":
408
+ extra_inputs = [
409
+ ui.input_text(
410
+ "spectrum_preprocessing_order",
411
+ "Sequence of characters for preprocessing order (C (centroiding), F (filtering), M (matching), N (noise removal), L (low-entropy transformation), W (weight factor transformation)). M must be included, C before M if used.",
412
+ "[FCNMWL,CWM]",
413
+ ),
414
+ ui.input_text("window_size_centroiding", "Centroiding window-size:", "[0.5]"),
415
+ ui.input_text("window_size_matching", "Matching window-size:", "[0.1,0.5]"),
416
+ ]
417
+ else:
418
+ extra_inputs = [
419
+ ui.input_text(
420
+ "spectrum_preprocessing_order",
421
+ "Sequence of characters for preprocessing order (F (filtering), N (noise removal), L (low-entropy transformation), W (weight factor transformation)).",
422
+ "[FNLW,WNL]",
423
+ )
424
+ ]
425
+
426
+ numeric_inputs = [
427
+ ui.input_text("mz_min", "Minimum m/z for filtering:", '[0]'),
428
+ ui.input_text("mz_max", "Maximum m/z for filtering:", '[99999999]'),
429
+ ui.input_text("int_min", "Minimum intensity for filtering:", '[0]'),
430
+ ui.input_text("int_max", "Maximum intensity for filtering:", '[999999999]'),
431
+ ui.input_text("noise_threshold", "Noise removal threshold:", '[0.0]'),
432
+ ui.input_text("wf_mz", "Mass/charge weight factor:", '[0.0]'),
433
+ ui.input_text("wf_int", "Intensity weight factor:", '[1.0]'),
434
+ ui.input_text("LET_threshold", "Low-entropy threshold:", '[0.0]'),
435
+ ui.input_text("entropy_dimension", "Entropy dimension (Renyi/Tsallis only):", '[1.1]')
436
+ ]
437
+
438
+
439
+ run_button_parameter_tuning = ui.download_button("run_btn_parameter_tuning", "Tune parameters", style="font-size:16px; padding:15px 30px; width:200px; height:80px")
440
+ back_button = ui.input_action_button("back", "Back to main menu", style="font-size:16px; padding:15px 30px; width:200px; height:80px")
441
+
442
+ if platform == "HRMS":
443
+ inputs_columns = ui.layout_columns(
444
+ ui.div(base_inputs[0:6], style="display:flex; flex-direction:column; gap:10px;"),
445
+ ui.div([base_inputs[6:7], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
446
+ ui.div(numeric_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
447
+ ui.div(numeric_inputs[5:9], style="display:flex; flex-direction:column; gap:10px;"),
448
+ col_widths=(3, 3, 3, 3),
449
+ )
450
+ elif platform == "NRMS":
451
+ inputs_columns = ui.layout_columns(
452
+ ui.div(base_inputs[0:6], style="display:flex; flex-direction:column; gap:10px;"),
453
+ ui.div([base_inputs[6:7], *extra_inputs], style="display:flex; flex-direction:column; gap:10px;"),
454
+ ui.div(numeric_inputs[0:5], style="display:flex; flex-direction:column; gap:10px;"),
455
+ ui.div(numeric_inputs[5:9], style="display:flex; flex-direction:column; gap:10px;"),
456
+ col_widths=(3, 3, 3, 3),
457
+ )
458
+
459
+ log_panel = ui.card(
460
+ ui.card_header("Identification log"),
461
+ ui.output_text_verbatim("match_log"),
462
+ style="max-height:300px; overflow:auto"
463
+ )
464
+
465
+ return ui.div(
466
+ ui.TagList(
467
+ ui.h2("Tune parameters"),
468
+ inputs_columns,
469
+ run_button_parameter_tuning,
470
+ back_button,
471
+ log_panel
472
+ ),
473
+ )
474
+
475
+
476
+
477
+
348
478
  app_ui = ui.page_fluid(
349
479
  ui.output_ui("main_ui"),
350
480
  ui.output_text("status_output")
@@ -361,8 +491,15 @@ def server(input, output, session):
361
491
 
362
492
  run_status_plot_spectra = reactive.Value("")
363
493
  run_status_spec_lib_matching = reactive.Value("")
494
+ run_status_plot_spectra_within_spec_lib_matching = reactive.Value("")
495
+ run_status_parameter_tuning = reactive.Value("")
496
+ is_tuning_running = reactive.Value(False)
364
497
  match_log_rv = reactive.Value("")
365
498
  is_matching_rv = reactive.Value(False)
499
+ is_any_job_running = reactive.Value(False)
500
+ latest_csv_path_rv = reactive.Value("")
501
+ latest_df_rv = reactive.Value(None)
502
+ is_running_rv = reactive.Value(False)
366
503
 
367
504
  query_ids_rv = reactive.Value([])
368
505
  query_file_path_rv = reactive.Value(None)
@@ -377,6 +514,96 @@ def server(input, output, session):
377
514
  converted_reference_path_rv = reactive.Value(None)
378
515
 
379
516
 
517
+ def _reset_plot_spectra_state():
518
+ query_status_rv.set("")
519
+ reference_status_rv.set("")
520
+ query_ids_rv.set([])
521
+ reference_ids_rv.set([])
522
+ query_file_path_rv.set(None)
523
+ reference_file_path_rv.set(None)
524
+ query_result_rv.set(None)
525
+ reference_result_rv.set(None)
526
+ converted_query_path_rv.set(None)
527
+ converted_reference_path_rv.set(None)
528
+ try:
529
+ ui.update_selectize("spectrum_ID1", choices=[], selected=None)
530
+ ui.update_selectize("spectrum_ID2", choices=[], selected=None)
531
+ except Exception:
532
+ pass
533
+
534
+
535
+ def _reset_spec_lib_matching_state():
536
+ match_log_rv.set("")
537
+ is_matching_rv.set(False)
538
+ is_any_job_running.set(False)
539
+ try:
540
+ ui.update_selectize("spectrum_ID1", choices=[], selected=None)
541
+ ui.update_selectize("spectrum_ID2", choices=[], selected=None)
542
+ except Exception:
543
+ pass
544
+
545
+
546
+ def _reset_parameter_tuning_state():
547
+ match_log_rv.set("")
548
+ is_tuning_running.set(False)
549
+ is_any_job_running.set(False)
550
+
551
+
552
+ @reactive.effect
553
+ @reactive.event(input.back)
554
+ def _clear_on_back_from_pages():
555
+ page = current_page()
556
+ if page == "plot_spectra":
557
+ _reset_plot_spectra_state()
558
+ elif page == "run_spec_lib_matching":
559
+ _reset_spec_lib_matching_state()
560
+ elif page == "run_parameter_tuning":
561
+ _reset_parameter_tuning_state()
562
+
563
+ @reactive.effect
564
+ def _clear_on_enter_pages():
565
+ page = current_page()
566
+ if page == "plot_spectra":
567
+ _reset_plot_spectra_state()
568
+ elif page == "run_spec_lib_matching":
569
+ _reset_spec_lib_matching_state()
570
+ elif page == "run_parameter_tuning":
571
+ _reset_parameter_tuning_state()
572
+
573
+
574
+ def _drain_queue_nowait(q: asyncio.Queue) -> list[str]:
575
+ out = []
576
+ try:
577
+ while True:
578
+ out.append(q.get_nowait())
579
+ except asyncio.QueueEmpty:
580
+ pass
581
+ return out
582
+
583
+
584
+ class ReactiveWriter(io.TextIOBase):
585
+ def __init__(self, loop: asyncio.AbstractEventLoop):
586
+ self._loop = loop
587
+ def write(self, s: str):
588
+ if not s:
589
+ return 0
590
+ self._loop.call_soon_threadsafe(_LOG_QUEUE.put_nowait, s)
591
+ return len(s)
592
+ def flush(self):
593
+ pass
594
+
595
+
596
+ @reactive.effect
597
+ async def _pump_logs():
598
+ if not (is_any_job_running.get() or is_tuning_running.get() or is_matching_rv.get()):
599
+ return
600
+ reactive.invalidate_later(0.05)
601
+ msgs = _drain_queue_nowait(_LOG_QUEUE)
602
+ if msgs:
603
+ match_log_rv.set(match_log_rv.get() + "".join(msgs))
604
+ await reactive.flush()
605
+
606
+
380
607
  def process_database(file_path: str):
381
608
  suffix = Path(file_path).suffix.lower()
382
609
  return {"path": file_path, "suffix": suffix}
@@ -385,13 +612,14 @@ def server(input, output, session):
385
612
  def plot_query_status():
386
613
  return query_status_rv.get() or ""
387
614
 
615
+ @render.text
616
+ def plot_reference_status():
617
+ return reference_status_rv.get() or ""
618
+
388
619
 
389
620
  @reactive.effect
390
621
  @reactive.event(input.query_data)
391
622
  async def _on_query_upload():
392
- if current_page() != "plot_spectra":
393
- return
394
-
395
623
  files = input.query_data()
396
624
  req(files and len(files) > 0)
397
625
 
@@ -414,9 +642,6 @@ def server(input, output, session):
414
642
  @reactive.effect
415
643
  @reactive.event(input.reference_data)
416
644
  async def _on_reference_upload():
417
- if current_page() != "plot_spectra":
418
- return
419
-
420
645
  files = input.reference_data()
421
646
  req(files and len(files) > 0)
422
647
 
@@ -441,24 +666,6 @@ def server(input, output, session):
441
666
  return match_log_rv.get()
442
667
 
443
668
 
444
- class ReactiveWriter(io.TextIOBase):
445
- def __init__(self, rv):
446
- self.rv = rv
447
- def write(self, s: str):
448
- if not s:
449
- return 0
450
- self.rv.set(self.rv.get() + s)
451
- try:
452
- loop = asyncio.get_running_loop()
453
- loop.create_task(reactive.flush())
454
- except RuntimeError:
455
- pass
456
- return len(s)
457
- def flush(self):
458
- pass
459
-
460
-
461
-
462
669
  @reactive.Effect
463
670
  def _():
464
671
  if input.plot_spectra() > plot_clicks.get():
@@ -467,6 +674,9 @@ def server(input, output, session):
467
674
  elif input.run_spec_lib_matching() > match_clicks.get():
468
675
  current_page.set("run_spec_lib_matching")
469
676
  match_clicks.set(input.run_spec_lib_matching())
677
+ elif input.run_parameter_tuning() > match_clicks.get():
678
+ current_page.set("run_parameter_tuning")
679
+ match_clicks.set(input.run_parameter_tuning())
470
680
  elif hasattr(input, "back") and input.back() > back_clicks.get():
471
681
  current_page.set("main_menu")
472
682
  back_clicks.set(input.back())
@@ -474,8 +684,6 @@ def server(input, output, session):
474
684
 
475
685
  @render.image
476
686
  def image():
477
- from pathlib import Path
478
-
479
687
  dir = Path(__file__).resolve().parent
480
688
  img: ImgData = {"src": str(dir / "www/emblem.png"), "width": "320px", "height": "250px"}
481
689
  return img
@@ -512,6 +720,7 @@ def server(input, output, session):
512
720
  ),
513
721
  ui.input_action_button("plot_spectra", "Plot two spectra before and after preprocessing transformations.", style="font-size:18px; padding:20px 40px; width:550px; height:100px; margin-top:10px; margin-right:50px"),
514
722
  ui.input_action_button("run_spec_lib_matching", "Run spectral library matching to perform compound identification on a query library of spectra.", style="font-size:18px; padding:20px 40px; width:550px; height:100px; margin-top:10px; margin-right:50px"),
723
+ ui.input_action_button("run_parameter_tuning", "Tune parameters to maximize accuracy of compound identification given a query library with known spectrum IDs.", style="font-size:18px; padding:20px 40px; width:450px; height:120px; margin-top:10px; margin-right:50px"),
515
724
  ui.div(
516
725
  "References:",
517
726
  style="margin-top:35px; text-align:left; font-size:24px; font-weight:bold"
@@ -562,15 +771,14 @@ def server(input, output, session):
562
771
  return plot_spectra_ui(input.chromatography_platform())
563
772
  elif current_page() == "run_spec_lib_matching":
564
773
  return run_spec_lib_matching_ui(input.chromatography_platform())
774
+ elif current_page() == "run_parameter_tuning":
775
+ return run_parameter_tuning_ui(input.chromatography_platform())
565
776
 
566
777
 
567
778
 
568
779
  @reactive.effect
569
780
  @reactive.event(input.query_data)
570
781
  async def _populate_ids_from_query_upload():
571
- if current_page() != "plot_spectra":
572
- return
573
-
574
782
  files = input.query_data()
575
783
  if not files:
576
784
  return
@@ -578,7 +786,6 @@ def server(input, output, session):
578
786
  in_path = Path(files[0]["datapath"])
579
787
  suffix = in_path.suffix.lower()
580
788
 
581
- # Decide what CSV to read IDs from
582
789
  try:
583
790
  if suffix == ".csv":
584
791
  csv_path = in_path
@@ -587,17 +794,14 @@ def server(input, output, session):
587
794
  query_status_rv.set(f"Converting {in_path.name} → CSV …")
588
795
  await reactive.flush()
589
796
 
590
- # Choose an output temp path next to the upload
591
797
  tmp_csv_path = in_path.with_suffix(".converted.csv")
592
798
 
593
799
  out_obj = await asyncio.to_thread(build_library, str(in_path), str(tmp_csv_path))
594
800
 
595
- # out_obj may be a path (str/PathLike) OR a DataFrame. Normalize to a path.
596
801
  if isinstance(out_obj, (str, os.PathLike, Path)):
597
802
  csv_path = Path(out_obj)
598
803
  elif isinstance(out_obj, pd.DataFrame):
599
- # Write the DF to our chosen path
600
- out_obj.to_csv(tmp_csv_path, index=False)
804
+ out_obj.to_csv(tmp_csv_path, index=False, sep='\t')
601
805
  csv_path = tmp_csv_path
602
806
  else:
603
807
  raise TypeError(f"build_library returned unsupported type: {type(out_obj)}")
@@ -607,16 +811,12 @@ def server(input, output, session):
607
811
  query_status_rv.set(f"Reading IDs from: {csv_path.name} …")
608
812
  await reactive.flush()
609
813
 
610
- # Extract IDs from the CSV’s first column
611
814
  ids = await asyncio.to_thread(extract_first_column_ids, str(csv_path))
612
815
  query_ids_rv.set(ids)
613
816
 
614
- # Update dropdowns
615
817
  ui.update_selectize("spectrum_ID1", choices=ids, selected=(ids[0] if ids else None))
616
818
 
617
- query_status_rv.set(
618
- f"✅ Loaded {len(ids)} IDs from {csv_path.name}" if ids else f"⚠️ No IDs found in {csv_path.name}"
619
- )
819
+ query_status_rv.set(f"✅ Loaded {len(ids)} IDs from {csv_path.name}" if ids else f"⚠️ No IDs found in {csv_path.name}")
620
820
  await reactive.flush()
621
821
 
622
822
  except Exception as e:
@@ -628,9 +828,6 @@ def server(input, output, session):
628
828
  @reactive.effect
629
829
  @reactive.event(input.reference_data)
630
830
  async def _populate_ids_from_reference_upload():
631
- if current_page() != "plot_spectra":
632
- return
633
-
634
831
  files = input.reference_data()
635
832
  if not files:
636
833
  return
@@ -638,7 +835,6 @@ def server(input, output, session):
638
835
  in_path = Path(files[0]["datapath"])
639
836
  suffix = in_path.suffix.lower()
640
837
 
641
- # Decide what CSV to read IDs from
642
838
  try:
643
839
  if suffix == ".csv":
644
840
  csv_path = in_path
@@ -647,17 +843,14 @@ def server(input, output, session):
647
843
  reference_status_rv.set(f"Converting {in_path.name} → CSV …")
648
844
  await reactive.flush()
649
845
 
650
- # Choose an output temp path next to the upload
651
846
  tmp_csv_path = in_path.with_suffix(".converted.csv")
652
847
 
653
848
  out_obj = await asyncio.to_thread(build_library, str(in_path), str(tmp_csv_path))
654
849
 
655
- # out_obj may be a path (str/PathLike) OR a DataFrame. Normalize to a path.
656
850
  if isinstance(out_obj, (str, os.PathLike, Path)):
657
851
  csv_path = Path(out_obj)
658
852
  elif isinstance(out_obj, pd.DataFrame):
659
- # Write the DF to our chosen path
660
- out_obj.to_csv(tmp_csv_path, index=False)
853
+ out_obj.to_csv(tmp_csv_path, index=False, sep='\t')
661
854
  csv_path = tmp_csv_path
662
855
  else:
663
856
  raise TypeError(f"build_library returned unsupported type: {type(out_obj)}")
@@ -667,11 +860,9 @@ def server(input, output, session):
667
860
  reference_status_rv.set(f"Reading IDs from: {csv_path.name} …")
668
861
  await reactive.flush()
669
862
 
670
- # Extract IDs from the CSV’s first column
671
863
  ids = await asyncio.to_thread(extract_first_column_ids, str(csv_path))
672
864
  reference_ids_rv.set(ids)
673
865
 
674
- # Update dropdowns
675
866
  ui.update_selectize("spectrum_ID2", choices=ids, selected=(ids[0] if ids else None))
676
867
 
677
868
  reference_status_rv.set(
@@ -685,65 +876,47 @@ def server(input, output, session):
685
876
  raise
686
877
 
687
878
 
688
-
689
879
  @render.download(filename=lambda: f"plot.png")
690
880
  def run_btn_plot_spectra():
691
881
  spectrum_ID1 = input.spectrum_ID1() or None
692
882
  spectrum_ID2 = input.spectrum_ID2() or None
693
883
 
884
+ weights = [float(weight.strip()) for weight in input.weights().split(",") if weight.strip()]
885
+ weights = {'Cosine':weights[0], 'Shannon':weights[1], 'Renyi':weights[2], 'Tsallis':weights[3]}
886
+
694
887
  if input.chromatography_platform() == "HRMS":
695
- fig = generate_plots_on_HRMS_data(query_data=input.query_data()[0]['datapath'], reference_data=input.reference_data()[0]['datapath'], spectrum_ID1=spectrum_ID1, spectrum_ID2=spectrum_ID2, similarity_measure=input.similarity_measure(), spectrum_preprocessing_order=input.spectrum_preprocessing_order(), high_quality_reference_library=input.high_quality_reference_library(), mz_min=input.mz_min(), mz_max=input.mz_max(), int_min=input.int_min(), int_max=input.int_max(), window_size_centroiding=input.window_size_centroiding(), window_size_matching=input.window_size_matching(), noise_threshold=input.noise_threshold(), wf_mz=input.wf_mz(), wf_intensity=input.wf_int(), LET_threshold=input.LET_threshold(), entropy_dimension=input.entropy_dimension(), y_axis_transformation=input.y_axis_transformation(), return_plot=True)
696
- #run_status_plot_spectra.set("✅ Plotting has finished.")
888
+ fig = generate_plots_on_HRMS_data(query_data=input.query_data()[0]['datapath'], reference_data=input.reference_data()[0]['datapath'], spectrum_ID1=spectrum_ID1, spectrum_ID2=spectrum_ID2, similarity_measure=input.similarity_measure(), weights=weights, spectrum_preprocessing_order=input.spectrum_preprocessing_order(), high_quality_reference_library=input.high_quality_reference_library(), mz_min=input.mz_min(), mz_max=input.mz_max(), int_min=input.int_min(), int_max=input.int_max(), window_size_centroiding=input.window_size_centroiding(), window_size_matching=input.window_size_matching(), noise_threshold=input.noise_threshold(), wf_mz=input.wf_mz(), wf_intensity=input.wf_int(), LET_threshold=input.LET_threshold(), entropy_dimension=input.entropy_dimension(), y_axis_transformation=input.y_axis_transformation(), return_plot=True)
889
+ plt.show()
697
890
  elif input.chromatography_platform() == "NRMS":
698
891
  fig = generate_plots_on_NRMS_data(query_data=input.query_data()[0]['datapath'], reference_data=input.reference_data()[0]['datapath'], spectrum_ID1=spectrum_ID1, spectrum_ID2=spectrum_ID2, similarity_measure=input.similarity_measure(), spectrum_preprocessing_order=input.spectrum_preprocessing_order(), high_quality_reference_library=input.high_quality_reference_library(), mz_min=input.mz_min(), mz_max=input.mz_max(), int_min=input.int_min(), int_max=input.int_max(), noise_threshold=input.noise_threshold(), wf_mz=input.wf_mz(), wf_intensity=input.wf_int(), LET_threshold=input.LET_threshold(), entropy_dimension=input.entropy_dimension(), y_axis_transformation=input.y_axis_transformation(), return_plot=True)
892
+ plt.show()
699
893
  with io.BytesIO() as buf:
700
894
  fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
895
+ plt.close()
701
896
  yield buf.getvalue()
702
897
 
703
898
 
704
- @render.text
705
- def status_output():
706
- return run_status_plot_spectra.get()
707
- return run_status_spec_lib_matching.get()
708
899
 
709
-
710
- class ReactiveWriter(io.TextIOBase):
711
- def __init__(self, rv: reactive.Value, loop: asyncio.AbstractEventLoop):
712
- self.rv = rv
713
- self.loop = loop
714
-
715
- def write(self, s: str):
716
- if not s:
717
- return 0
718
- def _apply():
719
- self.rv.set(self.rv.get() + s)
720
- self.loop.create_task(reactive.flush())
721
-
722
- self.loop.call_soon_threadsafe(_apply)
723
- return len(s)
724
-
725
- def flush(self):
726
- pass
727
-
728
-
729
- @render.download(filename="identification_output.csv")
900
+ @render.download(filename="identification_output.txt")
730
901
  async def run_btn_spec_lib_matching():
731
- # 1) quick first paint
732
- match_log_rv.set("Starting identification...\n")
902
+ match_log_rv.set("Running identification...\n")
733
903
  await reactive.flush()
734
904
 
735
- # 2) normalize inputs (same as before)
736
905
  hq = input.high_quality_reference_library()
737
906
  if isinstance(hq, str):
738
907
  hq = hq.lower() == "true"
739
908
  elif isinstance(hq, (int, float)):
740
909
  hq = bool(hq)
741
910
 
911
+ weights = [float(weight.strip()) for weight in input.weights().split(",") if weight.strip()]
912
+ weights = {'Cosine':weights[0], 'Shannon':weights[1], 'Renyi':weights[2], 'Tsallis':weights[3]}
913
+
742
914
  common_kwargs = dict(
743
915
  query_data=input.query_data()[0]["datapath"],
744
916
  reference_data=input.reference_data()[0]["datapath"],
745
917
  likely_reference_ids=None,
746
918
  similarity_measure=input.similarity_measure(),
919
+ weights=weights,
747
920
  spectrum_preprocessing_order=input.spectrum_preprocessing_order(),
748
921
  high_quality_reference_library=hq,
749
922
  mz_min=input.mz_min(), mz_max=input.mz_max(),
@@ -752,16 +925,15 @@ def server(input, output, session):
752
925
  wf_mz=input.wf_mz(), wf_intensity=input.wf_int(),
753
926
  LET_threshold=input.LET_threshold(), entropy_dimension=input.entropy_dimension(),
754
927
  n_top_matches_to_save=input.n_top_matches_to_save(),
755
- print_id_results=True, # ensure the library actually prints progress
756
- output_identification=str(Path.cwd() / "identification_output.csv"),
757
- output_similarity_scores=str(Path.cwd() / "similarity_scores.csv"),
928
+ print_id_results=True,
929
+ output_identification=str(Path.cwd() / "identification_output.txt"),
930
+ output_similarity_scores=str(Path.cwd() / "similarity_scores.txt"),
758
931
  return_ID_output=True,
759
932
  )
760
933
 
761
934
  loop = asyncio.get_running_loop()
762
- rw = ReactiveWriter(match_log_rv, loop)
935
+ rw = ReactiveWriter(loop)
763
936
 
764
- # 3) run the heavy function in a thread so the event loop can repaint
765
937
  try:
766
938
  with redirect_stdout(rw), redirect_stderr(rw):
767
939
  if input.chromatography_platform() == "HRMS":
@@ -772,9 +944,7 @@ def server(input, output, session):
772
944
  **common_kwargs
773
945
  )
774
946
  else:
775
- df_out = await asyncio.to_thread(
776
- run_spec_lib_matching_on_NRMS_data, **common_kwargs
777
- )
947
+ df_out = await asyncio.to_thread(run_spec_lib_matching_on_NRMS_data, **common_kwargs)
778
948
  match_log_rv.set(match_log_rv.get() + "\n✅ Identification finished.\n")
779
949
  await reactive.flush()
780
950
  except Exception as e:
@@ -782,8 +952,164 @@ def server(input, output, session):
782
952
  await reactive.flush()
783
953
  raise
784
954
 
785
- # 4) stream CSV back to the browser
786
- yield df_out.to_csv(index=False)
955
+ yield df_out.to_csv(index=True, sep='\t')
956
+
957
+
958
+
959
+ @render.download(filename="plot.png")
960
+ def run_btn_plot_spectra_within_spec_lib_matching():
961
+ req(input.query_data(), input.reference_data())
962
+
963
+ spectrum_ID1 = input.spectrum_ID1() or None
964
+ spectrum_ID2 = input.spectrum_ID2() or None
965
+
966
+ hq = input.high_quality_reference_library()
967
+ if isinstance(hq, str):
968
+ hq = hq.lower() == "true"
969
+ elif isinstance(hq, (int, float)):
970
+ hq = bool(hq)
971
+
972
+ weights = [float(weight.strip()) for weight in input.weights().split(",") if weight.strip()]
973
+ weights = {'Cosine':weights[0], 'Shannon':weights[1], 'Renyi':weights[2], 'Tsallis':weights[3]}
974
+
975
+ common = dict(
976
+ query_data=input.query_data()[0]['datapath'],
977
+ reference_data=input.reference_data()[0]['datapath'],
978
+ spectrum_ID1=spectrum_ID1,
979
+ spectrum_ID2=spectrum_ID2,
980
+ similarity_measure=input.similarity_measure(),
981
+ weights=weights,
982
+ spectrum_preprocessing_order=input.spectrum_preprocessing_order(),
983
+ high_quality_reference_library=hq,
984
+ mz_min=input.mz_min(), mz_max=input.mz_max(),
985
+ int_min=input.int_min(), int_max=input.int_max(),
986
+ noise_threshold=input.noise_threshold(),
987
+ wf_mz=input.wf_mz(), wf_intensity=input.wf_int(),
988
+ LET_threshold=input.LET_threshold(), entropy_dimension=input.entropy_dimension(),
989
+ y_axis_transformation="normalized",
990
+ return_plot=True
991
+ )
992
+
993
+ if input.chromatography_platform() == "HRMS":
994
+ fig = generate_plots_on_HRMS_data(
995
+ window_size_centroiding=input.window_size_centroiding(),
996
+ window_size_matching=input.window_size_matching(),
997
+ **common
998
+ )
999
+ plt.show()
1000
+ else:
1001
+ fig = generate_plots_on_NRMS_data(**common)
1002
+ plt.show()
1003
+
1004
+ with io.BytesIO() as buf:
1005
+ fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
1006
+ plt.close()
1007
+ yield buf.getvalue()
1008
+
1009
+
1010
+ @render.download(filename="parameter_tuning_output.txt")
1011
+ async def run_btn_parameter_tuning():
1012
+ is_any_job_running.set(True)
1013
+ is_tuning_running.set(True)
1014
+ match_log_rv.set("Running grid search of all parameters specified...\n")
1015
+ await reactive.flush()
1016
+
1017
+ similarity_measure_tmp = list(input.similarity_measure())
1018
+ high_quality_reference_library_tmp = [x.strip().lower() == "true" for x in input.high_quality_reference_library().strip().strip("[]").split(",") if x.strip()]
1019
+ spectrum_preprocessing_order_tmp = strip_text(input.spectrum_preprocessing_order())
1020
+ mz_min_tmp = strip_numeric(input.mz_min())
1021
+ mz_max_tmp = strip_numeric(input.mz_max())
1022
+ int_min_tmp = strip_numeric(input.int_min())
1023
+ int_max_tmp = strip_numeric(input.int_max())
1024
+ noise_threshold_tmp = strip_numeric(input.noise_threshold())
1025
+ wf_mz_tmp = strip_numeric(input.wf_mz())
1026
+ wf_int_tmp = strip_numeric(input.wf_int())
1027
+ LET_threshold_tmp = strip_numeric(input.LET_threshold())
1028
+ entropy_dimension_tmp = strip_numeric(input.entropy_dimension())
1029
+ weights_tmp = strip_weights(input.weights())
1030
+
1031
+ common_kwargs = dict(
1032
+ query_data=input.query_data()[0]["datapath"],
1033
+ reference_data=input.reference_data()[0]["datapath"],
1034
+ output_path=str(Path.cwd() / "parameter_tuning_output.txt"),
1035
+ return_output=True,
1036
+ )
1037
+
1038
+ loop = asyncio.get_running_loop()
1039
+ rw = ReactiveWriter(loop)
1040
+
1041
+ try:
1042
+ if input.chromatography_platform() == "HRMS":
1043
+ window_size_centroiding_tmp = strip_numeric(input.window_size_centroiding())
1044
+ window_size_matching_tmp = strip_numeric(input.window_size_matching())
1045
+ grid = {
1046
+ 'similarity_measure': similarity_measure_tmp,
1047
+ 'weight': weights_tmp,
1048
+ 'spectrum_preprocessing_order': spectrum_preprocessing_order_tmp,
1049
+ 'mz_min': mz_min_tmp,
1050
+ 'mz_max': mz_max_tmp,
1051
+ 'int_min': int_min_tmp,
1052
+ 'int_max': int_max_tmp,
1053
+ 'noise_threshold': noise_threshold_tmp,
1054
+ 'wf_mz': wf_mz_tmp,
1055
+ 'wf_int': wf_int_tmp,
1056
+ 'LET_threshold': LET_threshold_tmp,
1057
+ 'entropy_dimension': entropy_dimension_tmp,
1058
+ 'high_quality_reference_library': high_quality_reference_library_tmp,
1059
+ 'window_size_centroiding': window_size_centroiding_tmp,
1060
+ 'window_size_matching': window_size_matching_tmp,
1061
+ }
1062
+ df_out = await asyncio.to_thread(_run_with_redirects, tune_params_on_HRMS_data_shiny, rw, **common_kwargs, grid=grid)
1063
+ else:
1064
+ grid = {
1065
+ 'similarity_measure': similarity_measure_tmp,
1066
+ 'weight': weights_tmp,
1067
+ 'spectrum_preprocessing_order': spectrum_preprocessing_order_tmp,
1068
+ 'mz_min': mz_min_tmp,
1069
+ 'mz_max': mz_max_tmp,
1070
+ 'int_min': int_min_tmp,
1071
+ 'int_max': int_max_tmp,
1072
+ 'noise_threshold': noise_threshold_tmp,
1073
+ 'wf_mz': wf_mz_tmp,
1074
+ 'wf_int': wf_int_tmp,
1075
+ 'LET_threshold': LET_threshold_tmp,
1076
+ 'entropy_dimension': entropy_dimension_tmp,
1077
+ 'high_quality_reference_library': high_quality_reference_library_tmp,
1078
+ }
1079
+ df_out = await asyncio.to_thread(_run_with_redirects, tune_params_on_NRMS_data_shiny, rw, **common_kwargs, grid=grid)
1080
+
1081
+ match_log_rv.set(match_log_rv.get() + "\n✅ Parameter tuning finished.\n")
1082
+ except Exception as e:
1083
+ match_log_rv.set(match_log_rv.get() + f"\n❌ Error: {e}\n")
1084
+ raise
1085
+ finally:
1086
+ is_tuning_running.set(False)
1087
+ is_any_job_running.set(False)
1088
+ await reactive.flush()
1089
+
1090
+ yield df_out.to_csv(index=False).encode("utf-8", sep='\t')
1091
+
1092
+
1093
+
1094
+
1095
+
1096
+ @reactive.effect
1097
+ async def _pump_reactive_writer_logs():
1098
+ if not is_tuning_running.get():
1099
+ return
1100
+
1101
+ reactive.invalidate_later(0.1)
1102
+ msgs = _drain_queue_nowait(_LOG_QUEUE)
1103
+ if msgs:
1104
+ match_log_rv.set(match_log_rv.get() + "".join(msgs))
1105
+ await reactive.flush()
1106
+
1107
+
1108
+ @render.text
1109
+ def status_output():
1110
+ return run_status_plot_spectra.get()
1111
+ return run_status_spec_lib_matching.get()
1112
+ return run_status_parameter_tuning.get()
787
1113
 
788
1114
 
789
1115
  app = App(app_ui, server)