driftlessmap 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. driftlessmap/__init__.py +16 -0
  2. driftlessmap/__main__.py +5 -0
  3. driftlessmap/about.py +24 -0
  4. driftlessmap/allen_downloader.py +908 -0
  5. driftlessmap/app.py +8085 -0
  6. driftlessmap/atlas_downloader.py +370 -0
  7. driftlessmap/atlas_loader.py +526 -0
  8. driftlessmap/atlas_processor.py +784 -0
  9. driftlessmap/atlas_transform.py +111 -0
  10. driftlessmap/atlas_view.py +1435 -0
  11. driftlessmap/cell_detection.py +27 -0
  12. driftlessmap/coordinate_validation.py +19 -0
  13. driftlessmap/czi_reader.py +236 -0
  14. driftlessmap/data/WHS_atlas_labels.pkl +0 -0
  15. driftlessmap/data/allen_mice_atlas_labels.pkl +0 -0
  16. driftlessmap/data/atlas_labels.pkl +0 -0
  17. driftlessmap/data/query.csv +1328 -0
  18. driftlessmap/download_utils.py +85 -0
  19. driftlessmap/herbs.png +0 -0
  20. driftlessmap/icons/backward.svg +15 -0
  21. driftlessmap/icons/dot.svg +11 -0
  22. driftlessmap/icons/down-arrow.svg +10 -0
  23. driftlessmap/icons/fast_backward.svg +14 -0
  24. driftlessmap/icons/fast_forward.svg +14 -0
  25. driftlessmap/icons/forward.svg +14 -0
  26. driftlessmap/icons/layers/add.png +0 -0
  27. driftlessmap/icons/layers/ai.png +0 -0
  28. driftlessmap/icons/layers/eye_off.png +0 -0
  29. driftlessmap/icons/layers/eye_on.png +0 -0
  30. driftlessmap/icons/layers/eye_white.png +0 -0
  31. driftlessmap/icons/layers/hi.png +0 -0
  32. driftlessmap/icons/layers/trash.png +0 -0
  33. driftlessmap/icons/object.svg +22 -0
  34. driftlessmap/icons/sidebar/add.png +0 -0
  35. driftlessmap/icons/sidebar/atlascontrol.png +0 -0
  36. driftlessmap/icons/sidebar/atlascontrol.svg +32 -0
  37. driftlessmap/icons/sidebar/bnd.svg +14 -0
  38. driftlessmap/icons/sidebar/c_section.png +0 -0
  39. driftlessmap/icons/sidebar/c_section2.png +0 -0
  40. driftlessmap/icons/sidebar/cell.svg +24 -0
  41. driftlessmap/icons/sidebar/check.svg +9 -0
  42. driftlessmap/icons/sidebar/compare.svg +18 -0
  43. driftlessmap/icons/sidebar/contour.svg +14 -0
  44. driftlessmap/icons/sidebar/h_section.png +0 -0
  45. driftlessmap/icons/sidebar/info.svg +14 -0
  46. driftlessmap/icons/sidebar/layers.png +0 -0
  47. driftlessmap/icons/sidebar/layers.svg +23 -0
  48. driftlessmap/icons/sidebar/line.svg +10 -0
  49. driftlessmap/icons/sidebar/link.svg +18 -0
  50. driftlessmap/icons/sidebar/link_off.svg +18 -0
  51. driftlessmap/icons/sidebar/merge.svg +11 -0
  52. driftlessmap/icons/sidebar/object.svg +13 -0
  53. driftlessmap/icons/sidebar/probe.png +0 -0
  54. driftlessmap/icons/sidebar/probe.svg +23 -0
  55. driftlessmap/icons/sidebar/rotation_horizontal.svg +14 -0
  56. driftlessmap/icons/sidebar/rotation_vertical.svg +14 -0
  57. driftlessmap/icons/sidebar/s_section.png +0 -0
  58. driftlessmap/icons/sidebar/tool.png +0 -0
  59. driftlessmap/icons/sidebar/tool.svg +19 -0
  60. driftlessmap/icons/sidebar/trash.png +0 -0
  61. driftlessmap/icons/sidebar/tree_checked.svg +10 -0
  62. driftlessmap/icons/sidebar/treeview.png +0 -0
  63. driftlessmap/icons/sidebar/treeview.svg +10 -0
  64. driftlessmap/icons/sidebar/treeview2.png +0 -0
  65. driftlessmap/icons/sidebar/virus.svg +24 -0
  66. driftlessmap/icons/tdown.svg +16 -0
  67. driftlessmap/icons/toolbar/accept.svg +18 -0
  68. driftlessmap/icons/toolbar/accept2.svg +24 -0
  69. driftlessmap/icons/toolbar/aim.svg +44 -0
  70. driftlessmap/icons/toolbar/aim_not.svg +28 -0
  71. driftlessmap/icons/toolbar/anchor.svg +14 -0
  72. driftlessmap/icons/toolbar/anticlockwise_rotation.svg +12 -0
  73. driftlessmap/icons/toolbar/atlas_icon.png +0 -0
  74. driftlessmap/icons/toolbar/boundary_register.svg +18 -0
  75. driftlessmap/icons/toolbar/cancel.svg +32 -0
  76. driftlessmap/icons/toolbar/cell_select.svg +22 -0
  77. driftlessmap/icons/toolbar/cell_select_not.svg +12 -0
  78. driftlessmap/icons/toolbar/check.svg +9 -0
  79. driftlessmap/icons/toolbar/clockwise_rotation.svg +11 -0
  80. driftlessmap/icons/toolbar/closed_path.svg +16 -0
  81. driftlessmap/icons/toolbar/closed_path2 copy.svg +19 -0
  82. driftlessmap/icons/toolbar/closed_path2.svg +20 -0
  83. driftlessmap/icons/toolbar/eraser.png +0 -0
  84. driftlessmap/icons/toolbar/eraser.svg +19 -0
  85. driftlessmap/icons/toolbar/eye.svg +17 -0
  86. driftlessmap/icons/toolbar/eye_closed.svg +19 -0
  87. driftlessmap/icons/toolbar/fill.svg +12 -0
  88. driftlessmap/icons/toolbar/gps.svg +15 -0
  89. driftlessmap/icons/toolbar/h_flip.png +0 -0
  90. driftlessmap/icons/toolbar/handle.png +0 -0
  91. driftlessmap/icons/toolbar/image_icon.png +0 -0
  92. driftlessmap/icons/toolbar/image_icon.svg +25 -0
  93. driftlessmap/icons/toolbar/info.svg +14 -0
  94. driftlessmap/icons/toolbar/inpart.png +0 -0
  95. driftlessmap/icons/toolbar/inpart.svg +17 -0
  96. driftlessmap/icons/toolbar/lasso.png +0 -0
  97. driftlessmap/icons/toolbar/lasso.svg +14 -0
  98. driftlessmap/icons/toolbar/left90.png +0 -0
  99. driftlessmap/icons/toolbar/line.svg +10 -0
  100. driftlessmap/icons/toolbar/line_sites.svg +11 -0
  101. driftlessmap/icons/toolbar/linear_silicon.png +0 -0
  102. driftlessmap/icons/toolbar/linear_silicon.svg +88 -0
  103. driftlessmap/icons/toolbar/list.svg +14 -0
  104. driftlessmap/icons/toolbar/location.svg +14 -0
  105. driftlessmap/icons/toolbar/magic-wand.svg +16 -0
  106. driftlessmap/icons/toolbar/magic_white.png +0 -0
  107. driftlessmap/icons/toolbar/mask.svg +10 -0
  108. driftlessmap/icons/toolbar/match.svg +20 -0
  109. driftlessmap/icons/toolbar/matchbnd.svg +18 -0
  110. driftlessmap/icons/toolbar/matching.svg +36 -0
  111. driftlessmap/icons/toolbar/merge.svg +11 -0
  112. driftlessmap/icons/toolbar/move_down.png +0 -0
  113. driftlessmap/icons/toolbar/move_left.png +0 -0
  114. driftlessmap/icons/toolbar/move_right.png +0 -0
  115. driftlessmap/icons/toolbar/move_up.png +0 -0
  116. driftlessmap/icons/toolbar/moving.png +0 -0
  117. driftlessmap/icons/toolbar/multi-probe.svg +119 -0
  118. driftlessmap/icons/toolbar/multi_pencil.svg +23 -0
  119. driftlessmap/icons/toolbar/multi_probe.png +0 -0
  120. driftlessmap/icons/toolbar/object.svg +13 -0
  121. driftlessmap/icons/toolbar/open_path.svg +22 -0
  122. driftlessmap/icons/toolbar/outpart.png +0 -0
  123. driftlessmap/icons/toolbar/outpart.svg +26 -0
  124. driftlessmap/icons/toolbar/pencil.png +0 -0
  125. driftlessmap/icons/toolbar/pencil.svg +19 -0
  126. driftlessmap/icons/toolbar/plasso.png +0 -0
  127. driftlessmap/icons/toolbar/probe.svg +19 -0
  128. driftlessmap/icons/toolbar/radar.svg +32 -0
  129. driftlessmap/icons/toolbar/rotation.svg +45 -0
  130. driftlessmap/icons/toolbar/rotation_clockwise.svg +18 -0
  131. driftlessmap/icons/toolbar/rotation_counter_clockwise.svg +18 -0
  132. driftlessmap/icons/toolbar/ruler.svg +27 -0
  133. driftlessmap/icons/toolbar/separate_sites.svg +13 -0
  134. driftlessmap/icons/toolbar/single_pencil.svg +16 -0
  135. driftlessmap/icons/toolbar/toa.svg +11 -0
  136. driftlessmap/icons/toolbar/toa_delete.svg +36 -0
  137. driftlessmap/icons/toolbar/toaa.svg +11 -0
  138. driftlessmap/icons/toolbar/toh.svg +11 -0
  139. driftlessmap/icons/toolbar/toh_delete.svg +36 -0
  140. driftlessmap/icons/toolbar/tohh.svg +11 -0
  141. driftlessmap/icons/toolbar/trans.png +0 -0
  142. driftlessmap/icons/toolbar/trans.svg +16 -0
  143. driftlessmap/icons/toolbar/triangulation.svg +10 -0
  144. driftlessmap/icons/toolbar/two_window.png +0 -0
  145. driftlessmap/icons/toolbar/unmerge.svg +16 -0
  146. driftlessmap/icons/toolbar/v_flip.png +0 -0
  147. driftlessmap/icons/toolbar/virus.svg +24 -0
  148. driftlessmap/icons/toolbar/virus_register.svg +31 -0
  149. driftlessmap/icons/toolbar/vis2d.svg +29 -0
  150. driftlessmap/icons/toolbar/window2.png +0 -0
  151. driftlessmap/icons/toolbar/window3.png +0 -0
  152. driftlessmap/icons/toolbar/window4.png +0 -0
  153. driftlessmap/icons/tree_close.svg +14 -0
  154. driftlessmap/icons/tree_open.svg +14 -0
  155. driftlessmap/icons/up-arrow.svg +10 -0
  156. driftlessmap/image_curves.py +579 -0
  157. driftlessmap/image_reader.py +186 -0
  158. driftlessmap/image_stacks.py +359 -0
  159. driftlessmap/image_view.py +646 -0
  160. driftlessmap/images_reader.py +5 -0
  161. driftlessmap/label_tree.py +278 -0
  162. driftlessmap/layer_validation.py +25 -0
  163. driftlessmap/layers_control.py +554 -0
  164. driftlessmap/main_window.ui +883 -0
  165. driftlessmap/movable_points.py +254 -0
  166. driftlessmap/obj_items.py +122 -0
  167. driftlessmap/object_control.py +1762 -0
  168. driftlessmap/persistence.py +376 -0
  169. driftlessmap/popup_message.py +16 -0
  170. driftlessmap/probe_csv.py +319 -0
  171. driftlessmap/probe_reconstruction.py +409 -0
  172. driftlessmap/probe_utiles.py +1442 -0
  173. driftlessmap/qss/atlas_view_group_box.qss +15 -0
  174. driftlessmap/qss/box_label.qss +7 -0
  175. driftlessmap/qss/channel_selector.qss +19 -0
  176. driftlessmap/qss/color_combo.qss +121 -0
  177. driftlessmap/qss/decor_label.qss +9 -0
  178. driftlessmap/qss/dialogs.qss +11 -0
  179. driftlessmap/qss/hidden_line_edit.qss +5 -0
  180. driftlessmap/qss/label_tree.qss +123 -0
  181. driftlessmap/qss/main_window.qss +243 -0
  182. driftlessmap/qss/menu_bar.qss +42 -0
  183. driftlessmap/qss/multi_handle_slider.qss +14 -0
  184. driftlessmap/qss/obj_ctrl_bottom_button.qss +29 -0
  185. driftlessmap/qss/object_text_button.qss +19 -0
  186. driftlessmap/qss/page_control.qss +36 -0
  187. driftlessmap/qss/rotation_button.qss +9 -0
  188. driftlessmap/qss/side_bar.qss +23 -0
  189. driftlessmap/qss/spinbox.qss +106 -0
  190. driftlessmap/qss/tabs.qss +211 -0
  191. driftlessmap/qss/tool_bar.qss +77 -0
  192. driftlessmap/resources.py +34 -0
  193. driftlessmap/roi_analysis.py +375 -0
  194. driftlessmap/run_driftlessmap.py +4 -0
  195. driftlessmap/slice_stacks.py +268 -0
  196. driftlessmap/slice_validation.py +20 -0
  197. driftlessmap/toolbox.py +569 -0
  198. driftlessmap/triangulation.py +517 -0
  199. driftlessmap/triangulation_points.py +108 -0
  200. driftlessmap/user_settings.py +92 -0
  201. driftlessmap/uuuuuu.py +1168 -0
  202. driftlessmap/version.py +3 -0
  203. driftlessmap/widgets_utils.py +140 -0
  204. driftlessmap/wtiles.py +715 -0
  205. driftlessmap-1.1.0.dist-info/METADATA +186 -0
  206. driftlessmap-1.1.0.dist-info/RECORD +210 -0
  207. driftlessmap-1.1.0.dist-info/WHEEL +5 -0
  208. driftlessmap-1.1.0.dist-info/entry_points.txt +2 -0
  209. driftlessmap-1.1.0.dist-info/licenses/LICENSE.txt +23 -0
  210. driftlessmap-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,319 @@
1
+ """CSV exports for probe contacts, trajectory metadata, and regions."""
2
+
3
+ import csv
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+
8
+
9
+ REGION_CSV_FIELDS = (
10
+ "probe_name",
11
+ "structure_id",
12
+ "structure_acronym",
13
+ "structure_name",
14
+ "contact_count",
15
+ "path_length_um",
16
+ )
17
+
18
+
19
+ def _reconstruction(probe_data):
20
+ reconstruction = probe_data.get("reconstruction")
21
+ if not isinstance(reconstruction, dict):
22
+ raise ValueError(
23
+ "This legacy probe has no coordinate reconstruction. Re-merge the "
24
+ "probe with the current DriftlessMap version before exporting it."
25
+ )
26
+ return reconstruction
27
+
28
+
29
+ def _vector(values, name):
30
+ values = np.asarray(values, dtype=float)
31
+ if values.shape != (3,):
32
+ raise ValueError("{} must contain three coordinates.".format(name))
33
+ return values
34
+
35
+
36
+ def _coordinate_columns(record, source_axes):
37
+ herbs = _vector(record["herbs_vox"], "HERBS coordinate")
38
+ bregma_um = _vector(record["bregma_um"], "Bregma coordinate")
39
+ source = _vector(record["source_vox"], "Source-atlas coordinate")
40
+ row = {
41
+ "herbs_ML_vox": float(herbs[0]),
42
+ "herbs_AP_vox": float(herbs[1]),
43
+ "herbs_DV_dorsal_vox": float(herbs[2]),
44
+ "configured_ML_mm": float(bregma_um[0] / 1000.0),
45
+ "configured_AP_mm": float(bregma_um[1] / 1000.0),
46
+ "configured_DV_ventral_mm": float(-bregma_um[2] / 1000.0),
47
+ }
48
+ for axis in range(3):
49
+ row["source_axis_{}_name".format(axis)] = source_axes[axis]
50
+ row["source_axis_{}_vox".format(axis)] = float(source[axis])
51
+ if "allen_ccf_vox" in record:
52
+ allen = _vector(record["allen_ccf_vox"], "Allen CCF coordinate")
53
+ estimated = _vector(
54
+ record["estimated_stereotaxic_bregma_mm"],
55
+ "Estimated Bregma coordinate",
56
+ )
57
+ row.update(
58
+ {
59
+ "allen_AP_vox": float(allen[0]),
60
+ "allen_DV_vox": float(allen[1]),
61
+ "allen_ML_vox": float(allen[2]),
62
+ "estimated_AP_mm": float(estimated[0]),
63
+ "estimated_ML_mm": float(estimated[2]),
64
+ "affine_DV_mm_not_for_targeting": float(estimated[1]),
65
+ }
66
+ )
67
+ return row
68
+
69
+
70
+ def _prefixed_coordinate_columns(record, source_axes, prefix):
71
+ return {
72
+ "{}_{}".format(prefix, key): value
73
+ for key, value in _coordinate_columns(record, source_axes).items()
74
+ }
75
+
76
+
77
+ def _indexed_coordinate_record(coordinates, index):
78
+ record = {}
79
+ for key in (
80
+ "herbs_vox",
81
+ "bregma_um",
82
+ "source_vox",
83
+ "allen_ccf_vox",
84
+ "estimated_stereotaxic_bregma_mm",
85
+ ):
86
+ if key in coordinates:
87
+ record[key] = np.asarray(coordinates[key])[index]
88
+ return record
89
+
90
+
91
+ def _contact_distances(contacts):
92
+ distance_from_tip = np.asarray(
93
+ contacts.get(
94
+ "axial_distance_up_from_tip_um",
95
+ contacts["distance_from_tip_um"],
96
+ ),
97
+ dtype=float,
98
+ )
99
+ depth_from_insertion = np.asarray(
100
+ contacts.get(
101
+ "axial_depth_from_insertion_um",
102
+ contacts["distance_from_insertion_um"],
103
+ ),
104
+ dtype=float,
105
+ )
106
+ return distance_from_tip, depth_from_insertion
107
+
108
+
109
+ def iter_probe_contact_rows(probe_name, probe_data):
110
+ """Yield physical contacts ordered from deepest to shallowest."""
111
+ reconstruction = _reconstruction(probe_data)
112
+ contacts = reconstruction["coordinates"]["contacts"]
113
+ source_axes = reconstruction["atlas"]["source_axes"]
114
+ count = int(contacts["count"])
115
+ site_indexes = np.asarray(contacts["site_index"], dtype=int)
116
+ column_indexes = np.asarray(contacts["column_index"], dtype=int)
117
+ indexes_in_column = np.asarray(contacts["index_in_column"], dtype=int)
118
+ distance_from_tip, depth_from_insertion = _contact_distances(contacts)
119
+ probe_local = np.asarray(contacts["probe_local_um"], dtype=float)
120
+ structure_ids = np.asarray(contacts["structure_id"], dtype=int)
121
+ structure_acronyms = np.asarray(
122
+ contacts["structure_acronym"], dtype=object
123
+ )
124
+ structure_names = np.asarray(contacts["structure_name"], dtype=object)
125
+
126
+ depth_order = np.lexsort(
127
+ (
128
+ site_indexes,
129
+ indexes_in_column,
130
+ column_indexes,
131
+ distance_from_tip,
132
+ )
133
+ )
134
+ for depth_rank, index in enumerate(depth_order):
135
+ row = {
136
+ "probe_name": probe_name,
137
+ "depth_rank_deepest_first": int(depth_rank),
138
+ "site_index": int(site_indexes[index]),
139
+ "column_index": int(column_indexes[index]),
140
+ "index_in_column": int(indexes_in_column[index]),
141
+ "axial_distance_up_from_tip_um": float(
142
+ distance_from_tip[index]
143
+ ),
144
+ "axial_depth_from_insertion_um": float(
145
+ depth_from_insertion[index]
146
+ ),
147
+ "probe_lateral_um": float(probe_local[index, 1]),
148
+ "probe_surface_normal_um": float(probe_local[index, 2]),
149
+ }
150
+ coordinate = _indexed_coordinate_record(contacts, index)
151
+ row.update(_coordinate_columns(coordinate, source_axes))
152
+ row.update(
153
+ {
154
+ "structure_id": int(structure_ids[index]),
155
+ "structure_acronym": str(structure_acronyms[index]),
156
+ "structure_name": str(structure_names[index]),
157
+ }
158
+ )
159
+ yield row
160
+
161
+
162
+ def probe_trajectory_row(probe_name, probe_data):
163
+ """Return one complete trajectory-summary row."""
164
+ reconstruction = _reconstruction(probe_data)
165
+ atlas = reconstruction["atlas"]
166
+ probe = reconstruction["probe"]
167
+ coordinates = reconstruction["coordinates"]
168
+ source_axes = atlas["source_axes"]
169
+ settings = probe.get("settings") or {}
170
+ row = {
171
+ "probe_name": probe_name,
172
+ "probe_type": settings.get(
173
+ "probe_type_name", probe_data.get("probe_type_name", "unspecified")
174
+ ),
175
+ "atlas_name": atlas.get("source_name") or "Source atlas",
176
+ "atlas_voxel_size_um": float(atlas["voxel_size_um"]),
177
+ "contact_ordering": probe.get(
178
+ "contact_ordering", "column-major"
179
+ ),
180
+ "AP_tilt_deg_from_vertical": float(probe_data["ap_angle"]),
181
+ "AP_tilt_direction": probe_data.get("ap_tilt", "unspecified"),
182
+ "ML_tilt_deg_from_vertical": float(probe_data["ml_angle"]),
183
+ "ML_tilt_direction": probe_data.get("ml_tilt", "unspecified"),
184
+ "insertion_to_tip_length_um": float(probe_data["probe_length"]),
185
+ "vertical_depth_change_um": float(probe_data["dv"]),
186
+ }
187
+ if atlas.get("source_version"):
188
+ row["atlas_version"] = atlas["source_version"]
189
+ if atlas.get("identifier"):
190
+ row["atlas_identifier"] = atlas["identifier"]
191
+ if probe.get("site_face") is not None:
192
+ row["site_face"] = probe["site_face"]
193
+
194
+ fit = probe_data.get("trajectory_fit") or probe.get("trajectory_fit")
195
+ if fit:
196
+ row.update(
197
+ {
198
+ "fit_method": fit["method"],
199
+ "surface_method": fit["surface_method"],
200
+ "fit_points": int(fit["point_count"]),
201
+ "fit_inliers": int(fit["inlier_count"]),
202
+ "fit_RMS_error_um": float(fit["rms_error_um"]),
203
+ "fit_max_error_um": float(fit["max_error_um"]),
204
+ "fit_explained_fraction": float(
205
+ fit["explained_fraction"]
206
+ ),
207
+ "surface_adjustment_um": float(
208
+ fit["surface_adjustment_um"]
209
+ ),
210
+ }
211
+ )
212
+
213
+ row.update(
214
+ _prefixed_coordinate_columns(
215
+ coordinates["insertion"], source_axes, "insertion"
216
+ )
217
+ )
218
+ row.update(
219
+ _prefixed_coordinate_columns(
220
+ coordinates["tip"], source_axes, "tip"
221
+ )
222
+ )
223
+ transform = atlas.get("estimated_stereotaxic_transform")
224
+ if transform is not None:
225
+ row["coordinate_note"] = transform["targeting_note"]
226
+ return row
227
+
228
+
229
+ def iter_probe_region_rows(probe_name, probe_data):
230
+ """Yield one compact row for each traversed anatomical region."""
231
+ _reconstruction(probe_data)
232
+ region_ids = np.ravel(probe_data.get("region_label", []))
233
+ region_names = np.ravel(probe_data.get("label_name", []))
234
+ region_acronyms = np.ravel(probe_data.get("label_acronym", []))
235
+ region_contacts = np.ravel(probe_data.get("region_sites", []))
236
+ region_lengths = np.ravel(probe_data.get("region_length", []))
237
+ for index, region_id in enumerate(region_ids):
238
+ yield {
239
+ "probe_name": probe_name,
240
+ "structure_id": int(region_id),
241
+ "structure_acronym": (
242
+ str(region_acronyms[index])
243
+ if index < len(region_acronyms)
244
+ else ""
245
+ ),
246
+ "structure_name": (
247
+ str(region_names[index])
248
+ if index < len(region_names)
249
+ else ""
250
+ ),
251
+ "contact_count": (
252
+ int(round(float(region_contacts[index])))
253
+ if index < len(region_contacts)
254
+ else 0
255
+ ),
256
+ "path_length_um": (
257
+ float(region_lengths[index])
258
+ if index < len(region_lengths)
259
+ else 0.0
260
+ ),
261
+ }
262
+
263
+
264
+ def iter_probe_csv_rows(probe_name, probe_data):
265
+ """Yield contact-only rows for compatibility with the original exporter."""
266
+ yield from iter_probe_contact_rows(probe_name, probe_data)
267
+
268
+
269
+ def _write_rows(path, rows, fallback_fields):
270
+ rows = list(rows)
271
+ fieldnames = list(rows[0]) if rows else list(fallback_fields)
272
+ with open(path, "w", newline="", encoding="utf-8") as csv_file:
273
+ writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
274
+ writer.writeheader()
275
+ writer.writerows(rows)
276
+
277
+
278
+ def write_probe_csv(path, probe_name, probe_data):
279
+ """Write only the depth-sorted physical-contact table to ``path``."""
280
+ rows = list(iter_probe_contact_rows(probe_name, probe_data))
281
+ fallback_fields = (
282
+ "probe_name",
283
+ "depth_rank_deepest_first",
284
+ "site_index",
285
+ "column_index",
286
+ "index_in_column",
287
+ "axial_distance_up_from_tip_um",
288
+ "axial_depth_from_insertion_um",
289
+ )
290
+ _write_rows(path, rows, fallback_fields)
291
+
292
+
293
+ def _export_paths(path):
294
+ path = Path(path)
295
+ base_name = path.stem if path.suffix else path.name
296
+ for suffix in ("_contacts", "_trajectory", "_regions"):
297
+ if base_name.endswith(suffix):
298
+ base_name = base_name[: -len(suffix)]
299
+ break
300
+ parent = path.parent
301
+ return {
302
+ "contacts": parent / "{}_contacts.csv".format(base_name),
303
+ "trajectory": parent / "{}_trajectory.csv".format(base_name),
304
+ "regions": parent / "{}_regions.csv".format(base_name),
305
+ }
306
+
307
+
308
+ def write_probe_csv_files(path, probe_name, probe_data):
309
+ """Write separate contact, trajectory, and region CSV files."""
310
+ paths = _export_paths(path)
311
+ write_probe_csv(paths["contacts"], probe_name, probe_data)
312
+ trajectory = probe_trajectory_row(probe_name, probe_data)
313
+ _write_rows(paths["trajectory"], [trajectory], tuple(trajectory))
314
+ _write_rows(
315
+ paths["regions"],
316
+ iter_probe_region_rows(probe_name, probe_data),
317
+ REGION_CSV_FIELDS,
318
+ )
319
+ return paths