qplotter 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qplot/__init__.py +46 -0
- qplot/__main__.py +232 -0
- qplot/_auxiliary_paths.py +67 -0
- qplot/_build_info.json +1 -0
- qplot/_metadata.py +75 -0
- qplot/_repair.py +14 -0
- qplot/_shutdown_supervisor.py +4049 -0
- qplot/_version.py +12 -0
- qplot/_windows_shutdown_job.py +1166 -0
- qplot/configuration/__init__.py +3 -0
- qplot/configuration/config.py +578 -0
- qplot/configuration/config_schema.json +357 -0
- qplot/configuration/fonts.py +3 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +592 -0
- qplot/configuration/themes/blank.py +17 -0
- qplot/configuration/themes/dark.py +54 -0
- qplot/configuration/themes/light.py +54 -0
- qplot/datahandling/LoadFromDB.py +224 -0
- qplot/datahandling/__init__.py +47 -0
- qplot/datahandling/_trusted_live_helper.py +812 -0
- qplot/datahandling/_trusted_live_protocol.py +1745 -0
- qplot/datahandling/database.py +2340 -0
- qplot/datahandling/dimensions.py +43 -0
- qplot/datahandling/file_identity.py +461 -0
- qplot/datahandling/parameter_data.py +149 -0
- qplot/datahandling/plot_progress.py +17 -0
- qplot/datahandling/qcodes_cache.py +277 -0
- qplot/datahandling/qcodes_compat.py +14 -0
- qplot/datahandling/readSQL.py +2442 -0
- qplot/datahandling/readonly.py +2566 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +148 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +876 -0
- qplot/datahandling/trusted_heatmap.py +246 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1919 -0
- qplot/datahandling/trusted_live_supervisor.py +2161 -0
- qplot/datahandling/trusted_plot.py +283 -0
- qplot/datahandling/trusted_presentation.py +1065 -0
- qplot/datahandling/trusted_snapshot.py +1132 -0
- qplot/datahandling/trusted_work_coordinator.py +2157 -0
- qplot/datahandling/trusted_work_scheduler.py +1204 -0
- qplot/diagnostics.py +241 -0
- qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
- qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
- qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
- qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
- qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
- qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
- qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
- qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
- qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
- qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
- qplot/testdata.py +1804 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +123 -0
- qplot/tools/general.py +139 -0
- qplot/tools/heatmap_geometry.py +391 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +650 -0
- qplot/tools/sample_statistics.py +111 -0
- qplot/tools/worker.py +2838 -0
- qplot/windows/__init__.py +9 -0
- qplot/windows/_colorbar.py +422 -0
- qplot/windows/_commands.py +523 -0
- qplot/windows/_config_persistence.py +123 -0
- qplot/windows/_database_actions.py +4933 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_display_precision.py +48 -0
- qplot/windows/_dragdrop.py +70 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +210 -0
- qplot/windows/_mixed_appearance.py +303 -0
- qplot/windows/_mixed_plots.py +461 -0
- qplot/windows/_native_averaging.py +323 -0
- qplot/windows/_native_transforms.py +551 -0
- qplot/windows/_plot1d_snap.py +612 -0
- qplot/windows/_plot1d_traces.py +2236 -0
- qplot/windows/_plot2d_colorbar.py +1127 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2291 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +2082 -0
- qplot/windows/_plot_actions.py +2703 -0
- qplot/windows/_plot_annotation.py +186 -0
- qplot/windows/_plot_appearance.py +283 -0
- qplot/windows/_plot_axis_scaling.py +1521 -0
- qplot/windows/_plot_data_viewbox.py +94 -0
- qplot/windows/_plot_drop_axes.py +224 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +112 -0
- qplot/windows/_plot_marquee.py +659 -0
- qplot/windows/_plot_refresh.py +950 -0
- qplot/windows/_plot_source_promotion.py +178 -0
- qplot/windows/_plot_state.py +237 -0
- qplot/windows/_plot_status.py +115 -0
- qplot/windows/_plot_tiling.py +35 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +724 -0
- qplot/windows/_refresh_interval.py +172 -0
- qplot/windows/_run_controls.py +789 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +532 -0
- qplot/windows/_subplots/subplot2d.py +919 -0
- qplot/windows/_svg_export.py +209 -0
- qplot/windows/_trusted_derived_qt.py +1759 -0
- qplot/windows/_widgets/__init__.py +23 -0
- qplot/windows/_widgets/_run_formatting.py +387 -0
- qplot/windows/_widgets/details_tables.py +1119 -0
- qplot/windows/_widgets/dropbox.py +290 -0
- qplot/windows/_widgets/operations.py +460 -0
- qplot/windows/_widgets/preview.py +2997 -0
- qplot/windows/_widgets/run_list_items.py +488 -0
- qplot/windows/_widgets/toolbar.py +111 -0
- qplot/windows/_widgets/treeWidgets.py +2572 -0
- qplot/windows/_window_controls.py +414 -0
- qplot/windows/main.py +1632 -0
- qplot/windows/plot1d.py +313 -0
- qplot/windows/plot2d.py +1935 -0
- qplotter-1.6.0.dist-info/METADATA +322 -0
- qplotter-1.6.0.dist-info/RECORD +130 -0
- qplotter-1.6.0.dist-info/WHEEL +5 -0
- qplotter-1.6.0.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
- qplotter-1.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
"""Deterministic, bounded, Qt-independent Stage 5C derived rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import binascii
|
|
6
|
+
import math
|
|
7
|
+
import struct
|
|
8
|
+
import zlib
|
|
9
|
+
from bisect import bisect_left
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from fractions import Fraction
|
|
12
|
+
from typing import Any, TypeAlias, cast
|
|
13
|
+
|
|
14
|
+
from qplot.datahandling.trusted_live_queries import (
|
|
15
|
+
Trusted2DGridLayout,
|
|
16
|
+
TrustedDerivedSourceObservation,
|
|
17
|
+
)
|
|
18
|
+
from qplot.datahandling.trusted_work_scheduler import (
|
|
19
|
+
RenderingOptions,
|
|
20
|
+
TrustedWorkKind,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
TRUSTED_DERIVED_RENDERER_VERSION = "trusted-derived-renderer-v7"
|
|
24
|
+
TRUSTED_DERIVED_MAX_IMAGES = 8
|
|
25
|
+
TRUSTED_DERIVED_MAX_IMAGE_WIDTH = 2_048
|
|
26
|
+
TRUSTED_DERIVED_MAX_IMAGE_HEIGHT = 2_048
|
|
27
|
+
TRUSTED_DERIVED_MAX_DECODED_IMAGE_BYTES = 16 * 1024 * 1024
|
|
28
|
+
TRUSTED_DERIVED_MAX_ENCODED_IMAGE_BYTES = 8 * 1024 * 1024
|
|
29
|
+
TRUSTED_DERIVED_MAX_RETAINED_PAYLOAD_BYTES = 16 * 1024 * 1024
|
|
30
|
+
TRUSTED_DERIVED_MAX_PAYLOAD_DEPTH = 32
|
|
31
|
+
TRUSTED_DERIVED_MAX_PAYLOAD_NODES = 131_072
|
|
32
|
+
TRUSTED_DERIVED_MAX_PAYLOAD_ITEMS = 65_536
|
|
33
|
+
TRUSTED_DERIVED_MAX_TEXT_BYTES = 1024 * 1024
|
|
34
|
+
|
|
35
|
+
DerivedScalar: TypeAlias = None | bool | int | float | str | bytes
|
|
36
|
+
DerivedValue: TypeAlias = DerivedScalar | tuple["DerivedValue", ...]
|
|
37
|
+
DerivedPayload: TypeAlias = dict[str, DerivedValue]
|
|
38
|
+
CancelCheck: TypeAlias = Callable[[], None]
|
|
39
|
+
_DEFAULT_RENDERING_OPTIONS = RenderingOptions()
|
|
40
|
+
_VIRIDIS_RGB = bytes.fromhex(
|
|
41
|
+
"44015444025544035745055845065a45085b46095c460b5e460c5f460e61470f62471163471265471466471567471669"
|
|
42
|
+
"47186a48196b481a6c481c6e481d6f481e70482071482172482273482374472575472676472777472878472a79472b7a"
|
|
43
|
+
"472c7b462d7c462f7c46307d46317e45327f45347f453580453681443781443982433a83433b83433c84423d84423e85"
|
|
44
|
+
"4240854141864142864043874044873f45873f47883e48883e49893d4a893d4b893d4c893c4d8a3c4e8a3b508a3b518a"
|
|
45
|
+
"3a528b3a538b39548b39558b38568b38578c37588c37598c365a8c365b8c355c8c355d8c345e8d345f8d33608d33618d"
|
|
46
|
+
"32628d32638d31648d31658d31668d30678d30688d2f698d2f6a8d2e6b8e2e6c8e2e6d8e2d6e8e2d6f8e2c708e2c718e"
|
|
47
|
+
"2c728e2b738e2b748e2a758e2a768e2a778e29788e29798e287a8e287a8e287b8e277c8e277d8e277e8e267f8e26808e"
|
|
48
|
+
"26818e25828e25838d24848d24858d24868d23878d23888d23898d22898d228a8d228b8d218c8d218d8c218e8c208f8c"
|
|
49
|
+
"20908c20918c1f928c1f938b1f948b1f958b1f968b1e978a1e988a1e998a1e998a1e9a891e9b891e9c891e9d881e9e88"
|
|
50
|
+
"1e9f881ea0871fa1871fa2861fa38620a48520a58521a68521a78422a78423a88323a98224aa8225ab8126ac8127ad80"
|
|
51
|
+
"28ae7f29af7f2ab07e2bb17d2cb17d2eb27c2fb37b30b47a32b57a33b67935b77836b87738b97639b9763bba753dbb74"
|
|
52
|
+
"3ebc7340bd7242be7144be7045bf6f47c06e49c16d4bc26c4dc26b4fc36951c46853c56755c66657c66559c7645bc862"
|
|
53
|
+
"5ec96160c96062ca5f64cb5d67cc5c69cc5b6bcd596dce5870ce5672cf5574d05477d05279d1517cd24f7ed24e81d34c"
|
|
54
|
+
"83d34b86d44988d5478bd5468dd64490d64392d74195d73f97d83e9ad83c9dd93a9fd938a2da37a5da35a7db33aadb32"
|
|
55
|
+
"addc30afdc2eb2dd2cb5dd2bb7dd29bade27bdde26bfdf24c2df22c5df21c7e01fcae01ecde01dcfe11cd2e11bd4e11a"
|
|
56
|
+
"d7e219dae218dce218dfe318e1e318e4e318e7e419e9e419ece41aeee51bf1e51cf3e51ef6e61ff8e621fae622fde724"
|
|
57
|
+
)
|
|
58
|
+
_MISSING_CELL_RGBA = (230, 230, 230, 255)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TrustedDerivedRenderingError(RuntimeError):
|
|
62
|
+
"""A bounded source could not be converted into a safe payload."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _UnsupportedNumericData(TrustedDerivedRenderingError):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _option_integer(
|
|
70
|
+
options: RenderingOptions,
|
|
71
|
+
name: str,
|
|
72
|
+
default: int,
|
|
73
|
+
maximum: int,
|
|
74
|
+
) -> int:
|
|
75
|
+
value = dict(options.values).get(name, default)
|
|
76
|
+
if type(value) is not int or not 1 <= value <= maximum:
|
|
77
|
+
raise ValueError(f"Rendering option {name!r} must be from 1 through {maximum}.")
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _finite_number(value: object) -> int | float | None:
|
|
82
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
83
|
+
return None
|
|
84
|
+
return value if math.isfinite(value) else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _source_identity(observation: TrustedDerivedSourceObservation) -> DerivedValue:
|
|
88
|
+
return (
|
|
89
|
+
("run_id", observation.run_id),
|
|
90
|
+
("run_guid", observation.run_guid),
|
|
91
|
+
("result_table", observation.result_table_name),
|
|
92
|
+
("result_watermark", observation.result_watermark),
|
|
93
|
+
("helper_incarnation", observation.helper_incarnation),
|
|
94
|
+
("data_version", observation.data_version),
|
|
95
|
+
("schema_sha256", observation.result_schema_sha256),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _base_payload(
|
|
100
|
+
observation: TrustedDerivedSourceObservation,
|
|
101
|
+
kind: TrustedWorkKind,
|
|
102
|
+
*,
|
|
103
|
+
status: str,
|
|
104
|
+
description: str,
|
|
105
|
+
) -> DerivedPayload:
|
|
106
|
+
return {
|
|
107
|
+
"format": "qplot-trusted-derived-payload-v1",
|
|
108
|
+
"kind": kind.name.lower(),
|
|
109
|
+
"status": status,
|
|
110
|
+
"description": description,
|
|
111
|
+
"source": _source_identity(observation),
|
|
112
|
+
"images": (),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _metadata_payload(
|
|
117
|
+
observation: TrustedDerivedSourceObservation,
|
|
118
|
+
) -> DerivedPayload:
|
|
119
|
+
payload = _base_payload(
|
|
120
|
+
observation,
|
|
121
|
+
TrustedWorkKind.METADATA,
|
|
122
|
+
status="ok",
|
|
123
|
+
description="Bounded trusted run metadata.",
|
|
124
|
+
)
|
|
125
|
+
payload["metadata"] = (
|
|
126
|
+
("run_id", observation.run_id),
|
|
127
|
+
("guid", observation.run_guid),
|
|
128
|
+
("result_table_name", observation.result_table_name),
|
|
129
|
+
("result_count", observation.result_watermark),
|
|
130
|
+
("planned_shape", cast(DerivedValue, observation.planned_shape)),
|
|
131
|
+
("sampled_rows", len(observation.sample_rows)),
|
|
132
|
+
("sampled_columns", len(observation.sample_columns)),
|
|
133
|
+
(
|
|
134
|
+
"run_fields",
|
|
135
|
+
cast(
|
|
136
|
+
DerivedValue,
|
|
137
|
+
observation.run_fields
|
|
138
|
+
or (
|
|
139
|
+
("run_id", observation.run_id),
|
|
140
|
+
("guid", observation.run_guid),
|
|
141
|
+
("result_table_name", observation.result_table_name),
|
|
142
|
+
("result_count", observation.result_watermark),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
(
|
|
147
|
+
"parameters",
|
|
148
|
+
tuple(
|
|
149
|
+
(
|
|
150
|
+
parameter.name,
|
|
151
|
+
parameter.label,
|
|
152
|
+
parameter.unit,
|
|
153
|
+
parameter.depends_on,
|
|
154
|
+
parameter.paramtype,
|
|
155
|
+
)
|
|
156
|
+
for parameter in observation.parameters
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
(
|
|
160
|
+
"setpoint_summaries",
|
|
161
|
+
tuple(
|
|
162
|
+
(summary.name, summary.first, summary.last, summary.steps)
|
|
163
|
+
for summary in observation.setpoint_summaries
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
return payload
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def render_trusted_derived_payload(
|
|
171
|
+
observation: TrustedDerivedSourceObservation,
|
|
172
|
+
kind: TrustedWorkKind,
|
|
173
|
+
options: RenderingOptions = _DEFAULT_RENDERING_OPTIONS,
|
|
174
|
+
*,
|
|
175
|
+
cancel_check: CancelCheck = lambda: None,
|
|
176
|
+
) -> DerivedPayload:
|
|
177
|
+
"""Render one bounded observation into versioned primitive/PNG payloads."""
|
|
178
|
+
|
|
179
|
+
if not isinstance(observation, TrustedDerivedSourceObservation):
|
|
180
|
+
raise TypeError("observation must be TrustedDerivedSourceObservation.")
|
|
181
|
+
if not isinstance(kind, TrustedWorkKind):
|
|
182
|
+
raise TypeError("kind must be TrustedWorkKind.")
|
|
183
|
+
if not isinstance(options, RenderingOptions):
|
|
184
|
+
raise TypeError("options must be RenderingOptions.")
|
|
185
|
+
cancel_check()
|
|
186
|
+
if kind is TrustedWorkKind.METADATA:
|
|
187
|
+
return _metadata_payload(observation)
|
|
188
|
+
if observation.unsupported_reason:
|
|
189
|
+
return _base_payload(
|
|
190
|
+
observation,
|
|
191
|
+
kind,
|
|
192
|
+
status="unsupported",
|
|
193
|
+
description=observation.unsupported_reason,
|
|
194
|
+
)
|
|
195
|
+
if observation.result_watermark == 0 or not observation.sample_rows:
|
|
196
|
+
return _base_payload(
|
|
197
|
+
observation,
|
|
198
|
+
kind,
|
|
199
|
+
status="empty",
|
|
200
|
+
description="The captured result prefix is empty.",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
default_width, default_height = (
|
|
204
|
+
(160, 96) if kind is TrustedWorkKind.THUMBNAIL else (800, 500)
|
|
205
|
+
)
|
|
206
|
+
width = _option_integer(
|
|
207
|
+
options, "width", default_width, TRUSTED_DERIVED_MAX_IMAGE_WIDTH
|
|
208
|
+
)
|
|
209
|
+
height = _option_integer(
|
|
210
|
+
options, "height", default_height, TRUSTED_DERIVED_MAX_IMAGE_HEIGHT
|
|
211
|
+
)
|
|
212
|
+
if width * height * 4 > TRUSTED_DERIVED_MAX_DECODED_IMAGE_BYTES:
|
|
213
|
+
raise TrustedDerivedRenderingError("The decoded image budget was exceeded.")
|
|
214
|
+
|
|
215
|
+
column_indexes = {
|
|
216
|
+
name: index for index, name in enumerate(observation.sample_columns)
|
|
217
|
+
}
|
|
218
|
+
parameter_by_name = {
|
|
219
|
+
parameter.name: parameter for parameter in observation.parameters
|
|
220
|
+
}
|
|
221
|
+
dependents = observation.dependent_parameters
|
|
222
|
+
dependents = dependents[:TRUSTED_DERIVED_MAX_IMAGES]
|
|
223
|
+
images: list[DerivedValue] = []
|
|
224
|
+
unavailable_descriptions: list[str] = []
|
|
225
|
+
encoded_total = 0
|
|
226
|
+
for dependent_index, dependent in enumerate(dependents):
|
|
227
|
+
cancel_check()
|
|
228
|
+
parameter = parameter_by_name.get(dependent)
|
|
229
|
+
dependencies = parameter.depends_on if parameter is not None else ()
|
|
230
|
+
try:
|
|
231
|
+
if len(dependencies) > 2:
|
|
232
|
+
raise _UnsupportedNumericData(
|
|
233
|
+
"Results with more than two sweep dimensions are unsupported."
|
|
234
|
+
)
|
|
235
|
+
if (
|
|
236
|
+
not dependencies
|
|
237
|
+
or dependent not in column_indexes
|
|
238
|
+
or any(name not in column_indexes for name in dependencies)
|
|
239
|
+
):
|
|
240
|
+
raise _UnsupportedNumericData(
|
|
241
|
+
"The dependent or one of its sweep parameters is unavailable."
|
|
242
|
+
)
|
|
243
|
+
array_tree = any(
|
|
244
|
+
parameter_by_name[name].paramtype == "array"
|
|
245
|
+
for name in (*dependencies, dependent)
|
|
246
|
+
if name in parameter_by_name
|
|
247
|
+
)
|
|
248
|
+
if array_tree:
|
|
249
|
+
from .trusted_array_preview import render_array_preview
|
|
250
|
+
|
|
251
|
+
rgba, points = render_array_preview(
|
|
252
|
+
observation, dependencies, dependent, width, height, cancel_check,
|
|
253
|
+
)
|
|
254
|
+
dimensionality = len(dependencies)
|
|
255
|
+
elif len(dependencies) == 1:
|
|
256
|
+
rgba, points = _render_1d(
|
|
257
|
+
observation,
|
|
258
|
+
dependencies[0],
|
|
259
|
+
dependent,
|
|
260
|
+
width,
|
|
261
|
+
height,
|
|
262
|
+
cancel_check,
|
|
263
|
+
)
|
|
264
|
+
dimensionality = 1
|
|
265
|
+
elif len(dependencies) == 2:
|
|
266
|
+
layout = next(
|
|
267
|
+
(
|
|
268
|
+
candidate
|
|
269
|
+
for candidate in observation.validated_2d_layouts
|
|
270
|
+
if candidate.dependent == dependent
|
|
271
|
+
and candidate.dependencies == dependencies
|
|
272
|
+
),
|
|
273
|
+
None,
|
|
274
|
+
)
|
|
275
|
+
if layout is None:
|
|
276
|
+
raise _UnsupportedNumericData(
|
|
277
|
+
"A validated rectangular 2D layout is not yet available."
|
|
278
|
+
)
|
|
279
|
+
rgba, points = _render_2d(
|
|
280
|
+
observation,
|
|
281
|
+
layout,
|
|
282
|
+
width,
|
|
283
|
+
height,
|
|
284
|
+
cancel_check,
|
|
285
|
+
)
|
|
286
|
+
dimensionality = 2
|
|
287
|
+
except _UnsupportedNumericData as error:
|
|
288
|
+
unavailable_descriptions.append(str(error))
|
|
289
|
+
continue
|
|
290
|
+
cancel_check()
|
|
291
|
+
if points == 0:
|
|
292
|
+
continue
|
|
293
|
+
encoded = _encode_png_rgba(width, height, rgba, cancel_check)
|
|
294
|
+
encoded_total += len(encoded)
|
|
295
|
+
if encoded_total > TRUSTED_DERIVED_MAX_ENCODED_IMAGE_BYTES:
|
|
296
|
+
raise TrustedDerivedRenderingError("The encoded image budget was exceeded.")
|
|
297
|
+
images.append(
|
|
298
|
+
(
|
|
299
|
+
("encoding", "png"),
|
|
300
|
+
("width", width),
|
|
301
|
+
("height", height),
|
|
302
|
+
("dependent", dependent),
|
|
303
|
+
("dimensions", dimensionality),
|
|
304
|
+
("sampled_points", points),
|
|
305
|
+
("bytes", encoded),
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
if dependent_index + 1 >= TRUSTED_DERIVED_MAX_IMAGES:
|
|
309
|
+
break
|
|
310
|
+
if not images:
|
|
311
|
+
return _base_payload(
|
|
312
|
+
observation,
|
|
313
|
+
kind,
|
|
314
|
+
status="unsupported",
|
|
315
|
+
description=(
|
|
316
|
+
unavailable_descriptions[0]
|
|
317
|
+
if unavailable_descriptions
|
|
318
|
+
else "No bounded numeric 1D or 2D dependent data was available."
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
payload = _base_payload(
|
|
322
|
+
observation,
|
|
323
|
+
kind,
|
|
324
|
+
status="ok",
|
|
325
|
+
description=(
|
|
326
|
+
"Bounded trusted result-domain rendering; some dependents remain "
|
|
327
|
+
f"unavailable: {unavailable_descriptions[0]}"
|
|
328
|
+
if unavailable_descriptions
|
|
329
|
+
else "Bounded trusted result-domain rendering."
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
payload["images"] = tuple(images)
|
|
333
|
+
return payload
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _render_1d(
|
|
337
|
+
observation: TrustedDerivedSourceObservation,
|
|
338
|
+
x_name: str,
|
|
339
|
+
y_name: str,
|
|
340
|
+
width: int,
|
|
341
|
+
height: int,
|
|
342
|
+
cancel_check: CancelCheck,
|
|
343
|
+
) -> tuple[bytearray, int]:
|
|
344
|
+
indexes = {name: index for index, name in enumerate(observation.sample_columns)}
|
|
345
|
+
points: list[tuple[float, float]] = []
|
|
346
|
+
for index, row in enumerate(observation.sample_rows):
|
|
347
|
+
if index % 128 == 0:
|
|
348
|
+
cancel_check()
|
|
349
|
+
if len(row) != len(observation.sample_columns):
|
|
350
|
+
raise _UnsupportedNumericData(
|
|
351
|
+
"The bounded numeric sample has inconsistent vector lengths."
|
|
352
|
+
)
|
|
353
|
+
x = _finite_number(row[indexes[x_name]])
|
|
354
|
+
y = _finite_number(row[indexes[y_name]])
|
|
355
|
+
if x is not None and y is not None:
|
|
356
|
+
points.append((x, y))
|
|
357
|
+
rgba = bytearray(b"\xff" * (width * height * 4))
|
|
358
|
+
if not points:
|
|
359
|
+
return rgba, 0
|
|
360
|
+
x_values = tuple(point[0] for point in points)
|
|
361
|
+
y_values = tuple(point[1] for point in points)
|
|
362
|
+
x_min, x_max = min(x_values), max(x_values)
|
|
363
|
+
y_min, y_max = min(y_values), max(y_values)
|
|
364
|
+
margin = max(2, min(width, height) // 20)
|
|
365
|
+
prior: tuple[int, int] | None = None
|
|
366
|
+
for index, (x, y) in enumerate(points):
|
|
367
|
+
if index % 128 == 0:
|
|
368
|
+
cancel_check()
|
|
369
|
+
raw_px = _normalised_position(x, x_min, x_max) * (width - 1 - 2 * margin)
|
|
370
|
+
raw_py = _normalised_position(y, y_min, y_max) * (height - 1 - 2 * margin)
|
|
371
|
+
if not math.isfinite(raw_px) or not math.isfinite(raw_py):
|
|
372
|
+
raise _UnsupportedNumericData(
|
|
373
|
+
"The bounded numeric sample exceeds the supported finite range."
|
|
374
|
+
)
|
|
375
|
+
px = margin + round(raw_px)
|
|
376
|
+
py = height - 1 - margin - round(raw_py)
|
|
377
|
+
if prior is not None:
|
|
378
|
+
_line(rgba, width, height, prior[0], prior[1], px, py, (24, 92, 170, 255))
|
|
379
|
+
prior = (px, py)
|
|
380
|
+
if len(points) == 1 and prior is not None:
|
|
381
|
+
_pixel(
|
|
382
|
+
rgba,
|
|
383
|
+
width,
|
|
384
|
+
min(width - 1, max(0, prior[0])),
|
|
385
|
+
min(height - 1, max(0, prior[1])),
|
|
386
|
+
(24, 92, 170, 255),
|
|
387
|
+
)
|
|
388
|
+
return rgba, len(points)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _render_2d(
|
|
392
|
+
observation: TrustedDerivedSourceObservation,
|
|
393
|
+
layout: Trusted2DGridLayout,
|
|
394
|
+
width: int,
|
|
395
|
+
height: int,
|
|
396
|
+
cancel_check: CancelCheck,
|
|
397
|
+
) -> tuple[bytearray, int]:
|
|
398
|
+
indexes = {name: index for index, name in enumerate(observation.sample_columns)}
|
|
399
|
+
fast_name = layout.dependencies[layout.fast_axis_index]
|
|
400
|
+
slow_name = layout.dependencies[1 - layout.fast_axis_index]
|
|
401
|
+
z_name = layout.dependent
|
|
402
|
+
if any(name not in indexes for name in (slow_name, fast_name, z_name)):
|
|
403
|
+
raise _UnsupportedNumericData(
|
|
404
|
+
"The validated 2D layout is absent from the bounded numeric sample."
|
|
405
|
+
)
|
|
406
|
+
slow_count = layout.shape[1 - layout.fast_axis_index]
|
|
407
|
+
fast_count = layout.shape[layout.fast_axis_index]
|
|
408
|
+
sampled_slow = layout.sample_slow_indexes
|
|
409
|
+
sampled_fast = layout.sample_fast_indexes
|
|
410
|
+
slow_bins = {value: index for index, value in enumerate(sampled_slow)}
|
|
411
|
+
fast_bins = {value: index for index, value in enumerate(sampled_fast)}
|
|
412
|
+
if layout.fast_axis_index == 0:
|
|
413
|
+
sampled_vertical = sampled_fast
|
|
414
|
+
sampled_horizontal = sampled_slow
|
|
415
|
+
vertical_count = fast_count
|
|
416
|
+
horizontal_count = slow_count
|
|
417
|
+
else:
|
|
418
|
+
sampled_vertical = sampled_slow
|
|
419
|
+
sampled_horizontal = sampled_fast
|
|
420
|
+
vertical_count = slow_count
|
|
421
|
+
horizontal_count = fast_count
|
|
422
|
+
grid_width = len(sampled_horizontal)
|
|
423
|
+
grid_size = len(sampled_vertical) * grid_width
|
|
424
|
+
grid_sum = [0.0] * grid_size
|
|
425
|
+
grid_count = [0] * grid_size
|
|
426
|
+
points = 0
|
|
427
|
+
for index, row in enumerate(observation.sample_rows):
|
|
428
|
+
if index % 128 == 0:
|
|
429
|
+
cancel_check()
|
|
430
|
+
if len(row) != len(observation.sample_columns):
|
|
431
|
+
raise _UnsupportedNumericData(
|
|
432
|
+
"The bounded numeric sample has inconsistent vector lengths."
|
|
433
|
+
)
|
|
434
|
+
row_id = row[0]
|
|
435
|
+
if type(row_id) is not int:
|
|
436
|
+
raise _UnsupportedNumericData(
|
|
437
|
+
"The bounded grid sample has an invalid result id."
|
|
438
|
+
)
|
|
439
|
+
grid_index = _layout_grid_index(
|
|
440
|
+
layout,
|
|
441
|
+
row_id,
|
|
442
|
+
slow_count=slow_count,
|
|
443
|
+
fast_count=fast_count,
|
|
444
|
+
)
|
|
445
|
+
if grid_index is None:
|
|
446
|
+
continue
|
|
447
|
+
slow = _finite_number(row[indexes[slow_name]])
|
|
448
|
+
fast = _finite_number(row[indexes[fast_name]])
|
|
449
|
+
z = _finite_number(row[indexes[z_name]])
|
|
450
|
+
if slow is None or fast is None:
|
|
451
|
+
raise _UnsupportedNumericData(
|
|
452
|
+
"The validated grid sample has a non-numeric dependency."
|
|
453
|
+
)
|
|
454
|
+
if z is None:
|
|
455
|
+
continue
|
|
456
|
+
slow_index, fast_index = grid_index
|
|
457
|
+
slow_bin = slow_bins.get(slow_index)
|
|
458
|
+
if slow_bin is None:
|
|
459
|
+
slow_bin = _scaled_grid_index(
|
|
460
|
+
slow_index,
|
|
461
|
+
slow_count,
|
|
462
|
+
len(sampled_slow),
|
|
463
|
+
)
|
|
464
|
+
fast_bin = fast_bins.get(fast_index)
|
|
465
|
+
if fast_bin is None:
|
|
466
|
+
fast_bin = _scaled_grid_index(
|
|
467
|
+
fast_index,
|
|
468
|
+
fast_count,
|
|
469
|
+
len(sampled_fast),
|
|
470
|
+
)
|
|
471
|
+
scientific_slow_bin = (
|
|
472
|
+
len(sampled_slow) - 1 - slow_bin if layout.slow_reversed else slow_bin
|
|
473
|
+
)
|
|
474
|
+
row_fast_reversed = layout.fast_reversed ^ (
|
|
475
|
+
layout.serpentine and bool(slow_index % 2)
|
|
476
|
+
)
|
|
477
|
+
scientific_fast_bin = (
|
|
478
|
+
len(sampled_fast) - 1 - fast_bin if row_fast_reversed else fast_bin
|
|
479
|
+
)
|
|
480
|
+
if layout.fast_axis_index == 0:
|
|
481
|
+
vertical_bin = scientific_fast_bin
|
|
482
|
+
horizontal_bin = scientific_slow_bin
|
|
483
|
+
else:
|
|
484
|
+
vertical_bin = scientific_slow_bin
|
|
485
|
+
horizontal_bin = scientific_fast_bin
|
|
486
|
+
display_row = len(sampled_vertical) - 1 - vertical_bin
|
|
487
|
+
target = display_row * grid_width + horizontal_bin
|
|
488
|
+
combined = grid_sum[target] + z
|
|
489
|
+
if not math.isfinite(combined):
|
|
490
|
+
raise _UnsupportedNumericData(
|
|
491
|
+
"The bounded numeric sample exceeds the supported finite range."
|
|
492
|
+
)
|
|
493
|
+
grid_sum[target] = combined
|
|
494
|
+
grid_count[target] += 1
|
|
495
|
+
points += 1
|
|
496
|
+
rgba = bytearray(width * height * 4)
|
|
497
|
+
if points == 0:
|
|
498
|
+
for offset in range(0, len(rgba), 4):
|
|
499
|
+
rgba[offset : offset + 4] = bytes(_MISSING_CELL_RGBA)
|
|
500
|
+
return rgba, 0
|
|
501
|
+
grid = tuple(
|
|
502
|
+
grid_sum[index] / count if count else None
|
|
503
|
+
for index, count in enumerate(grid_count)
|
|
504
|
+
)
|
|
505
|
+
finite = tuple(value for value in grid if value is not None)
|
|
506
|
+
z_min, z_max = min(finite), max(finite)
|
|
507
|
+
source_rows = _nearest_sample_bins(
|
|
508
|
+
height,
|
|
509
|
+
vertical_count,
|
|
510
|
+
sampled_vertical,
|
|
511
|
+
)
|
|
512
|
+
source_columns = _nearest_sample_bins(
|
|
513
|
+
width,
|
|
514
|
+
horizontal_count,
|
|
515
|
+
sampled_horizontal,
|
|
516
|
+
)
|
|
517
|
+
for py in range(height):
|
|
518
|
+
if py % 32 == 0:
|
|
519
|
+
cancel_check()
|
|
520
|
+
source_row = source_rows[py]
|
|
521
|
+
for px, source_column in enumerate(source_columns):
|
|
522
|
+
value = grid[source_row * grid_width + source_column]
|
|
523
|
+
color = (
|
|
524
|
+
_MISSING_CELL_RGBA
|
|
525
|
+
if value is None
|
|
526
|
+
else _viridis_rgba(value, z_min, z_max)
|
|
527
|
+
)
|
|
528
|
+
_pixel(rgba, width, px, py, color)
|
|
529
|
+
return rgba, points
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _layout_grid_index(
|
|
533
|
+
layout: Trusted2DGridLayout,
|
|
534
|
+
row_id: int,
|
|
535
|
+
*,
|
|
536
|
+
slow_count: int,
|
|
537
|
+
fast_count: int,
|
|
538
|
+
) -> tuple[int, int] | None:
|
|
539
|
+
relative = row_id - layout.first_row_id
|
|
540
|
+
if relative < 0:
|
|
541
|
+
return None
|
|
542
|
+
slow_index, remainder = divmod(relative, layout.slow_id_stride)
|
|
543
|
+
if remainder % layout.fast_id_stride:
|
|
544
|
+
return None
|
|
545
|
+
fast_index = remainder // layout.fast_id_stride
|
|
546
|
+
if slow_index >= slow_count or fast_index >= fast_count:
|
|
547
|
+
return None
|
|
548
|
+
return slow_index, fast_index
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _scaled_grid_index(index: int, source_count: int, target_count: int) -> int:
|
|
552
|
+
if source_count <= 1 or target_count <= 1:
|
|
553
|
+
return 0
|
|
554
|
+
return min(target_count - 1, index * (target_count - 1) // (source_count - 1))
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _nearest_sample_bins(
|
|
558
|
+
pixel_count: int,
|
|
559
|
+
source_count: int,
|
|
560
|
+
sampled_indexes: tuple[int, ...],
|
|
561
|
+
) -> tuple[int, ...]:
|
|
562
|
+
"""Map display pixels to their nearest sampled source-domain coordinate.
|
|
563
|
+
|
|
564
|
+
Representative samples are almost uniform, but they also contain adjacent
|
|
565
|
+
edge anchors used to prove acquisition direction. Treating every sample
|
|
566
|
+
rank as equally wide would make a single edge cell occupy the same image
|
|
567
|
+
width as a sample representing thousands of source cells. Integer-scaled
|
|
568
|
+
nearest-neighbour lookup preserves the true source-index spacing without
|
|
569
|
+
losing either endpoint or using imprecise large-axis floats.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
if pixel_count <= 0 or source_count <= 0 or not sampled_indexes:
|
|
573
|
+
raise ValueError("Source-domain sampling requires positive dimensions.")
|
|
574
|
+
targets: tuple[int, ...]
|
|
575
|
+
if pixel_count == 1:
|
|
576
|
+
denominator = 2
|
|
577
|
+
targets = (source_count - 1,)
|
|
578
|
+
else:
|
|
579
|
+
denominator = pixel_count - 1
|
|
580
|
+
targets = tuple(pixel * (source_count - 1) for pixel in range(pixel_count))
|
|
581
|
+
scaled_samples = tuple(index * denominator for index in sampled_indexes)
|
|
582
|
+
output: list[int] = []
|
|
583
|
+
for target in targets:
|
|
584
|
+
upper = bisect_left(scaled_samples, target)
|
|
585
|
+
if upper == 0:
|
|
586
|
+
output.append(0)
|
|
587
|
+
elif upper == len(scaled_samples):
|
|
588
|
+
output.append(len(scaled_samples) - 1)
|
|
589
|
+
else:
|
|
590
|
+
lower = upper - 1
|
|
591
|
+
output.append(
|
|
592
|
+
lower
|
|
593
|
+
if target - scaled_samples[lower] <= scaled_samples[upper] - target
|
|
594
|
+
else upper
|
|
595
|
+
)
|
|
596
|
+
return tuple(output)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _normalised_position(value, low, high):
|
|
600
|
+
"""Map finite samples before pixel scaling can overflow their range."""
|
|
601
|
+
if high == low:
|
|
602
|
+
return 0.
|
|
603
|
+
samples = (value, low, high)
|
|
604
|
+
integer_count = sum(isinstance(sample, int) for sample in samples)
|
|
605
|
+
if (any(isinstance(sample, Fraction) for sample in samples)
|
|
606
|
+
or (0 < integer_count < 3 and max(map(abs, samples)) >= 2**53)):
|
|
607
|
+
# Mixed array records can retain an integer between adjacent floats.
|
|
608
|
+
# Subtract exactly before normalizing, rather than rounding that
|
|
609
|
+
# integer to its neighbouring float and losing a recorded step.
|
|
610
|
+
# Repeated integer heatmap cells retain their exact fractional means.
|
|
611
|
+
value, low, high = map(Fraction, samples)
|
|
612
|
+
return float((value - low) / (high - low))
|
|
613
|
+
span = high - low
|
|
614
|
+
if math.isfinite(span):
|
|
615
|
+
return (value - low) / span
|
|
616
|
+
scale = max(abs(low), abs(high))
|
|
617
|
+
return (value / scale - low / scale) / (high / scale - low / scale)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _viridis_rgba(
|
|
621
|
+
value: float,
|
|
622
|
+
low: float,
|
|
623
|
+
high: float,
|
|
624
|
+
) -> tuple[int, int, int, int]:
|
|
625
|
+
fraction = 0.5 if high == low else _normalised_position(value, low, high)
|
|
626
|
+
fraction = max(0.0, min(1.0, fraction))
|
|
627
|
+
color_index = min(255, int(fraction * 256.0))
|
|
628
|
+
offset = color_index * 3
|
|
629
|
+
return (
|
|
630
|
+
_VIRIDIS_RGB[offset],
|
|
631
|
+
_VIRIDIS_RGB[offset + 1],
|
|
632
|
+
_VIRIDIS_RGB[offset + 2],
|
|
633
|
+
255,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _pixel(
|
|
638
|
+
rgba: bytearray,
|
|
639
|
+
width: int,
|
|
640
|
+
x: int,
|
|
641
|
+
y: int,
|
|
642
|
+
color: tuple[int, int, int, int],
|
|
643
|
+
) -> None:
|
|
644
|
+
offset = (y * width + x) * 4
|
|
645
|
+
rgba[offset : offset + 4] = bytes(color)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _line(
|
|
649
|
+
rgba: bytearray,
|
|
650
|
+
width: int,
|
|
651
|
+
height: int,
|
|
652
|
+
x0: int,
|
|
653
|
+
y0: int,
|
|
654
|
+
x1: int,
|
|
655
|
+
y1: int,
|
|
656
|
+
color: tuple[int, int, int, int],
|
|
657
|
+
) -> None:
|
|
658
|
+
dx, sx = abs(x1 - x0), 1 if x0 < x1 else -1
|
|
659
|
+
dy, sy = -abs(y1 - y0), 1 if y0 < y1 else -1
|
|
660
|
+
error = dx + dy
|
|
661
|
+
while True:
|
|
662
|
+
if 0 <= x0 < width and 0 <= y0 < height:
|
|
663
|
+
_pixel(rgba, width, x0, y0, color)
|
|
664
|
+
if x0 == x1 and y0 == y1:
|
|
665
|
+
break
|
|
666
|
+
doubled = 2 * error
|
|
667
|
+
if doubled >= dy:
|
|
668
|
+
error += dy
|
|
669
|
+
x0 += sx
|
|
670
|
+
if doubled <= dx:
|
|
671
|
+
error += dx
|
|
672
|
+
y0 += sy
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _png_chunk(kind: bytes, data: bytes) -> bytes:
|
|
676
|
+
checksum = binascii.crc32(kind)
|
|
677
|
+
checksum = binascii.crc32(data, checksum) & 0xFFFFFFFF
|
|
678
|
+
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", checksum)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _encode_png_rgba(
|
|
682
|
+
width: int,
|
|
683
|
+
height: int,
|
|
684
|
+
rgba: bytearray,
|
|
685
|
+
cancel_check: CancelCheck,
|
|
686
|
+
) -> bytes:
|
|
687
|
+
expected = width * height * 4
|
|
688
|
+
if len(rgba) != expected or expected > TRUSTED_DERIVED_MAX_DECODED_IMAGE_BYTES:
|
|
689
|
+
raise TrustedDerivedRenderingError("The decoded image has invalid bounds.")
|
|
690
|
+
rows = bytearray(height * (width * 4 + 1))
|
|
691
|
+
source_stride = width * 4
|
|
692
|
+
target_stride = source_stride + 1
|
|
693
|
+
for y in range(height):
|
|
694
|
+
if y % 32 == 0:
|
|
695
|
+
cancel_check()
|
|
696
|
+
source = y * source_stride
|
|
697
|
+
target = y * target_stride
|
|
698
|
+
rows[target] = 0
|
|
699
|
+
rows[target + 1 : target + target_stride] = rgba[
|
|
700
|
+
source : source + source_stride
|
|
701
|
+
]
|
|
702
|
+
cancel_check()
|
|
703
|
+
compressed = zlib.compress(bytes(rows), level=9)
|
|
704
|
+
result = (
|
|
705
|
+
b"\x89PNG\r\n\x1a\n"
|
|
706
|
+
+ _png_chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0))
|
|
707
|
+
+ _png_chunk(b"IDAT", compressed)
|
|
708
|
+
+ _png_chunk(b"IEND", b"")
|
|
709
|
+
)
|
|
710
|
+
if len(result) > TRUSTED_DERIVED_MAX_ENCODED_IMAGE_BYTES:
|
|
711
|
+
raise TrustedDerivedRenderingError("The encoded image is too large.")
|
|
712
|
+
return result
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def trusted_derived_payload_size(payload: DerivedPayload) -> int:
|
|
716
|
+
"""Conservatively size the retained primitive graph without serialising it."""
|
|
717
|
+
|
|
718
|
+
total = 0
|
|
719
|
+
nodes = 0
|
|
720
|
+
active_containers: set[int] = set()
|
|
721
|
+
stack: list[tuple[bool, Any, int]] = [(False, payload, 0)]
|
|
722
|
+
while stack:
|
|
723
|
+
leaving, value, depth = stack.pop()
|
|
724
|
+
if leaving:
|
|
725
|
+
active_containers.discard(cast(int, value))
|
|
726
|
+
continue
|
|
727
|
+
if depth > TRUSTED_DERIVED_MAX_PAYLOAD_DEPTH:
|
|
728
|
+
raise TrustedDerivedRenderingError(
|
|
729
|
+
"A derived payload is too deeply nested."
|
|
730
|
+
)
|
|
731
|
+
nodes += 1
|
|
732
|
+
if nodes > TRUSTED_DERIVED_MAX_PAYLOAD_NODES:
|
|
733
|
+
raise TrustedDerivedRenderingError("A derived payload has too many nodes.")
|
|
734
|
+
if value is None or isinstance(value, bool):
|
|
735
|
+
total += 8
|
|
736
|
+
elif isinstance(value, (int, float)):
|
|
737
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
738
|
+
raise TrustedDerivedRenderingError(
|
|
739
|
+
"A derived payload contains a non-finite scalar."
|
|
740
|
+
)
|
|
741
|
+
total += 32
|
|
742
|
+
elif isinstance(value, str):
|
|
743
|
+
encoded = value.encode("utf-8")
|
|
744
|
+
if len(encoded) > TRUSTED_DERIVED_MAX_TEXT_BYTES:
|
|
745
|
+
raise TrustedDerivedRenderingError(
|
|
746
|
+
"A derived payload text value is oversized."
|
|
747
|
+
)
|
|
748
|
+
total += len(encoded) + 32
|
|
749
|
+
elif isinstance(value, bytes):
|
|
750
|
+
if len(value) > TRUSTED_DERIVED_MAX_RETAINED_PAYLOAD_BYTES:
|
|
751
|
+
raise TrustedDerivedRenderingError(
|
|
752
|
+
"A derived payload byte value is oversized."
|
|
753
|
+
)
|
|
754
|
+
total += len(value) + 32
|
|
755
|
+
elif isinstance(value, tuple):
|
|
756
|
+
if len(value) > TRUSTED_DERIVED_MAX_PAYLOAD_ITEMS:
|
|
757
|
+
raise TrustedDerivedRenderingError(
|
|
758
|
+
"A derived payload tuple has too many items."
|
|
759
|
+
)
|
|
760
|
+
identity = id(value)
|
|
761
|
+
if identity in active_containers:
|
|
762
|
+
raise TrustedDerivedRenderingError("A derived payload is cyclic.")
|
|
763
|
+
active_containers.add(identity)
|
|
764
|
+
total += 32 + len(value) * 8
|
|
765
|
+
stack.append((True, identity, depth))
|
|
766
|
+
stack.extend((False, item, depth + 1) for item in value)
|
|
767
|
+
elif isinstance(value, dict):
|
|
768
|
+
if len(value) > TRUSTED_DERIVED_MAX_PAYLOAD_ITEMS:
|
|
769
|
+
raise TrustedDerivedRenderingError(
|
|
770
|
+
"A derived payload dictionary has too many items."
|
|
771
|
+
)
|
|
772
|
+
identity = id(value)
|
|
773
|
+
if identity in active_containers:
|
|
774
|
+
raise TrustedDerivedRenderingError("A derived payload is cyclic.")
|
|
775
|
+
active_containers.add(identity)
|
|
776
|
+
total += 64 + len(value) * 16
|
|
777
|
+
stack.append((True, identity, depth))
|
|
778
|
+
stack.extend((False, item, depth + 1) for item in value.keys())
|
|
779
|
+
stack.extend((False, item, depth + 1) for item in value.values())
|
|
780
|
+
else:
|
|
781
|
+
raise TrustedDerivedRenderingError("A derived payload is not primitive.")
|
|
782
|
+
if total > TRUSTED_DERIVED_MAX_RETAINED_PAYLOAD_BYTES:
|
|
783
|
+
raise TrustedDerivedRenderingError(
|
|
784
|
+
"The retained payload budget was exceeded."
|
|
785
|
+
)
|
|
786
|
+
return total
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _pair_mapping(value: object, *, name: str, maximum: int) -> dict[str, object]:
|
|
790
|
+
if not isinstance(value, tuple) or len(value) > maximum:
|
|
791
|
+
raise TrustedDerivedRenderingError(f"The derived {name} structure is invalid.")
|
|
792
|
+
output: dict[str, object] = {}
|
|
793
|
+
for item in value:
|
|
794
|
+
if (
|
|
795
|
+
not isinstance(item, tuple)
|
|
796
|
+
or len(item) != 2
|
|
797
|
+
or not isinstance(item[0], str)
|
|
798
|
+
or item[0] in output
|
|
799
|
+
):
|
|
800
|
+
raise TrustedDerivedRenderingError(
|
|
801
|
+
f"The derived {name} structure is invalid."
|
|
802
|
+
)
|
|
803
|
+
output[item[0]] = item[1]
|
|
804
|
+
return output
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def validate_trusted_derived_payload(payload: DerivedPayload) -> int:
|
|
808
|
+
"""Validate the retained/cache schema and return its conservative size."""
|
|
809
|
+
|
|
810
|
+
if not isinstance(payload, dict) or any(
|
|
811
|
+
not isinstance(name, str) for name in payload
|
|
812
|
+
):
|
|
813
|
+
raise TrustedDerivedRenderingError("A derived payload root is invalid.")
|
|
814
|
+
allowed = {
|
|
815
|
+
"format",
|
|
816
|
+
"kind",
|
|
817
|
+
"status",
|
|
818
|
+
"description",
|
|
819
|
+
"source",
|
|
820
|
+
"images",
|
|
821
|
+
"metadata",
|
|
822
|
+
}
|
|
823
|
+
required = {"format", "kind", "status", "description", "source", "images"}
|
|
824
|
+
if set(payload) - allowed or not required.issubset(payload):
|
|
825
|
+
raise TrustedDerivedRenderingError("A derived payload field is invalid.")
|
|
826
|
+
if payload["format"] != "qplot-trusted-derived-payload-v1":
|
|
827
|
+
raise TrustedDerivedRenderingError("A derived payload format is unsupported.")
|
|
828
|
+
if payload["kind"] not in {"thumbnail", "preview", "metadata"}:
|
|
829
|
+
raise TrustedDerivedRenderingError("A derived payload kind is invalid.")
|
|
830
|
+
if payload["status"] not in {"ok", "empty", "unsupported", "error"}:
|
|
831
|
+
raise TrustedDerivedRenderingError("A derived payload status is invalid.")
|
|
832
|
+
if not isinstance(payload["description"], str):
|
|
833
|
+
raise TrustedDerivedRenderingError("A derived payload description is invalid.")
|
|
834
|
+
_pair_mapping(payload["source"], name="source", maximum=32)
|
|
835
|
+
images = payload["images"]
|
|
836
|
+
if not isinstance(images, tuple) or len(images) > TRUSTED_DERIVED_MAX_IMAGES:
|
|
837
|
+
raise TrustedDerivedRenderingError("The derived image collection is invalid.")
|
|
838
|
+
encoded_total = 0
|
|
839
|
+
for image in images:
|
|
840
|
+
fields = _pair_mapping(image, name="image", maximum=16)
|
|
841
|
+
if set(fields) != {
|
|
842
|
+
"encoding",
|
|
843
|
+
"width",
|
|
844
|
+
"height",
|
|
845
|
+
"dependent",
|
|
846
|
+
"dimensions",
|
|
847
|
+
"sampled_points",
|
|
848
|
+
"bytes",
|
|
849
|
+
}:
|
|
850
|
+
raise TrustedDerivedRenderingError("A derived image field is invalid.")
|
|
851
|
+
width, height = fields["width"], fields["height"]
|
|
852
|
+
encoded = fields["bytes"]
|
|
853
|
+
if (
|
|
854
|
+
fields["encoding"] != "png"
|
|
855
|
+
or type(width) is not int
|
|
856
|
+
or type(height) is not int
|
|
857
|
+
or not 1 <= width <= TRUSTED_DERIVED_MAX_IMAGE_WIDTH
|
|
858
|
+
or not 1 <= height <= TRUSTED_DERIVED_MAX_IMAGE_HEIGHT
|
|
859
|
+
or width * height * 4 > TRUSTED_DERIVED_MAX_DECODED_IMAGE_BYTES
|
|
860
|
+
or not isinstance(fields["dependent"], str)
|
|
861
|
+
or fields["dimensions"] not in (1, 2)
|
|
862
|
+
or type(fields["sampled_points"]) is not int
|
|
863
|
+
or fields["sampled_points"] < 0
|
|
864
|
+
or not isinstance(encoded, bytes)
|
|
865
|
+
or not encoded.startswith(b"\x89PNG\r\n\x1a\n")
|
|
866
|
+
):
|
|
867
|
+
raise TrustedDerivedRenderingError("A derived image value is invalid.")
|
|
868
|
+
encoded_total += len(encoded)
|
|
869
|
+
if encoded_total > TRUSTED_DERIVED_MAX_ENCODED_IMAGE_BYTES:
|
|
870
|
+
raise TrustedDerivedRenderingError("The encoded image budget was exceeded.")
|
|
871
|
+
metadata = payload.get("metadata")
|
|
872
|
+
if metadata is not None:
|
|
873
|
+
if payload["kind"] != "metadata":
|
|
874
|
+
raise TrustedDerivedRenderingError("Derived metadata has the wrong kind.")
|
|
875
|
+
_pair_mapping(metadata, name="metadata", maximum=64)
|
|
876
|
+
return trusted_derived_payload_size(payload)
|