phasor-handler 2.2.1__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 (37) hide show
  1. phasor_handler/__init__.py +9 -0
  2. phasor_handler/app.py +249 -0
  3. phasor_handler/img/icons/chevron-down.svg +3 -0
  4. phasor_handler/img/icons/chevron-up.svg +3 -0
  5. phasor_handler/img/logo.ico +0 -0
  6. phasor_handler/models/dir_manager.py +100 -0
  7. phasor_handler/scripts/contrast.py +131 -0
  8. phasor_handler/scripts/convert.py +155 -0
  9. phasor_handler/scripts/meta_reader.py +467 -0
  10. phasor_handler/scripts/plot.py +110 -0
  11. phasor_handler/scripts/register.py +86 -0
  12. phasor_handler/themes/__init__.py +8 -0
  13. phasor_handler/themes/dark_theme.py +330 -0
  14. phasor_handler/tools/__init__.py +1 -0
  15. phasor_handler/tools/check_stylesheet.py +15 -0
  16. phasor_handler/tools/misc.py +20 -0
  17. phasor_handler/widgets/__init__.py +5 -0
  18. phasor_handler/widgets/analysis/components/__init__.py +9 -0
  19. phasor_handler/widgets/analysis/components/bnc.py +426 -0
  20. phasor_handler/widgets/analysis/components/circle_roi.py +850 -0
  21. phasor_handler/widgets/analysis/components/image_view.py +667 -0
  22. phasor_handler/widgets/analysis/components/meta_info.py +481 -0
  23. phasor_handler/widgets/analysis/components/roi_list.py +659 -0
  24. phasor_handler/widgets/analysis/components/trace_plot.py +621 -0
  25. phasor_handler/widgets/analysis/view.py +1735 -0
  26. phasor_handler/widgets/conversion/view.py +83 -0
  27. phasor_handler/widgets/registration/view.py +110 -0
  28. phasor_handler/workers/__init__.py +2 -0
  29. phasor_handler/workers/analysis_worker.py +0 -0
  30. phasor_handler/workers/histogram_worker.py +55 -0
  31. phasor_handler/workers/registration_worker.py +242 -0
  32. phasor_handler-2.2.1.dist-info/METADATA +134 -0
  33. phasor_handler-2.2.1.dist-info/RECORD +37 -0
  34. phasor_handler-2.2.1.dist-info/WHEEL +5 -0
  35. phasor_handler-2.2.1.dist-info/entry_points.txt +5 -0
  36. phasor_handler-2.2.1.dist-info/licenses/LICENSE.md +21 -0
  37. phasor_handler-2.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,481 @@
1
+ """
2
+ Metadata Information Viewer Component
3
+
4
+ This module contains the MetadataViewer dialog that displays experiment metadata
5
+ in a formatted, user-friendly way.
6
+ """
7
+
8
+ from PyQt6.QtWidgets import (
9
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, QPushButton,
10
+ QScrollArea, QWidget, QFrame, QGridLayout, QTabWidget, QSplitter,
11
+ QGroupBox, QTreeWidget, QTreeWidgetItem, QHeaderView
12
+ )
13
+ from PyQt6.QtCore import Qt, QSize
14
+ from PyQt6.QtGui import QFont, QTextOption
15
+ import json
16
+ import datetime
17
+
18
+
19
+ class MetadataViewer(QDialog):
20
+ """
21
+ Dialog window that displays experiment metadata in a user-friendly format.
22
+
23
+ Handles both dictionary and object-based metadata formats and provides
24
+ multiple views (Overview, Raw JSON, Tree View) for comprehensive data inspection.
25
+ """
26
+
27
+ def __init__(self, parent=None):
28
+ super().__init__(parent)
29
+ self.setWindowTitle("Experiment Metadata Viewer")
30
+ self.setModal(False) # Allow interaction with main window while open
31
+ self.resize(400, 500)
32
+
33
+ self.metadata = None
34
+ self.directory_path = None
35
+
36
+ self.setupUI()
37
+
38
+ def setupUI(self):
39
+ """Set up the user interface."""
40
+ layout = QVBoxLayout()
41
+
42
+ # Header with directory path
43
+ self.header_label = QLabel("No experiment data loaded")
44
+ self.header_label.setStyleSheet("""
45
+ QLabel {
46
+ font-size: 14px;
47
+ font-weight: bold;
48
+ padding: 10px;
49
+ background-color: #2E4A67;
50
+ border: 1px solid #ddd;
51
+ border-radius: 5px;
52
+ }
53
+ """)
54
+ layout.addWidget(self.header_label)
55
+
56
+ # Create tab widget for different views
57
+ self.tab_widget = QTabWidget()
58
+
59
+ # Overview tab
60
+ self.overview_tab = self.create_overview_tab()
61
+ self.tab_widget.addTab(self.overview_tab, "Overview")
62
+
63
+ # Detailed tree view tab
64
+ self.tree_tab = self.create_tree_tab()
65
+ self.tab_widget.addTab(self.tree_tab, "Tree View")
66
+
67
+ # Raw JSON tab
68
+ self.raw_tab = self.create_raw_tab()
69
+ self.tab_widget.addTab(self.raw_tab, "Raw JSON")
70
+
71
+ layout.addWidget(self.tab_widget)
72
+
73
+ # Button bar
74
+ button_layout = QHBoxLayout()
75
+
76
+ self.refresh_button = QPushButton("Refresh")
77
+ self.refresh_button.clicked.connect(self.refresh_metadata)
78
+ button_layout.addWidget(self.refresh_button)
79
+
80
+ button_layout.addStretch()
81
+
82
+ self.close_button = QPushButton("Close")
83
+ self.close_button.clicked.connect(self.close)
84
+ button_layout.addWidget(self.close_button)
85
+
86
+ layout.addLayout(button_layout)
87
+
88
+ self.setLayout(layout)
89
+
90
+ def create_overview_tab(self):
91
+ """Create the overview tab with key experiment information."""
92
+ scroll_area = QScrollArea()
93
+ scroll_widget = QWidget()
94
+ layout = QVBoxLayout()
95
+
96
+ # Experiment Summary Group
97
+ self.exp_summary_group = QGroupBox("Experiment Summary")
98
+ self.exp_summary_layout = QGridLayout()
99
+ self.exp_summary_group.setLayout(self.exp_summary_layout)
100
+ layout.addWidget(self.exp_summary_group)
101
+
102
+ # Timing Information Group
103
+ self.timing_group = QGroupBox("Timing Information")
104
+ self.timing_layout = QGridLayout()
105
+ self.timing_group.setLayout(self.timing_layout)
106
+ layout.addWidget(self.timing_group)
107
+
108
+ # Stimulation Information Group
109
+ self.stim_group = QGroupBox("Stimulation Information")
110
+ self.stim_layout = QGridLayout()
111
+ self.stim_group.setLayout(self.stim_layout)
112
+ layout.addWidget(self.stim_group)
113
+
114
+ # Image Information Group
115
+ self.image_group = QGroupBox("Image Information")
116
+ self.image_layout = QGridLayout()
117
+ self.image_group.setLayout(self.image_layout)
118
+ layout.addWidget(self.image_group)
119
+
120
+ layout.addStretch()
121
+ scroll_widget.setLayout(layout)
122
+ scroll_area.setWidget(scroll_widget)
123
+ scroll_area.setWidgetResizable(True)
124
+
125
+ return scroll_area
126
+
127
+ def create_tree_tab(self):
128
+ """Create the tree view tab for hierarchical data exploration."""
129
+ self.tree_widget = QTreeWidget()
130
+ self.tree_widget.setHeaderLabels(["Key", "Value", "Type"])
131
+
132
+ # Set column widths
133
+ header = self.tree_widget.header()
134
+ header.setStretchLastSection(False)
135
+ header.setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)
136
+ header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
137
+ header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
138
+
139
+ return self.tree_widget
140
+
141
+ def create_raw_tab(self):
142
+ """Create the raw JSON tab for viewing unformatted data."""
143
+ self.raw_text_edit = QTextEdit()
144
+ self.raw_text_edit.setReadOnly(True)
145
+
146
+ # Set monospace font for better readability
147
+ font = QFont("Consolas", 10)
148
+ if not font.exactMatch():
149
+ font = QFont("Courier New", 10)
150
+ self.raw_text_edit.setFont(font)
151
+
152
+ return self.raw_text_edit
153
+
154
+ def set_metadata(self, metadata, directory_path=None):
155
+ """
156
+ Set the metadata to display.
157
+
158
+ Args:
159
+ metadata: The experiment metadata (dict or object)
160
+ directory_path: Path to the experiment directory
161
+ """
162
+ self.metadata = metadata
163
+ self.directory_path = directory_path
164
+
165
+ if directory_path:
166
+ self.header_label.setText(f"Experiment Metadata: {directory_path}")
167
+ else:
168
+ self.header_label.setText("Experiment Metadata")
169
+
170
+ self.update_display()
171
+
172
+ def update_display(self):
173
+ """Update all tabs with current metadata."""
174
+ if self.metadata is None:
175
+ self.clear_display()
176
+ return
177
+
178
+ self.update_overview_tab()
179
+ self.update_tree_tab()
180
+ self.update_raw_tab()
181
+
182
+ def clear_display(self):
183
+ """Clear all displayed content."""
184
+ # Clear overview tab
185
+ self.clear_group_layout(self.exp_summary_layout)
186
+ self.clear_group_layout(self.timing_layout)
187
+ self.clear_group_layout(self.stim_layout)
188
+ self.clear_group_layout(self.image_layout)
189
+
190
+ # Clear tree tab
191
+ self.tree_widget.clear()
192
+
193
+ # Clear raw tab
194
+ self.raw_text_edit.clear()
195
+
196
+ self.header_label.setText("No experiment data loaded")
197
+
198
+ def clear_group_layout(self, layout):
199
+ """Clear all widgets from a group layout."""
200
+ while layout.count():
201
+ child = layout.takeAt(0)
202
+ if child.widget():
203
+ child.widget().deleteLater()
204
+
205
+ def update_overview_tab(self):
206
+ """Update the overview tab with key information."""
207
+ # Clear existing content
208
+ self.clear_group_layout(self.exp_summary_layout)
209
+ self.clear_group_layout(self.timing_layout)
210
+ self.clear_group_layout(self.stim_layout)
211
+ self.clear_group_layout(self.image_layout)
212
+
213
+ if isinstance(self.metadata, dict):
214
+ self.update_overview_from_dict()
215
+ else:
216
+ self.update_overview_from_object()
217
+
218
+ def update_overview_from_dict(self):
219
+ """Update overview from dictionary metadata."""
220
+ # Experiment Summary
221
+ row = 0
222
+ for key in ['device_name', 'n_frames']:
223
+ if key in self.metadata:
224
+ value = self.metadata[key]
225
+ if key == "device_name": key = "Device Name"
226
+ elif key == "n_frames": key = "Number of Frames"
227
+
228
+ self.add_info_row(self.exp_summary_layout, row, key.replace('_', ' '),
229
+ str(value), self.format_value_with_unit(key, value))
230
+ row += 1
231
+
232
+ # Timing Information
233
+ row = 0
234
+ time_keys = ['time_stamps', 'elapsed_times', 'acquisition_start_time', 'year', 'hour']
235
+ for key in time_keys:
236
+ if key in self.metadata:
237
+ value = self.metadata[key]
238
+ if key == "year":
239
+ year = self.metadata["year"]
240
+ month = self.metadata["month"]
241
+ day = self.metadata["day"]
242
+ display_value = f"{day:02d}/{month:02d}/{year:04d}"
243
+ key = "Date (DDMMYYYY)"
244
+ elif key == "hour":
245
+ hour = self.metadata["hour"]
246
+ minute = self.metadata["minute"]
247
+ second = self.metadata["second"]
248
+ display_value = f"{hour:02d}:{minute:02d}:{second:02d}"
249
+ key = "Local Time"
250
+ elif isinstance(value, list) and len(value) > 0:
251
+ interval = (sum([value[i+1] - value[i] for i in range(len(value)-1)])/(len(value)-1))/1000
252
+ start_display = "0" if value[0] == 0 else f"{value[0]/1000:.3f}"
253
+ display_value = f"From {start_display} to {value[-1]/1000:.3f} seconds with an average interval of {interval:.3f} seconds"
254
+ else:
255
+ display_value = str(value)
256
+ self.add_info_row(self.timing_layout, row, key.replace('_', ' ').title(),
257
+ display_value) if key != "Date (DDMMYYYY)" else self.add_info_row(self.timing_layout, row, key, display_value)
258
+ row += 1
259
+
260
+ # Stimulation Information
261
+ row = 0
262
+ stim_keys = ['stimulation_timeframes', 'stimulation_ms', 'duty_cycle', 'stimulated_roi_location', 'stimulated_rois']
263
+ for key in stim_keys:
264
+ if key in self.metadata:
265
+ value = self.metadata[key]
266
+ if isinstance(value, list):
267
+ if key == 'stimulation_timeframes':
268
+ display_value = ', '.join(map(str, value))
269
+ key = "Stimulation Timeframes"
270
+ elif key == 'stimulation_ms':
271
+ display_value = ', '.join(map(str, [int(v/1000) for v in value]))
272
+ key = "Stimulation Time (s)"
273
+ elif key == 'duty_cycle':
274
+ duty_cycle_counts = {val: value.count(val) for val in set(value)}
275
+ display_value = ' | '.join([f'{val}: {count}X' for val, count in duty_cycle_counts.items()])
276
+ key = "Duty Cycle"
277
+ elif key == 'stimulated_roi_location':
278
+ display_value = ', '.join(map(str, [len(x) for x in value]))
279
+ key = "Number of stimulated ROIs"
280
+ elif key == 'stimulated_rois':
281
+ if not value or all(not sublist for sublist in value):
282
+ display_value = "NA"
283
+ else:
284
+ display_value = ', '.join([', '.join(map(str, x)) for x in value])
285
+ key = "Stimulated ROIs"
286
+ else:
287
+ display_value = str(value)
288
+ self.add_info_row(self.stim_layout, row, key.replace('_', ' '),
289
+ display_value)
290
+ row += 1
291
+
292
+ # Image Information
293
+ row = 0
294
+ image_keys = ['pixel_size', 'FOV_size']
295
+ for key in image_keys:
296
+ if key in self.metadata:
297
+ value = self.metadata[key]
298
+ if key == "pixel_size": key = "Pixel Size (µm)"
299
+ elif key == "FOV_size":
300
+ key = "FOV Size (µm)"
301
+ value = value[:-7]
302
+
303
+ self.add_info_row(self.image_layout, row, key.replace('_', ' '),
304
+ str(value))
305
+ row += 1
306
+
307
+ def update_overview_from_object(self):
308
+ """Update overview from object metadata."""
309
+ # Get all non-private attributes
310
+ attrs = [attr for attr in dir(self.metadata) if not attr.startswith('_')]
311
+
312
+ # Categorize attributes
313
+ exp_attrs = []
314
+ time_attrs = []
315
+ stim_attrs = []
316
+ image_attrs = []
317
+
318
+ for attr in attrs:
319
+ if any(keyword in attr.lower() for keyword in ['frame', 'duration', 'rate']):
320
+ exp_attrs.append(attr)
321
+ elif any(keyword in attr.lower() for keyword in ['time', 'elapsed']):
322
+ time_attrs.append(attr)
323
+ elif any(keyword in attr.lower() for keyword in ['stim', 'duty']):
324
+ stim_attrs.append(attr)
325
+ elif any(keyword in attr.lower() for keyword in ['image', 'width', 'height', 'channel', 'bit']):
326
+ image_attrs.append(attr)
327
+ else:
328
+ exp_attrs.append(attr) # Default to experiment summary
329
+
330
+ # Populate each section
331
+ self.populate_section_from_attrs(self.exp_summary_layout, exp_attrs)
332
+ self.populate_section_from_attrs(self.timing_layout, time_attrs)
333
+ self.populate_section_from_attrs(self.stim_layout, stim_attrs)
334
+ self.populate_section_from_attrs(self.image_layout, image_attrs)
335
+
336
+ def populate_section_from_attrs(self, layout, attrs):
337
+ """Populate a section with attributes from the metadata object."""
338
+ for row, attr in enumerate(attrs):
339
+ try:
340
+ value = getattr(self.metadata, attr)
341
+ if isinstance(value, list) and len(value) > 0:
342
+ display_value = f"Array with {len(value)} entries"
343
+ if len(value) <= 10:
344
+ display_value += f": {value}"
345
+ else:
346
+ display_value += f" (first few: {value[:3]}...)"
347
+ else:
348
+ display_value = str(value)
349
+ self.add_info_row(layout, row, attr.replace('_', ' ').title(), display_value)
350
+ except Exception as e:
351
+ self.add_info_row(layout, row, attr.replace('_', ' ').title(), f"Error: {e}")
352
+
353
+ def add_info_row(self, layout, row, label, value, tooltip=None):
354
+ """Add an information row to a layout."""
355
+ label_widget = QLabel(f"{label}:")
356
+ label_widget.setStyleSheet("font-weight: bold;")
357
+
358
+ value_widget = QLabel(str(value))
359
+ value_widget.setWordWrap(True)
360
+ value_widget.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
361
+
362
+ if tooltip:
363
+ value_widget.setToolTip(tooltip)
364
+
365
+ layout.addWidget(label_widget, row, 0)
366
+ layout.addWidget(value_widget, row, 1)
367
+
368
+ def format_value_with_unit(self, key, value):
369
+ """Format values with appropriate units for tooltips."""
370
+ if 'ms' in key.lower():
371
+ return f"{value} milliseconds"
372
+ elif 'um' in key.lower():
373
+ return f"{value} micrometers"
374
+ elif 'rate' in key.lower():
375
+ return f"{value} Hz"
376
+ else:
377
+ return str(value)
378
+
379
+ def update_tree_tab(self):
380
+ """Update the tree view tab."""
381
+ self.tree_widget.clear()
382
+
383
+ if self.metadata is None:
384
+ return
385
+
386
+ if isinstance(self.metadata, dict):
387
+ self.add_dict_to_tree(self.metadata, self.tree_widget.invisibleRootItem())
388
+ else:
389
+ self.add_object_to_tree(self.metadata, self.tree_widget.invisibleRootItem())
390
+
391
+ # Expand first level
392
+ for i in range(self.tree_widget.topLevelItemCount()):
393
+ item = self.tree_widget.topLevelItem(i)
394
+ if item.childCount() < 20: # Don't auto-expand large sections
395
+ item.setExpanded(True)
396
+
397
+ def add_dict_to_tree(self, data, parent_item, max_depth=5, current_depth=0):
398
+ """Recursively add dictionary data to tree widget."""
399
+ if current_depth >= max_depth:
400
+ return
401
+
402
+ for key, value in data.items():
403
+ self.add_value_to_tree(key, value, parent_item, current_depth)
404
+
405
+ def add_object_to_tree(self, obj, parent_item, max_depth=5, current_depth=0):
406
+ """Add object attributes to tree widget."""
407
+ if current_depth >= max_depth:
408
+ return
409
+
410
+ attrs = [attr for attr in dir(obj) if not attr.startswith('_')]
411
+ for attr in attrs:
412
+ try:
413
+ value = getattr(obj, attr)
414
+ if not callable(value): # Skip methods
415
+ self.add_value_to_tree(attr, value, parent_item, current_depth)
416
+ except Exception:
417
+ pass # Skip attributes that can't be accessed
418
+
419
+ def add_value_to_tree(self, key, value, parent_item, current_depth):
420
+ """Add a key-value pair to the tree."""
421
+ item = QTreeWidgetItem(parent_item)
422
+ item.setText(0, str(key))
423
+
424
+ value_type = type(value).__name__
425
+ item.setText(2, value_type)
426
+
427
+ if isinstance(value, (dict, list, tuple)) and len(value) > 0:
428
+ if isinstance(value, dict):
429
+ item.setText(1, f"Dictionary with {len(value)} items")
430
+ if current_depth < 4: # Prevent infinite recursion
431
+ self.add_dict_to_tree(value, item, current_depth=current_depth+1)
432
+ elif isinstance(value, (list, tuple)):
433
+ if len(value) <= 5:
434
+ item.setText(1, str(value))
435
+ else:
436
+ item.setText(1, f"{value_type} with {len(value)} items: {value[:3]}...")
437
+ # Add first few items for lists
438
+ if current_depth < 3:
439
+ for i, list_val in enumerate(value[:10]): # Limit to first 10 items
440
+ self.add_value_to_tree(f"[{i}]", list_val, item, current_depth+1)
441
+ else:
442
+ # Truncate very long strings
443
+ str_value = str(value)
444
+ if len(str_value) > 200:
445
+ str_value = str_value[:200] + "..."
446
+ item.setText(1, str_value)
447
+
448
+ def update_raw_tab(self):
449
+ """Update the raw JSON tab."""
450
+ if self.metadata is None:
451
+ self.raw_text_edit.clear()
452
+ return
453
+
454
+ try:
455
+ if isinstance(self.metadata, dict):
456
+ json_text = json.dumps(self.metadata, indent=2, default=str)
457
+ else:
458
+ # Convert object to dict for JSON serialization
459
+ obj_dict = {}
460
+ attrs = [attr for attr in dir(self.metadata) if not attr.startswith('_')]
461
+ for attr in attrs:
462
+ try:
463
+ value = getattr(self.metadata, attr)
464
+ if not callable(value):
465
+ obj_dict[attr] = value
466
+ except Exception:
467
+ obj_dict[attr] = f"<Error accessing {attr}>"
468
+
469
+ json_text = json.dumps(obj_dict, indent=2, default=str)
470
+
471
+ self.raw_text_edit.setPlainText(json_text)
472
+ except Exception as e:
473
+ self.raw_text_edit.setPlainText(f"Error displaying metadata: {e}")
474
+
475
+ def refresh_metadata(self):
476
+ """Refresh the metadata display."""
477
+ self.update_display()
478
+
479
+ def sizeHint(self):
480
+ """Provide size hint for the dialog."""
481
+ return QSize(900, 700)