pyKES 0.2.0__tar.gz → 0.2.2__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.
- {pykes-0.2.0 → pykes-0.2.2}/PKG-INFO +1 -1
- {pykes-0.2.0 → pykes-0.2.2}/pyproject.toml +1 -1
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/database/data_processing.py +238 -56
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/database/database_experiments.py +4 -4
- pykes-0.2.2/src/pyKES/streamlit_app/chunked_processing.py +280 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/streamlit_app/components/data_upload_component.py +152 -63
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/streamlit_app/config_interface.py +5 -4
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/version_information.py +53 -67
- {pykes-0.2.0 → pykes-0.2.2}/README.md +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/__init__.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/database/__init__.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/fitting_ODE.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/pathways/__init__.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/pathways/pathways.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/pathways/transform_pathways_data.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/plotting/__init__.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/plotting/lighten_colors.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/plotting/plotting_pathways_transformed.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/plotting/plotting_tools.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/reaction_ODE.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/reaction_model.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/streamlit_app/components/__init__.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/streamlit_app/components/analysis_results_component.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/streamlit_app/components/home_component.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/streamlit_app/components/results_table_component.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/streamlit_app/components/time_series_component.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/__init__.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/calculate_absorption.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/find_nearest.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/get_experiments.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/harmonize_time_series.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/make_json_serializable.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/max_rate.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/offset_correction.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/resolve_attributes.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/time_series_resampling.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/unit_handler/__init__.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/unit_handler/config.py +0 -0
- {pykes-0.2.0 → pykes-0.2.2}/src/pyKES/utilities/unit_handler/quantity.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyKES"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Python package for kinetic modelling of chemical reaction networks."
|
|
9
9
|
keywords = ["kinetics", "photocatalysis", "water-splitting", "reaction-networks", "ode"]
|
|
10
10
|
authors = [
|
|
@@ -131,6 +131,155 @@ def read_in_single_experiment(file_name: str,
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
def ensure_processed_column(database: ExperimentalDataset) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Make sure ``overview_df`` carries the ``Processed`` flag as text.
|
|
137
|
+
|
|
138
|
+
Idempotent, and called by both entry points that touch the flag, so
|
|
139
|
+
neither depends on the other having run first.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
database : ExperimentalDataset
|
|
144
|
+
Dataset whose ``overview_df`` is given the column if it lacks one.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
None : None
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# The flag is text, not a bool: it round-trips through Excel and HDF5, and
|
|
152
|
+
# comes back as the strings this module compares against. Seeding it with a
|
|
153
|
+
# bool gives the column bool dtype, which pandas then refuses to write
|
|
154
|
+
# 'True' into once the first experiment succeeds.
|
|
155
|
+
if "Processed" not in database.overview_df.columns:
|
|
156
|
+
database.overview_df["Processed"] = "False"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def select_unprocessed_experiments(database: ExperimentalDataset,
|
|
160
|
+
overview_df_experiment_column: Optional[str] = 'Experiment') -> list:
|
|
161
|
+
"""
|
|
162
|
+
List the experiments of the overview sheet that still need processing.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
database : ExperimentalDataset
|
|
167
|
+
Dataset whose ``overview_df`` lists the experiments. A missing
|
|
168
|
+
``Processed`` column is added to it in place.
|
|
169
|
+
overview_df_experiment_column : str, optional
|
|
170
|
+
Column of ``overview_df`` holding the experiment names.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
list of str
|
|
175
|
+
Experiments that are either not flagged as processed or not yet held
|
|
176
|
+
by ``database.experiments``.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
ensure_processed_column(database)
|
|
180
|
+
|
|
181
|
+
unprocessed = (
|
|
182
|
+
database.overview_df["Processed"].ne('True')
|
|
183
|
+
| ~database.overview_df[overview_df_experiment_column].isin(database.experiments))
|
|
184
|
+
|
|
185
|
+
return database.overview_df.loc[unprocessed,
|
|
186
|
+
overview_df_experiment_column].astype(str).tolist()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def ingest_experiment(experiment_name: str,
|
|
190
|
+
database: ExperimentalDataset,
|
|
191
|
+
metadata_retrival_function: callable,
|
|
192
|
+
raw_data_reading_function: callable,
|
|
193
|
+
processing_function: callable,
|
|
194
|
+
overview_df_experiment_column: Optional[str] = 'Experiment',
|
|
195
|
+
directory: Optional[Path] = None,
|
|
196
|
+
external_version: Optional[dict] = None) -> dict:
|
|
197
|
+
"""
|
|
198
|
+
Process one experiment of the overview sheet and add it to the dataset.
|
|
199
|
+
|
|
200
|
+
One step of `read_in_experiments_single_threaded`, exposed separately so a
|
|
201
|
+
caller can drive the ingestion one experiment at a time — which is what the
|
|
202
|
+
Streamlit page does to keep its progress bar alive in the browser.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
experiment_name : str
|
|
207
|
+
Experiment to process, as named in ``overview_df``.
|
|
208
|
+
database : ExperimentalDataset
|
|
209
|
+
Dataset the experiment is added to; mutated in place on success.
|
|
210
|
+
metadata_retrival_function : callable
|
|
211
|
+
Returns the metadata dict for an experiment name.
|
|
212
|
+
raw_data_reading_function : callable
|
|
213
|
+
Reads the raw data for an experiment from ``directory``.
|
|
214
|
+
processing_function : callable
|
|
215
|
+
Turns raw data and metadata into the processed-data dict.
|
|
216
|
+
overview_df_experiment_column : str, optional
|
|
217
|
+
Column of ``overview_df`` holding the experiment names.
|
|
218
|
+
directory : Path, optional
|
|
219
|
+
Directory the raw-data files are read from.
|
|
220
|
+
external_version : dict, optional
|
|
221
|
+
Provenance of the external app, stamped onto the experiment. Already
|
|
222
|
+
resolved — unlike the loop functions, this is not defaulted to the
|
|
223
|
+
dataset's own entry.
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
result : dict
|
|
228
|
+
Result of `read_in_single_experiment`.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
ensure_processed_column(database)
|
|
232
|
+
|
|
233
|
+
result = read_in_single_experiment(
|
|
234
|
+
file_name = experiment_name,
|
|
235
|
+
database = database,
|
|
236
|
+
metadata_retrival_function = metadata_retrival_function,
|
|
237
|
+
raw_data_reading_function = raw_data_reading_function,
|
|
238
|
+
processing_function = processing_function,
|
|
239
|
+
directory = directory,
|
|
240
|
+
legacy_mode = False,
|
|
241
|
+
external_version = external_version
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if not result['success']:
|
|
245
|
+
print(f"Failed to process {result['file']}: {result['error']}")
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
database.add_experiment(result['data'])
|
|
249
|
+
|
|
250
|
+
database.overview_df.loc[
|
|
251
|
+
database.overview_df[overview_df_experiment_column].eq(result['data'].experiment_name),
|
|
252
|
+
"Processed",
|
|
253
|
+
] = 'True'
|
|
254
|
+
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def finalize_processing_run(database: ExperimentalDataset,
|
|
259
|
+
results: list,
|
|
260
|
+
external_version: Optional[dict] = None) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Stamp the dataset version after a processing or reprocessing run.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
database : ExperimentalDataset
|
|
267
|
+
Dataset whose ``version`` dict is updated in place.
|
|
268
|
+
results : list of dict
|
|
269
|
+
Results of the run; the dataset is only stamped when at least one
|
|
270
|
+
experiment succeeded.
|
|
271
|
+
external_version : dict, optional
|
|
272
|
+
Provenance of the external app.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
None : None
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
if any(result['success'] for result in results):
|
|
280
|
+
database.stamp_version(processed=True, external_version=external_version)
|
|
281
|
+
|
|
282
|
+
|
|
134
283
|
def read_in_experiments_single_threaded(database: ExperimentalDataset,
|
|
135
284
|
metadata_retrival_function: callable,
|
|
136
285
|
raw_data_reading_function: callable,
|
|
@@ -158,8 +307,8 @@ def read_in_experiments_single_threaded(database: ExperimentalDataset,
|
|
|
158
307
|
directory : Path, optional
|
|
159
308
|
Directory the raw-data files are read from.
|
|
160
309
|
external_version : dict, optional
|
|
161
|
-
Provenance of the external app (e.g. its
|
|
162
|
-
every processed experiment. Defaults to the dataset's own
|
|
310
|
+
Provenance of the external app (e.g. its own project version), stamped
|
|
311
|
+
onto every processed experiment. Defaults to the dataset's own
|
|
163
312
|
``version['external_version']``.
|
|
164
313
|
progress_callback : callable, optional
|
|
165
314
|
Called as ``(completed, total, experiment_name)`` once before the loop
|
|
@@ -173,24 +322,8 @@ def read_in_experiments_single_threaded(database: ExperimentalDataset,
|
|
|
173
322
|
`read_in_single_experiment`.
|
|
174
323
|
"""
|
|
175
324
|
|
|
176
|
-
|
|
177
|
-
# comes back as the strings this module compares against below. Seeding it
|
|
178
|
-
# with a bool gives the column bool dtype, which pandas then refuses to
|
|
179
|
-
# write 'True' into once the first experiment succeeds.
|
|
180
|
-
if "Processed" not in database.overview_df.columns:
|
|
181
|
-
database.overview_df["Processed"] = "False"
|
|
182
|
-
|
|
183
|
-
# Returning only the experiments which have not been processed
|
|
184
|
-
# Do not contain "Processed" column or "Processed" is not True
|
|
185
|
-
# also returns experiments which are not in the database.experiments dict,
|
|
325
|
+
experiments = select_unprocessed_experiments(database, overview_df_experiment_column)
|
|
186
326
|
|
|
187
|
-
mask = (
|
|
188
|
-
database.overview_df["Processed"].ne('True')
|
|
189
|
-
| ~database.overview_df[overview_df_experiment_column].isin(database.experiments))
|
|
190
|
-
|
|
191
|
-
experiments = database.overview_df.loc[mask,
|
|
192
|
-
overview_df_experiment_column].astype(str).tolist()
|
|
193
|
-
|
|
194
327
|
total_experiments = len(experiments)
|
|
195
328
|
external_version = resolve_external_version(database, external_version)
|
|
196
329
|
|
|
@@ -201,37 +334,21 @@ def read_in_experiments_single_threaded(database: ExperimentalDataset,
|
|
|
201
334
|
|
|
202
335
|
for completed, experiment_name in enumerate(experiments, start=1):
|
|
203
336
|
|
|
204
|
-
|
|
205
|
-
|
|
337
|
+
results.append(ingest_experiment(
|
|
338
|
+
experiment_name = experiment_name,
|
|
206
339
|
database = database,
|
|
207
340
|
metadata_retrival_function = metadata_retrival_function,
|
|
208
341
|
raw_data_reading_function = raw_data_reading_function,
|
|
209
342
|
processing_function = processing_function,
|
|
343
|
+
overview_df_experiment_column = overview_df_experiment_column,
|
|
210
344
|
directory = directory,
|
|
211
|
-
legacy_mode = False,
|
|
212
345
|
external_version = external_version
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
results.append(result)
|
|
216
|
-
|
|
217
|
-
if result['success']:
|
|
218
|
-
# Add experiment to database
|
|
219
|
-
database.add_experiment(result['data'])
|
|
220
|
-
|
|
221
|
-
# Setting "Processed" to True in dataframe
|
|
222
|
-
database.overview_df.loc[
|
|
223
|
-
database.overview_df[overview_df_experiment_column].eq(result['data'].experiment_name),
|
|
224
|
-
"Processed",
|
|
225
|
-
] = 'True'
|
|
226
|
-
|
|
227
|
-
else:
|
|
228
|
-
print(f"Failed to process {result['file']}: {result['error']}")
|
|
346
|
+
))
|
|
229
347
|
|
|
230
348
|
if progress_callback is not None:
|
|
231
349
|
progress_callback(completed, total_experiments, experiment_name)
|
|
232
350
|
|
|
233
|
-
|
|
234
|
-
database.stamp_version(processed=True, external_version=external_version)
|
|
351
|
+
finalize_processing_run(database, results, external_version)
|
|
235
352
|
|
|
236
353
|
return results
|
|
237
354
|
|
|
@@ -270,8 +387,7 @@ def read_in_experiments_multiprocessing(database: ExperimentalDataset,
|
|
|
270
387
|
else:
|
|
271
388
|
print(f"Failed to process {result['file']}: {result['error']}")
|
|
272
389
|
|
|
273
|
-
|
|
274
|
-
database.stamp_version(processed=True, external_version=external_version)
|
|
390
|
+
finalize_processing_run(database, results, external_version)
|
|
275
391
|
|
|
276
392
|
return results
|
|
277
393
|
|
|
@@ -351,6 +467,80 @@ def reprocess_single_experiment(experiment: Experiment,
|
|
|
351
467
|
}
|
|
352
468
|
|
|
353
469
|
|
|
470
|
+
def reprocess_experiment_by_name(experiment_name: str,
|
|
471
|
+
database: ExperimentalDataset,
|
|
472
|
+
processing_function: callable,
|
|
473
|
+
metadata_retrival_function: Optional[callable] = None,
|
|
474
|
+
external_version: Optional[dict] = None) -> dict:
|
|
475
|
+
"""
|
|
476
|
+
Reprocess one experiment of a dataset, addressed by name.
|
|
477
|
+
|
|
478
|
+
Name-addressed counterpart of `reprocess_single_experiment`, so that a
|
|
479
|
+
caller stepping through the experiments one at a time can reach both
|
|
480
|
+
pipelines through the same ``(experiment_name, **context)`` call shape as
|
|
481
|
+
`ingest_experiment`.
|
|
482
|
+
|
|
483
|
+
Parameters
|
|
484
|
+
----------
|
|
485
|
+
experiment_name : str
|
|
486
|
+
Experiment to reprocess; must be present in ``database.experiments``.
|
|
487
|
+
database : ExperimentalDataset
|
|
488
|
+
Dataset holding the experiment and the overview sheet.
|
|
489
|
+
processing_function : callable
|
|
490
|
+
``(raw_data_dict, metadata_dict) -> processed_data_dict``.
|
|
491
|
+
metadata_retrival_function : callable, optional
|
|
492
|
+
When given, metadata is refreshed from ``database.overview_df``.
|
|
493
|
+
external_version : dict, optional
|
|
494
|
+
Provenance of the external app, already resolved.
|
|
495
|
+
|
|
496
|
+
Returns
|
|
497
|
+
-------
|
|
498
|
+
result : dict
|
|
499
|
+
Result of `reprocess_single_experiment`.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
return reprocess_single_experiment(
|
|
503
|
+
experiment = database.experiments[experiment_name],
|
|
504
|
+
processing_function = processing_function,
|
|
505
|
+
metadata_retrival_function = metadata_retrival_function,
|
|
506
|
+
overview_df = database.overview_df,
|
|
507
|
+
external_version = external_version
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def select_experiments_to_reprocess(database: ExperimentalDataset,
|
|
512
|
+
experiment_names: Optional[list] = None) -> list:
|
|
513
|
+
"""
|
|
514
|
+
Resolve which experiments of a dataset a reprocessing run covers.
|
|
515
|
+
|
|
516
|
+
Parameters
|
|
517
|
+
----------
|
|
518
|
+
database : ExperimentalDataset
|
|
519
|
+
Dataset holding the experiments.
|
|
520
|
+
experiment_names : list of str, optional
|
|
521
|
+
Requested experiments. Defaults to every experiment in the dataset.
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
list of str
|
|
526
|
+
Experiments to reprocess.
|
|
527
|
+
|
|
528
|
+
Raises
|
|
529
|
+
------
|
|
530
|
+
ValueError
|
|
531
|
+
If any requested experiment is not held by the dataset.
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
if experiment_names is None:
|
|
535
|
+
return sorted(database.experiments.keys())
|
|
536
|
+
|
|
537
|
+
unknown_experiments = [name for name in experiment_names if name not in database.experiments]
|
|
538
|
+
if unknown_experiments:
|
|
539
|
+
raise ValueError(f"Experiments not present in the dataset: {unknown_experiments}")
|
|
540
|
+
|
|
541
|
+
return list(experiment_names)
|
|
542
|
+
|
|
543
|
+
|
|
354
544
|
def reprocess_experiments(database: ExperimentalDataset,
|
|
355
545
|
processing_function: callable,
|
|
356
546
|
metadata_retrival_function: Optional[callable] = None,
|
|
@@ -390,12 +580,7 @@ def reprocess_experiments(database: ExperimentalDataset,
|
|
|
390
580
|
One result dict per experiment, as returned by
|
|
391
581
|
`reprocess_single_experiment`.
|
|
392
582
|
"""
|
|
393
|
-
|
|
394
|
-
experiment_names = sorted(database.experiments.keys())
|
|
395
|
-
|
|
396
|
-
unknown_experiments = [name for name in experiment_names if name not in database.experiments]
|
|
397
|
-
if unknown_experiments:
|
|
398
|
-
raise ValueError(f"Experiments not present in the dataset: {unknown_experiments}")
|
|
583
|
+
experiment_names = select_experiments_to_reprocess(database, experiment_names)
|
|
399
584
|
|
|
400
585
|
total_experiments = len(experiment_names)
|
|
401
586
|
external_version = resolve_external_version(database, external_version)
|
|
@@ -407,21 +592,18 @@ def reprocess_experiments(database: ExperimentalDataset,
|
|
|
407
592
|
|
|
408
593
|
for completed, experiment_name in enumerate(experiment_names, start=1):
|
|
409
594
|
|
|
410
|
-
|
|
411
|
-
|
|
595
|
+
results.append(reprocess_experiment_by_name(
|
|
596
|
+
experiment_name = experiment_name,
|
|
597
|
+
database = database,
|
|
412
598
|
processing_function = processing_function,
|
|
413
599
|
metadata_retrival_function = metadata_retrival_function,
|
|
414
|
-
overview_df = database.overview_df,
|
|
415
600
|
external_version = external_version
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
results.append(result)
|
|
601
|
+
))
|
|
419
602
|
|
|
420
603
|
if progress_callback is not None:
|
|
421
604
|
progress_callback(completed, total_experiments, experiment_name)
|
|
422
605
|
|
|
423
|
-
|
|
424
|
-
database.stamp_version(processed=True, external_version=external_version)
|
|
606
|
+
finalize_processing_run(database, results, external_version)
|
|
425
607
|
|
|
426
608
|
return results
|
|
427
609
|
|
|
@@ -292,8 +292,8 @@ class ExperimentalDataset:
|
|
|
292
292
|
Whether a processing function was just run, which additionally
|
|
293
293
|
sets ``'last_processed'``.
|
|
294
294
|
external_version : dict, optional
|
|
295
|
-
Provenance of the external app (e.g. its
|
|
296
|
-
the existing ``'external_version'`` entry.
|
|
295
|
+
Provenance of the external app (e.g. its own project version),
|
|
296
|
+
merged into the existing ``'external_version'`` entry.
|
|
297
297
|
|
|
298
298
|
Returns
|
|
299
299
|
-------
|
|
@@ -318,7 +318,7 @@ class ExperimentalDataset:
|
|
|
318
318
|
alongside the data::
|
|
319
319
|
|
|
320
320
|
dataset.set_external_version({'app': 'photocat',
|
|
321
|
-
'
|
|
321
|
+
'version': get_project_version(__file__)})
|
|
322
322
|
|
|
323
323
|
Parameters
|
|
324
324
|
----------
|
|
@@ -648,7 +648,7 @@ def usage_example():
|
|
|
648
648
|
# Add dataset-level attributes
|
|
649
649
|
dataset.plotting_instruction = {'xlabel': 'Time (s)', 'ylabel': 'Current (mA)'}
|
|
650
650
|
dataset.group_mapping = {'GroupA': ['Exp1'], 'GroupB': ['Exp2']}
|
|
651
|
-
dataset.set_external_version({'app': 'usage_example', '
|
|
651
|
+
dataset.set_external_version({'app': 'usage_example', 'version': '0.3.0'})
|
|
652
652
|
|
|
653
653
|
exp1 = Experiment(
|
|
654
654
|
experiment_name="Exp1",
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Drive a long processing run one experiment per Streamlit rerun.
|
|
3
|
+
|
|
4
|
+
The pyKES app is also deployed as a static browser page via stlite, which runs
|
|
5
|
+
Streamlit inside Pyodide on the browser's *single* event loop — there is no
|
|
6
|
+
separate script-runner thread. A loop that processes every experiment inside
|
|
7
|
+
one script run therefore never lets Streamlit deliver anything: the progress
|
|
8
|
+
messages it writes are queued and only flushed once the run yields to the
|
|
9
|
+
event loop, which happens after the loop has already finished. The progress
|
|
10
|
+
bar was invisible in the browser for exactly this reason.
|
|
11
|
+
|
|
12
|
+
A job started here is instead advanced by one experiment per rerun. Each rerun
|
|
13
|
+
ends, the event loop gets control, and the bar painted by that rerun reaches
|
|
14
|
+
the screen.
|
|
15
|
+
|
|
16
|
+
The reruns are app-scoped, so the page that hosts a job has to skip its
|
|
17
|
+
expensive sections while `any_active_job` is true — the data-upload page
|
|
18
|
+
rewrites the whole HDF5 file in its download section, which must not happen
|
|
19
|
+
once per experiment. Fragment-scoped reruns would avoid that, but they are not
|
|
20
|
+
usable here: ``st.rerun(scope="fragment")`` is rejected outside a fragment
|
|
21
|
+
rerun, and the only way into one is a widget interaction or a ``run_every``
|
|
22
|
+
timer that would then poll for the entire duration of the job.
|
|
23
|
+
|
|
24
|
+
The same code path is used when running under ``streamlit run``; nothing here
|
|
25
|
+
is browser-specific.
|
|
26
|
+
|
|
27
|
+
See [docs/browser_deployment.md](../../../docs/browser_deployment.md).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import shutil
|
|
31
|
+
from typing import Callable, Optional
|
|
32
|
+
|
|
33
|
+
import streamlit as st
|
|
34
|
+
|
|
35
|
+
from pyKES.database.data_processing import finalize_processing_run
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Session-state key holding the results of the most recently finished job,
|
|
39
|
+
# derived from the job's own key. The results outlive the job itself so the page
|
|
40
|
+
# can report them after the final rerun has torn the job down.
|
|
41
|
+
JOB_RESULTS_KEY_TEMPLATE = "{job_key}_results"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def job_results_key(job_key: str) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Session-state key under which a finished job parks its results.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
job_key : str
|
|
51
|
+
Key identifying the job.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
str
|
|
56
|
+
Session-state key of the results list.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
return JOB_RESULTS_KEY_TEMPLATE.format(job_key=job_key)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def start_chunked_job(job_key: str,
|
|
63
|
+
experiment_names: list,
|
|
64
|
+
context: dict,
|
|
65
|
+
staging_directory: Optional[str] = None) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Register a processing run to be advanced one experiment per rerun.
|
|
68
|
+
|
|
69
|
+
The caller is expected to trigger a full ``st.rerun`` afterwards, so the
|
|
70
|
+
page re-renders with the job active.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
job_key : str
|
|
75
|
+
Session-state key identifying this job. One per page section.
|
|
76
|
+
experiment_names : list of str
|
|
77
|
+
Experiments to process, in order.
|
|
78
|
+
context : dict
|
|
79
|
+
Keyword arguments handed to the step function alongside the experiment
|
|
80
|
+
name — the processing callables, the staging directory and the
|
|
81
|
+
already-resolved external version. Must include the
|
|
82
|
+
`ExperimentalDataset` under ``'database'``: both step functions take it,
|
|
83
|
+
and the job is stamped against it when it finishes.
|
|
84
|
+
staging_directory : str, optional
|
|
85
|
+
Directory holding uploaded files for the duration of the job. Removed
|
|
86
|
+
when the job finishes.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
None : None
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
st.session_state.pop(job_results_key(job_key), None)
|
|
94
|
+
|
|
95
|
+
st.session_state[job_key] = {
|
|
96
|
+
'experiment_names': experiment_names,
|
|
97
|
+
'completed': 0,
|
|
98
|
+
'results': [],
|
|
99
|
+
'painted': False,
|
|
100
|
+
'staging_directory': staging_directory,
|
|
101
|
+
'context': context,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def active_job(job_key: str) -> Optional[dict]:
|
|
106
|
+
"""
|
|
107
|
+
Return the job registered under a key, if there is one.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
job_key : str
|
|
112
|
+
Session-state key identifying the job.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
dict or None
|
|
117
|
+
The job state, or None when no job is running.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
return st.session_state.get(job_key)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def collect_job_results(job_key: str) -> Optional[list]:
|
|
124
|
+
"""
|
|
125
|
+
Take the results parked by the most recently finished job.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
job_key : str
|
|
130
|
+
Session-state key identifying the job.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
list of dict or None
|
|
135
|
+
The results, or None when no job has finished since the last call.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
return st.session_state.pop(job_results_key(job_key), None)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def paint_job_progress(job: dict) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Draw the progress bar for the experiment that is about to be processed.
|
|
144
|
+
|
|
145
|
+
Painting is a rerun of its own, doing no work, so that the bar reaches the
|
|
146
|
+
screen *before* the next experiment occupies the event loop. Painting and
|
|
147
|
+
processing in the same rerun would leave the bar one experiment behind and
|
|
148
|
+
never show the first one — which is the whole bug this module exists for.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
job : dict
|
|
153
|
+
Job state; ``painted`` is set in place.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
None : None
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
completed = job['completed']
|
|
161
|
+
total = len(job['experiment_names'])
|
|
162
|
+
|
|
163
|
+
st.progress(
|
|
164
|
+
completed / total,
|
|
165
|
+
text = f"Processing experiment {completed + 1}/{total}: {job['experiment_names'][completed]}",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
job['painted'] = True
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def run_job_step(job: dict, step_function: Callable[..., dict]) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Process the next experiment of a job.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
job : dict
|
|
178
|
+
Job state; ``results``, ``completed`` and ``painted`` are updated in
|
|
179
|
+
place.
|
|
180
|
+
step_function : callable
|
|
181
|
+
``(experiment_name, **context) -> result_dict``, e.g.
|
|
182
|
+
`pyKES.database.data_processing.ingest_experiment`.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
None : None
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
experiment_name = job['experiment_names'][job['completed']]
|
|
190
|
+
|
|
191
|
+
job['results'].append(step_function(experiment_name, **job['context']))
|
|
192
|
+
job['completed'] += 1
|
|
193
|
+
job['painted'] = False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def finish_chunked_job(job_key: str, job: dict) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Stamp the dataset, clean up the staging directory and retire the job.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
job_key : str
|
|
203
|
+
Session-state key identifying the job.
|
|
204
|
+
job : dict
|
|
205
|
+
Job state to retire.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
None : None
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
# Stamped once for the whole run, as the non-chunked loop functions do,
|
|
213
|
+
# rather than after every experiment.
|
|
214
|
+
finalize_processing_run(job['context']['database'],
|
|
215
|
+
job['results'],
|
|
216
|
+
job['context'].get('external_version'))
|
|
217
|
+
|
|
218
|
+
if job['staging_directory'] is not None:
|
|
219
|
+
shutil.rmtree(job['staging_directory'])
|
|
220
|
+
|
|
221
|
+
st.session_state[job_results_key(job_key)] = job['results']
|
|
222
|
+
del st.session_state[job_key]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def render_chunked_job(job_key: str, step_function: Callable[..., dict]) -> None:
|
|
226
|
+
"""
|
|
227
|
+
Advance a job by one phase and rerun for the next.
|
|
228
|
+
|
|
229
|
+
Alternates painting and processing, so every experiment is announced
|
|
230
|
+
before it runs and the announcement reaches the screen while it runs.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
job_key : str
|
|
235
|
+
Session-state key identifying the job.
|
|
236
|
+
step_function : callable
|
|
237
|
+
``(experiment_name, **context) -> result_dict``.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
None : None
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
job = active_job(job_key)
|
|
245
|
+
|
|
246
|
+
if job is None:
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# st.rerun raises, so each branch below ends the script run.
|
|
250
|
+
if job['completed'] == len(job['experiment_names']):
|
|
251
|
+
finish_chunked_job(job_key, job)
|
|
252
|
+
st.rerun()
|
|
253
|
+
|
|
254
|
+
if job['painted']:
|
|
255
|
+
run_job_step(job, step_function)
|
|
256
|
+
else:
|
|
257
|
+
paint_job_progress(job)
|
|
258
|
+
|
|
259
|
+
st.rerun()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def any_active_job(job_keys: list) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Report whether any of the given jobs is currently running.
|
|
265
|
+
|
|
266
|
+
Pages use this to skip sections that are too expensive to re-render on
|
|
267
|
+
every step of a job.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
job_keys : list of str
|
|
272
|
+
Session-state keys of the jobs the page can start.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
bool
|
|
277
|
+
True while at least one of them is active.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
return any(active_job(job_key) is not None for job_key in job_keys)
|