ezmsg-tools 0.2.0__tar.gz → 0.3.0__tar.gz

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 (35) hide show
  1. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/PKG-INFO +7 -2
  2. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/pyproject.toml +7 -1
  3. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/__version__.py +2 -2
  4. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/sigmon/cli.py +21 -6
  5. ezmsg_tools-0.3.0/src/ezmsg/tools/viewer/__init__.py +0 -0
  6. ezmsg_tools-0.3.0/src/ezmsg/tools/viewer/cli.py +256 -0
  7. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/.github/workflows/docs.yml +0 -0
  8. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/.github/workflows/python-publish.yml +0 -0
  9. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/.github/workflows/python-tests.yml +0 -0
  10. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/.gitignore +0 -0
  11. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/.pre-commit-config.yaml +0 -0
  12. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/LICENSE +0 -0
  13. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/README.md +0 -0
  14. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/docs/Makefile +0 -0
  15. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/docs/make.bat +0 -0
  16. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/docs/source/_templates/autosummary/module.rst +0 -0
  17. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/docs/source/api/index.rst +0 -0
  18. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/docs/source/conf.py +0 -0
  19. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/docs/source/index.md +0 -0
  20. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/scripts_nbs/generator/eeg_generator_graph.py +0 -0
  21. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/scripts_nbs/orphan/run_attach.py +0 -0
  22. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/scripts_nbs/profiler/ecog_preproc.py +0 -0
  23. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/scripts_nbs/profiler/parse_profiler.ipynb +0 -0
  24. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/scripts_nbs/profiler/profiler.py +0 -0
  25. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/__init__.py +0 -0
  26. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/dag.py +0 -0
  27. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/perfmon/__init__.py +0 -0
  28. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/perfmon/cli.py +0 -0
  29. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/shmem/__init__.py +0 -0
  30. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/shmem/shmem.py +0 -0
  31. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/shmem/shmem_mirror.py +0 -0
  32. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/sigmon/__init__.py +0 -0
  33. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/src/ezmsg/tools/sigmon/dag_widget.py +0 -0
  34. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/tests/test_shmem_mirror.py +0 -0
  35. {ezmsg_tools-0.2.0 → ezmsg_tools-0.3.0}/tests/test_shmem_sink.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ezmsg-tools
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: ezmsg namespace package containing tools for working with ezmsg graphs.
5
5
  Author-email: Chadwick Boulay <chadwick.boulay@gmail.com>
6
6
  License-Expression: MIT
@@ -21,10 +21,15 @@ Requires-Dist: typer>=0.15.1; extra == 'perfmon'
21
21
  Provides-Extra: sigmon
22
22
  Requires-Dist: ezmsg-qt; extra == 'sigmon'
23
23
  Requires-Dist: pandas; extra == 'sigmon'
24
- Requires-Dist: phosphor>=0.2; extra == 'sigmon'
24
+ Requires-Dist: phosphor>=0.4; extra == 'sigmon'
25
25
  Requires-Dist: pygraphviz>=1.14; extra == 'sigmon'
26
26
  Requires-Dist: pyside6>=6.7; extra == 'sigmon'
27
27
  Requires-Dist: typer>=0.15.1; extra == 'sigmon'
28
+ Provides-Extra: viewer
29
+ Requires-Dist: ezmsg-qt; extra == 'viewer'
30
+ Requires-Dist: phosphor>=0.4; extra == 'viewer'
31
+ Requires-Dist: pyside6>=6.7; extra == 'viewer'
32
+ Requires-Dist: typer>=0.15.1; extra == 'viewer'
28
33
  Description-Content-Type: text/markdown
29
34
 
30
35
  # ezmsg-tools
@@ -53,10 +53,16 @@ sigmon = [
53
53
  "PySide6>=6.7",
54
54
  "pygraphviz>=1.14",
55
55
  "typer>=0.15.1",
56
- "phosphor>=0.2",
56
+ "phosphor>=0.4",
57
57
  "pandas",
58
58
  "ezmsg-qt",
59
59
  ]
60
+ viewer = [
61
+ "PySide6>=6.7",
62
+ "typer>=0.15.1",
63
+ "phosphor>=0.4",
64
+ "ezmsg-qt",
65
+ ]
60
66
 
61
67
  [project.scripts]
62
68
  ezmsg-performance-monitor = "ezmsg.tools.perfmon.cli:main"
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.0'
32
- __version_tuple__ = version_tuple = (0, 2, 0)
31
+ __version__ = version = '0.3.0'
32
+ __version_tuple__ = version_tuple = (0, 3, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -5,7 +5,7 @@ import sys
5
5
 
6
6
  import numpy as np
7
7
  import typer
8
- from ezmsg.qt import EzDynamicSubscriber, EzGuiBridge
8
+ from ezmsg.qt import EzSession, EzSubscriber
9
9
  from phosphor import (
10
10
  ScatterConfig,
11
11
  ScatterWidget,
@@ -60,6 +60,7 @@ class SigmonWindow(QMainWindow):
60
60
  def __init__(
61
61
  self,
62
62
  graph_address: tuple[str, int],
63
+ session: EzSession,
63
64
  parent: QWidget | None = None,
64
65
  ) -> None:
65
66
  super().__init__(parent)
@@ -67,7 +68,7 @@ class SigmonWindow(QMainWindow):
67
68
  self._graph_address = graph_address
68
69
 
69
70
  # Dynamic subscriber — switches topics when the user clicks a graph node.
70
- self._data_sub = EzDynamicSubscriber(parent=self)
71
+ self._data_sub = EzSubscriber(topic=None, parent=self, session=session)
71
72
  self._data_sub.connect(self._on_data)
72
73
 
73
74
  # Layout: splitter with DAG on left, plot on right.
@@ -97,7 +98,9 @@ class SigmonWindow(QMainWindow):
97
98
  shortcut.activated.connect(self._toggle_scatter)
98
99
 
99
100
  def _on_node_selected(self, topic: str) -> None:
100
- self._data_sub.subscribe(topic)
101
+ # logger.debug("Switching to topic: %s", topic)
102
+ print(f"Switching to topic: {topic}")
103
+ self._data_sub.set_topic(topic)
101
104
  self._first_message = True
102
105
  self._channel_labels = None
103
106
  self._channel_positions = None
@@ -146,6 +149,13 @@ class SigmonWindow(QMainWindow):
146
149
  )
147
150
  widget = SpectrumWidget(config)
148
151
 
152
+ elif "ch" in msg.dims and self._channel_positions is not None:
153
+ # ch but no time or freq; assume scatter
154
+ config = ScatterConfig(
155
+ positions=self._channel_positions,
156
+ channel_labels=labels,
157
+ )
158
+ widget = ScatterWidget(config)
149
159
  else:
150
160
  logger.warning("Unknown AxisArray dims: %s — defaulting to sweep", msg.dims)
151
161
  n_samples = msg.shape[0]
@@ -158,7 +168,7 @@ class SigmonWindow(QMainWindow):
158
168
  widget = SweepWidget(config)
159
169
 
160
170
  self._primary_config = config
161
- self._showing_scatter = False
171
+ self._showing_scatter = isinstance(widget, ScatterWidget)
162
172
  self._replace_plot_widget(widget)
163
173
 
164
174
  def _toggle_scatter(self) -> None:
@@ -190,6 +200,10 @@ class SigmonWindow(QMainWindow):
190
200
  sizes = self._splitter.sizes()
191
201
  old = self._splitter.widget(1)
192
202
  if old is not None:
203
+ # Stop the render loop before destroying the Qt widget,
204
+ # otherwise fastplotlib keeps painting a deleted canvas.
205
+ if hasattr(old, "_figure"):
206
+ old._figure.close()
193
207
  old.setParent(None)
194
208
  old.deleteLater()
195
209
  self._splitter.insertWidget(1, widget)
@@ -236,9 +250,10 @@ def _run(
236
250
  graph_address = (graph_ip, int(graph_port_str))
237
251
 
238
252
  app = QApplication.instance() or QApplication(sys.argv)
239
- window = SigmonWindow(graph_address)
253
+ session = EzSession(graph_address=graph_address)
254
+ window = SigmonWindow(graph_address, session)
240
255
  window.showMaximized()
241
- with EzGuiBridge(app, graph_address=graph_address):
256
+ with session:
242
257
  app.exec()
243
258
 
244
259
 
File without changes
@@ -0,0 +1,256 @@
1
+ """ezmsg-viewer — plot a specific ezmsg topic without the graph inspector."""
2
+
3
+ import logging
4
+ import sys
5
+ from enum import Enum
6
+
7
+ import numpy as np
8
+ import typer
9
+ from ezmsg.qt import EzSession, EzSubscriber
10
+ from ezmsg.util.messages.axisarray import AxisArray
11
+ from phosphor import (
12
+ ScatterConfig,
13
+ ScatterWidget,
14
+ SpectrumConfig,
15
+ SpectrumWidget,
16
+ SweepConfig,
17
+ SweepEvent,
18
+ SweepWidget,
19
+ )
20
+ from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ GRAPH_IP = "127.0.0.1"
25
+ GRAPH_PORT = 25978
26
+
27
+ # Event color palette (deterministic by label hash)
28
+ EVENT_COLORS = [
29
+ (1.0, 1.0, 0.4), # yellow
30
+ (0.4, 1.0, 1.0), # cyan
31
+ (1.0, 0.4, 1.0), # magenta
32
+ (1.0, 0.7, 0.3), # orange
33
+ (0.4, 1.0, 0.4), # green
34
+ (1.0, 0.4, 0.4), # red
35
+ ]
36
+
37
+
38
+ class PlotMode(str, Enum):
39
+ timeseries = "timeseries"
40
+ spectral = "spectral"
41
+ scatter = "scatter"
42
+
43
+
44
+ def _extract_channel_meta(msg) -> tuple[list[str] | None, np.ndarray | None]:
45
+ """Extract channel labels and 2D positions from AxisArray channel metadata."""
46
+ if "ch" not in msg.dims:
47
+ return None, None
48
+
49
+ ch_axis = msg.get_axis("ch")
50
+ ch_data = getattr(ch_axis, "data", None)
51
+ if ch_data is None or ch_data.dtype.names is None:
52
+ return None, None
53
+
54
+ labels = None
55
+ if "label" in ch_data.dtype.names:
56
+ labels = [str(v) for v in ch_data["label"]]
57
+
58
+ positions = None
59
+ if "x" in ch_data.dtype.names and "y" in ch_data.dtype.names:
60
+ x = ch_data["x"].astype(np.float32)
61
+ y = ch_data["y"].astype(np.float32)
62
+ if np.any(x != 0) or np.any(y != 0):
63
+ positions = np.column_stack([x, y])
64
+
65
+ return labels, positions
66
+
67
+
68
+ def _event_label(msg) -> str:
69
+ """Try to extract a human-readable label from an event message."""
70
+ if hasattr(msg, "dims") and hasattr(msg, "get_axis"):
71
+ if "ch" in msg.dims:
72
+ ch_axis = msg.get_axis("ch")
73
+ ch_data = getattr(ch_axis, "data", None)
74
+ if ch_data is not None and ch_data.dtype.names and "label" in ch_data.dtype.names:
75
+ labels = ch_data["label"]
76
+ if len(labels) > 0:
77
+ return str(labels[0])
78
+ return ""
79
+
80
+
81
+ class ViewerWindow(QMainWindow):
82
+ def __init__(
83
+ self,
84
+ session: EzSession,
85
+ mode: PlotMode,
86
+ data_topic: str,
87
+ event_topic: str | None = None,
88
+ event_filter: str | None = None,
89
+ parent: QWidget | None = None,
90
+ ) -> None:
91
+ super().__init__(parent)
92
+ self.setWindowTitle(f"ezmsg Viewer — {data_topic}")
93
+ self._mode = mode
94
+ self._event_filter = event_filter
95
+
96
+ self._data_sub = EzSubscriber(topic=data_topic, parent=self, session=session)
97
+ self._data_sub.connect(self._on_data)
98
+
99
+ self._event_sub: EzSubscriber | None = None
100
+ if event_topic:
101
+ self._event_sub = EzSubscriber(topic=event_topic, parent=self, session=session)
102
+ self._event_sub.connect(self._on_event)
103
+
104
+ self._plot_widget: QWidget | None = None
105
+ self._first_message = True
106
+ self._channel_labels: list[str] | None = None
107
+ self._channel_positions: np.ndarray | None = None
108
+
109
+ # ------------------------------------------------------------------
110
+ # Data handling
111
+ # ------------------------------------------------------------------
112
+
113
+ def _on_data(self, msg) -> None:
114
+ if self._first_message:
115
+ self._channel_labels, self._channel_positions = _extract_channel_meta(msg)
116
+ self._create_plot_widget(msg)
117
+ self._first_message = False
118
+ self._push_message(msg)
119
+
120
+ def _create_plot_widget(self, msg) -> None:
121
+ labels = self._channel_labels
122
+
123
+ if self._mode == PlotMode.timeseries:
124
+ if "time" in msg.dims:
125
+ time_axis = msg.get_axis("time")
126
+ srate = 1.0 / time_axis.gain
127
+ time_idx = msg.get_axis_idx("time")
128
+ n_samples = msg.shape[time_idx]
129
+ n_channels = msg.data.size // n_samples
130
+ else:
131
+ logger.warning("No 'time' dimension — using shape[0] as time")
132
+ n_samples = msg.shape[0]
133
+ n_channels = msg.data.size // n_samples if n_samples > 0 else 1
134
+ srate = 1000.0
135
+
136
+ config = SweepConfig(n_channels=n_channels, srate=srate, channel_labels=labels)
137
+ widget = SweepWidget(config)
138
+
139
+ elif self._mode == PlotMode.spectral:
140
+ if "freq" not in msg.dims:
141
+ logger.error("Spectral mode requires 'freq' dimension in data")
142
+ sys.exit(1)
143
+
144
+ freq_axis = msg.get_axis("freq")
145
+ freq_idx = msg.get_axis_idx("freq")
146
+ n_bins = msg.shape[freq_idx]
147
+ srate = 2.0 * freq_axis.gain * n_bins
148
+ n_channels = msg.data.size // n_bins
149
+
150
+ config = SpectrumConfig(n_channels=n_channels, srate=srate, n_bins=n_bins, channel_labels=labels)
151
+ widget = SpectrumWidget(config)
152
+
153
+ elif self._mode == PlotMode.scatter:
154
+ if self._channel_positions is None:
155
+ logger.error("Scatter mode requires channel position metadata (x, y fields in ch axis)")
156
+ sys.exit(1)
157
+
158
+ config = ScatterConfig(positions=self._channel_positions, channel_labels=labels)
159
+ widget = ScatterWidget(config)
160
+
161
+ self._plot_widget = widget
162
+ self.setCentralWidget(widget)
163
+
164
+ def _push_message(self, msg) -> None:
165
+ widget = self._plot_widget
166
+
167
+ if isinstance(widget, SweepWidget):
168
+ time_idx = msg.get_axis_idx("time") if "time" in msg.dims else 0
169
+ n_samples = msg.shape[time_idx]
170
+ n_channels = msg.data.size // n_samples if n_samples > 0 else 1
171
+ data_2d = np.moveaxis(msg.data, time_idx, 0).reshape(n_samples, n_channels)
172
+ # Pass the AxisArray time-axis offset so the sweep buffer
173
+ # tracks the same clock as the event timestamps.
174
+ ts = msg.get_axis("time").offset if "time" in msg.dims else None
175
+ widget.push_data(data_2d.astype(np.float32), timestamps=ts)
176
+
177
+ elif isinstance(widget, SpectrumWidget):
178
+ freq_idx = msg.get_axis_idx("freq") if "freq" in msg.dims else 0
179
+ n_bins = msg.shape[freq_idx]
180
+ n_channels = msg.data.size // n_bins if n_bins > 0 else 1
181
+ data_2d = np.moveaxis(msg.data, freq_idx, 0).reshape(n_bins, n_channels)
182
+ widget.push_data(data_2d.astype(np.float32))
183
+
184
+ elif isinstance(widget, ScatterWidget):
185
+ if len(msg.shape) > 1:
186
+ targ_idx = 0
187
+ if "time" in msg.dims or "freq" in msg.dims:
188
+ targ_idx = msg.get_axis_idx("time") if "time" in msg.dims else msg.get_axis_idx("freq")
189
+ n_items = msg.shape[targ_idx]
190
+ n_channels = msg.data.size // n_items if n_items > 0 else 1
191
+ data_2d = np.moveaxis(msg.data, targ_idx, 0).reshape(n_items, n_channels)
192
+ else:
193
+ data_2d = msg.data.reshape(1, msg.data.size)
194
+ widget.push_data(data_2d.astype(np.float32))
195
+
196
+ # ------------------------------------------------------------------
197
+ # Event handling
198
+ # ------------------------------------------------------------------
199
+
200
+ def _on_event(self, msg: AxisArray) -> None:
201
+ widget = self._plot_widget
202
+ if not isinstance(widget, SweepWidget):
203
+ return
204
+
205
+ if "time" not in msg.dims:
206
+ logger.warning("Event message must have 'time' dimension")
207
+ return
208
+
209
+ time_axis = msg.get_axis("time")
210
+ time_idx = msg.get_axis_idx("time")
211
+ timestamps = time_axis.value(list(range(msg.shape[time_idx])))
212
+ events: list[SweepEvent] = []
213
+ for ev_ix, ts in enumerate(timestamps):
214
+ label = msg.data[ev_ix, 0]
215
+ if self._event_filter and self._event_filter not in label:
216
+ continue
217
+ color = EVENT_COLORS[hash(label) % len(EVENT_COLORS)]
218
+ events.append(SweepEvent(t_elapsed=ts, label=label, color=color))
219
+ widget.push_events(events)
220
+
221
+
222
+ def _run(
223
+ data_topic: str = typer.Argument(..., help="ezmsg topic for continuous data"),
224
+ mode: PlotMode = typer.Option(PlotMode.timeseries, help="Plot mode"),
225
+ event_topic: str | None = typer.Option(None, "--events", help="ezmsg topic for event markers"),
226
+ event_filter: str | None = typer.Option(
227
+ None, "--event-filter", help="Only show events whose label contains this string"
228
+ ),
229
+ graph_addr: str = typer.Option(
230
+ ":".join((GRAPH_IP, str(GRAPH_PORT))),
231
+ help="ezmsg graph address (ip:port)",
232
+ ),
233
+ ) -> None:
234
+ graph_ip, graph_port_str = graph_addr.split(":")
235
+ graph_address = (graph_ip, int(graph_port_str))
236
+
237
+ app = QApplication.instance() or QApplication(sys.argv)
238
+ session = EzSession(graph_address=graph_address)
239
+ window = ViewerWindow(
240
+ session=session,
241
+ mode=mode,
242
+ data_topic=data_topic,
243
+ event_topic=event_topic,
244
+ event_filter=event_filter,
245
+ )
246
+ window.showMaximized()
247
+ with session:
248
+ app.exec()
249
+
250
+
251
+ def main() -> None:
252
+ typer.run(_run)
253
+
254
+
255
+ if __name__ == "__main__":
256
+ main()
File without changes
File without changes
File without changes
File without changes
File without changes