spectra-plot 0.2.7__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 (102) hide show
  1. {spectra_plot-0.2.7/spectra_plot.egg-info → spectra_plot-0.3.0}/PKG-INFO +3 -1
  2. spectra_plot-0.3.0/VERSION +1 -0
  3. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/pyproject.toml +4 -1
  4. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/__init__.py +10 -0
  5. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_codec_fb.py +18 -0
  6. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_easy.py +345 -4
  7. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_embed.py +3 -3
  8. {spectra_plot-0.2.7 → spectra_plot-0.3.0/spectra_plot.egg-info}/PKG-INFO +3 -1
  9. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra_plot.egg-info/requires.txt +2 -0
  10. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_easy.py +3 -2
  11. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_easy_embed.py +2 -2
  12. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_embed.py +2 -2
  13. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_phase2.py +1 -1
  14. spectra_plot-0.2.7/VERSION +0 -1
  15. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/MANIFEST.in +0 -0
  16. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/setup.cfg +0 -0
  17. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_animation.py +0 -0
  18. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_axes.py +0 -0
  19. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_blob.py +0 -0
  20. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_cli.py +0 -0
  21. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_codec.py +0 -0
  22. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_download.py +0 -0
  23. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_errors.py +0 -0
  24. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/__init__.py +0 -0
  25. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/__init__.py +0 -0
  26. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/AckStatePayload.py +0 -0
  27. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/CmdAssignFiguresPayload.py +0 -0
  28. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/CmdCloseWindowPayload.py +0 -0
  29. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/CmdRemoveFigurePayload.py +0 -0
  30. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/CmdSetActivePayload.py +0 -0
  31. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/DiffOp.py +0 -0
  32. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/DiffOpType.py +0 -0
  33. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/EvtFigureDestroyedPayload.py +0 -0
  34. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/EvtInputPayload.py +0 -0
  35. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/EvtTopicListChangedPayload.py +0 -0
  36. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/EvtWindowClosedPayload.py +0 -0
  37. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/HelloPayload.py +0 -0
  38. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/InputType.py +0 -0
  39. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqAddSeriesPayload.py +0 -0
  40. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqAppendDataPayload.py +0 -0
  41. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCloseFigurePayload.py +0 -0
  42. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCloseWindowPayload.py +0 -0
  43. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCreateAxesPayload.py +0 -0
  44. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCreateFigurePayload.py +0 -0
  45. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCreateWindowPayload.py +0 -0
  46. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqDeclareTopicPayload.py +0 -0
  47. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqDestroyFigurePayload.py +0 -0
  48. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqDetachFigurePayload.py +0 -0
  49. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqListTopicsPayload.py +0 -0
  50. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqPublishTopicSamplesPayload.py +0 -0
  51. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqReconnectPayload.py +0 -0
  52. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqRemoveSeriesPayload.py +0 -0
  53. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqSetDataPayload.py +0 -0
  54. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqShowPayload.py +0 -0
  55. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqSubscribeTopicPayload.py +0 -0
  56. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqUnsubscribeTopicPayload.py +0 -0
  57. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqUpdateBatchPayload.py +0 -0
  58. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqUpdatePropertyPayload.py +0 -0
  59. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespAxesCreatedPayload.py +0 -0
  60. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespErrPayload.py +0 -0
  61. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespFigureCreatedPayload.py +0 -0
  62. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespFigureListPayload.py +0 -0
  63. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespOkPayload.py +0 -0
  64. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespSeriesAddedPayload.py +0 -0
  65. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespSubscribeTopicPayload.py +0 -0
  66. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespTopicListPayload.py +0 -0
  67. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/SnapshotAxisState.py +0 -0
  68. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/SnapshotFigureState.py +0 -0
  69. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/SnapshotKnobState.py +0 -0
  70. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/SnapshotSeriesState.py +0 -0
  71. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/StateDiffPayload.py +0 -0
  72. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/StateSnapshotPayload.py +0 -0
  73. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/TopicInfoEntry.py +0 -0
  74. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/WelcomePayload.py +0 -0
  75. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/__init__.py +0 -0
  76. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_figure.py +0 -0
  77. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_launcher.py +0 -0
  78. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_log.py +0 -0
  79. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_persistence.py +0 -0
  80. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_protocol.py +0 -0
  81. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_series.py +0 -0
  82. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_session.py +0 -0
  83. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/_transport.py +0 -0
  84. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/backends/__init__.py +0 -0
  85. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/backends/_qt_compat.py +0 -0
  86. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/backends/backend_qtagg.py +0 -0
  87. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/embed.py +0 -0
  88. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra/topic.py +0 -0
  89. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra_plot.egg-info/SOURCES.txt +0 -0
  90. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra_plot.egg-info/dependency_links.txt +0 -0
  91. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra_plot.egg-info/entry_points.txt +0 -0
  92. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/spectra_plot.egg-info/top_level.txt +0 -0
  93. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_codec.py +0 -0
  94. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_cross_codec.py +0 -0
  95. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_download.py +0 -0
  96. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_packaging_metadata.py +0 -0
  97. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_phase3.py +0 -0
  98. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_phase4.py +0 -0
  99. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_phase5.py +0 -0
  100. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_qt_backend.py +0 -0
  101. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_topic_publisher.py +0 -0
  102. {spectra_plot-0.2.7 → spectra_plot-0.3.0}/tests/test_windows_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spectra-plot
3
- Version: 0.2.7
3
+ Version: 0.3.0
4
4
  Summary: GPU-accelerated scientific plotting via IPC
5
5
  License: MIT
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -15,8 +15,10 @@ Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Topic :: Scientific/Engineering :: Visualization
16
16
  Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
+ Requires-Dist: flatbuffers>=2.0
18
19
  Provides-Extra: numpy
19
20
  Requires-Dist: numpy>=1.20; extra == "numpy"
20
21
  Provides-Extra: dev
21
22
  Requires-Dist: pytest>=7.0; extra == "dev"
22
23
  Requires-Dist: numpy>=1.20; extra == "dev"
24
+ Requires-Dist: flatbuffers>=2.0; extra == "dev"
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -9,6 +9,9 @@ description = "GPU-accelerated scientific plotting via IPC"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
11
11
  requires-python = ">=3.9"
12
+ dependencies = [
13
+ "flatbuffers>=2.0",
14
+ ]
12
15
  classifiers = [
13
16
  "Development Status :: 3 - Alpha",
14
17
  "Intended Audience :: Science/Research",
@@ -24,7 +27,7 @@ classifiers = [
24
27
 
25
28
  [project.optional-dependencies]
26
29
  numpy = ["numpy>=1.20"]
27
- dev = ["pytest>=7.0", "numpy>=1.20"]
30
+ dev = ["pytest>=7.0", "numpy>=1.20", "flatbuffers>=2.0"]
28
31
 
29
32
  [project.scripts]
30
33
  spectra-backend = "spectra._cli:backend_main"
@@ -8,6 +8,8 @@ Usage::
8
8
 
9
9
  sp.plot([1, 4, 9, 16]) # that's it. window opens.
10
10
  sp.plot(x, y, color="red", label="data") # with style
11
+ sp.hline(0) # horizontal reference line
12
+ sp.fplot(lambda x: x**2, -2, 2) # plot a function
11
13
  sp.scatter(x, y) # scatter plot
12
14
  sp.hist(data, bins=50) # histogram
13
15
  sp.plot3(x, y, z) # 3D line
@@ -52,6 +54,10 @@ from ._easy import (
52
54
  violin_plot,
53
55
  hline,
54
56
  vline,
57
+ fplot,
58
+ ihline,
59
+ ivline,
60
+ ifplot,
55
61
  plot3,
56
62
  scatter3,
57
63
  surf,
@@ -105,6 +111,10 @@ __all__ = [
105
111
  "violin_plot",
106
112
  "hline",
107
113
  "vline",
114
+ "fplot",
115
+ "ihline",
116
+ "ivline",
117
+ "ifplot",
108
118
  "plot3",
109
119
  "scatter3",
110
120
  "surf",
@@ -560,3 +560,21 @@ def decode_fb_req_update_property(data: bytes) -> dict:
560
560
  "str_val": str_val_raw.decode("utf-8") if str_val_raw else "",
561
561
  }
562
562
 
563
+
564
+ def decode_fb_snapshot_knobs(data: bytes) -> dict:
565
+ """Extract knob name → value from a STATE_SNAPSHOT / RESP_SNAPSHOT payload."""
566
+ from ._fb_generated.spectra.ipc.fb.StateSnapshotPayload import StateSnapshotPayload
567
+
568
+ raw = _strip(data)
569
+ snap = StateSnapshotPayload.GetRootAs(bytearray(raw), 0)
570
+ out: dict = {}
571
+ n = snap.KnobsLength()
572
+ for i in range(n):
573
+ k = snap.Knobs(i)
574
+ if k is None:
575
+ continue
576
+ name = k.Name()
577
+ if name:
578
+ out[name.decode("utf-8")] = float(k.Value())
579
+ return out
580
+
@@ -137,6 +137,10 @@ class _EasyState:
137
137
  self._shutting_down = False
138
138
  self._axes_bounds: dict = {} # id(axes) -> [xmin, xmax, ymin, ymax]
139
139
  self._subplot_cache: dict = {} # (figure_id, rows, cols, index) -> Axes
140
+ self._pending_knobs: List[dict] = []
141
+ self._interactive_bindings: List = []
142
+ self._interactive_started = False
143
+ self._knob_values: dict = {}
140
144
 
141
145
  def _ensure_session(self):
142
146
  """Lazily create the backend session."""
@@ -160,8 +164,10 @@ class _EasyState:
160
164
  """Show the current figure if it hasn't been shown yet."""
161
165
  fig = self._current_fig
162
166
  if fig is not None and id(fig) in self._pending_show:
167
+ self._flush_pending_knobs()
163
168
  self._pending_show.discard(id(fig))
164
169
  fig.show()
170
+ _ensure_interactive_loop()
165
171
 
166
172
  def _ensure_axes(self):
167
173
  """Lazily create axes on the current figure."""
@@ -178,6 +184,32 @@ class _EasyState:
178
184
  self._current_axes3d = fig.subplot3d(1, 1, 1)
179
185
  return self._current_axes3d
180
186
 
187
+ def _flush_pending_knobs(self) -> None:
188
+ """Push queued knob definitions to the backend before the window opens."""
189
+ if not self._pending_knobs:
190
+ return
191
+ from . import _protocol as P
192
+ from . import _codec as codec
193
+
194
+ session = self._ensure_session()
195
+ for spec in self._pending_knobs:
196
+ payload = codec.encode_req_update_property(
197
+ figure_id=spec["figure_id"],
198
+ axes_index=0,
199
+ series_index=0,
200
+ prop="add_knob",
201
+ f1=spec["value"],
202
+ f2=spec["min"],
203
+ f3=spec["max"],
204
+ str_val=spec["name"],
205
+ )
206
+ try:
207
+ session._request(P.REQ_UPDATE_PROPERTY, payload)
208
+ except Exception:
209
+ pass
210
+ self._knob_values[spec["name"]] = spec["value"]
211
+ self._pending_knobs.clear()
212
+
181
213
  def shutdown(self):
182
214
  """Clean shutdown — stop live threads, close session."""
183
215
  self._shutting_down = True
@@ -187,6 +219,10 @@ class _EasyState:
187
219
  t.join(timeout=2.0)
188
220
  self._live_threads.clear()
189
221
  self._live_stop_events.clear()
222
+ self._pending_knobs.clear()
223
+ self._interactive_bindings.clear()
224
+ self._interactive_started = False
225
+ self._knob_values.clear()
190
226
  if self._session is not None:
191
227
  try:
192
228
  self._session.close()
@@ -686,8 +722,22 @@ def violin_plot(
686
722
  return series
687
723
 
688
724
 
689
- def hline(y: float, color: Union[str, Tuple, List, None] = "gray", label: str = ""):
690
- """Draw a horizontal line across the plot."""
725
+ def hline(
726
+ y: float,
727
+ color: Union[str, Tuple, List, None] = "gray",
728
+ label: str = "",
729
+ *,
730
+ interactive: bool = False,
731
+ ymin: float = -10.0,
732
+ ymax: float = 10.0,
733
+ knob_name: str = "Y line",
734
+ ):
735
+ """Draw a horizontal reference line at y (e.g. ``hline(0)`` for the x-axis).
736
+
737
+ Set ``interactive=True`` (or use ``ihline``) to drag a slider that moves the line.
738
+ """
739
+ if interactive:
740
+ return ihline(y, ymin, ymax, color=color, label=label, knob_name=knob_name)
691
741
  ax = _state._ensure_axes()
692
742
  series = ax.line([-1e12, 1e12], [y, y], label=label)
693
743
  c = _parse_color(color)
@@ -697,8 +747,22 @@ def hline(y: float, color: Union[str, Tuple, List, None] = "gray", label: str =
697
747
  return series
698
748
 
699
749
 
700
- def vline(x: float, color: Union[str, Tuple, List, None] = "gray", label: str = ""):
701
- """Draw a vertical line across the plot."""
750
+ def vline(
751
+ x: float,
752
+ color: Union[str, Tuple, List, None] = "gray",
753
+ label: str = "",
754
+ *,
755
+ interactive: bool = False,
756
+ xmin: float = -10.0,
757
+ xmax: float = 10.0,
758
+ knob_name: str = "X line",
759
+ ):
760
+ """Draw a vertical reference line at x (e.g. ``vline(0)`` for the y-axis).
761
+
762
+ Set ``interactive=True`` (or use ``ivline``) to drag a slider that moves the line.
763
+ """
764
+ if interactive:
765
+ return ivline(x, xmin, xmax, color=color, label=label, knob_name=knob_name)
702
766
  ax = _state._ensure_axes()
703
767
  series = ax.line([x, x], [-1e12, 1e12], label=label)
704
768
  c = _parse_color(color)
@@ -708,6 +772,283 @@ def vline(x: float, color: Union[str, Tuple, List, None] = "gray", label: str =
708
772
  return series
709
773
 
710
774
 
775
+ def fplot(
776
+ func,
777
+ xmin: Optional[float] = None,
778
+ xmax: Optional[float] = None,
779
+ n: int = 200,
780
+ color: Union[str, Tuple, List, None] = None,
781
+ width: Optional[float] = None,
782
+ label: str = "",
783
+ *,
784
+ interactive: bool = False,
785
+ params: Optional[List[Tuple[str, float, float, float]]] = None,
786
+ **kwargs,
787
+ ):
788
+ """Plot y = func(x) over an x range.
789
+
790
+ Usage::
791
+
792
+ sp.fplot(lambda x: x ** 2, -2, 2)
793
+ sp.fplot(math.sin) # uses current axes x-range or [-1, 1]
794
+ sp.fplot(lambda x: x ** 2, color="red", label="parabola")
795
+ sp.ifplot(lambda x, a, b: a * x * x + b, -3, 3, params=[("a", 0.2, -1, 1)])
796
+
797
+ If *xmin* / *xmax* are omitted, uses accumulated axes bounds from prior plots
798
+ on the same axes, otherwise ``[-1, 1]``.
799
+
800
+ Set ``interactive=True`` with *params* ``[(name, value, min, max), ...]`` to
801
+ expose sliders for each coefficient (or use ``ifplot``).
802
+ """
803
+ if interactive:
804
+ return ifplot(func, xmin, xmax, n=n, color=color, width=width, label=label,
805
+ params=params or [], **kwargs)
806
+
807
+ ax = _state._ensure_axes()
808
+
809
+ if xmin is None or xmax is None:
810
+ ax_key = (ax._figure_id, ax._index)
811
+ if ax_key in _state._axes_bounds:
812
+ bounds = _state._axes_bounds[ax_key]
813
+ if xmin is None:
814
+ xmin = bounds[0]
815
+ if xmax is None:
816
+ xmax = bounds[1]
817
+ else:
818
+ if xmin is None:
819
+ xmin = -1.0
820
+ if xmax is None:
821
+ xmax = 1.0
822
+
823
+ if xmin == xmax:
824
+ xmax = xmin + 1.0
825
+
826
+ n = max(int(n), 2)
827
+ step = (xmax - xmin) / (n - 1)
828
+ xs = [xmin + i * step for i in range(n)]
829
+ ys = [float(func(x)) for x in xs]
830
+
831
+ series = ax.line(xs, ys, label=label)
832
+ _apply_series_style(series, color=color, width=width, **kwargs)
833
+ _auto_fit_axes(ax, xs, ys)
834
+ _state._show_if_pending()
835
+ return series
836
+
837
+
838
+ # ─── Interactive reference lines & function plots ─────────────────────────────
839
+
840
+ _REF_SPAN = 1e12
841
+
842
+
843
+ class _InteractiveHLine:
844
+ __slots__ = ("series", "knob_name")
845
+
846
+ def __init__(self, series, knob_name: str) -> None:
847
+ self.series = series
848
+ self.knob_name = knob_name
849
+
850
+ def apply(self, knobs: dict) -> None:
851
+ if self.knob_name not in knobs:
852
+ return
853
+ y = float(knobs[self.knob_name])
854
+ self.series.set_data([-_REF_SPAN, _REF_SPAN], [y, y])
855
+
856
+
857
+ class _InteractiveVLine:
858
+ __slots__ = ("series", "knob_name")
859
+
860
+ def __init__(self, series, knob_name: str) -> None:
861
+ self.series = series
862
+ self.knob_name = knob_name
863
+
864
+ def apply(self, knobs: dict) -> None:
865
+ if self.knob_name not in knobs:
866
+ return
867
+ x = float(knobs[self.knob_name])
868
+ self.series.set_data([x, x], [-_REF_SPAN, _REF_SPAN])
869
+
870
+
871
+ class _InteractiveFplot:
872
+ __slots__ = ("series", "func", "xs", "param_names")
873
+
874
+ def __init__(self, series, func, xs: List[float], param_names: List[str]) -> None:
875
+ self.series = series
876
+ self.func = func
877
+ self.xs = xs
878
+ self.param_names = param_names
879
+
880
+ def apply(self, knobs: dict) -> None:
881
+ pvals = [float(knobs.get(n, 0.0)) for n in self.param_names]
882
+ ys = [float(self.func(x, *pvals)) for x in self.xs]
883
+ self.series.set_data(self.xs, ys)
884
+
885
+
886
+ def _queue_knob(ax, name: str, value: float, vmin: float, vmax: float) -> None:
887
+ spec = {
888
+ "figure_id": ax._figure_id,
889
+ "name": name,
890
+ "value": float(value),
891
+ "min": float(vmin),
892
+ "max": float(vmax),
893
+ }
894
+ _state._knob_values[name] = float(value)
895
+ fig = _state._current_fig
896
+ if fig is not None and id(fig) not in _state._pending_show:
897
+ from . import _protocol as P
898
+ from . import _codec as codec
899
+
900
+ session = _state._ensure_session()
901
+ payload = codec.encode_req_update_property(
902
+ figure_id=spec["figure_id"],
903
+ axes_index=0,
904
+ series_index=0,
905
+ prop="add_knob",
906
+ f1=spec["value"],
907
+ f2=spec["min"],
908
+ f3=spec["max"],
909
+ str_val=spec["name"],
910
+ )
911
+ try:
912
+ session._request(P.REQ_UPDATE_PROPERTY, payload)
913
+ except Exception:
914
+ _state._pending_knobs.append(spec)
915
+ else:
916
+ _state._pending_knobs.append(spec)
917
+
918
+
919
+ def _poll_knob_values(session) -> dict:
920
+ from . import _protocol as P
921
+ from . import _codec as codec
922
+
923
+ try:
924
+ resp = session._request(P.REQ_GET_SNAPSHOT, b"")
925
+ payload = resp["payload"]
926
+ if payload and payload[0] == P.PAYLOAD_FORMAT_FLATBUFFERS:
927
+ from . import _codec_fb as fb_codec
928
+ return fb_codec.decode_fb_snapshot_knobs(payload)
929
+ except Exception:
930
+ pass
931
+ return dict(_state._knob_values)
932
+
933
+
934
+ def _ensure_interactive_loop() -> None:
935
+ if _state._interactive_started or not _state._interactive_bindings:
936
+ return
937
+ _state._interactive_started = True
938
+ from ._animation import ipc_sleep
939
+ from ._log import log
940
+
941
+ stop_event = threading.Event()
942
+ _state._live_stop_events.append(stop_event)
943
+
944
+ def _loop() -> None:
945
+ session = _state._ensure_session()
946
+ session._live_thread_count += 1
947
+ try:
948
+ while not stop_event.is_set():
949
+ ipc_sleep(0.05, session)
950
+ knobs = _poll_knob_values(session)
951
+ _state._knob_values.update(knobs)
952
+ for binding in _state._interactive_bindings:
953
+ try:
954
+ binding.apply(knobs)
955
+ except Exception as e:
956
+ log.warning("interactive update error: %s", e)
957
+ finally:
958
+ session._live_thread_count -= 1
959
+
960
+ thread = threading.Thread(target=_loop, daemon=True, name="spectra-interactive")
961
+ thread.start()
962
+ _state._live_threads.append(thread)
963
+
964
+
965
+ def ihline(
966
+ y: float,
967
+ ymin: float = -10.0,
968
+ ymax: float = 10.0,
969
+ color: Union[str, Tuple, List, None] = "gray",
970
+ label: str = "",
971
+ knob_name: str = "Y line",
972
+ ):
973
+ """Interactive horizontal line — drag the PARAMETERS slider to move y."""
974
+ series = hline(y, color=color, label=label)
975
+ ax = _state._ensure_axes()
976
+ _queue_knob(ax, knob_name, y, ymin, ymax)
977
+ _state._interactive_bindings.append(_InteractiveHLine(series, knob_name))
978
+ _state._show_if_pending()
979
+ return series
980
+
981
+
982
+ def ivline(
983
+ x: float,
984
+ xmin: float = -10.0,
985
+ xmax: float = 10.0,
986
+ color: Union[str, Tuple, List, None] = "gray",
987
+ label: str = "",
988
+ knob_name: str = "X line",
989
+ ):
990
+ """Interactive vertical line — drag the PARAMETERS slider to move x."""
991
+ series = vline(x, color=color, label=label)
992
+ ax = _state._ensure_axes()
993
+ _queue_knob(ax, knob_name, x, xmin, xmax)
994
+ _state._interactive_bindings.append(_InteractiveVLine(series, knob_name))
995
+ _state._show_if_pending()
996
+ return series
997
+
998
+
999
+ def ifplot(
1000
+ func,
1001
+ xmin: Optional[float] = None,
1002
+ xmax: Optional[float] = None,
1003
+ n: int = 200,
1004
+ color: Union[str, Tuple, List, None] = None,
1005
+ width: Optional[float] = None,
1006
+ label: str = "",
1007
+ params: Optional[List[Tuple[str, float, float, float]]] = None,
1008
+ **kwargs,
1009
+ ):
1010
+ """Interactive function plot — sliders retune each entry in *params*.
1011
+
1012
+ *func* signature: ``func(x, *param_values)`` matching *params* order::
1013
+
1014
+ sp.ifplot(lambda x, a, b: a * x * x + b, -3, 3,
1015
+ params=[("a", 0.2, -1, 1), ("b", 0, -2, 2)])
1016
+ """
1017
+ param_specs = params or []
1018
+ ax = _state._ensure_axes()
1019
+
1020
+ if xmin is None or xmax is None:
1021
+ ax_key = (ax._figure_id, ax._index)
1022
+ if ax_key in _state._axes_bounds:
1023
+ bounds = _state._axes_bounds[ax_key]
1024
+ xmin = xmin if xmin is not None else bounds[0]
1025
+ xmax = xmax if xmax is not None else bounds[1]
1026
+ else:
1027
+ xmin = xmin if xmin is not None else -1.0
1028
+ xmax = xmax if xmax is not None else 1.0
1029
+ if xmin == xmax:
1030
+ xmax = xmin + 1.0
1031
+
1032
+ n = max(int(n), 2)
1033
+ step = (xmax - xmin) / (n - 1)
1034
+ xs = [xmin + i * step for i in range(n)]
1035
+ pvals = [p[1] for p in param_specs]
1036
+ ys = [float(func(x, *pvals)) for x in xs]
1037
+
1038
+ series = ax.line(xs, ys, label=label)
1039
+ _apply_series_style(series, color=color, width=width, **kwargs)
1040
+ _auto_fit_axes(ax, xs, ys)
1041
+
1042
+ param_names: List[str] = []
1043
+ for name, value, vmin, vmax in param_specs:
1044
+ _queue_knob(ax, name, value, vmin, vmax)
1045
+ param_names.append(name)
1046
+
1047
+ _state._interactive_bindings.append(_InteractiveFplot(series, func, xs, param_names))
1048
+ _state._show_if_pending()
1049
+ return series
1050
+
1051
+
711
1052
  # ─── Auto-fit helper ─────────────────────────────────────────────────────────
712
1053
 
713
1054
  def _auto_fit_axes(ax, x: List[float], y: List[float]) -> None:
@@ -76,7 +76,7 @@ def _find_library() -> str:
76
76
  return env_path
77
77
 
78
78
  # 2. Next to this file (installed package)
79
- pkg_dir = Path(__file__).parent
79
+ pkg_dir = Path(__file__).resolve().parent
80
80
  for candidate in [
81
81
  pkg_dir / "libspectra_embed.so",
82
82
  pkg_dir / "spectra_embed.dll",
@@ -93,9 +93,9 @@ def _find_library() -> str:
93
93
  if candidate.is_file():
94
94
  return str(candidate)
95
95
 
96
- # 4. System search
96
+ # 4. System search (only accept resolvable absolute paths)
97
97
  found = ctypes.util.find_library("spectra_embed")
98
- if found:
98
+ if found and os.path.isabs(found) and os.path.isfile(found):
99
99
  return found
100
100
 
101
101
  raise FileNotFoundError(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spectra-plot
3
- Version: 0.2.7
3
+ Version: 0.3.0
4
4
  Summary: GPU-accelerated scientific plotting via IPC
5
5
  License: MIT
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -15,8 +15,10 @@ Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Topic :: Scientific/Engineering :: Visualization
16
16
  Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
+ Requires-Dist: flatbuffers>=2.0
18
19
  Provides-Extra: numpy
19
20
  Requires-Dist: numpy>=1.20; extra == "numpy"
20
21
  Provides-Extra: dev
21
22
  Requires-Dist: pytest>=7.0; extra == "dev"
22
23
  Requires-Dist: numpy>=1.20; extra == "dev"
24
+ Requires-Dist: flatbuffers>=2.0; extra == "dev"
@@ -1,7 +1,9 @@
1
+ flatbuffers>=2.0
1
2
 
2
3
  [dev]
3
4
  pytest>=7.0
4
5
  numpy>=1.20
6
+ flatbuffers>=2.0
5
7
 
6
8
  [numpy]
7
9
  numpy>=1.20
@@ -190,7 +190,7 @@ class TestAPISurface:
190
190
  """Verify all expected functions are importable."""
191
191
 
192
192
  def test_plot_functions(self):
193
- from spectra import plot, scatter, stem, hist, bar, hline, vline
193
+ from spectra import plot, scatter, stem, hist, bar, hline, vline, fplot
194
194
  assert callable(plot)
195
195
  assert callable(scatter)
196
196
  assert callable(stem)
@@ -198,6 +198,7 @@ class TestAPISurface:
198
198
  assert callable(bar)
199
199
  assert callable(hline)
200
200
  assert callable(vline)
201
+ assert callable(fplot)
201
202
 
202
203
  def test_3d_functions(self):
203
204
  from spectra import plot3, scatter3, surf
@@ -247,7 +248,7 @@ class TestAPISurface:
247
248
  def test_all_in_all(self):
248
249
  import spectra
249
250
  expected = [
250
- "plot", "scatter", "stem", "hist", "bar", "hline", "vline",
251
+ "plot", "scatter", "stem", "hist", "bar", "hline", "vline", "fplot",
251
252
  "plot3", "scatter3", "surf", "plotn", "subplots",
252
253
  "figure", "subplot", "gcf", "gca",
253
254
  "title", "xlabel", "ylabel", "xlim", "ylim", "grid", "legend",
@@ -6,8 +6,8 @@ import tempfile
6
6
 
7
7
  # Skip render tests if libspectra_embed.so is not built.
8
8
  try:
9
- from spectra._embed import _find_library
10
- _find_library()
9
+ from spectra._embed import _load_lib
10
+ _load_lib()
11
11
  _EMBED_AVAILABLE = True
12
12
  except (ImportError, FileNotFoundError, OSError):
13
13
  _EMBED_AVAILABLE = False
@@ -8,7 +8,7 @@ import ctypes
8
8
  # Use a flag + @pytest.mark.skipif on each test class instead.
9
9
  try:
10
10
  from spectra._embed import (
11
- _find_library,
11
+ _load_lib,
12
12
  EmbedSurface,
13
13
  EmbedFigure,
14
14
  EmbedAxes,
@@ -22,7 +22,7 @@ try:
22
22
  KEY_R,
23
23
  KEY_ESCAPE,
24
24
  )
25
- _find_library() # probe now so we know the library is actually present
25
+ _load_lib() # probe load (not just path lookup) so missing deps skip cleanly
26
26
  _EMBED_AVAILABLE = True
27
27
  except (ImportError, FileNotFoundError, OSError):
28
28
  _EMBED_AVAILABLE = False
@@ -259,7 +259,7 @@ class TestConvenienceAPI:
259
259
  def test_version_exists(self):
260
260
  import spectra as sp
261
261
  assert hasattr(sp, "__version__")
262
- assert sp.__version__ == "0.2.0"
262
+ assert sp.__version__ == "0.2.1"
263
263
 
264
264
  def test_session_class_exported(self):
265
265
  from spectra import Session
@@ -1 +0,0 @@
1
- 0.2.7
File without changes
File without changes