Skip to content

Commit 07dbcee

Browse files
committed
Fix OpenGL compat on macOS M-series, update joblib usage, and ignore .DS_Store
1 parent a7657c2 commit 07dbcee

File tree

5 files changed

+111
-71
lines changed

5 files changed

+111
-71
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,9 @@ msdfgen
5353
*.sh
5454
tools/*.png
5555
tools/*.ttf
56+
57+
# macOS
58+
.DS_Store
59+
60+
# Virtual environments
61+
venv/

phy/apps/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ class FeatureMixin(object):
303303
def _get_feature_spike_ids(self, cluster_id, load_all=False):
304304
"""Return spike ids to be used in the feature view."""
305305
if load_all:
306-
return self.supervisor.get_spike_ids(cluster_id)
306+
return self.get_spike_ids(cluster_id)
307307
# Background spikes.
308308
if cluster_id is None:
309309
return self.selector(self.n_spikes_features_background, [])

phy/cluster/views/featureview3d.py

Lines changed: 95 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
1-
# -*- coding: utf-8 -*-
2-
3-
"""3D Feature view."""
4-
5-
# -----------------------------------------------------------------------------
6-
# Imports
7-
# -----------------------------------------------------------------------------
8-
91
import logging
10-
112
import numpy as np
12-
from phylib.utils import Bunch, emit
3+
from phylib.utils import Bunch, emit, connect, unconnect
134
from phy.utils.color import selected_cluster_color
145
from phy.plot.visuals import ScatterVisual, TextVisual, LineVisual
156
from phy.plot.transform import range_transform, NDC
@@ -26,7 +17,8 @@ def _get_point_color(clu_idx=None):
2617
if clu_idx is not None:
2718
color = selected_cluster_color(clu_idx, .5)
2819
else:
29-
color = (.5,) * 4
20+
# Lighter gray for background points so they are visible on black background
21+
color = (0.5, 0.5, 0.5, 0.5)
3022
assert len(color) == 4
3123
return color
3224

@@ -417,7 +409,17 @@ def get_clusters_data(self, fixed_channels=None, load_all=None):
417409
return []
418410

419411
# Choose the channels based on the first selected cluster.
420-
channel_ids = list(bunchs[0].get('channel_ids', [])) if bunchs else []
412+
if bunchs and len(bunchs) > 0:
413+
first_bunch = bunchs[0]
414+
# Prioritize 'channel_ids' from the bunch itself
415+
if 'channel_ids' in first_bunch and len(first_bunch.channel_ids) > 0:
416+
channel_ids = list(first_bunch.channel_ids)
417+
else:
418+
# Fallback to empty if not present (shouldn't happen with valid features)
419+
channel_ids = []
420+
else:
421+
channel_ids = []
422+
421423
logger.debug(f"Extracted channel_ids from first bunch: {channel_ids[:5] if len(channel_ids) > 5 else channel_ids}")
422424

423425
# Always update channel_ids if not in fixed_channels mode
@@ -629,6 +631,39 @@ def on_request_split(self, sender=None):
629631

630632
return np.array(spike_ids_to_split, dtype=np.int64)
631633

634+
def attach(self, gui):
635+
"""Attach the view to the GUI."""
636+
super(Feature3DView, self).attach(gui)
637+
# Manually connect the split event to the controller
638+
# This ensures that when 'K' is pressed, this view's on_request_split is called
639+
connect(self.on_request_split)
640+
641+
# Add actions - the shortcuts are automatically handled by the Actions system
642+
self.actions.add(self.zoom_in, name='Zoom in')
643+
self.actions.add(self.zoom_out, name='Zoom out')
644+
self.actions.add(self.reset_view, name='Reset view')
645+
self.actions.separator()
646+
# Register the toggle action so the default shortcut is picked up.
647+
self.actions.add(
648+
self.toggle_automatic_channel_selection,
649+
checkable=True,
650+
checked=not self.fixed_channels,
651+
)
652+
653+
# Projection toggle (Orthographic/Perspective)
654+
self.actions.add(
655+
self.toggle_projection_mode,
656+
name='Orthographic projection',
657+
checkable=True,
658+
checked=(self.projection_mode == 'orthographic')
659+
)
660+
661+
# Create axis actions at startup
662+
self._create_axis_actions()
663+
664+
# Force an initial plot to ensure the view is not blank on startup.
665+
self.plot()
666+
632667
def plot(self, **kwargs):
633668
"""Update the view with the selected clusters."""
634669
logger.debug("Feature3DView.plot() called")
@@ -655,13 +690,18 @@ def plot(self, **kwargs):
655690
primary_channel = self.channel_ids[0]
656691
primary_channel_label = self.channel_labels.get(primary_channel, str(primary_channel))
657692
primary_channel_text = f" (ch{primary_channel_label})"
693+
694+
logger.debug(f"Updating axes for primary channel: {primary_channel} (label: {primary_channel_label})")
658695

659696
# Preserve the chosen PC (PC1/PC2/PC3) for each axis when relabeling Primary
660697
for axis_name in ('x', 'y', 'z'):
661698
current_label = getattr(self, f'{axis_name}_axis')
699+
# Update if it's explicitly a Primary axis OR if it's just initialized generic PC
662700
if '(Primary' in current_label:
663701
pc = current_label.split(' ')[0] # e.g., 'PC1'
664-
setattr(self, f'{axis_name}_axis', f"{pc} (Primary{primary_channel_text})")
702+
new_label = f"{pc} (Primary{primary_channel_text})"
703+
setattr(self, f'{axis_name}_axis', new_label)
704+
logger.debug(f"Updated {axis_name}_axis to {new_label}")
665705

666706
if not bunchs:
667707
logger.debug("No cluster data, clearing view")
@@ -690,30 +730,49 @@ def plot(self, **kwargs):
690730

691731
# Get and plot background data (gray points)
692732
if self.channel_ids:
693-
logger.debug("Getting background data")
694-
background_data = self.features(None, channel_ids=self.channel_ids)
695-
# Handle both list and single-bunch returns
696-
background = background_data[0] if isinstance(background_data, (list, tuple)) and background_data else background_data
697-
if background:
698-
background.cluster_id = None
699-
x_bg = self._get_axis_data(background, self.x_axis)
700-
y_bg = self._get_axis_data(background, self.y_axis)
701-
z_bg = self._get_axis_data(background, self.z_axis)
702-
points_3d = np.column_stack([x_bg, y_bg, z_bg])
703-
704-
# Store cluster data
705-
cluster_info = {
706-
'points_3d': points_3d,
707-
'cluster_id': None,
708-
'clu_idx': None,
709-
'color': _get_point_color(None),
710-
'spike_ids': background.get('spike_ids'),
711-
'bunch': background
712-
}
713-
self._cluster_data.append(cluster_info)
733+
logger.debug(f"Attempting to get background data for channels: {self.channel_ids}")
734+
try:
735+
# Request background features for the current channels
736+
# Note: We use None for cluster_id to get background
737+
background_data = self.features(None, channel_ids=self.channel_ids, load_all=False)
738+
739+
# Handle both list and single-bunch returns
740+
background = background_data[0] if isinstance(background_data, (list, tuple)) and background_data else background_data
714741

715-
all_points_3d.append(points_3d)
716-
all_cluster_ids.extend([None] * len(points_3d))
742+
if background:
743+
spike_ids = background.get('spike_ids')
744+
logger.debug(f"Background data received: {len(spike_ids) if spike_ids is not None else 0} spikes")
745+
746+
background.cluster_id = None
747+
x_bg = self._get_axis_data(background, self.x_axis)
748+
y_bg = self._get_axis_data(background, self.y_axis)
749+
z_bg = self._get_axis_data(background, self.z_axis)
750+
751+
# Just plot whatever we got, even if some are zeros
752+
if len(x_bg) > 0:
753+
points_3d = np.column_stack([x_bg, y_bg, z_bg])
754+
755+
# Store cluster data
756+
cluster_info = {
757+
'points_3d': points_3d,
758+
'cluster_id': None,
759+
'clu_idx': None,
760+
'color': _get_point_color(None),
761+
'spike_ids': spike_ids,
762+
'bunch': background
763+
}
764+
self._cluster_data.append(cluster_info)
765+
766+
all_points_3d.append(points_3d)
767+
all_cluster_ids.extend([None] * len(points_3d))
768+
else:
769+
logger.debug("Background data has 0 length after axis extraction")
770+
else:
771+
logger.debug("No background data returned from features()")
772+
except Exception as e:
773+
logger.error(f"Error retrieving background data: {e}")
774+
else:
775+
logger.debug("Skipping background: No channel_ids set")
717776

718777
# Plot each cluster
719778
for clu_idx, bunch in enumerate(bunchs):
@@ -999,36 +1058,6 @@ def zoom_out(self):
9991058
self.scale_3d /= 1.1
10001059
self._update_projections()
10011060

1002-
def attach(self, gui):
1003-
"""Attach the view to the GUI."""
1004-
super(Feature3DView, self).attach(gui)
1005-
1006-
# Add actions - the shortcuts are automatically handled by the Actions system
1007-
self.actions.add(self.zoom_in, name='Zoom in')
1008-
self.actions.add(self.zoom_out, name='Zoom out')
1009-
self.actions.add(self.reset_view, name='Reset view')
1010-
self.actions.separator()
1011-
# Register the toggle action so the default shortcut is picked up.
1012-
self.actions.add(
1013-
self.toggle_automatic_channel_selection,
1014-
checkable=True,
1015-
checked=not self.fixed_channels,
1016-
)
1017-
1018-
# Projection toggle (Orthographic/Perspective)
1019-
self.actions.add(
1020-
self.toggle_projection_mode,
1021-
name='Orthographic projection',
1022-
checkable=True,
1023-
checked=(self.projection_mode == 'orthographic')
1024-
)
1025-
1026-
# Create axis actions at startup
1027-
self._create_axis_actions()
1028-
1029-
# Force an initial plot to ensure the view is not blank on startup.
1030-
self.plot()
1031-
10321061
def toggle_projection_mode(self, checked):
10331062
"""Toggle between orthographic and perspective projection."""
10341063
self.projection_mode = 'orthographic' if checked else 'perspective'
@@ -1172,4 +1201,3 @@ def status(self):
11721201
f'Primary: ch{primary_channel_label} ({channel_mode}) | '
11731202
f'Rotation: X={self.rotation_x:.2f}, Y={self.rotation_y:.2f}, Z={self.rotation_z:.2f} | '
11741203
f'Scale: {self.scale_3d:.2f}')
1175-

phy/gui/qt.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
)
4040
from PyQt5.QtGui import ( # noqa
4141
QKeySequence, QIcon, QColor, QMouseEvent, QGuiApplication,
42-
QFontDatabase, QWindow, QOpenGLWindow)
42+
QFontDatabase, QWindow, QOpenGLWindow, QSurfaceFormat)
4343
from PyQt5.QtWebEngineWidgets import (QWebEngineView, # noqa
4444
QWebEnginePage,
4545
# QWebSettings,
@@ -58,6 +58,13 @@
5858
# on Ubuntu.
5959
#QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
6060

61+
# Force OpenGL 2.1 Compatibility Profile for macOS M1/M2 to support legacy shaders.
62+
if sys.platform == 'darwin':
63+
fmt = QSurfaceFormat()
64+
fmt.setVersion(2, 1)
65+
fmt.setProfile(QSurfaceFormat.CompatibilityProfile)
66+
QSurfaceFormat.setDefaultFormat(fmt)
67+
6168

6269
# -----------------------------------------------------------------------------
6370
# Testing functions: mock dialogs in automated tests

phy/utils/context.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,7 @@ def _set_memory(self, cache_dir):
9595
try:
9696
from joblib import Memory
9797
self._memory = Memory(
98-
location=self.cache_dir, mmap_mode=None, verbose=self.verbose,
99-
bytes_limit=self.cache_limit)
98+
location=self.cache_dir, mmap_mode=None, verbose=self.verbose)
10099
logger.debug("Initialize joblib cache dir at `%s`.", self.cache_dir)
101100
logger.debug("Reducing the size of the cache if needed.")
102101
self._memory.reduce_size()

0 commit comments

Comments
 (0)