pyKES 0.2.2__tar.gz → 0.2.3__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.
Files changed (39) hide show
  1. {pykes-0.2.2 → pykes-0.2.3}/PKG-INFO +1 -1
  2. {pykes-0.2.2 → pykes-0.2.3}/pyproject.toml +1 -1
  3. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/streamlit_app/chunked_processing.py +101 -33
  4. {pykes-0.2.2 → pykes-0.2.3}/README.md +0 -0
  5. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/__init__.py +0 -0
  6. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/database/__init__.py +0 -0
  7. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/database/data_processing.py +0 -0
  8. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/database/database_experiments.py +0 -0
  9. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/fitting_ODE.py +0 -0
  10. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/pathways/__init__.py +0 -0
  11. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/pathways/pathways.py +0 -0
  12. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/pathways/transform_pathways_data.py +0 -0
  13. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/plotting/__init__.py +0 -0
  14. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/plotting/lighten_colors.py +0 -0
  15. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/plotting/plotting_pathways_transformed.py +0 -0
  16. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/plotting/plotting_tools.py +0 -0
  17. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/reaction_ODE.py +0 -0
  18. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/reaction_model.py +0 -0
  19. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/streamlit_app/components/__init__.py +0 -0
  20. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/streamlit_app/components/analysis_results_component.py +0 -0
  21. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/streamlit_app/components/data_upload_component.py +0 -0
  22. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/streamlit_app/components/home_component.py +0 -0
  23. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/streamlit_app/components/results_table_component.py +0 -0
  24. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/streamlit_app/components/time_series_component.py +0 -0
  25. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/streamlit_app/config_interface.py +0 -0
  26. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/__init__.py +0 -0
  27. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/calculate_absorption.py +0 -0
  28. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/find_nearest.py +0 -0
  29. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/get_experiments.py +0 -0
  30. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/harmonize_time_series.py +0 -0
  31. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/make_json_serializable.py +0 -0
  32. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/max_rate.py +0 -0
  33. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/offset_correction.py +0 -0
  34. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/resolve_attributes.py +0 -0
  35. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/time_series_resampling.py +0 -0
  36. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/unit_handler/__init__.py +0 -0
  37. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/unit_handler/config.py +0 -0
  38. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/unit_handler/quantity.py +0 -0
  39. {pykes-0.2.2 → pykes-0.2.3}/src/pyKES/utilities/version_information.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyKES
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Python package for kinetic modelling of chemical reaction networks.
5
5
  Keywords: kinetics,photocatalysis,water-splitting,reaction-networks,ode
6
6
  Author: Jacob Schneidewind
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "pyKES"
7
- version = "0.2.2"
7
+ version = "0.2.3"
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 = [
@@ -9,17 +9,21 @@ messages it writes are queued and only flushed once the run yields to the
9
9
  event loop, which happens after the loop has already finished. The progress
10
10
  bar was invisible in the browser for exactly this reason.
11
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.
12
+ A job started here is instead advanced by one experiment per run of an
13
+ ``st.fragment(run_every=...)``. The timer that drives those runs lives in the
14
+ *frontend*: Streamlit sends it as an auto-rerun instruction, and the browser
15
+ asks for the next run. Each run therefore **ends normally** and its elements
16
+ are delivered before the next one starts.
17
+
18
+ Ending normally is the part that matters, and it is why `st.rerun` cannot be
19
+ used to drive this instead. A script run that ends in `st.rerun` is followed
20
+ immediately by the next one, and `AppSession` clears the browser queue on every
21
+ ``SCRIPT_STARTED`` — so a progress bar drawn by a run that then reruns is
22
+ discarded before it is ever sent, unless a flush happens to fall between the
23
+ two. On a server it usually does, because the flush runs on another thread. In
24
+ the browser it does not, and the bar is never seen at all. Measured in headless
25
+ Chrome against stlite 1.7.3: with `st.rerun` the page went from blank straight
26
+ to the finished state; with the timer the bar advanced step by step.
23
27
 
24
28
  The same code path is used when running under ``streamlit run``; nothing here
25
29
  is browser-specific.
@@ -40,6 +44,12 @@ from pyKES.database.data_processing import finalize_processing_run
40
44
  # can report them after the final rerun has torn the job down.
41
45
  JOB_RESULTS_KEY_TEMPLATE = "{job_key}_results"
42
46
 
47
+ # How often the frontend asks for the next step of a running job. It only paces
48
+ # the hand-over between steps — the step itself takes as long as it takes — so
49
+ # it is set well below the duration of any real experiment and costs one round
50
+ # trip per step.
51
+ JOB_STEP_INTERVAL_SECONDS = 0.1
52
+
43
53
 
44
54
  def job_results_key(job_key: str) -> str:
45
55
  """
@@ -96,7 +106,6 @@ def start_chunked_job(job_key: str,
96
106
  'experiment_names': experiment_names,
97
107
  'completed': 0,
98
108
  'results': [],
99
- 'painted': False,
100
109
  'staging_directory': staging_directory,
101
110
  'context': context,
102
111
  }
@@ -138,19 +147,37 @@ def collect_job_results(job_key: str) -> Optional[list]:
138
147
  return st.session_state.pop(job_results_key(job_key), None)
139
148
 
140
149
 
150
+ def job_is_complete(job: dict) -> bool:
151
+ """
152
+ Report whether every experiment of a job has been processed.
153
+
154
+ Parameters
155
+ ----------
156
+ job : dict
157
+ Job state.
158
+
159
+ Returns
160
+ -------
161
+ bool
162
+ True once no experiment is left.
163
+ """
164
+
165
+ return job['completed'] == len(job['experiment_names'])
166
+
167
+
141
168
  def paint_job_progress(job: dict) -> None:
142
169
  """
143
170
  Draw the progress bar for the experiment that is about to be processed.
144
171
 
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.
172
+ Called at the end of every step, so the run that delivers this bar is over
173
+ before the experiment it names occupies the worker. The bar therefore
174
+ stands on screen for exactly as long as that experiment takes.
149
175
 
150
176
  Parameters
151
177
  ----------
152
178
  job : dict
153
- Job state; ``painted`` is set in place.
179
+ Job state. Must not be complete — the caller finishes the job first, so
180
+ ``completed`` always indexes an experiment here.
154
181
 
155
182
  Returns
156
183
  -------
@@ -165,8 +192,6 @@ def paint_job_progress(job: dict) -> None:
165
192
  text = f"Processing experiment {completed + 1}/{total}: {job['experiment_names'][completed]}",
166
193
  )
167
194
 
168
- job['painted'] = True
169
-
170
195
 
171
196
  def run_job_step(job: dict, step_function: Callable[..., dict]) -> None:
172
197
  """
@@ -175,8 +200,7 @@ def run_job_step(job: dict, step_function: Callable[..., dict]) -> None:
175
200
  Parameters
176
201
  ----------
177
202
  job : dict
178
- Job state; ``results``, ``completed`` and ``painted`` are updated in
179
- place.
203
+ Job state; ``results`` and ``completed`` are updated in place.
180
204
  step_function : callable
181
205
  ``(experiment_name, **context) -> result_dict``, e.g.
182
206
  `pyKES.database.data_processing.ingest_experiment`.
@@ -190,7 +214,6 @@ def run_job_step(job: dict, step_function: Callable[..., dict]) -> None:
190
214
 
191
215
  job['results'].append(step_function(experiment_name, **job['context']))
192
216
  job['completed'] += 1
193
- job['painted'] = False
194
217
 
195
218
 
196
219
  def finish_chunked_job(job_key: str, job: dict) -> None:
@@ -222,12 +245,40 @@ def finish_chunked_job(job_key: str, job: dict) -> None:
222
245
  del st.session_state[job_key]
223
246
 
224
247
 
225
- def render_chunked_job(job_key: str, step_function: Callable[..., dict]) -> None:
248
+ @st.fragment(run_every=JOB_STEP_INTERVAL_SECONDS)
249
+ def render_job_progress(job_key: str) -> None:
226
250
  """
227
- Advance a job by one phase and rerun for the next.
251
+ Draw the progress bar of a running job. Draws nothing else, ever.
228
252
 
229
- Alternates painting and processing, so every experiment is announced
230
- before it runs and the announcement reaches the screen while it runs.
253
+ Deliberately a separate fragment from `advance_job`: a fragment rerun
254
+ clears the deltas of the fragments *in that run* and preserves everyone
255
+ else's, so a bar drawn here survives every step the worker takes. Drawn
256
+ from inside the worker instead, it was wiped by the worker's own next tick
257
+ before it was ever sent, and the browser showed the first experiment for
258
+ the whole run.
259
+
260
+ Parameters
261
+ ----------
262
+ job_key : str
263
+ Session-state key identifying the job.
264
+
265
+ Returns
266
+ -------
267
+ None : None
268
+ """
269
+
270
+ job = active_job(job_key)
271
+
272
+ if job is None or job_is_complete(job):
273
+ return
274
+
275
+ paint_job_progress(job)
276
+
277
+
278
+ @st.fragment(run_every=JOB_STEP_INTERVAL_SECONDS)
279
+ def advance_job(job_key: str, step_function: Callable[..., dict]) -> None:
280
+ """
281
+ Process one experiment per timer tick. Writes nothing to the page.
231
282
 
232
283
  Parameters
233
284
  ----------
@@ -246,17 +297,34 @@ def render_chunked_job(job_key: str, step_function: Callable[..., dict]) -> None
246
297
  if job is None:
247
298
  return
248
299
 
249
- # st.rerun raises, so each branch below ends the script run.
250
- if job['completed'] == len(job['experiment_names']):
300
+ if job_is_complete(job):
251
301
  finish_chunked_job(job_key, job)
302
+
303
+ # App-scoped: the page re-renders once without either fragment, so the
304
+ # timers stop and the sections skipped during the job come back.
252
305
  st.rerun()
253
306
 
254
- if job['painted']:
255
- run_job_step(job, step_function)
256
- else:
257
- paint_job_progress(job)
307
+ run_job_step(job, step_function)
308
+
309
+
310
+ def render_chunked_job(job_key: str, step_function: Callable[..., dict]) -> None:
311
+ """
312
+ Run a job to completion, one experiment per timer tick, showing progress.
313
+
314
+ Parameters
315
+ ----------
316
+ job_key : str
317
+ Session-state key identifying the job.
318
+ step_function : callable
319
+ ``(experiment_name, **context) -> result_dict``.
320
+
321
+ Returns
322
+ -------
323
+ None : None
324
+ """
258
325
 
259
- st.rerun()
326
+ render_job_progress(job_key)
327
+ advance_job(job_key, step_function)
260
328
 
261
329
 
262
330
  def any_active_job(job_keys: list) -> bool:
File without changes
File without changes
File without changes
File without changes
File without changes