bindmc 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. bindmc/main.py +67 -0
  2. bindmc/webgui/__init__.py +0 -0
  3. bindmc/webgui/app.py +54 -0
  4. bindmc/webgui/classes/BindingConstant.py +23 -0
  5. bindmc/webgui/classes/ChemicalShiftParam.py +40 -0
  6. bindmc/webgui/classes/Component.py +111 -0
  7. bindmc/webgui/classes/ExptData.py +485 -0
  8. bindmc/webgui/classes/ExptDataType.py +92 -0
  9. bindmc/webgui/classes/FitResult.py +173 -0
  10. bindmc/webgui/classes/MCMCSim.py +232 -0
  11. bindmc/webgui/classes/Model.py +86 -0
  12. bindmc/webgui/classes/RawData.py +36 -0
  13. bindmc/webgui/classes/Simulation.py +104 -0
  14. bindmc/webgui/classes/UIBindings.py +19 -0
  15. bindmc/webgui/classes/__init__.py +28 -0
  16. bindmc/webgui/components/__init__.py +29 -0
  17. bindmc/webgui/components/base.py +24 -0
  18. bindmc/webgui/components/bayes.py +689 -0
  19. bindmc/webgui/components/bayes_priors.py +351 -0
  20. bindmc/webgui/components/binding_model.py +330 -0
  21. bindmc/webgui/components/body.py +276 -0
  22. bindmc/webgui/components/data_gen.py +419 -0
  23. bindmc/webgui/components/data_import.py +450 -0
  24. bindmc/webgui/components/data_model.py +609 -0
  25. bindmc/webgui/components/fitting.py +886 -0
  26. bindmc/webgui/components/graph.py +649 -0
  27. bindmc/webgui/components/header.py +124 -0
  28. bindmc/webgui/components/simulation.py +385 -0
  29. bindmc/webgui/export/__init__.py +0 -0
  30. bindmc/webgui/export/notebook_exporter.py +727 -0
  31. bindmc/webgui/state/__init__.py +1 -0
  32. bindmc/webgui/state/statemanager.py +2043 -0
  33. bindmc/webgui/utils.py +322 -0
  34. bindmc-0.1.0.dist-info/METADATA +22 -0
  35. bindmc-0.1.0.dist-info/RECORD +37 -0
  36. bindmc-0.1.0.dist-info/WHEEL +5 -0
  37. bindmc-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,649 @@
1
+ import json
2
+
3
+ from nicegui import ui
4
+
5
+ from .base import BaseComponent
6
+ from ..classes import Simulation, FitResult
7
+ from ..utils import safe_filename
8
+ import pandas as pd
9
+ import uuid
10
+
11
+ GRAPH_LEGEND_TITLE_W = 15
12
+ X_AXIS_ROW_INDEX = "Row index"
13
+
14
+
15
+ class Graph(BaseComponent):
16
+ def __init__(self, state_manager, mode="sim", css_classes="h-[50vh]"):
17
+ self.mode = mode
18
+ self.sm = state_manager
19
+
20
+ self.data_frames = dict() # for now, make these DataFrames with sensible colnames, and which incorporate component concs.
21
+ self.line_styles = dict()
22
+ self.curr_x = None
23
+ self._comp_names = set() # set of all component names in the graph
24
+
25
+ # Plotly's default color sequence for consistent coloring
26
+ self.plotly_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
27
+ '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
28
+ self._color_mapping = {} # Maps column names to color indices
29
+
30
+ self.css_classes = css_classes
31
+
32
+ # set up sim_fig layout
33
+ if mode == "sim":
34
+ if not hasattr(self.sm, "sim_fig_data"):
35
+ self.sm.sim_fig_data = {
36
+ "data": [],
37
+ "layout": {
38
+ "title": "Simulation Results",
39
+ "xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
40
+ "yaxis": {"title": {"text": "Concentration [M]"}},
41
+ "showlegend": True,
42
+ },
43
+ }
44
+ self.graph_data = self.sm.sim_fig_data
45
+
46
+ if mode == "data_preview":
47
+ if not hasattr(self.sm, "data_preview_fig_data"):
48
+ self.sm.data_preview_fig_data = {
49
+ "data": [],
50
+ "layout": {
51
+ "title": "Data Preview",
52
+ "xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
53
+ "yaxis": {"title": {"text": "Y-axis"}},
54
+ "showlegend": True,
55
+ },
56
+ }
57
+ self.graph_data = self.sm.data_preview_fig_data
58
+
59
+ if mode == "expt_preview":
60
+ if not hasattr(self.sm, "expt_preview_fig_data"):
61
+ self.sm.expt_preview_fig_data = {
62
+ "data": [],
63
+ "layout": {
64
+ "title": "Experimental Data Preview",
65
+ "xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
66
+ "yaxis": {"title": {"text": "Y-axis"}},
67
+ "showlegend": True,
68
+ },
69
+ }
70
+ self.graph_data = self.sm.expt_preview_fig_data
71
+
72
+ # set up fit_fig layout
73
+ if mode == "fit":
74
+ if not hasattr(self.sm, "fit_fig_data"):
75
+ self.sm.fit_fig_data = {
76
+ "data": [],
77
+ "layout": {
78
+ "title": "Fit Results",
79
+ "xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
80
+ "yaxis": {"title": {"text": "Y-axis"}},
81
+ "showlegend": True,
82
+ },
83
+ }
84
+ self.graph_data = self.sm.fit_fig_data
85
+
86
+ if mode == "fit_speciation":
87
+ if not hasattr(self.sm, "fit_speciation_fig_data"):
88
+ self.sm.fit_speciation_fig_data = {
89
+ "data": [],
90
+ "layout": {
91
+ "title": "Fit Speciation",
92
+ "xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
93
+ "yaxis": {"title": {"text": "Y-axis"}},
94
+ "showlegend": True,
95
+ },
96
+ }
97
+ self.graph_data = self.sm.fit_speciation_fig_data
98
+
99
+ # Plotly can render with a stale width when created inside hidden containers (e.g., inactive tabs).
100
+ # Setting autosize helps it adapt to the parent element when it is (re)drawn.
101
+ if isinstance(getattr(self, "graph_data", None), dict):
102
+ self.graph_data.setdefault("layout", {})
103
+ self.graph_data["layout"].setdefault("autosize", True)
104
+ self._default_y_axis_title = (
105
+ self.graph_data.get("layout", {})
106
+ .get("yaxis", {})
107
+ .get("title", {})
108
+ .get("text", "Y-axis")
109
+ )
110
+
111
+ super().__init__(state_manager)
112
+
113
+
114
+ def get_trimmed_cols(self,df,trimtail=2):
115
+ """Get the trimmed columns of a DataFrame."""
116
+ if df is None:
117
+ return []
118
+ return [col[:-trimtail] for col in df.columns] # remove trailing _x
119
+
120
+
121
+ @property
122
+ def comp_names(self):
123
+ """Get the component names for the graph."""
124
+ return self._comp_names
125
+
126
+ def add_comp_name(self, name):
127
+ """Add a component name to the graph."""
128
+ self._comp_names.add(name)
129
+
130
+ def get_color_for_column(self, col_name: str) -> str:
131
+ """Get a consistent color for a column name using Plotly's default color cycle."""
132
+ if col_name not in self._color_mapping:
133
+ # Assign next available color index
134
+ next_index = len(self._color_mapping) % len(self.plotly_colors)
135
+ self._color_mapping[col_name] = next_index
136
+
137
+ color_index = self._color_mapping[col_name]
138
+ return self.plotly_colors[color_index]
139
+
140
+
141
+ def setup_nicegui(self):
142
+ self._generate_graph()
143
+ self._generate_options()
144
+
145
+ def setup_bindings(self):
146
+ super().setup_bindings()
147
+ # if self.mode == "sim":
148
+ # self.sm.add_listener("simulation_completed", self._update_graph)
149
+ # elif self.mode == 'fit':
150
+ # self.sm.add_listener('fit_results_updated', self._update_fit_results)
151
+
152
+ def load_simulations_data(self):
153
+ if len(self.sm.simulations) > 0:
154
+ for sim in self.sm.simulations.values():
155
+ self.add_graph_lines_xy(sim.comp_concs, sim.results[[c for c in sim.results.columns if c not in sim.comp_concs.columns]], scatter="lines", run_name=sim.name, run_id=str(sim.id))
156
+ #self.add_graph_lines(sim.results, sim.name, sim.id)
157
+ self._update_graph()
158
+
159
+
160
+ def _update_graph(self, e=None):
161
+ self.update_x_axis_selects()
162
+ self.update_graph_x()
163
+ self.graph.update()
164
+
165
+ def update_graph(self):
166
+ self._update_graph()
167
+
168
+ # def add_line(self, x, y, name,mode='markers'):
169
+ # """Add a line to the graph."""
170
+ # self.graph_data["data"].append({
171
+ # "type": "scatter",
172
+ # "mode": mode,
173
+ # "x": x,
174
+ # "y": y,
175
+ # "name": name,
176
+ # "visible": True,
177
+ # "trace_id": name, # Unique trace ID for this line
178
+ # })
179
+
180
+ def add_graph_lines_xy(self, x: pd.DataFrame , y: pd.DataFrame, run_name: str ="", run_id: str | uuid.UUID ="", scatter: str ="lines", redraw: bool =False, color: str | None = None):
181
+
182
+ """Add a set of lines from x and y to the graph.
183
+ x and y should be dataframes of the same length.
184
+ run_name and run_id are used to identify the simulation.
185
+ scatter is the type of line to plot, e.g. 'lines', 'markers', etc.
186
+ redraw is a boolean indicating whether to redraw the graph after adding the lines.
187
+ color is an optional color specification for the traces.
188
+ """
189
+
190
+ if run_id is None:
191
+ raise ValueError("run_id cannot be None")
192
+ #run_id = str(uuid.uuid4())
193
+
194
+ self.data_frames[str(run_id)] = (x,y) # Store the original x and y data for later use
195
+ self.line_styles[str(run_id)] = scatter
196
+
197
+ for ii,col in enumerate(x.columns):
198
+ self.add_comp_name(col)
199
+
200
+ for ii,col in enumerate(y.columns):
201
+ trace_data = {
202
+ "type": "scatter",
203
+ "mode": scatter,
204
+ "x": x[x.columns[0]].tolist(), # TODO clean up?
205
+ "y": y[col].tolist(),
206
+ "name": run_name[:GRAPH_LEGEND_TITLE_W] + " " + col,
207
+ "species": col,
208
+ "trace_id": str(run_id) # is the uuid for the fit/etc
209
+ + "-"
210
+ + col, # Unique trace ID for this simulation
211
+ "visible": True,
212
+ #'legendgroup': self.sm.modelName,
213
+ #'legendgrouptitle': dict(text=self.sm.modelName)
214
+ }
215
+
216
+ # Use consistent color based on column name, or provided color
217
+ if color is not None:
218
+ col_color = color
219
+ else:
220
+ # Use Plotly's default color cycle for consistency across calc/expt
221
+ col_color = self.get_color_for_column(col)
222
+
223
+ if scatter == "lines":
224
+ trace_data["line"] = {"color": col_color}
225
+ else:
226
+ trace_data["marker"] = {"color": col_color}
227
+
228
+ self.graph_data["data"].append(trace_data)
229
+ if (
230
+ col[:-5] in [d[:-4] for d in x.columns if d.endswith("_tot") ]
231
+ and hasattr(self, "chkNoComp")
232
+ and self.chkNoComp.value is False
233
+ ):
234
+ self.graph_data["data"][-1]["visible"] = "legendonly"
235
+ # elif, plot non-concentration data... TODO
236
+ if redraw:
237
+ self.graph.update()
238
+
239
+
240
+ def add_graph_lines(self, df, run_name, run_id, scatter="lines",redraw=False):
241
+ """Add a set of lines from a dataframe to the graph.
242
+ Dataframe should have columns labelled _tot and _free for
243
+ components and species respectively."""
244
+
245
+ # Split df into x (component concentrations) and y (species concentrations) DataFrames
246
+ # to maintain consistency with add_graph_lines_xy storage format
247
+ comp_cols = [col for col in df.columns if col.endswith("_tot")]
248
+ spec_cols = [col for col in df.columns if col.endswith("_free")]
249
+ other_cols = [col for col in df.columns if not (col.endswith("_tot") or col.endswith("_free"))]
250
+
251
+ # x DataFrame includes component concentrations and any other columns (like row index, time, etc.)
252
+ x_df = df[comp_cols + other_cols] if comp_cols + other_cols else df[[df.columns[0]]]
253
+ # y DataFrame includes species concentrations
254
+ y_df = df[spec_cols] if spec_cols else pd.DataFrame()
255
+
256
+ self.data_frames[str(run_id)] = (x_df, y_df)
257
+ self.line_styles[str(run_id)] = scatter
258
+
259
+ for ii, col in enumerate(df.columns):
260
+ if col.endswith("_tot"):
261
+ # this is a component concentration, so we skip it
262
+ self.add_comp_name(col)
263
+ continue
264
+ elif col.endswith("_free"):
265
+ # this is a species concentration, so we plot it
266
+ col_color = self.get_color_for_column(col)
267
+ trace_data = {
268
+ "type": "scatter",
269
+ "mode": scatter,
270
+ "x": df[df.columns[0]].tolist(), # TODO clean up?
271
+ "y": df[col].tolist(),
272
+ "name": run_name[:GRAPH_LEGEND_TITLE_W] + " " + col,
273
+ "species": col,
274
+ "trace_id": run_id
275
+ + "-"
276
+ + col, # Unique trace ID for this simulation
277
+ "visible": True,
278
+ #'legendgroup': self.sm.modelName,
279
+ #'legendgrouptitle': dict(text=self.sm.modelName)
280
+ }
281
+
282
+ if scatter == "lines":
283
+ trace_data["line"] = {"color": col_color}
284
+ else:
285
+ trace_data["marker"] = {"color": col_color}
286
+
287
+ self.graph_data["data"].append(trace_data)
288
+ if (
289
+ col[:-5] in [d[:-4] for d in df.columns if d.endswith("_tot") ]
290
+ and hasattr(self, "chkNoComp")
291
+ and self.chkNoComp.value is False
292
+ ):
293
+ self.graph_data["data"][-1]["visible"] = "legendonly"
294
+ else:
295
+ # assume it's a concentration
296
+ col_color = self.get_color_for_column(col)
297
+ trace_data = {
298
+ "type": "scatter",
299
+ "mode": scatter,
300
+ "x": df[df.columns[0]].tolist(), # TODO clean up?
301
+ "y": df[col].tolist(),
302
+ "name": run_name[:GRAPH_LEGEND_TITLE_W] + " " + col,
303
+ "species": col,
304
+ "trace_id": run_id
305
+ + "-"
306
+ + col, # Unique trace ID for this simulation
307
+ "visible": True,
308
+ #'legendgroup': self.sm.modelName,
309
+ #'legendgrouptitle': dict(text=self.sm.modelName)
310
+ }
311
+
312
+ if scatter == "lines":
313
+ trace_data["line"] = {"color": col_color}
314
+ else:
315
+ trace_data["marker"] = {"color": col_color}
316
+
317
+ self.graph_data["data"].append(trace_data)
318
+
319
+ # elif, plot non-concentration data... TODO
320
+ if redraw:
321
+ self.graph.update()
322
+
323
+
324
+ def _generate_graph(self):
325
+ """Generate the graph component."""
326
+
327
+ self.graph = (
328
+ ui.plotly(self.graph_data)
329
+ .classes(f"w-full min-w-0 {self.css_classes}")
330
+ .style("width: 100%;")
331
+ )
332
+ self.graph.on("plotly_restyle", self.plotly_restyle_handler)
333
+
334
+ def _generate_options(self):
335
+ """Generate the options for the graph."""
336
+ with ui.card().classes("w-full mt-4"):
337
+ with ui.row().classes("w-full items-center"):
338
+ ui.label("Plotting options")
339
+ ui.space()
340
+ ui.button("Save", icon="save", on_click=self.download_graph_png).props("dense")
341
+ with ui.row():
342
+ ui.label("x-axis:")
343
+ self.chkratio = ui.checkbox("Ratio?").on_value_change(
344
+ lambda e: self.xAxDenominatorSelect.set_value(
345
+ 1 if not e.value else self.xAxDenominatorSelect.value
346
+ )
347
+ )
348
+ self.xAxNumeratorSelect = ui.select([1, 2, 3]).props("inline")
349
+ ui.label("/").bind_visibility_from(self.chkratio, "value")
350
+ self.xAxDenominatorSelect = (
351
+ ui.select([1, 2, 3])
352
+ .props("inline")
353
+ .bind_visibility_from(self.chkratio, "value")
354
+ )
355
+ ui.button("Apply").on_click(self.update_graph_x)
356
+
357
+ with ui.row():
358
+ self.chkNormalizeY = ui.checkbox(
359
+ "Normalize Y per trace (0-1)",
360
+ value=False,
361
+ on_change=lambda e: self.update_graph_y(),
362
+ )
363
+
364
+ if self.mode != "expt_preview":
365
+ with ui.row():
366
+ self.chkNoComp = ui.checkbox(
367
+ "Plot [Component]_free",
368
+ value=True,
369
+ on_change=(self.update_plot_compfree),
370
+ )
371
+
372
+ def _default_export_filename(self) -> str:
373
+ active_sim = self.sm.active_sim_or_none
374
+ active_fit = self.sm.active_fit_or_none
375
+ if self.mode == "sim":
376
+ if active_sim is not None:
377
+ return f"simulation_{safe_filename(active_sim.name, fallback='simulation')}_results"
378
+ return "simulation_results"
379
+ if self.mode == "fit":
380
+ if active_fit is not None:
381
+ return f"fit_{safe_filename(active_fit.name, fallback='fit')}_results"
382
+ return "fit_results"
383
+ if self.mode == "fit_speciation":
384
+ if active_fit is not None:
385
+ return f"fit_{safe_filename(active_fit.name, fallback='fit')}_speciation"
386
+ return "fit_speciation"
387
+ title = self.graph_data.get("layout", {}).get("title", "graph")
388
+ title_text = title.get("text", "graph") if isinstance(title, dict) else str(title)
389
+ return safe_filename(title_text, fallback="graph")
390
+
391
+ async def download_graph_png(self) -> None:
392
+ filename = self._default_export_filename()
393
+ if not self.graph_data.get("data"):
394
+ ui.notify(f"No plotted data available for {filename}.", type="warning")
395
+ return
396
+
397
+ js = f"""
398
+ (() => {{
399
+ const root = getElement({self.graph.id});
400
+ const container = root?.$el ?? root;
401
+ const plot = container?.querySelector('.js-plotly-plot') ?? container;
402
+ if (!plot || typeof Plotly === 'undefined') return false;
403
+ Plotly.downloadImage(plot, {{format: 'png', filename: {json.dumps(filename)}, scale: 2}});
404
+ return true;
405
+ }})()
406
+ """
407
+ ok = await ui.run_javascript(js)
408
+ if not ok:
409
+ ui.notify(f"Unable to export {filename}.png", type="negative")
410
+
411
+
412
+ def update_plot_compfree(self, e):
413
+ """Update the plot to show or hide component free concentrations."""
414
+
415
+ # comp_names = [d["species"][:-4] for d in self.graph_data["data"] if d["species"].endswith("_tot")]
416
+
417
+
418
+ for d in self.graph_data["data"]:
419
+ if d["species"].endswith("_free"):
420
+ col = d["species"][:-5] # Remove '_free' to get the component name
421
+ col = col+"_tot"
422
+ if col in self.comp_names:
423
+ if self.chkNoComp.value is False:
424
+ d["visible"] = "legendonly"
425
+ elif self.chkNoComp.value is True:
426
+ d["visible"] = True
427
+ self.graph.update()
428
+
429
+ def plotly_restyle_handler(self, e):
430
+ # update is update item
431
+ # trace is array of traces
432
+
433
+ change = e.args["0"]
434
+ traces = e.args["1"]
435
+
436
+ # for multiples, we want to do if change.keys() == ['visible']
437
+ # then for each item in change['visible'] (which will be a list)
438
+ # change the corresponding trace
439
+ if list(change.keys()) == [
440
+ "visible"
441
+ ]: # because if it's not just one entry, we don't want to mess things up.
442
+ for ii, changeitem in enumerate(change["visible"]):
443
+ if changeitem == "legendonly":
444
+ # if the trace is set to legendonly, we need to set this parameter in the simFigData
445
+ # so that it persists if we add more traces
446
+ self.graph_data["data"][traces[ii]]["visible"] = "legendonly"
447
+
448
+ def update_graph_x(self, e=None):
449
+ numName = self.xAxNumeratorSelect.value
450
+ deNomName = self.xAxDenominatorSelect.value if not None else 1
451
+ # self.graph_data["data"] = [] # Clear existing data for the new plot
452
+
453
+
454
+
455
+ throw_warning = False
456
+ for d in self.graph_data["data"]:
457
+ run_id = '-'.join(d["trace_id"].split("-",5)[0:5]) # Get the simulation ID from the trace_id
458
+ if run_id not in self.data_frames.keys():
459
+ ui.notify("Data not found for trace ID: " + run_id, type="warning")
460
+ continue
461
+ x_df = self.data_frames[run_id][0]
462
+ x_cols = x_df.columns
463
+
464
+ if numName == X_AXIS_ROW_INDEX:
465
+ d["x"] = list(range(len(x_df)))
466
+ continue
467
+ if numName not in x_cols :
468
+ ui.notify(
469
+ f"Numerator '{numName}' not found in simulation data for trace ID: {run_id}",
470
+ type="warning",
471
+ )
472
+ continue
473
+
474
+ if deNomName != 1 and deNomName not in x_cols:
475
+ ui.notify(
476
+ f"Denomination '{deNomName}' not found in simulation data for trace ID: {run_id}",
477
+ type="warning",
478
+ )
479
+ continue
480
+
481
+ if deNomName == 1 or deNomName is None:
482
+ d["x"] = self.data_frames[run_id][0][numName].tolist()
483
+ else:
484
+ d["x"] = (self.data_frames[run_id][0][numName] / self.data_frames[run_id][0][deNomName]).tolist()
485
+
486
+
487
+ self.graph_data.setdefault("layout", {})
488
+ self.graph_data["layout"].setdefault("xaxis", {})
489
+
490
+ if numName == X_AXIS_ROW_INDEX:
491
+ x_title_text = X_AXIS_ROW_INDEX
492
+ elif isinstance(deNomName, int) and deNomName == 1:
493
+ # if denominator is 1, we just plot the numerator
494
+ x_title_text = f"[{numName}]"
495
+ else:
496
+ x_title_text = f"[{numName}] / [{deNomName}]"
497
+
498
+ self.graph_data["layout"]["xaxis"]["title"] = {"text": x_title_text}
499
+
500
+ if throw_warning:
501
+ ui.notify(
502
+ "Some lines could not be plotted because you have requested an x-axis which they do not support.",
503
+ type="warning",
504
+ )
505
+ self.update_graph_y(update=False)
506
+ self.graph.update()
507
+ # 'x': (self.sd.compConcs['G']/self.sd.compConcs['H']).tolist(),
508
+
509
+ def _normalize_trace_values(self, y_values):
510
+ series = pd.to_numeric(pd.Series(y_values), errors="coerce")
511
+ if series.notna().sum() == 0:
512
+ return y_values
513
+
514
+ y_min = series.min(skipna=True)
515
+ y_max = series.max(skipna=True)
516
+ if pd.isna(y_min) or pd.isna(y_max) or y_max == y_min:
517
+ return [0.0 if pd.notna(v) else None for v in series.tolist()]
518
+
519
+ return [
520
+ None if pd.isna(v) else (float(v) - float(y_min)) / (float(y_max) - float(y_min))
521
+ for v in series.tolist()
522
+ ]
523
+
524
+ def update_graph_y(self, e=None, update=True):
525
+ normalize = hasattr(self, "chkNormalizeY") and bool(self.chkNormalizeY.value)
526
+
527
+ for d in self.graph_data["data"]:
528
+ trace_id = d.get("trace_id", "")
529
+ run_id = "-".join(trace_id.split("-", 5)[0:5])
530
+ if run_id not in self.data_frames:
531
+ continue
532
+
533
+ x_df, y_df = self.data_frames[run_id]
534
+ species = d.get("species")
535
+ y_values = None
536
+ if species in y_df.columns:
537
+ y_values = y_df[species].tolist()
538
+ elif species in x_df.columns:
539
+ y_values = x_df[species].tolist()
540
+
541
+ if y_values is None:
542
+ continue
543
+
544
+ d["y"] = self._normalize_trace_values(y_values) if normalize else y_values
545
+
546
+ self.graph_data.setdefault("layout", {})
547
+ self.graph_data["layout"].setdefault("yaxis", {})
548
+ self.graph_data["layout"]["yaxis"]["title"] = {
549
+ "text": "Normalized y (0-1)" if normalize else self._default_y_axis_title
550
+ }
551
+
552
+ if update:
553
+ self.graph.update()
554
+
555
+ def compspec_name_to_obj(self, name, sim):
556
+ if name is None:
557
+ return None
558
+ if name == 1:
559
+ return 1
560
+ elif name.endswith("_tot"):
561
+ # it's a component
562
+ if name[:-4] in list(sim.comp_concs.columns):
563
+ # i = self.sd.componentNames.index(name[:-4])
564
+ return sim.comp_concs[name[:-4]]
565
+ elif name.endswith("_free"):
566
+ # s = name[:-5] # remove '_free'
567
+ # combo = [c.name for c in self.sd.components]+ [s for s in self.sd.species]
568
+ if name in list(sim.results.columns):
569
+ # i = self.sd.species.index(s)
570
+ return sim.results[name] # return the results for that species
571
+ else:
572
+ return None # If the name does not match any known format, return None
573
+
574
+ def update_x_axis_selects(self, e=None):
575
+ # Prefer component-derived options for simulation workflows.
576
+ # Fall back to x-dataframe columns only when no component names exist.
577
+ x_axis_cols = [str(comp_name) for comp_name in self.comp_names]
578
+ if len(x_axis_cols) == 0:
579
+ for x_df, _ in self.data_frames.values():
580
+ for col in x_df.columns:
581
+ col_name = str(col)
582
+ if col_name not in x_axis_cols:
583
+ x_axis_cols.append(col_name)
584
+
585
+ uniq_opts = [X_AXIS_ROW_INDEX, *x_axis_cols]
586
+ self.xAxNumeratorSelect.set_options(uniq_opts)
587
+ self.xAxDenominatorSelect.set_options([1, *uniq_opts])
588
+
589
+ if self.xAxNumeratorSelect.value is not None and self.xAxNumeratorSelect.value not in uniq_opts:
590
+ self.xAxNumeratorSelect.value = X_AXIS_ROW_INDEX
591
+ self.xAxDenominatorSelect.value = 1
592
+ return
593
+
594
+ if self.xAxNumeratorSelect.value is None:
595
+ # first varying comp
596
+ # would prefer to, for example, set this to the first varying component concentration
597
+ # TODO
598
+ if len(uniq_opts)>1: # because row index is always present but not a helpful option
599
+ # look in the first dataframe and see which - if either - of these components vary
600
+ first_x_data = next(iter(self.data_frames.values()))[0]
601
+ # Find component with largest variation (max - min)
602
+ variations = (first_x_data.max() - first_x_data.min()).sort_values(ascending=False)
603
+
604
+ # Set to first component that has variation and is in options
605
+ for comp_name in variations.index:
606
+ if str(comp_name) in uniq_opts:
607
+ self.xAxNumeratorSelect.value = str(comp_name)
608
+ self.xAxDenominatorSelect.value = 1
609
+ return
610
+
611
+ self.xAxNumeratorSelect.value = uniq_opts[1] if len(uniq_opts) > 1 else uniq_opts[0] if len(uniq_opts) > 0 else None
612
+ self.xAxDenominatorSelect.value = 1
613
+
614
+ def clear_graph(self, e=None, update=True):
615
+ """Clear the graph."""
616
+ self.graph_data["data"] = []
617
+ self.data_frames = {}
618
+ self._comp_names = set()
619
+ self._color_mapping = {}
620
+ if update:
621
+ self.graph.update()
622
+
623
+
624
+ def remove_data(self, run_id: str):
625
+ if isinstance(run_id, Simulation):
626
+ run_id = str(run_id.id)
627
+ if isinstance(run_id, uuid.UUID):
628
+ run_id = str(run_id)
629
+ if isinstance(run_id, FitResult):
630
+ run_id = str(run_id.id)
631
+
632
+ """Remove data for a specific run_id from the graph."""
633
+ if run_id in self.data_frames:
634
+ del self.data_frames[run_id]
635
+ if run_id in self.line_styles:
636
+ del self.line_styles[run_id]
637
+
638
+ # Remove traces from the graph data
639
+ self.graph_data["data"] = [
640
+ d for d in self.graph_data["data"] if not d["trace_id"].startswith(run_id)
641
+ ]
642
+
643
+ self.regenerate_comp_names()
644
+
645
+ # Update the graph
646
+ self.graph.update()
647
+
648
+ def regenerate_comp_names(self):
649
+ self._comp_names = set(col for x in self.data_frames.values() for col in x[0].columns)