waveorder 2.2.1b0__py3-none-any.whl → 3.0.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 (58) hide show
  1. waveorder/_version.py +16 -3
  2. waveorder/acq/__init__.py +0 -0
  3. waveorder/acq/acq_functions.py +166 -0
  4. waveorder/assets/HSV_legend.png +0 -0
  5. waveorder/assets/JCh_legend.png +0 -0
  6. waveorder/assets/waveorder_plugin_logo.png +0 -0
  7. waveorder/calib/Calibration.py +1512 -0
  8. waveorder/calib/Optimization.py +470 -0
  9. waveorder/calib/__init__.py +0 -0
  10. waveorder/calib/calibration_workers.py +464 -0
  11. waveorder/cli/apply_inverse_models.py +328 -0
  12. waveorder/cli/apply_inverse_transfer_function.py +379 -0
  13. waveorder/cli/compute_transfer_function.py +432 -0
  14. waveorder/cli/gui_widget.py +58 -0
  15. waveorder/cli/main.py +39 -0
  16. waveorder/cli/monitor.py +163 -0
  17. waveorder/cli/option_eat_all.py +47 -0
  18. waveorder/cli/parsing.py +122 -0
  19. waveorder/cli/printing.py +16 -0
  20. waveorder/cli/reconstruct.py +67 -0
  21. waveorder/cli/settings.py +187 -0
  22. waveorder/cli/utils.py +175 -0
  23. waveorder/filter.py +1 -2
  24. waveorder/focus.py +136 -25
  25. waveorder/io/__init__.py +0 -0
  26. waveorder/io/_reader.py +61 -0
  27. waveorder/io/core_functions.py +272 -0
  28. waveorder/io/metadata_reader.py +195 -0
  29. waveorder/io/utils.py +175 -0
  30. waveorder/io/visualization.py +160 -0
  31. waveorder/models/inplane_oriented_thick_pol3d_vector.py +3 -3
  32. waveorder/models/isotropic_fluorescent_thick_3d.py +92 -0
  33. waveorder/models/isotropic_fluorescent_thin_3d.py +331 -0
  34. waveorder/models/isotropic_thin_3d.py +73 -72
  35. waveorder/models/phase_thick_3d.py +103 -4
  36. waveorder/napari.yaml +36 -0
  37. waveorder/plugin/__init__.py +9 -0
  38. waveorder/plugin/gui.py +1094 -0
  39. waveorder/plugin/gui.ui +1440 -0
  40. waveorder/plugin/job_manager.py +42 -0
  41. waveorder/plugin/main_widget.py +1605 -0
  42. waveorder/plugin/tab_recon.py +3294 -0
  43. waveorder/scripts/__init__.py +0 -0
  44. waveorder/scripts/launch_napari.py +13 -0
  45. waveorder/scripts/repeat-cal-acq-rec.py +147 -0
  46. waveorder/scripts/repeat-calibration.py +31 -0
  47. waveorder/scripts/samples.py +85 -0
  48. waveorder/scripts/simulate_zarr_acq.py +204 -0
  49. waveorder/util.py +1 -1
  50. waveorder/visuals/napari_visuals.py +1 -1
  51. waveorder-3.0.0.dist-info/METADATA +350 -0
  52. waveorder-3.0.0.dist-info/RECORD +69 -0
  53. {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info}/WHEEL +1 -1
  54. waveorder-3.0.0.dist-info/entry_points.txt +5 -0
  55. {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info/licenses}/LICENSE +13 -1
  56. waveorder-2.2.1b0.dist-info/METADATA +0 -187
  57. waveorder-2.2.1b0.dist-info/RECORD +0 -27
  58. {waveorder-2.2.1b0.dist-info → waveorder-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,432 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+ import numpy as np
5
+ from iohub.ngff import Position, open_ome_zarr
6
+
7
+ from waveorder import focus
8
+ from waveorder.cli.parsing import (
9
+ config_filepath,
10
+ input_position_dirpaths,
11
+ output_dirpath,
12
+ )
13
+ from waveorder.cli.printing import echo_headline, echo_settings
14
+ from waveorder.cli.settings import ReconstructionSettings
15
+ from waveorder.io import utils
16
+ from waveorder.models import (
17
+ inplane_oriented_thick_pol3d,
18
+ inplane_oriented_thick_pol3d_vector,
19
+ isotropic_fluorescent_thick_3d,
20
+ isotropic_fluorescent_thin_3d,
21
+ isotropic_thin_3d,
22
+ phase_thick_3d,
23
+ )
24
+
25
+
26
+ def _position_list_from_shape_scale_offset(
27
+ shape: int, scale: float, offset: float
28
+ ) -> list:
29
+ """
30
+ Generates a list of positions based on the given array shape, pixel size (scale), and offset.
31
+
32
+ Examples
33
+ --------
34
+ >>> _position_list_from_shape_scale_offset(5, 1.0, 0.0)
35
+ [2.0, 1.0, 0.0, -1.0, -2.0]
36
+ >>> _position_list_from_shape_scale_offset(4, 0.5, 1.0)
37
+ [1.5, 1.0, 0.5, 0.0]
38
+ """
39
+ return list((-np.arange(shape) + (shape // 2) + offset) * scale)
40
+
41
+
42
+ def generate_and_save_vector_birefringence_transfer_function(
43
+ settings: ReconstructionSettings, dataset: Position, zyx_shape: tuple
44
+ ):
45
+ """Generates and saves the vector birefringence transfer function
46
+ to the dataset, based on the settings.
47
+
48
+ Parameters
49
+ ----------
50
+ settings : ReconstructionSettings
51
+ dataset : NGFF Node
52
+ The dataset that will be updated.
53
+ zyx_shape : tuple
54
+ A tuple of integers specifying the input data's shape in (Z, Y, X) order
55
+ """
56
+ echo_headline(
57
+ "Generating vector birefringence transfer function with settings:"
58
+ )
59
+ echo_settings(settings.birefringence.transfer_function)
60
+ echo_settings(settings.phase.transfer_function)
61
+
62
+ num_elements = np.array(zyx_shape).prod()
63
+ max_tf_elements = 1e7 # empirical, based on memory usage
64
+ transverse_downsample_factor = np.ceil(
65
+ np.sqrt(num_elements / max_tf_elements)
66
+ )
67
+ echo_headline(
68
+ f"Downsampling transfer function in X and Y by {transverse_downsample_factor}x"
69
+ )
70
+ phase_settings_dict = settings.phase.transfer_function.model_dump()
71
+ phase_settings_dict.pop("z_focus_offset") # not used in 3D
72
+
73
+ sfZYX_transfer_function, _, singular_system = (
74
+ inplane_oriented_thick_pol3d_vector.calculate_transfer_function(
75
+ zyx_shape=zyx_shape,
76
+ scheme=str(len(settings.input_channel_names)) + "-State",
77
+ **settings.birefringence.transfer_function.model_dump(),
78
+ **phase_settings_dict,
79
+ fourier_oversample_factor=int(transverse_downsample_factor),
80
+ )
81
+ )
82
+
83
+ U, S, Vh = singular_system
84
+ chunks = (1, 1, 1, zyx_shape[1], zyx_shape[2])
85
+
86
+ # Add dummy channels
87
+ for i in range(3):
88
+ dataset.append_channel(f"ch{i}")
89
+
90
+ dataset.create_image(
91
+ "vector_transfer_function",
92
+ sfZYX_transfer_function.cpu().numpy(),
93
+ chunks=chunks,
94
+ )
95
+ dataset.create_image(
96
+ "vector_singular_system_U",
97
+ U.cpu().numpy(),
98
+ chunks=chunks,
99
+ )
100
+ dataset.create_image(
101
+ "vector_singular_system_S",
102
+ S[None].cpu().numpy(),
103
+ chunks=chunks,
104
+ )
105
+ dataset.create_image(
106
+ "vector_singular_system_Vh",
107
+ Vh.cpu().numpy(),
108
+ chunks=chunks,
109
+ )
110
+
111
+
112
+ def generate_and_save_birefringence_transfer_function(
113
+ settings: ReconstructionSettings, dataset
114
+ ):
115
+ """Generates and saves the birefringence transfer function to the dataset, based on the settings.
116
+
117
+ Parameters
118
+ ----------
119
+ settings: ReconstructionSettings
120
+ dataset: NGFF Node
121
+ The dataset that will be updated.
122
+ """
123
+ echo_headline("Generating birefringence transfer function with settings:")
124
+ echo_settings(settings.birefringence.transfer_function)
125
+
126
+ # Calculate transfer functions
127
+ intensity_to_stokes_matrix = (
128
+ inplane_oriented_thick_pol3d.calculate_transfer_function(
129
+ scheme=str(len(settings.input_channel_names)) + "-State",
130
+ **settings.birefringence.transfer_function.model_dump(),
131
+ )
132
+ )
133
+ # Save
134
+ dataset["intensity_to_stokes_matrix"] = (
135
+ intensity_to_stokes_matrix.cpu().numpy()[None, None, None, ...]
136
+ )
137
+
138
+
139
+ def generate_and_save_phase_transfer_function(
140
+ settings: ReconstructionSettings,
141
+ dataset: Position,
142
+ zyx_shape: tuple,
143
+ ):
144
+ """Generates and saves the phase transfer function to the dataset, based on the settings.
145
+
146
+ Parameters
147
+ ----------
148
+ settings: ReconstructionSettings
149
+ dataset: Position
150
+ The dataset that will be updated.
151
+ zyx_shape : tuple
152
+ A tuple of integers specifying the input data's shape in (Z, Y, X) order
153
+ """
154
+ echo_headline("Generating phase transfer function with settings:")
155
+ echo_settings(settings.phase.transfer_function)
156
+
157
+ settings_dict = settings.phase.transfer_function.model_dump()
158
+ if settings.reconstruction_dimension == 2:
159
+ # Convert zyx_shape and z_pixel_size into yx_shape and z_position_list
160
+ settings_dict["yx_shape"] = [zyx_shape[1], zyx_shape[2]]
161
+ settings_dict["z_position_list"] = (
162
+ _position_list_from_shape_scale_offset(
163
+ shape=zyx_shape[0],
164
+ scale=settings_dict["z_pixel_size"],
165
+ offset=settings_dict["z_focus_offset"],
166
+ )
167
+ )
168
+
169
+ # Remove unused parameters
170
+ settings_dict.pop("z_pixel_size")
171
+ settings_dict.pop("z_padding")
172
+ settings_dict.pop("z_focus_offset")
173
+
174
+ # Calculate transfer functions
175
+ (
176
+ absorption_transfer_function,
177
+ phase_transfer_function,
178
+ ) = isotropic_thin_3d.calculate_transfer_function(
179
+ **settings_dict,
180
+ )
181
+
182
+ # Calculate singular system
183
+ U, S, Vh = isotropic_thin_3d.calculate_singular_system(
184
+ absorption_transfer_function,
185
+ phase_transfer_function,
186
+ )
187
+
188
+ # Save
189
+ dataset.create_image(
190
+ "singular_system_U",
191
+ U.cpu().numpy()[None],
192
+ )
193
+ dataset.create_image(
194
+ "singular_system_S",
195
+ S.cpu().numpy()[None, None],
196
+ )
197
+ dataset.create_image(
198
+ "singular_system_Vh",
199
+ Vh.cpu().numpy()[None],
200
+ )
201
+
202
+ elif settings.reconstruction_dimension == 3:
203
+ settings_dict.pop("z_focus_offset") # not used in 3D
204
+
205
+ # Calculate transfer functions
206
+ (
207
+ real_potential_transfer_function,
208
+ imaginary_potential_transfer_function,
209
+ ) = phase_thick_3d.calculate_transfer_function(
210
+ zyx_shape=zyx_shape,
211
+ **settings_dict,
212
+ )
213
+ # Save
214
+ dataset.create_image(
215
+ "real_potential_transfer_function",
216
+ real_potential_transfer_function.cpu().numpy()[None, None, ...],
217
+ chunks=(1, 1, 1, zyx_shape[1], zyx_shape[2]),
218
+ )
219
+ dataset.create_image(
220
+ "imaginary_potential_transfer_function",
221
+ imaginary_potential_transfer_function.cpu().numpy()[
222
+ None, None, ...
223
+ ],
224
+ chunks=(1, 1, 1, zyx_shape[1], zyx_shape[2]),
225
+ )
226
+
227
+
228
+ def generate_and_save_fluorescence_transfer_function(
229
+ settings: ReconstructionSettings,
230
+ dataset: Position,
231
+ zyx_shape: tuple,
232
+ ):
233
+ """Generates and saves the fluorescence transfer function to the dataset, based on the settings.
234
+
235
+ Parameters
236
+ ----------
237
+ settings: ReconstructionSettings
238
+ dataset: Position
239
+ The dataset that will be updated.
240
+ zyx_shape : tuple
241
+ A tuple of integers specifying the input data's shape in (Z, Y, X) order
242
+ """
243
+ echo_headline("Generating fluorescence transfer function with settings:")
244
+ echo_settings(settings.fluorescence.transfer_function)
245
+ settings_dict = settings.fluorescence.transfer_function.model_dump()
246
+
247
+ if settings.reconstruction_dimension == 2:
248
+ # Convert zyx_shape and z_pixel_size into yx_shape and z_position_list
249
+ settings_dict["yx_shape"] = [zyx_shape[1], zyx_shape[2]]
250
+ settings_dict["z_position_list"] = (
251
+ _position_list_from_shape_scale_offset(
252
+ shape=zyx_shape[0],
253
+ scale=settings_dict["z_pixel_size"],
254
+ offset=settings_dict["z_focus_offset"],
255
+ )
256
+ )
257
+
258
+ # Remove unused parameters
259
+ settings_dict.pop("z_pixel_size")
260
+ settings_dict.pop("z_padding")
261
+ settings_dict.pop("z_focus_offset")
262
+
263
+ # Calculate 2D fluorescence transfer functions
264
+ fluorescent_2d_to_3d_transfer_function = (
265
+ isotropic_fluorescent_thin_3d.calculate_transfer_function(
266
+ **settings_dict,
267
+ )
268
+ )
269
+
270
+ # Calculate singular system for 2D reconstruction
271
+ U, S, Vh = isotropic_fluorescent_thin_3d.calculate_singular_system(
272
+ fluorescent_2d_to_3d_transfer_function
273
+ )
274
+
275
+ # Get yx_shape for chunk sizes
276
+ yx_shape = zyx_shape[1:]
277
+
278
+ # Save singular system components
279
+ dataset.create_image(
280
+ "singular_system_U",
281
+ U.cpu().numpy()[None, ...],
282
+ chunks=(1, 1, 1, yx_shape[0], yx_shape[1]),
283
+ )
284
+ dataset.create_image(
285
+ "singular_system_S",
286
+ S.cpu().numpy()[None, None, ...],
287
+ chunks=(1, 1, 1, yx_shape[0], yx_shape[1]),
288
+ )
289
+ dataset.create_image(
290
+ "singular_system_Vh",
291
+ Vh.cpu().numpy()[None, ...],
292
+ chunks=(1, 1, zyx_shape[0], yx_shape[0], yx_shape[1]),
293
+ )
294
+
295
+ elif settings.reconstruction_dimension == 3:
296
+ # Remove unused parameters for 3D
297
+ settings_dict.pop("z_focus_offset")
298
+
299
+ # Calculate transfer functions
300
+ optical_transfer_function = (
301
+ isotropic_fluorescent_thick_3d.calculate_transfer_function(
302
+ zyx_shape=zyx_shape,
303
+ **settings_dict,
304
+ )
305
+ )
306
+ # Save
307
+ dataset.create_image(
308
+ "optical_transfer_function",
309
+ optical_transfer_function.cpu().numpy()[None, None, ...],
310
+ chunks=(1, 1, 1, zyx_shape[1], zyx_shape[2]),
311
+ )
312
+
313
+
314
+ def compute_transfer_function_cli(
315
+ input_position_dirpath: Path,
316
+ config_filepath: Path,
317
+ output_dirpath: Path,
318
+ ) -> None:
319
+ """CLI command to compute the transfer function given a configuration file path
320
+ and a desired output path.
321
+ """
322
+
323
+ # Load config file
324
+ settings = utils.yaml_to_model(config_filepath, ReconstructionSettings)
325
+
326
+ echo_headline(
327
+ f"Generating transfer functions and storing in {output_dirpath}\n"
328
+ )
329
+
330
+ # Read shape from input dataset
331
+ input_dataset = open_ome_zarr(
332
+ input_position_dirpath, layout="fov", mode="r"
333
+ )
334
+ zyx_shape = input_dataset.data.shape[
335
+ 2:
336
+ ] # only loads a single position "0"
337
+
338
+ # Check input channel names
339
+ if not set(settings.input_channel_names).issubset(
340
+ input_dataset.channel_names
341
+ ):
342
+ raise ValueError(
343
+ f"Each of the input_channel_names = {settings.input_channel_names} in {config_filepath} must appear in the dataset {input_position_dirpaths[0]} which currently contains channel_names = {input_dataset.channel_names}."
344
+ )
345
+
346
+ # Find in-focus slices for 2D reconstruction in "auto" mode
347
+ if (
348
+ settings.phase is not None
349
+ and settings.reconstruction_dimension == 2
350
+ and settings.phase.transfer_function.z_focus_offset == "auto"
351
+ ):
352
+
353
+ c_idx = input_dataset.get_channel_index(
354
+ settings.input_channel_names[0]
355
+ )
356
+ zyx_array = input_dataset["0"][0, c_idx]
357
+
358
+ in_focus_index = focus.focus_from_transverse_band(
359
+ zyx_array,
360
+ NA_det=settings.phase.transfer_function.numerical_aperture_detection,
361
+ lambda_ill=settings.phase.transfer_function.wavelength_illumination,
362
+ pixel_size=settings.phase.transfer_function.yx_pixel_size,
363
+ mode="min",
364
+ polynomial_fit_order=4,
365
+ )
366
+
367
+ z_focus_offset = in_focus_index - (zyx_shape[0] // 2)
368
+ settings.phase.transfer_function.z_focus_offset = z_focus_offset
369
+ print("Found z_focus_offset:", z_focus_offset)
370
+
371
+ # Prepare output dataset
372
+ num_channels = (
373
+ 2 if settings.reconstruction_dimension == 2 else 1
374
+ ) # space for SVD
375
+ output_dataset = open_ome_zarr(
376
+ output_dirpath,
377
+ layout="fov",
378
+ mode="w",
379
+ channel_names=num_channels * ["None"],
380
+ )
381
+
382
+ # Pass settings to appropriate calculate_transfer_function and save
383
+ if settings.birefringence is not None:
384
+ generate_and_save_birefringence_transfer_function(
385
+ settings, output_dataset
386
+ )
387
+ if settings.phase is not None:
388
+ generate_and_save_phase_transfer_function(
389
+ settings, output_dataset, zyx_shape
390
+ )
391
+ if settings.fluorescence is not None:
392
+ generate_and_save_fluorescence_transfer_function(
393
+ settings, output_dataset, zyx_shape
394
+ )
395
+ if settings.birefringence is not None and settings.phase is not None:
396
+ generate_and_save_vector_birefringence_transfer_function(
397
+ settings, output_dataset, zyx_shape
398
+ )
399
+
400
+ # Write settings to metadata
401
+ output_dataset.zattrs["settings"] = settings.model_dump()
402
+
403
+ echo_headline(f"Closing {output_dirpath}\n")
404
+ output_dataset.close()
405
+
406
+ echo_headline(
407
+ f"Recreate this transfer function with:\n$ waveorder compute-tf {input_position_dirpaths} -c {config_filepath} -o {output_dirpath}"
408
+ )
409
+
410
+
411
+ @click.command("compute-tf")
412
+ @input_position_dirpaths()
413
+ @config_filepath()
414
+ @output_dirpath()
415
+ def _compute_transfer_function_cli(
416
+ input_position_dirpaths: list[Path],
417
+ config_filepath: Path,
418
+ output_dirpath: Path,
419
+ ) -> None:
420
+ """
421
+ Compute a transfer function using a dataset and configuration file.
422
+
423
+ Calculates the transfer function based on the shape of the first position
424
+ in the list `input-position-dirpaths`.
425
+
426
+ See /examples for example configuration files.
427
+
428
+ >> waveorder compute-tf -i ./input.zarr/0/0/0 -c ./examples/birefringence.yml -o ./transfer_function.zarr
429
+ """
430
+ compute_transfer_function_cli(
431
+ input_position_dirpaths[0], config_filepath, output_dirpath
432
+ )
@@ -0,0 +1,58 @@
1
+ import sys
2
+
3
+ import click
4
+
5
+ try:
6
+ from waveorder.plugin import tab_recon
7
+ except:
8
+ pass
9
+
10
+ try:
11
+ from qtpy.QtWidgets import QApplication, QStyle, QVBoxLayout, QWidget
12
+ except:
13
+ pass
14
+
15
+ try:
16
+ import qdarktheme # pip install pyqtdarktheme==2.1.0 --ignore-requires-python
17
+ except:
18
+ pass
19
+
20
+ PLUGIN_NAME = "waveorder: Computational Toolkit for Label-Free Imaging"
21
+ PLUGIN_ICON = "🔬"
22
+
23
+
24
+ @click.command()
25
+ def gui():
26
+ """GUI for waveorder: Computational Toolkit for Label-Free Imaging"""
27
+
28
+ app = QApplication(sys.argv)
29
+ app.setStyle(
30
+ "Fusion"
31
+ ) # Other options: "Fusion", "Windows", "macOS", "WindowsVista"
32
+ try:
33
+ qdarktheme.setup_theme("dark")
34
+ except Exception as e:
35
+ print(e.args)
36
+ pass
37
+ window = MainWindow()
38
+ window.setWindowTitle(PLUGIN_ICON + " " + PLUGIN_NAME + " " + PLUGIN_ICON)
39
+
40
+ pixmapi = getattr(QStyle.StandardPixmap, "SP_TitleBarMenuButton")
41
+ icon = app.style().standardIcon(pixmapi)
42
+ window.setWindowIcon(icon)
43
+
44
+ window.show()
45
+ sys.exit(app.exec())
46
+
47
+
48
+ class MainWindow(QWidget):
49
+ def __init__(self):
50
+ super().__init__()
51
+ recon_tab = tab_recon.Ui_ReconTab_Form(stand_alone=True)
52
+ layout = QVBoxLayout()
53
+ self.setLayout(layout)
54
+ layout.addWidget(recon_tab.recon_tab_mainScrollArea)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ gui()
waveorder/cli/main.py ADDED
@@ -0,0 +1,39 @@
1
+ import click
2
+
3
+ from waveorder.cli.apply_inverse_transfer_function import (
4
+ _apply_inverse_transfer_function_cli,
5
+ )
6
+ from waveorder.cli.compute_transfer_function import (
7
+ _compute_transfer_function_cli,
8
+ )
9
+ from waveorder.cli.reconstruct import _reconstruct_cli
10
+
11
+ try:
12
+ from waveorder.cli.gui_widget import gui
13
+ except:
14
+ pass
15
+
16
+ CONTEXT = {"help_option_names": ["-h", "--help"]}
17
+
18
+
19
+ # `waveorder -h` will show subcommands in the order they are added
20
+ class NaturalOrderGroup(click.Group):
21
+ def list_commands(self, ctx):
22
+ return self.commands.keys()
23
+
24
+
25
+ @click.group(context_settings=CONTEXT, cls=NaturalOrderGroup)
26
+ def cli():
27
+ """\033[92mwaveorder: Computational Toolkit for Label-Free Imaging\033[0m\n"""
28
+
29
+
30
+ cli.add_command(_reconstruct_cli)
31
+ cli.add_command(_compute_transfer_function_cli)
32
+ cli.add_command(_apply_inverse_transfer_function_cli)
33
+ try:
34
+ cli.add_command(gui)
35
+ except:
36
+ pass
37
+
38
+ if __name__ == "__main__":
39
+ cli()
@@ -0,0 +1,163 @@
1
+ import shutil
2
+ import sys
3
+ import time
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import submitit
8
+
9
+
10
+ def _move_cursor_up(n_lines, do_print=True):
11
+ if do_print:
12
+ sys.stdout.write("\033[F" * n_lines)
13
+
14
+
15
+ def _print_status(
16
+ jobs, position_dirpaths, elapsed_list, print_indices=None, do_print=True
17
+ ):
18
+
19
+ columns = [15, 30, 40, 50]
20
+
21
+ # header
22
+ if do_print:
23
+ sys.stdout.write(
24
+ "\033[K" # clear line
25
+ "\033[96mID" # cyan
26
+ f"\033[{columns[0]}G WELL "
27
+ f"\033[{columns[1]}G STATUS "
28
+ f"\033[{columns[2]}G NODE "
29
+ f"\033[{columns[2]}G ELAPSED\n"
30
+ )
31
+
32
+ if print_indices is None:
33
+ print_indices = range(len(jobs))
34
+
35
+ complete_count = 0
36
+
37
+ for i, (job, position_dirpath) in enumerate(zip(jobs, position_dirpaths)):
38
+ try:
39
+ node_name = job.get_info()["NodeList"] # slowest, so do this first
40
+ except:
41
+ node_name = "SUBMITTED"
42
+
43
+ if job.state == "COMPLETED":
44
+ color = "\033[32m" # green
45
+ complete_count += 1
46
+ elif job.state == "RUNNING":
47
+ color = "\033[93m" # yellow
48
+ elapsed_list[i] += 1 # inexact timing
49
+ else:
50
+ color = "\033[91m" # red
51
+
52
+ if i in print_indices:
53
+ if do_print:
54
+ sys.stdout.write(
55
+ f"\033[K" # clear line
56
+ f"{color}{job.job_id}"
57
+ f"\033[{columns[0]}G {'/'.join(position_dirpath.parts[-3:])}"
58
+ f"\033[{columns[1]}G {job.state}"
59
+ f"\033[{columns[2]}G {node_name}"
60
+ f"\033[{columns[3]}G {elapsed_list[i]} s\n"
61
+ )
62
+ sys.stdout.flush()
63
+ if do_print:
64
+ print(
65
+ f"\033[32m{complete_count}/{len(jobs)} jobs complete. "
66
+ "<ctrl+z> to move monitor to background. "
67
+ "<ctrl+c> twice to cancel jobs."
68
+ )
69
+
70
+ return elapsed_list
71
+
72
+
73
+ def _get_jobs_to_print(jobs, num_to_print):
74
+ job_indices_to_print = []
75
+
76
+ # if number of jobs is smaller than termanal size, print all
77
+ if len(jobs) <= num_to_print:
78
+ return list(range(len(jobs)))
79
+
80
+ # prioritize incomplete jobs
81
+ for i, job in enumerate(jobs):
82
+ if not job.done():
83
+ job_indices_to_print.append(i)
84
+ if len(job_indices_to_print) == num_to_print:
85
+ return job_indices_to_print
86
+
87
+ # fill in the rest with complete jobs
88
+ for i, job in enumerate(jobs):
89
+ job_indices_to_print.append(i)
90
+ if len(job_indices_to_print) == num_to_print:
91
+ return job_indices_to_print
92
+
93
+ # shouldn't reach here
94
+ return job_indices_to_print
95
+
96
+
97
+ def monitor_jobs(
98
+ jobs: list[submitit.Job], position_dirpaths: list[Path], do_print=True
99
+ ):
100
+ """Displays the status of a list of submitit jobs with corresponding paths.
101
+
102
+ Parameters
103
+ ----------
104
+ jobs : list[submitit.Job]
105
+ List of submitit jobs
106
+ position_dirpaths : list[Path]
107
+ List of corresponding position paths
108
+ """
109
+ NON_JOB_LINES = 3
110
+
111
+ if not len(jobs) == len(position_dirpaths):
112
+ raise ValueError(
113
+ "The number of jobs and position_dirpaths should be the same."
114
+ )
115
+
116
+ elapsed_list = [0] * len(jobs) # timer for each job
117
+
118
+ # print all jobs once if terminal is too small
119
+ if shutil.get_terminal_size().lines - NON_JOB_LINES < len(jobs):
120
+ _print_status(jobs, position_dirpaths, elapsed_list, do_print=do_print)
121
+
122
+ # main monitor loop
123
+ try:
124
+ while not all(job.done() for job in jobs):
125
+ terminal_lines = shutil.get_terminal_size().lines
126
+ num_jobs_to_print = np.min(
127
+ [terminal_lines - NON_JOB_LINES, len(jobs)]
128
+ )
129
+
130
+ job_indices_to_print = _get_jobs_to_print(jobs, num_jobs_to_print)
131
+
132
+ elapsed_list = _print_status(
133
+ jobs,
134
+ position_dirpaths,
135
+ elapsed_list,
136
+ job_indices_to_print,
137
+ do_print,
138
+ )
139
+
140
+ time.sleep(1)
141
+ _move_cursor_up(num_jobs_to_print + 2, do_print)
142
+
143
+ # Print final status
144
+ time.sleep(1)
145
+ _print_status(jobs, position_dirpaths, elapsed_list, do_print=do_print)
146
+
147
+ # cancel jobs if ctrl+c
148
+ except KeyboardInterrupt:
149
+ for job in jobs:
150
+ job.cancel()
151
+ print("All jobs cancelled.\033[97m")
152
+
153
+ # Print STDOUT and STDERR for first incomplete job
154
+ incomplete_count = 0
155
+ for job in jobs:
156
+ if not job.done():
157
+ if incomplete_count == 0:
158
+ print("\033[32mSTDOUT")
159
+ print(job.stdout())
160
+ print("\033[91mSTDERR")
161
+ print(job.stderr())
162
+
163
+ print("\033[97m") # print white