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,328 @@
1
+ """
2
+ This module converts GUI-level reconstruction calls into library calls
3
+ """
4
+
5
+ import numpy as np
6
+ import torch
7
+
8
+ from waveorder.cli.settings import FluorescenceSettings, PhaseSettings
9
+ from waveorder.models import (
10
+ inplane_oriented_thick_pol3d,
11
+ inplane_oriented_thick_pol3d_vector,
12
+ isotropic_fluorescent_thick_3d,
13
+ isotropic_fluorescent_thin_3d,
14
+ isotropic_thin_3d,
15
+ phase_thick_3d,
16
+ )
17
+ from waveorder.stokes import _s12_to_orientation, stokes_after_adr
18
+
19
+
20
+ def radians_to_nanometers(retardance_rad, wavelength_illumination_um):
21
+ """
22
+ waveorder returns retardance in radians, while waveorder displays and saves
23
+ retardance in nanometers. This function converts from radians to nanometers
24
+ using the illumination wavelength (which is internally handled in um
25
+ in waveorder).
26
+ """
27
+ return retardance_rad * wavelength_illumination_um * 1e3 / (2 * np.pi)
28
+
29
+
30
+ def birefringence(
31
+ czyx_data,
32
+ cyx_no_sample_data,
33
+ wavelength_illumination,
34
+ recon_dim,
35
+ biref_inverse_dict,
36
+ transfer_function_dataset,
37
+ ):
38
+ # Load transfer function
39
+ intensity_to_stokes_matrix = torch.tensor(
40
+ transfer_function_dataset["intensity_to_stokes_matrix"][0, 0, 0]
41
+ )
42
+
43
+ # Apply reconstruction
44
+ # (retardance, orientation, transmittance, depolarization)
45
+ reconstructed_parameters = (
46
+ inplane_oriented_thick_pol3d.apply_inverse_transfer_function(
47
+ czyx_data,
48
+ intensity_to_stokes_matrix,
49
+ cyx_no_sample_data=cyx_no_sample_data,
50
+ project_stokes_to_2d=(recon_dim == 2),
51
+ **biref_inverse_dict,
52
+ )
53
+ )
54
+
55
+ # Convert retardance
56
+ retardance = radians_to_nanometers(
57
+ reconstructed_parameters[0], wavelength_illumination
58
+ )
59
+
60
+ return torch.stack((retardance,) + reconstructed_parameters[1:])
61
+
62
+
63
+ def phase(
64
+ czyx_data,
65
+ recon_dim,
66
+ settings_phase: PhaseSettings,
67
+ transfer_function_dataset,
68
+ ):
69
+ # [phase only, 2]
70
+ if recon_dim == 2:
71
+ # Load transfer functions
72
+ U = torch.from_numpy(transfer_function_dataset["singular_system_U"][0])
73
+ S = torch.from_numpy(
74
+ transfer_function_dataset["singular_system_S"][0, 0]
75
+ )
76
+ Vh = torch.from_numpy(
77
+ transfer_function_dataset["singular_system_Vh"][0]
78
+ )
79
+
80
+ # Apply
81
+ (
82
+ absorption_yx,
83
+ phase_yx,
84
+ ) = isotropic_thin_3d.apply_inverse_transfer_function(
85
+ czyx_data[0],
86
+ (U, S, Vh),
87
+ **settings_phase.apply_inverse.model_dump(),
88
+ )
89
+ # Stack to C1YX
90
+ output = phase_yx[None, None]
91
+ # TODO: Write phase and absorption to CZYX
92
+ # torch.stack((phase_yx[None], absorption_yx[None]))
93
+
94
+ # [phase only, 3]
95
+ elif recon_dim == 3:
96
+ # Load transfer functions
97
+ real_potential_transfer_function = torch.tensor(
98
+ transfer_function_dataset["real_potential_transfer_function"][0, 0]
99
+ )
100
+ imaginary_potential_transfer_function = torch.tensor(
101
+ transfer_function_dataset["imaginary_potential_transfer_function"][
102
+ 0, 0
103
+ ]
104
+ )
105
+
106
+ # Apply
107
+ output = phase_thick_3d.apply_inverse_transfer_function(
108
+ czyx_data[0],
109
+ real_potential_transfer_function,
110
+ imaginary_potential_transfer_function,
111
+ z_padding=settings_phase.transfer_function.z_padding,
112
+ **settings_phase.apply_inverse.model_dump(),
113
+ )
114
+
115
+ # Pad to CZYX
116
+ while output.ndim != 4:
117
+ output = torch.unsqueeze(output, 0)
118
+
119
+ return output
120
+
121
+
122
+ def birefringence_and_phase(
123
+ czyx_data,
124
+ cyx_no_sample_data,
125
+ wavelength_illumination,
126
+ recon_dim,
127
+ biref_inverse_dict,
128
+ settings_phase: PhaseSettings,
129
+ transfer_function_dataset,
130
+ ):
131
+ # Load birefringence transfer function
132
+ intensity_to_stokes_matrix = torch.tensor(
133
+ transfer_function_dataset["intensity_to_stokes_matrix"][0, 0, 0]
134
+ )
135
+
136
+ # [biref and phase, 2]
137
+ if recon_dim == 2:
138
+ # Load transfer functions
139
+ U = torch.from_numpy(
140
+ transfer_function_dataset["vector_singular_system_U"][0]
141
+ )
142
+ S = torch.from_numpy(
143
+ transfer_function_dataset["vector_singular_system_S"][0, 0]
144
+ )
145
+ Vh = torch.from_numpy(
146
+ transfer_function_dataset["vector_singular_system_Vh"][0]
147
+ )
148
+
149
+ # Apply
150
+ reconstructed_parameters_2d = (
151
+ inplane_oriented_thick_pol3d.apply_inverse_transfer_function(
152
+ czyx_data,
153
+ intensity_to_stokes_matrix,
154
+ cyx_no_sample_data=cyx_no_sample_data,
155
+ project_stokes_to_2d=True,
156
+ **biref_inverse_dict,
157
+ )
158
+ )
159
+
160
+ reconstructed_parameters_3d = (
161
+ inplane_oriented_thick_pol3d.apply_inverse_transfer_function(
162
+ czyx_data,
163
+ intensity_to_stokes_matrix,
164
+ cyx_no_sample_data=cyx_no_sample_data,
165
+ project_stokes_to_2d=False,
166
+ **biref_inverse_dict,
167
+ )
168
+ )
169
+
170
+ brightfield_3d = reconstructed_parameters_3d[2]
171
+
172
+ (
173
+ _,
174
+ yx_phase,
175
+ ) = isotropic_thin_3d.apply_inverse_transfer_function(
176
+ brightfield_3d,
177
+ (U, S, Vh),
178
+ **settings_phase.apply_inverse.model_dump(),
179
+ )
180
+
181
+ # Convert retardance
182
+ retardance = radians_to_nanometers(
183
+ reconstructed_parameters_2d[0], wavelength_illumination
184
+ )
185
+
186
+ output = torch.stack(
187
+ (retardance,)
188
+ + reconstructed_parameters_2d[1:]
189
+ + (torch.unsqueeze(yx_phase, 0),)
190
+ ) # CZYX
191
+
192
+ # [biref and phase, 3]
193
+ elif recon_dim == 3:
194
+ # Load phase transfer functions
195
+ intensity_to_stokes_matrix = torch.tensor(
196
+ transfer_function_dataset["intensity_to_stokes_matrix"][0, 0, 0]
197
+ )
198
+ # Load transfer functions
199
+ real_potential_transfer_function = torch.tensor(
200
+ transfer_function_dataset["real_potential_transfer_function"][0, 0]
201
+ )
202
+ imaginary_potential_transfer_function = torch.tensor(
203
+ transfer_function_dataset["imaginary_potential_transfer_function"][
204
+ 0, 0
205
+ ]
206
+ )
207
+
208
+ # Apply
209
+ reconstructed_parameters_3d = (
210
+ inplane_oriented_thick_pol3d.apply_inverse_transfer_function(
211
+ czyx_data,
212
+ intensity_to_stokes_matrix,
213
+ cyx_no_sample_data=cyx_no_sample_data,
214
+ project_stokes_to_2d=False,
215
+ **biref_inverse_dict,
216
+ )
217
+ )
218
+
219
+ brightfield_3d = reconstructed_parameters_3d[2]
220
+
221
+ zyx_phase = phase_thick_3d.apply_inverse_transfer_function(
222
+ brightfield_3d,
223
+ real_potential_transfer_function,
224
+ imaginary_potential_transfer_function,
225
+ z_padding=settings_phase.transfer_function.z_padding,
226
+ **settings_phase.apply_inverse.model_dump(),
227
+ )
228
+
229
+ # Convert retardance
230
+ retardance = radians_to_nanometers(
231
+ reconstructed_parameters_3d[0], wavelength_illumination
232
+ )
233
+ # Load singular system
234
+ U = torch.tensor(
235
+ np.array(transfer_function_dataset["vector_singular_system_U"])
236
+ )
237
+ S = torch.tensor(
238
+ np.array(transfer_function_dataset["vector_singular_system_S"][0])
239
+ )
240
+ Vh = torch.tensor(
241
+ np.array(transfer_function_dataset["vector_singular_system_Vh"])
242
+ )
243
+ singular_system = (U, S, Vh)
244
+
245
+ # Convert retardance and orientation to stokes
246
+ stokes = stokes_after_adr(*reconstructed_parameters_3d)
247
+
248
+ stokes = torch.nan_to_num_(
249
+ torch.stack(stokes), nan=0.0
250
+ ) # very rare nans from previous like
251
+
252
+ # Apply reconstruction
253
+ joint_recon_params = inplane_oriented_thick_pol3d_vector.apply_inverse_transfer_function(
254
+ szyx_data=stokes,
255
+ singular_system=singular_system,
256
+ intensity_to_stokes_matrix=None,
257
+ **settings_phase.apply_inverse.model_dump(),
258
+ )
259
+
260
+ new_ret = (
261
+ joint_recon_params[1] ** 2 + joint_recon_params[2] ** 2
262
+ ) ** (0.5)
263
+ new_ori = _s12_to_orientation(
264
+ joint_recon_params[1], -joint_recon_params[2]
265
+ )
266
+
267
+ # Convert stokes to retardance and orientation
268
+ # new_ret, new_ori, _ = estimate_ar_from_stokes012(*joint_recon_params)
269
+
270
+ # Convert retardance
271
+ new_ret_nm = radians_to_nanometers(new_ret, wavelength_illumination)
272
+
273
+ # Save
274
+ output = torch.stack(
275
+ (retardance,)
276
+ + reconstructed_parameters_3d[1:]
277
+ + (zyx_phase,)
278
+ + (new_ret_nm,)
279
+ + (new_ori,)
280
+ + (joint_recon_params[0],)
281
+ )
282
+ return output
283
+
284
+
285
+ def fluorescence(
286
+ czyx_data,
287
+ recon_dim,
288
+ settings_fluorescence: FluorescenceSettings,
289
+ transfer_function_dataset,
290
+ ):
291
+ # [fluo, 2]
292
+ if recon_dim == 2:
293
+ # Load transfer functions for 2D thin fluorescence reconstruction
294
+ U = torch.from_numpy(transfer_function_dataset["singular_system_U"][0])
295
+ S = torch.from_numpy(
296
+ transfer_function_dataset["singular_system_S"][0, 0]
297
+ )
298
+ Vh = torch.from_numpy(
299
+ transfer_function_dataset["singular_system_Vh"][0]
300
+ )
301
+
302
+ # Apply 2D fluorescence reconstruction
303
+ output = isotropic_fluorescent_thin_3d.apply_inverse_transfer_function(
304
+ czyx_data[0],
305
+ (U, S, Vh),
306
+ **settings_fluorescence.apply_inverse.model_dump(),
307
+ )
308
+ # [fluo, 3]
309
+ elif recon_dim == 3:
310
+ # Load transfer functions
311
+ optical_transfer_function = torch.tensor(
312
+ transfer_function_dataset["optical_transfer_function"][0, 0]
313
+ )
314
+
315
+ # Apply
316
+ output = (
317
+ isotropic_fluorescent_thick_3d.apply_inverse_transfer_function(
318
+ czyx_data[0],
319
+ optical_transfer_function,
320
+ settings_fluorescence.transfer_function.z_padding,
321
+ **settings_fluorescence.apply_inverse.model_dump(),
322
+ )
323
+ )
324
+ # Pad to CZYX
325
+ while output.ndim != 4:
326
+ output = torch.unsqueeze(output, 0)
327
+
328
+ return output
@@ -0,0 +1,379 @@
1
+ import itertools
2
+ import warnings
3
+ from functools import partial
4
+ from pathlib import Path
5
+
6
+ import click
7
+ import numpy as np
8
+ import torch
9
+ import torch.multiprocessing as mp
10
+ from iohub import open_ome_zarr
11
+
12
+ from waveorder.cli import apply_inverse_models
13
+ from waveorder.cli.parsing import (
14
+ config_filepath,
15
+ input_position_dirpaths,
16
+ output_dirpath,
17
+ processes_option,
18
+ transfer_function_dirpath,
19
+ )
20
+ from waveorder.cli.printing import echo_headline, echo_settings
21
+ from waveorder.cli.settings import ReconstructionSettings
22
+ from waveorder.cli.utils import (
23
+ apply_inverse_to_zyx_and_save,
24
+ create_empty_hcs_zarr,
25
+ generate_valid_position_key,
26
+ is_single_position_store,
27
+ )
28
+ from waveorder.io import utils
29
+
30
+
31
+ def _check_background_consistency(
32
+ background_shape, data_shape, input_channel_names
33
+ ):
34
+ data_cyx_shape = (len(input_channel_names),) + data_shape[3:]
35
+ if background_shape != data_cyx_shape:
36
+ raise ValueError(
37
+ f"Background shape {background_shape} does not match data shape {data_cyx_shape}"
38
+ )
39
+
40
+
41
+ def get_reconstruction_output_metadata(position_path: Path, config_path: Path):
42
+ # Get non-OME-Zarr plate-level metadata if it's available
43
+ plate_metadata = {}
44
+ try:
45
+ input_plate = open_ome_zarr(
46
+ position_path.parent.parent.parent, mode="r"
47
+ )
48
+ plate_metadata = dict(input_plate.zattrs)
49
+ plate_metadata.pop("plate")
50
+ except (RuntimeError, FileNotFoundError):
51
+ warnings.warn(
52
+ "Position is not part of a plate...no plate metadata will be copied."
53
+ )
54
+
55
+ # Load the first position to infer dataset information
56
+ input_dataset = open_ome_zarr(str(position_path), mode="r")
57
+ T, _, Z, Y, X = input_dataset.data.shape
58
+
59
+ settings = utils.yaml_to_model(config_path, ReconstructionSettings)
60
+
61
+ # Simplify important settings names
62
+ recon_biref = settings.birefringence is not None
63
+ recon_phase = settings.phase is not None
64
+ recon_fluo = settings.fluorescence is not None
65
+ recon_dim = settings.reconstruction_dimension
66
+
67
+ # Prepare output dataset
68
+ channel_names = []
69
+ if recon_biref:
70
+ channel_names.append("Retardance")
71
+ channel_names.append("Orientation")
72
+ channel_names.append("BF")
73
+ channel_names.append("Pol")
74
+ if recon_phase:
75
+ if recon_dim == 2:
76
+ channel_names.append("Phase2D")
77
+ # channel_names.append("Absorption2D")
78
+ elif recon_dim == 3:
79
+ channel_names.append("Phase3D")
80
+ if recon_biref and recon_phase:
81
+ channel_names.append("Retardance_Joint_Decon")
82
+ channel_names.append("Orientation_Joint_Decon")
83
+ channel_names.append("Phase_Joint_Decon")
84
+ if recon_fluo:
85
+ fluor_name = settings.input_channel_names[0]
86
+ if recon_dim == 2:
87
+ channel_names.append(fluor_name + "_Density2D")
88
+ elif recon_dim == 3:
89
+ channel_names.append(fluor_name + "_Density3D")
90
+
91
+ if recon_dim == 2:
92
+ output_z_shape = 1
93
+ elif recon_dim == 3:
94
+ output_z_shape = input_dataset.data.shape[2]
95
+
96
+ return {
97
+ "shape": (T, len(channel_names), output_z_shape, Y, X),
98
+ "chunks": (1, 1, 1, Y, X),
99
+ "scale": input_dataset.scale,
100
+ "channel_names": channel_names,
101
+ "dtype": np.float32,
102
+ "plate_metadata": plate_metadata,
103
+ }
104
+
105
+
106
+ def apply_inverse_transfer_function_single_position(
107
+ input_position_dirpath: Path,
108
+ transfer_function_dirpath: Path,
109
+ config_filepath: Path,
110
+ output_position_dirpath: Path,
111
+ num_processes,
112
+ output_channel_names: list[str],
113
+ ) -> None:
114
+
115
+ echo_headline("\nStarting reconstruction...")
116
+
117
+ # Load datasets
118
+ transfer_function_dataset = open_ome_zarr(transfer_function_dirpath)
119
+ input_dataset = open_ome_zarr(input_position_dirpath)
120
+ output_dataset = open_ome_zarr(output_position_dirpath, mode="r+")
121
+
122
+ # Load config file
123
+ settings = utils.yaml_to_model(config_filepath, ReconstructionSettings)
124
+
125
+ # Check input channel names
126
+ if not set(settings.input_channel_names).issubset(
127
+ input_dataset.channel_names
128
+ ):
129
+ raise ValueError(
130
+ f"Each of the input_channel_names = {settings.input_channel_names} in {config_filepath} must appear in the dataset {input_position_dirpath} which currently contains channel_names = {input_dataset.channel_names}."
131
+ )
132
+
133
+ # Find input channel indices
134
+ input_channel_indices = []
135
+ for input_channel_name in settings.input_channel_names:
136
+ input_channel_indices.append(
137
+ input_dataset.channel_names.index(input_channel_name)
138
+ )
139
+
140
+ # Find output channel indices
141
+ output_channel_indices = []
142
+ for output_channel_name in output_channel_names:
143
+ output_channel_indices.append(
144
+ output_dataset.channel_names.index(output_channel_name)
145
+ )
146
+
147
+ # Find time indices
148
+ if settings.time_indices == "all":
149
+ time_indices = range(input_dataset.data.shape[0])
150
+ elif isinstance(settings.time_indices, list):
151
+ time_indices = settings.time_indices
152
+ elif isinstance(settings.time_indices, int):
153
+ time_indices = [settings.time_indices]
154
+
155
+ # Check for invalid times
156
+ time_ubound = input_dataset.data.shape[0] - 1
157
+ if np.max(time_indices) > time_ubound:
158
+ raise ValueError(
159
+ f"time_indices = {time_indices} includes a time index beyond the maximum index of the dataset = {time_ubound}"
160
+ )
161
+
162
+ # Simplify important settings names
163
+ recon_biref = settings.birefringence is not None
164
+ recon_phase = settings.phase is not None
165
+ recon_fluo = settings.fluorescence is not None
166
+ recon_dim = settings.reconstruction_dimension
167
+
168
+ # Prepare birefringence parameters
169
+ if settings.birefringence is not None:
170
+ # settings.birefringence has more parameters than waveorder needs,
171
+ # so this section converts the settings to a dict and separates the
172
+ # waveorder parameters (biref_inverse_dict) from the waveorder
173
+ # parameters (cyx_no_sample_data, and wavelength_illumination)
174
+ biref_inverse_dict = settings.birefringence.apply_inverse.model_dump()
175
+
176
+ # Resolve background path into array
177
+ background_path = biref_inverse_dict.pop("background_path")
178
+ if background_path != "":
179
+ cyx_no_sample_data = utils.load_background(background_path)
180
+ _check_background_consistency(
181
+ cyx_no_sample_data.shape,
182
+ input_dataset.data.shape,
183
+ settings.input_channel_names,
184
+ )
185
+ else:
186
+ cyx_no_sample_data = None
187
+
188
+ # Get illumination wavelength for retardance radians -> nanometers conversion
189
+ biref_wavelength_illumination = biref_inverse_dict.pop(
190
+ "wavelength_illumination"
191
+ )
192
+
193
+ # Prepare the apply_inverse_model_function and its arguments
194
+
195
+ # [biref only]
196
+ if recon_biref and (not recon_phase):
197
+ echo_headline("Reconstructing birefringence with settings:")
198
+ echo_settings(settings.birefringence)
199
+
200
+ # Setup parameters for apply_inverse_to_zyx_and_save
201
+ apply_inverse_model_function = apply_inverse_models.birefringence
202
+ apply_inverse_args = {
203
+ "cyx_no_sample_data": cyx_no_sample_data,
204
+ "wavelength_illumination": biref_wavelength_illumination,
205
+ "recon_dim": recon_dim,
206
+ "biref_inverse_dict": biref_inverse_dict,
207
+ "transfer_function_dataset": transfer_function_dataset,
208
+ }
209
+
210
+ # [phase only]
211
+ if recon_phase and (not recon_biref):
212
+ echo_headline("Reconstructing phase with settings:")
213
+ echo_settings(settings.phase.apply_inverse)
214
+
215
+ # Setup parameters for apply_inverse_to_zyx_and_save
216
+ apply_inverse_model_function = apply_inverse_models.phase
217
+ apply_inverse_args = {
218
+ "recon_dim": recon_dim,
219
+ "settings_phase": settings.phase,
220
+ "transfer_function_dataset": transfer_function_dataset,
221
+ }
222
+
223
+ # [biref and phase]
224
+ if recon_biref and recon_phase:
225
+ echo_headline("Reconstructing birefringence and phase with settings:")
226
+ echo_settings(settings.birefringence.apply_inverse)
227
+ echo_settings(settings.phase.apply_inverse)
228
+
229
+ # Setup parameters for apply_inverse_to_zyx_and_save
230
+ apply_inverse_model_function = (
231
+ apply_inverse_models.birefringence_and_phase
232
+ )
233
+ apply_inverse_args = {
234
+ "cyx_no_sample_data": cyx_no_sample_data,
235
+ "wavelength_illumination": biref_wavelength_illumination,
236
+ "recon_dim": recon_dim,
237
+ "biref_inverse_dict": biref_inverse_dict,
238
+ "settings_phase": settings.phase,
239
+ "transfer_function_dataset": transfer_function_dataset,
240
+ }
241
+
242
+ # [fluo]
243
+ if recon_fluo:
244
+ echo_headline("Reconstructing fluorescence with settings:")
245
+ echo_settings(settings.fluorescence.apply_inverse)
246
+
247
+ # Setup parameters for apply_inverse_to_zyx_and_save
248
+ apply_inverse_model_function = apply_inverse_models.fluorescence
249
+ apply_inverse_args = {
250
+ "recon_dim": recon_dim,
251
+ "settings_fluorescence": settings.fluorescence,
252
+ "transfer_function_dataset": transfer_function_dataset,
253
+ }
254
+
255
+ # Make the partial function for apply inverse
256
+ partial_apply_inverse_to_zyx_and_save = partial(
257
+ apply_inverse_to_zyx_and_save,
258
+ apply_inverse_model_function,
259
+ input_dataset,
260
+ output_position_dirpath,
261
+ input_channel_indices,
262
+ output_channel_indices,
263
+ **apply_inverse_args,
264
+ )
265
+
266
+ # Multiprocessing logic
267
+ if num_processes > 1:
268
+ # Loop through T, processing and writing as we go
269
+ click.echo(
270
+ f"\nStarting multiprocess pool with {num_processes} processes"
271
+ )
272
+ with mp.Pool(num_processes) as p:
273
+ p.starmap(
274
+ partial_apply_inverse_to_zyx_and_save,
275
+ itertools.product(time_indices),
276
+ )
277
+ else:
278
+ for t_idx in time_indices:
279
+ partial_apply_inverse_to_zyx_and_save(t_idx)
280
+
281
+ # Save metadata at position level
282
+ output_dataset.zattrs["settings"] = settings.model_dump()
283
+
284
+ echo_headline(f"Closing {output_position_dirpath}\n")
285
+
286
+ output_dataset.close()
287
+ transfer_function_dataset.close()
288
+ input_dataset.close()
289
+
290
+ echo_headline(
291
+ f"Recreate this reconstruction with:\n$ waveorder apply-inv-tf {input_position_dirpath} {transfer_function_dirpath} -c {config_filepath} -o {output_position_dirpath}"
292
+ )
293
+
294
+
295
+ def apply_inverse_transfer_function_cli(
296
+ input_position_dirpaths: list[Path],
297
+ transfer_function_dirpath: Path,
298
+ config_filepath: Path,
299
+ output_dirpath: Path,
300
+ num_processes,
301
+ ) -> None:
302
+ # Prepare output store
303
+ output_metadata = get_reconstruction_output_metadata(
304
+ input_position_dirpaths[0], config_filepath
305
+ )
306
+
307
+ # Generate position keys - use valid HCS keys for single-position stores
308
+ position_keys = []
309
+ for i, input_path in enumerate(input_position_dirpaths):
310
+ if is_single_position_store(input_path):
311
+ position_key = generate_valid_position_key(i)
312
+ else:
313
+ # Use original HCS plate structure
314
+ position_key = input_path.parts[-3:]
315
+ position_keys.append(position_key)
316
+
317
+ create_empty_hcs_zarr(
318
+ store_path=output_dirpath,
319
+ position_keys=position_keys,
320
+ **output_metadata,
321
+ )
322
+
323
+ # Initialize torch threads
324
+ if num_processes > 1:
325
+ torch.set_num_threads(1)
326
+ torch.set_num_interop_threads(1)
327
+
328
+ # Loop through positions
329
+ for i, input_position_dirpath in enumerate(input_position_dirpaths):
330
+ # Use the same position key generation logic
331
+ if is_single_position_store(input_position_dirpath):
332
+ position_key = generate_valid_position_key(i)
333
+ else:
334
+ position_key = input_position_dirpath.parts[-3:]
335
+
336
+ output_position_path = output_dirpath / Path(*position_key)
337
+
338
+ apply_inverse_transfer_function_single_position(
339
+ input_position_dirpath,
340
+ transfer_function_dirpath,
341
+ config_filepath,
342
+ output_position_path,
343
+ num_processes,
344
+ output_metadata["channel_names"],
345
+ )
346
+
347
+
348
+ @click.command("apply-inv-tf")
349
+ @input_position_dirpaths()
350
+ @transfer_function_dirpath()
351
+ @config_filepath()
352
+ @output_dirpath()
353
+ @processes_option(default=1)
354
+ def _apply_inverse_transfer_function_cli(
355
+ input_position_dirpaths: list[Path],
356
+ transfer_function_dirpath: Path,
357
+ config_filepath: Path,
358
+ output_dirpath: Path,
359
+ num_processes,
360
+ ) -> None:
361
+ """
362
+ Apply an inverse transfer function to a dataset using a configuration file.
363
+
364
+ Applies a transfer function to all positions in the list `input-position-dirpaths`,
365
+ so all positions must have the same TCZYX shape.
366
+
367
+ Appends channels to ./output.zarr, so multiple reconstructions can fill a single store.
368
+
369
+ See /examples for example configuration files.
370
+
371
+ >> waveorder apply-inv-tf -i ./input.zarr/*/*/* -t ./transfer-function.zarr -c /examples/birefringence.yml -o ./output.zarr
372
+ """
373
+ apply_inverse_transfer_function_cli(
374
+ input_position_dirpaths,
375
+ transfer_function_dirpath,
376
+ config_filepath,
377
+ output_dirpath,
378
+ num_processes,
379
+ )