spectra-plot 0.2.6__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.
- {spectra_plot-0.2.6/spectra_plot.egg-info → spectra_plot-0.3.0}/PKG-INFO +3 -1
- spectra_plot-0.3.0/VERSION +1 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/pyproject.toml +4 -1
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/__init__.py +10 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_codec.py +17 -11
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_codec_fb.py +74 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_easy.py +345 -4
- spectra_plot-0.3.0/spectra/_embed.py +1678 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_launcher.py +30 -18
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_transport.py +18 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/embed.py +148 -55
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/topic.py +38 -10
- {spectra_plot-0.2.6 → spectra_plot-0.3.0/spectra_plot.egg-info}/PKG-INFO +3 -1
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra_plot.egg-info/SOURCES.txt +1 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra_plot.egg-info/requires.txt +2 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_easy.py +3 -2
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_easy_embed.py +2 -2
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_embed.py +147 -2
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_phase2.py +1 -1
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_phase4.py +40 -117
- spectra_plot-0.3.0/tests/test_topic_publisher.py +127 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_windows_compat.py +4 -4
- spectra_plot-0.2.6/VERSION +0 -1
- spectra_plot-0.2.6/spectra/_embed.py +0 -504
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/MANIFEST.in +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/setup.cfg +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_animation.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_axes.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_blob.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_cli.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_download.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_errors.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/__init__.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/__init__.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/AckStatePayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/CmdAssignFiguresPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/CmdCloseWindowPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/CmdRemoveFigurePayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/CmdSetActivePayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/DiffOp.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/DiffOpType.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/EvtFigureDestroyedPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/EvtInputPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/EvtTopicListChangedPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/EvtWindowClosedPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/HelloPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/InputType.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqAddSeriesPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqAppendDataPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCloseFigurePayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCloseWindowPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCreateAxesPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCreateFigurePayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqCreateWindowPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqDeclareTopicPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqDestroyFigurePayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqDetachFigurePayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqListTopicsPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqPublishTopicSamplesPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqReconnectPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqRemoveSeriesPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqSetDataPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqShowPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqSubscribeTopicPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqUnsubscribeTopicPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqUpdateBatchPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/ReqUpdatePropertyPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespAxesCreatedPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespErrPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespFigureCreatedPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespFigureListPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespOkPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespSeriesAddedPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespSubscribeTopicPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/RespTopicListPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/SnapshotAxisState.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/SnapshotFigureState.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/SnapshotKnobState.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/SnapshotSeriesState.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/StateDiffPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/StateSnapshotPayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/TopicInfoEntry.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/WelcomePayload.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_fb_generated/spectra/ipc/fb/__init__.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_figure.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_log.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_persistence.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_protocol.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_series.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/_session.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/backends/__init__.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/backends/_qt_compat.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra/backends/backend_qtagg.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra_plot.egg-info/dependency_links.txt +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra_plot.egg-info/entry_points.txt +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/spectra_plot.egg-info/top_level.txt +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_codec.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_cross_codec.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_download.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_packaging_metadata.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_phase3.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_phase5.py +0 -0
- {spectra_plot-0.2.6 → spectra_plot-0.3.0}/tests/test_qt_backend.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spectra-plot
|
|
3
|
-
Version: 0.
|
|
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",
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""IPC payload codec mirroring src/ipc/codec.hpp.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
a raw tag byte (never 0x00 or 0x01 in practice, but 0x00 is reserved).
|
|
7
|
-
Decode functions auto-detect the format and delegate to _codec_fb when needed.
|
|
3
|
+
All encode_* functions emit FlatBuffers (0x01 prefix) via _codec_fb.
|
|
4
|
+
PayloadEncoder remains for a few legacy write paths (chunked set-data, anim).
|
|
5
|
+
Decode functions auto-detect FlatBuffers vs legacy TLV and delegate to _codec_fb.
|
|
8
6
|
"""
|
|
9
7
|
|
|
10
8
|
import struct
|
|
@@ -306,17 +304,25 @@ def encode_req_remove_series(figure_id: int, series_index: int) -> bytes:
|
|
|
306
304
|
def encode_req_close_figure(figure_id: int) -> bytes:
|
|
307
305
|
return fb_codec.encode_fb_req_close_figure(figure_id=figure_id)
|
|
308
306
|
|
|
307
|
+
def decode_req_update_batch(data: bytes) -> list:
|
|
308
|
+
"""Decode REQ_UPDATE_BATCH. Returns a list of update dicts."""
|
|
309
|
+
if fb_codec._is_fb(data):
|
|
310
|
+
return fb_codec.decode_fb_req_update_batch(data)
|
|
311
|
+
updates = []
|
|
312
|
+
dec = PayloadDecoder(data)
|
|
313
|
+
while dec.next():
|
|
314
|
+
if dec.tag == P.TAG_BATCH_ITEM:
|
|
315
|
+
updates.append(decode_req_update_property(dec.as_blob()))
|
|
316
|
+
return updates
|
|
317
|
+
|
|
318
|
+
|
|
309
319
|
def encode_req_update_batch(updates: list) -> bytes:
|
|
310
320
|
"""Encode REQ_UPDATE_BATCH — multiple property updates in one message.
|
|
311
321
|
|
|
312
322
|
Each item in `updates` is a dict with keys matching encode_req_update_property kwargs:
|
|
313
323
|
figure_id, axes_index, series_index, prop, f1..f4, bool_val, str_val
|
|
314
324
|
"""
|
|
315
|
-
|
|
316
|
-
for upd in updates:
|
|
317
|
-
item_bytes = encode_req_update_property(**upd)
|
|
318
|
-
enc.put_blob(P.TAG_BATCH_ITEM, item_bytes)
|
|
319
|
-
return enc.take()
|
|
325
|
+
return fb_codec.encode_fb_req_update_batch(updates)
|
|
320
326
|
|
|
321
327
|
|
|
322
328
|
def encode_req_reconnect(session_id: int, session_token: str = "") -> bytes:
|
|
@@ -217,6 +217,38 @@ def encode_fb_req_update_property(
|
|
|
217
217
|
return _finalize(builder)
|
|
218
218
|
|
|
219
219
|
|
|
220
|
+
def encode_fb_req_update_batch(updates: List[dict]) -> bytes:
|
|
221
|
+
"""Encode REQ_UPDATE_BATCH with nested property updates."""
|
|
222
|
+
builder = flatbuffers.Builder(1024)
|
|
223
|
+
offsets = []
|
|
224
|
+
for upd in updates:
|
|
225
|
+
prop_off = builder.CreateString(upd.get("prop", ""))
|
|
226
|
+
str_val = upd.get("str_val", "")
|
|
227
|
+
str_off = builder.CreateString(str_val) if str_val else None
|
|
228
|
+
FBReqUpdProp.Start(builder)
|
|
229
|
+
FBReqUpdProp.AddFigureId(builder, upd.get("figure_id", 0))
|
|
230
|
+
FBReqUpdProp.AddAxesIndex(builder, upd.get("axes_index", 0))
|
|
231
|
+
FBReqUpdProp.AddSeriesIndex(builder, upd.get("series_index", 0))
|
|
232
|
+
FBReqUpdProp.AddProperty(builder, prop_off)
|
|
233
|
+
FBReqUpdProp.AddF1(builder, upd.get("f1", 0.0))
|
|
234
|
+
FBReqUpdProp.AddF2(builder, upd.get("f2", 0.0))
|
|
235
|
+
FBReqUpdProp.AddF3(builder, upd.get("f3", 0.0))
|
|
236
|
+
FBReqUpdProp.AddF4(builder, upd.get("f4", 0.0))
|
|
237
|
+
FBReqUpdProp.AddBoolVal(builder, upd.get("bool_val", False))
|
|
238
|
+
if str_off is not None:
|
|
239
|
+
FBReqUpdProp.AddStrVal(builder, str_off)
|
|
240
|
+
offsets.append(FBReqUpdProp.End(builder))
|
|
241
|
+
FBReqUpdBatch.StartUpdatesVector(builder, len(offsets))
|
|
242
|
+
for off in reversed(offsets):
|
|
243
|
+
builder.PrependUOffsetTRelative(off)
|
|
244
|
+
upds_off = builder.EndVector()
|
|
245
|
+
FBReqUpdBatch.Start(builder)
|
|
246
|
+
FBReqUpdBatch.AddUpdates(builder, upds_off)
|
|
247
|
+
root = builder.EndObject()
|
|
248
|
+
builder.Finish(root)
|
|
249
|
+
return _finalize(builder)
|
|
250
|
+
|
|
251
|
+
|
|
220
252
|
def encode_fb_req_show(figure_id: int, window_id: int = 0) -> bytes:
|
|
221
253
|
builder = flatbuffers.Builder(64)
|
|
222
254
|
FBReqShow.Start(builder)
|
|
@@ -330,6 +362,30 @@ def decode_fb_evt_window_closed(data: bytes) -> Tuple[int, int, str]:
|
|
|
330
362
|
)
|
|
331
363
|
|
|
332
364
|
|
|
365
|
+
def decode_fb_req_update_batch(data: bytes) -> List[dict]:
|
|
366
|
+
buf = _strip(data) if _is_fb(data) else data
|
|
367
|
+
fb = FBReqUpdBatch.ReqUpdateBatchPayload.GetRootAs(buf, 0)
|
|
368
|
+
updates = []
|
|
369
|
+
if fb.UpdatesLength():
|
|
370
|
+
for i in range(fb.UpdatesLength()):
|
|
371
|
+
upd = fb.Updates(i)
|
|
372
|
+
updates.append(
|
|
373
|
+
{
|
|
374
|
+
"figure_id": upd.FigureId(),
|
|
375
|
+
"axes_index": upd.AxesIndex(),
|
|
376
|
+
"series_index": upd.SeriesIndex(),
|
|
377
|
+
"prop": (upd.Property() or b"").decode("utf-8", errors="replace"),
|
|
378
|
+
"f1": upd.F1(),
|
|
379
|
+
"f2": upd.F2(),
|
|
380
|
+
"f3": upd.F3(),
|
|
381
|
+
"f4": upd.F4(),
|
|
382
|
+
"bool_val": upd.BoolVal(),
|
|
383
|
+
"str_val": (upd.StrVal() or b"").decode("utf-8", errors="replace"),
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
return updates
|
|
387
|
+
|
|
388
|
+
|
|
333
389
|
def decode_fb_evt_figure_destroyed(data: bytes) -> Tuple[int, str]:
|
|
334
390
|
buf = _strip(data) if _is_fb(data) else data
|
|
335
391
|
fb = FBEvtFigDestroyed.EvtFigureDestroyedPayload.GetRootAs(buf, 0)
|
|
@@ -504,3 +560,21 @@ def decode_fb_req_update_property(data: bytes) -> dict:
|
|
|
504
560
|
"str_val": str_val_raw.decode("utf-8") if str_val_raw else "",
|
|
505
561
|
}
|
|
506
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(
|
|
690
|
-
|
|
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(
|
|
701
|
-
|
|
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:
|