From 507c485b692ce811ba739e7425ca27cef90a692c Mon Sep 17 00:00:00 2001 From: ShallIBe Date: Thu, 20 Aug 2026 01:42:04 -0700 Subject: [PATCH 1/2] Add animated detector slice diagrams Add a self-contained animation for detector slices with operations. Preserve exact Stim SVG frames at integer ticks and interpolate detector regions between ticks. --- doc/python_api_reference_vDev.md | 14 +- doc/stim.pyi | 14 +- doc/usage_command_line.md | 6 +- file_lists/source_files_no_main | 1 + glue/python/src/stim/__init__.pyi | 14 +- src/stim.h | 1 + src/stim/circuit/circuit.pybind.cc | 14 +- src/stim/circuit/circuit_pybind_test.py | 6 + src/stim/cmd/command_diagram.cc | 12 + src/stim/cmd/command_diagram.pybind.cc | 9 + .../detector_slice_animation.cc | 612 ++++++++++++++++++ .../detector_slice/detector_slice_animation.h | 20 + 12 files changed, 718 insertions(+), 5 deletions(-) create mode 100644 src/stim/diagram/detector_slice/detector_slice_animation.cc create mode 100644 src/stim/diagram/detector_slice/detector_slice_animation.h diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md index 01ce7fe4b..d5a6323fc 100644 --- a/doc/python_api_reference_vDev.md +++ b/doc/python_api_reference_vDev.md @@ -1686,7 +1686,7 @@ def detector_error_model( # (in class stim.Circuit) def diagram( self, - type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "interactive", "interactive-html"] = 'timeline-text', + type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "animated-detslice-with-ops", "interactive", "interactive-html"] = 'timeline-text', *, tick: Union[None, int, range] = None, filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), @@ -1750,6 +1750,13 @@ def diagram( operations were applied. "detslice-with-ops-svg-html": Same as detslice-with-ops-svg but the SVG image is inside a resizable HTML iframe. + "animated-detslice-with-ops": A self-contained interactive + HTML animation of detslice-with-ops-svg diagrams. Exact + Stim SVG frames are shown at integer diagram ticks, and + detector region boundaries are interpolated between + ticks. The animation starts playing and loops + automatically. The HTML embeds every selected integer + frame, so its size grows with the selected tick range. "interactive" or "interactive-html": An HTML web page containing Crumble (an interactive editor for 2D stabilizer circuits) initialized with the given circuit @@ -1765,6 +1772,11 @@ def diagram( Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. + + For animated-detslice-with-ops, `tick=None` animates all + available diagram ticks. Passing an integer shows one + exact frame. Passing `range(A, B)` animates the half-open + range containing ticks A through B-1. rows: In diagrams that have multiple separate pieces, such as timeslice diagrams and detslice diagrams, this controls how many rows of pieces there will be. If not specified, a number of rows that creates diff --git a/doc/stim.pyi b/doc/stim.pyi index d71851394..a342fcc81 100644 --- a/doc/stim.pyi +++ b/doc/stim.pyi @@ -1049,7 +1049,7 @@ class Circuit: """ def diagram( self, - type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "interactive", "interactive-html"] = 'timeline-text', + type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "animated-detslice-with-ops", "interactive", "interactive-html"] = 'timeline-text', *, tick: Union[None, int, range] = None, filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), @@ -1113,6 +1113,13 @@ class Circuit: operations were applied. "detslice-with-ops-svg-html": Same as detslice-with-ops-svg but the SVG image is inside a resizable HTML iframe. + "animated-detslice-with-ops": A self-contained interactive + HTML animation of detslice-with-ops-svg diagrams. Exact + Stim SVG frames are shown at integer diagram ticks, and + detector region boundaries are interpolated between + ticks. The animation starts playing and loops + automatically. The HTML embeds every selected integer + frame, so its size grows with the selected tick range. "interactive" or "interactive-html": An HTML web page containing Crumble (an interactive editor for 2D stabilizer circuits) initialized with the given circuit @@ -1128,6 +1135,11 @@ class Circuit: Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. + + For animated-detslice-with-ops, `tick=None` animates all + available diagram ticks. Passing an integer shows one + exact frame. Passing `range(A, B)` animates the half-open + range containing ticks A through B-1. rows: In diagrams that have multiple separate pieces, such as timeslice diagrams and detslice diagrams, this controls how many rows of pieces there will be. If not specified, a number of rows that creates diff --git a/doc/usage_command_line.md b/doc/usage_command_line.md index 2e5418e32..5c8954b8c 100644 --- a/doc/usage_command_line.md +++ b/doc/usage_command_line.md @@ -986,6 +986,11 @@ OPTIONS INPUT MUST BE A CIRCUIT. + "animated-detslice-with-ops": A self-contained HTML animation of + detslice-with-ops-svg diagrams. + + INPUT MUST BE A CIRCUIT. + EXAMPLES Example #1 @@ -2067,4 +2072,3 @@ EXAMPLES 1 1 ``` - diff --git a/file_lists/source_files_no_main b/file_lists/source_files_no_main index c074a6669..f6d468128 100644 --- a/file_lists/source_files_no_main +++ b/file_lists/source_files_no_main @@ -22,6 +22,7 @@ src/stim/diagram/basic_3d_diagram.cc src/stim/diagram/circuit_timeline_helper.cc src/stim/diagram/crumble.cc src/stim/diagram/crumble_data.cc +src/stim/diagram/detector_slice/detector_slice_animation.cc src/stim/diagram/detector_slice/detector_slice_set.cc src/stim/diagram/diagram_util.cc src/stim/diagram/gate_data_3d.cc diff --git a/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi index d71851394..a342fcc81 100644 --- a/glue/python/src/stim/__init__.pyi +++ b/glue/python/src/stim/__init__.pyi @@ -1049,7 +1049,7 @@ class Circuit: """ def diagram( self, - type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "interactive", "interactive-html"] = 'timeline-text', + type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "animated-detslice-with-ops", "interactive", "interactive-html"] = 'timeline-text', *, tick: Union[None, int, range] = None, filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), @@ -1113,6 +1113,13 @@ class Circuit: operations were applied. "detslice-with-ops-svg-html": Same as detslice-with-ops-svg but the SVG image is inside a resizable HTML iframe. + "animated-detslice-with-ops": A self-contained interactive + HTML animation of detslice-with-ops-svg diagrams. Exact + Stim SVG frames are shown at integer diagram ticks, and + detector region boundaries are interpolated between + ticks. The animation starts playing and loops + automatically. The HTML embeds every selected integer + frame, so its size grows with the selected tick range. "interactive" or "interactive-html": An HTML web page containing Crumble (an interactive editor for 2D stabilizer circuits) initialized with the given circuit @@ -1128,6 +1135,11 @@ class Circuit: Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. + + For animated-detslice-with-ops, `tick=None` animates all + available diagram ticks. Passing an integer shows one + exact frame. Passing `range(A, B)` animates the half-open + range containing ticks A through B-1. rows: In diagrams that have multiple separate pieces, such as timeslice diagrams and detslice diagrams, this controls how many rows of pieces there will be. If not specified, a number of rows that creates diff --git a/src/stim.h b/src/stim.h index ed724ddca..5eff64ad8 100644 --- a/src/stim.h +++ b/src/stim.h @@ -27,6 +27,7 @@ #include "stim/diagram/coord.h" #include "stim/diagram/crumble.h" #include "stim/diagram/crumble_data.h" +#include "stim/diagram/detector_slice/detector_slice_animation.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/diagram/diagram_util.h" #include "stim/diagram/gate_data_3d.h" diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc index d0731737d..35a28e264 100644 --- a/src/stim/circuit/circuit.pybind.cc +++ b/src/stim/circuit/circuit.pybind.cc @@ -2493,7 +2493,7 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ 'stim._DiagramHelper': + @signature def diagram(self, type: Literal["timeline-text", "timeline-svg", "timeline-svg-html", "timeline-3d", "timeline-3d-html", "detslice-text", "detslice-svg", "detslice-svg-html", "matchgraph-svg", "matchgraph-svg-html", "matchgraph-3d", "matchgraph-3d-html", "timeslice-svg", "timeslice-svg-html", "detslice-with-ops-svg", "detslice-with-ops-svg-html", "animated-detslice-with-ops", "interactive", "interactive-html"] = 'timeline-text', *, tick: Union[None, int, range] = None, filter_coords: Iterable[Union[Iterable[float], stim.DemTarget]] = ((),), rows: int | None = None) -> 'stim._DiagramHelper': Returns a diagram of the circuit, from a variety of options. Args: @@ -2552,6 +2552,13 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_") + assert "requestAnimationFrame(animate)" in html + with pytest.raises(ValueError, match="rows"): + c.diagram("animated-detslice-with-ops", rows=2) assert c.diagram("timeslice-svg", tick=1) is not None assert c.diagram("time-slice-svg", tick=1) is not None assert c.diagram("time+detector-slice-svg", tick=1) is not None diff --git a/src/stim/cmd/command_diagram.cc b/src/stim/cmd/command_diagram.cc index 6f6246a82..1911389ec 100644 --- a/src/stim/cmd/command_diagram.cc +++ b/src/stim/cmd/command_diagram.cc @@ -18,6 +18,7 @@ #include "command_help.h" #include "stim/diagram/crumble.h" +#include "stim/diagram/detector_slice/detector_slice_animation.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/diagram/graph/match_graph_3d_drawer.h" #include "stim/diagram/graph/match_graph_svg_drawer.h" @@ -40,6 +41,7 @@ enum class DiagramTypes { TIMELINE_3D_HTML, TIME_SLICE_SVG, TIME_SLICE_PLUS_DETECTOR_SLICE_SVG, + ANIMATED_DETECTOR_SLICE_WITH_OPS, MATCH_GRAPH_SVG, MATCH_GRAPH_3D, MATCH_GRAPH_3D_HTML, @@ -108,6 +110,7 @@ DiagramTypes _read_diagram_type(int argc, const char **argv) { {"timeline-3d-html", DiagramTypes::TIMELINE_3D_HTML}, {"timeslice-svg", DiagramTypes::TIME_SLICE_SVG}, {"detslice-with-ops-svg", DiagramTypes::TIME_SLICE_PLUS_DETECTOR_SLICE_SVG}, + {"animated-detslice-with-ops", DiagramTypes::ANIMATED_DETECTOR_SLICE_WITH_OPS}, {"matchgraph-svg", DiagramTypes::MATCH_GRAPH_SVG}, {"matchgraph-3d", DiagramTypes::MATCH_GRAPH_3D}, {"matchgraph-3d-html", DiagramTypes::MATCH_GRAPH_3D_HTML}, @@ -211,6 +214,10 @@ int stim::command_diagram(int argc, const char **argv) { tick_num, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, coord_filter); + } else if (type == DiagramTypes::ANIMATED_DETECTOR_SLICE_WITH_OPS) { + auto circuit = _read_circuit(in, argc, argv); + auto coord_filter = _read_coord_filter(argc, argv); + out << make_detector_slice_animation_html(circuit, tick_start, tick_num, coord_filter); } else if (type == DiagramTypes::TIMELINE_3D) { auto circuit = _read_circuit(in, argc, argv); DiagramTimeline3DDrawer::circuit_to_basic_3d_diagram(circuit).to_gltf_scene().to_json().write(out); @@ -468,6 +475,11 @@ SubCommandHelp stim::command_diagram_help() { combined with the detector slices after those operations are applied. + INPUT MUST BE A CIRCUIT. + + "animated-detslice-with-ops": A self-contained HTML animation of + detslice-with-ops-svg diagrams. + INPUT MUST BE A CIRCUIT. )PARAGRAPH"), }); diff --git a/src/stim/cmd/command_diagram.pybind.cc b/src/stim/cmd/command_diagram.pybind.cc index 9ef8b45ae..428bfe313 100644 --- a/src/stim/cmd/command_diagram.pybind.cc +++ b/src/stim/cmd/command_diagram.pybind.cc @@ -18,6 +18,7 @@ #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/diagram/base64.h" #include "stim/diagram/crumble.h" +#include "stim/diagram/detector_slice/detector_slice_animation.h" #include "stim/diagram/detector_slice/detector_slice_set.h" #include "stim/diagram/graph/match_graph_3d_drawer.h" #include "stim/diagram/graph/match_graph_svg_drawer.h" @@ -327,6 +328,14 @@ DiagramHelper stim_pybind::circuit_diagram( DiagramType d_type = type.find("html") != std::string::npos ? DiagramType::DIAGRAM_TYPE_SVG_HTML : DiagramType::DIAGRAM_TYPE_SVG; return DiagramHelper{d_type, out.str()}; + } else if (type == "animated-detslice-with-ops") { + if (!rows.is_none()) { + throw std::invalid_argument("`rows` isn't used with type='animated-detslice-with-ops'"); + } + return DiagramHelper{ + DiagramType::DIAGRAM_TYPE_HTML, + make_detector_slice_animation_html(circuit, tick_min, num_ticks, filter_coords), + }; } else if (type == "timeline-3d") { std::stringstream out; DiagramTimeline3DDrawer::circuit_to_basic_3d_diagram(circuit).to_gltf_scene().to_json().write(out); diff --git a/src/stim/diagram/detector_slice/detector_slice_animation.cc b/src/stim/diagram/detector_slice/detector_slice_animation.cc new file mode 100644 index 000000000..27ac219ac --- /dev/null +++ b/src/stim/diagram/detector_slice/detector_slice_animation.cc @@ -0,0 +1,612 @@ +#include "stim/diagram/detector_slice/detector_slice_animation.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "stim/diagram/base64.h" +#include "stim/diagram/timeline/timeline_svg_drawer.h" + +using namespace stim; +using namespace stim_draw_internal; + +namespace { + +constexpr size_t MIN_BOUNDARY_SAMPLES = 64; +constexpr size_t MAX_BOUNDARY_SAMPLES = 256; +constexpr size_t ESTIMATED_TRANSITION_BYTES = 1400; + +enum class DetectorSliceSvgPathKind { + CIRCLE, + PATH, +}; + +struct DetectorSliceSvgPathSegment { + bool is_line; + Coord<2> control1; + Coord<2> control2; + Coord<2> end; +}; + +struct DetectorSliceSvgPath { + DetectorSliceSvgPathKind kind; + Coord<2> start; + float radius; + std::vector segments; +}; + +struct DiagramTimelineSvgFrame { + uint64_t tick; + std::string svg; + struct DetectorRegion { + DetectorSliceSvgPath path; + std::string style; + }; + std::map detector_regions; +}; + +struct StringBuffer : std::streambuf { + std::string data; + + explicit StringBuffer(size_t capacity) { + data.reserve(capacity); + } + + std::streamsize xsputn(const char *s, std::streamsize n) override { + data.append(s, n); + return n; + } + + int overflow(int c) override { + if (c != traits_type::eof()) { + data.push_back((char)c); + } + return traits_type::not_eof(c); + } +}; + +std::string_view attribute(std::string_view tag, std::string_view name) { + size_t start = tag.find(name); + while (start != std::string_view::npos) { + size_t equals = start + name.size(); + if (equals + 1 < tag.size() && tag[equals] == '=' && tag[equals + 1] == '"' && + (start == 0 || std::isspace((uint8_t)tag[start - 1]))) { + start = equals + 2; + size_t end = tag.find('"', start); + if (end != std::string_view::npos) { + return tag.substr(start, end - start); + } + } + start = tag.find(name, start + 1); + } + return {}; +} + +float parse_float(std::string_view text) { + std::string copy(text); + char *end; + float result = strtof(copy.c_str(), &end); + if (end != copy.c_str() + copy.size()) { + throw std::invalid_argument("Malformed number in detector slice SVG."); + } + return result; +} + +struct SvgPathReader { + std::string text; + const char *p; + + explicit SvgPathReader(std::string_view text) : text(text), p(this->text.c_str()) { + } + + void skip_separators() { + while (*p == ' ' || *p == ',') { + p++; + } + } + + float read_float() { + skip_separators(); + char *end; + float result = strtof(p, &end); + if (end == p) { + throw std::invalid_argument("Malformed detector slice SVG path."); + } + p = end; + return result; + } + + Coord<2> read_coord() { + return {read_float(), read_float()}; + } +}; + +DetectorSliceSvgPath parse_shape(std::string_view tag) { + if (tag.substr(0, 7) == "', start); + if (end == std::string_view::npos) { + break; + } + auto tag = group.substr(start, end - start + 1); + auto fill = attribute(tag, "fill"); + if (fill.find("grad") != std::string_view::npos) { + out << ':' << attribute(tag, "cx") << ',' << attribute(tag, "cy") << ',' << attribute(tag, "r") << ',' + << fill; + } + start = end + 1; + } + return out.str(); +} + +DiagramTimelineSvgFrame parse_frame(uint64_t tick, std::string svg) { + DiagramTimelineSvgFrame result{tick, std::move(svg), {}}; + size_t start = 0; + while ((start = result.svg.find("", id_end); + if (group_end == std::string::npos) { + throw std::invalid_argument("Malformed detector slice SVG group."); + } + if (id_end > id_start && id_end < result.svg.size() && result.svg[id_end] == ':') { + uint64_t id = std::stoull(result.svg.substr(id_start, id_end - id_start)); + auto group = std::string_view(result.svg).substr(start, group_end + 4 - start); + size_t path_start = group.find("', shape_start); + if (shape_start == std::string_view::npos || shape_end == std::string_view::npos) { + throw std::invalid_argument("Missing detector slice SVG shape."); + } + auto shape_tag = group.substr(shape_start, shape_end - shape_start + 1); + auto inserted = result.detector_regions.insert( + {id, {parse_shape(shape_tag), style_signature(group, shape_tag)}}); + if (!inserted.second) { + throw std::invalid_argument("Duplicate detector slice SVG group."); + } + } + start = group_end + 4; + } + return result; +} + +std::vector make_frames( + const Circuit &circuit, uint64_t tick_slice_start, uint64_t tick_slice_num, SpanRef filter) { + uint64_t circuit_num_ticks = circuit.count_ticks(); + if (tick_slice_start > circuit_num_ticks) { + return {}; + } + tick_slice_num = std::min(tick_slice_num, circuit_num_ticks - tick_slice_start + 1); + if (!circuit.operations.empty() && circuit.operations.back().gate_type == GateType::TICK) { + tick_slice_num = std::min(tick_slice_num, circuit_num_ticks - tick_slice_start); + } + + std::vector result; + result.reserve(tick_slice_num); + for (uint64_t k = 0; k < tick_slice_num; k++) { + std::stringstream out; + DiagramTimelineSvgDrawer::make_diagram_write_to( + circuit, + out, + tick_slice_start + k, + 1, + DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, + filter); + result.push_back(parse_frame(tick_slice_start + k, out.str())); + } + return result; +} + +bool same_path(const DetectorSliceSvgPath &a, const DetectorSliceSvgPath &b) { + if (a.kind != b.kind || a.start != b.start || a.radius != b.radius || a.segments.size() != b.segments.size()) { + return false; + } + for (size_t k = 0; k < a.segments.size(); k++) { + const auto &x = a.segments[k]; + const auto &y = b.segments[k]; + if (x.is_line != y.is_line || x.control1 != y.control1 || x.control2 != y.control2 || x.end != y.end) { + return false; + } + } + return true; +} + +Coord<2> cubic_point(Coord<2> a, Coord<2> b, Coord<2> c, Coord<2> d, float t) { + float s = 1 - t; + return a * (s * s * s) + b * (3 * s * s * t) + c * (3 * s * t * t) + d * (t * t * t); +} + +size_t sample_count(const DetectorSliceSvgPath &path) { + size_t requested = std::max(MIN_BOUNDARY_SAMPLES, path.segments.size() * 4); + size_t result = MIN_BOUNDARY_SAMPLES; + while (result < requested && result < MAX_BOUNDARY_SAMPLES) { + result *= 2; + } + return result; +} + +std::vector> sample_path(const DetectorSliceSvgPath &path, size_t count) { + std::vector> result(count); + if (path.kind == DetectorSliceSvgPathKind::CIRCLE) { + for (size_t k = 0; k < result.size(); k++) { + float angle = 2 * 3.14159265358979323846f * k / result.size(); + result[k] = path.start + Coord<2>{cosf(angle), sinf(angle)} * path.radius; + } + return result; + } + + std::vector> polyline{path.start}; + auto start = path.start; + for (const auto &segment : path.segments) { + size_t steps = segment.is_line ? 1 : 16; + for (size_t k = 1; k <= steps; k++) { + float t = (float)k / steps; + polyline.push_back( + segment.is_line ? segment.end : cubic_point(start, segment.control1, segment.control2, segment.end, t)); + } + start = segment.end; + } + + std::vector cumulative(polyline.size()); + for (size_t k = 1; k < polyline.size(); k++) { + cumulative[k] = cumulative[k - 1] + (polyline[k] - polyline[k - 1]).norm(); + } + float total = cumulative.back(); + size_t segment = 1; + for (size_t k = 0; k < result.size(); k++) { + float distance = total * k / result.size(); + while (segment + 1 < cumulative.size() && cumulative[segment] < distance) { + segment++; + } + float length = cumulative[segment] - cumulative[segment - 1]; + float t = length == 0 ? 0 : (distance - cumulative[segment - 1]) / length; + result[k] = polyline[segment - 1] + (polyline[segment] - polyline[segment - 1]) * t; + } + return result; +} + +std::vector> aligned_to(const std::vector> &source, const std::vector> &target) { + float best_error = INFINITY; + size_t best_shift = 0; + bool best_reversed = false; + for (size_t reversed = 0; reversed < 2; reversed++) { + for (size_t shift = 0; shift < target.size(); shift++) { + float error = 0; + for (size_t k = 0; k < source.size(); k++) { + size_t j = reversed ? (shift + target.size() - k) % target.size() : (shift + k) % target.size(); + float dx = source[k].xyz[0] - target[j].xyz[0]; + float dy = source[k].xyz[1] - target[j].xyz[1]; + error += dx * dx + dy * dy; + if (error >= best_error) { + break; + } + } + if (error < best_error) { + best_error = error; + best_shift = shift; + best_reversed = reversed; + } + } + } + std::vector> result(target.size()); + for (size_t k = 0; k < target.size(); k++) { + size_t j = best_reversed ? (best_shift + target.size() - k) % target.size() : (best_shift + k) % target.size(); + result[k] = target[j]; + } + return result; +} + +std::vector> collapsed(const std::vector> &points) { + Coord<2> center{}; + float area_twice = 0; + for (size_t k = 0; k < points.size(); k++) { + const auto &a = points[k]; + const auto &b = points[(k + 1) % points.size()]; + float cross = a.xyz[0] * b.xyz[1] - b.xyz[0] * a.xyz[1]; + center += (a + b) * cross; + area_twice += cross; + } + if (std::abs(area_twice) > 1e-6f) { + center /= 3 * area_twice; + } else { + center = {}; + for (const auto &point : points) { + center += point; + } + center /= points.size(); + } + return std::vector>(points.size(), center); +} + +void write_points(const std::vector> &source, const std::vector> &target, std::ostream &out) { + std::string bytes; + bytes.reserve((source.size() + target.size()) * 2 * sizeof(float)); + for (const auto *points : {&source, &target}) { + for (const auto &point : *points) { + for (float value : point.xyz) { + uint32_t bits; + memcpy(&bits, &value, sizeof(bits)); + for (size_t k = 0; k < sizeof(bits); k++) { + bytes.push_back((char)(bits >> (8 * k))); + } + } + } + } + out << '"'; + write_data_as_base64_to(bytes, out); + out << '"'; +} + +void write_transitions(std::vector &frames, std::ostream &out) { + out << '['; + for (size_t k = 1; k < frames.size(); k++) { + if (k > 1) { + out << ','; + } + out << '['; + bool first = true; + const auto &source = frames[k - 1].detector_regions; + const auto &target = frames[k].detector_regions; + std::set ids; + for (const auto &entry : source) + ids.insert(entry.first); + for (const auto &entry : target) + ids.insert(entry.first); + for (uint64_t id : ids) { + auto s = source.find(id); + auto t = target.find(id); + bool has_source = s != source.end(); + bool has_target = t != target.end(); + bool geometry_changed = !has_source || !has_target || !same_path(s->second.path, t->second.path); + bool style_changed = has_source && has_target && s->second.style != t->second.style; + if (!geometry_changed && !style_changed) { + continue; + } + if (!first) { + out << ','; + } + first = false; + uint8_t flags = has_source | (has_target << 1) | (geometry_changed << 2) | (style_changed << 3); + size_t count = 0; + if (geometry_changed) { + count = std::max( + has_source ? sample_count(s->second.path) : 0, has_target ? sample_count(t->second.path) : 0); + } + out << "[\"" << id << "\"," << (uint32_t)flags << ',' << count << ','; + if (geometry_changed) { + auto a = has_source ? sample_path(s->second.path, count) : sample_path(t->second.path, count); + auto b = has_target ? sample_path(t->second.path, count) : sample_path(s->second.path, count); + if (!has_source) { + a = collapsed(b); + } else if (!has_target) { + b = collapsed(a); + } else { + b = aligned_to(a, b); + } + write_points(a, b, out); + } else { + out << "\"\""; + } + out << ']'; + } + out << ']'; + std::map().swap(frames[k - 1].detector_regions); + } + out << ']'; +} + +void write_json_string(std::ostream &out, std::string_view text) { + out << '"'; + for (uint8_t c : text) { + switch (c) { + case '"': + out << "\\\""; + break; + case '\\': + out << "\\\\"; + break; + case '\b': + out << "\\b"; + break; + case '\f': + out << "\\f"; + break; + case '\n': + out << "\\n"; + break; + case '\r': + out << "\\r"; + break; + case '\t': + out << "\\t"; + break; + case '<': + out << "\\u003c"; + break; + case '>': + out << "\\u003e"; + break; + case '&': + out << "\\u0026"; + break; + default: + if (c < 0x20) { + out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (uint32_t)c << std::dec; + } else { + out << (char)c; + } + } + } + out << '"'; +} + +void write_animation_frames(std::vector frames, uint64_t tick_slice_start, std::ostream &out) { + out << R"HTML( + + +
+
+)HTML"; +} + +} // namespace + +std::string stim_draw_internal::make_detector_slice_animation_html( + const Circuit &circuit, + uint64_t tick_slice_start, + uint64_t tick_slice_num, + SpanRef det_coord_filter) { + auto frames = make_frames(circuit, tick_slice_start, tick_slice_num, det_coord_filter); + size_t capacity = 4096; + for (const auto &frame : frames) { + capacity += frame.svg.size() + frame.detector_regions.size() * ESTIMATED_TRANSITION_BYTES; + } + StringBuffer buffer(capacity); + std::ostream out(&buffer); + write_animation_frames(std::move(frames), tick_slice_start, out); + return std::move(buffer.data); +} diff --git a/src/stim/diagram/detector_slice/detector_slice_animation.h b/src/stim/diagram/detector_slice/detector_slice_animation.h new file mode 100644 index 000000000..38eb8446c --- /dev/null +++ b/src/stim/diagram/detector_slice/detector_slice_animation.h @@ -0,0 +1,20 @@ +#ifndef _STIM_DIAGRAM_DETECTOR_SLICE_DETECTOR_SLICE_ANIMATION_H +#define _STIM_DIAGRAM_DETECTOR_SLICE_DETECTOR_SLICE_ANIMATION_H + +#include +#include + +#include "stim/circuit/circuit.h" +#include "stim/diagram/detector_slice/detector_slice_set.h" + +namespace stim_draw_internal { + +std::string make_detector_slice_animation_html( + const stim::Circuit &circuit, + uint64_t tick_slice_start, + uint64_t tick_slice_num, + stim::SpanRef det_coord_filter); + +} // namespace stim_draw_internal + +#endif From 1524b11f15de866cb87184785df1fc7285b26b78 Mon Sep 17 00:00:00 2001 From: ShallIBe Date: Mon, 24 Aug 2026 18:11:36 -0700 Subject: [PATCH 2/2] Address animated detector slice review feedback Remove SVG parsing by collecting detector geometry during rendering. Use std::stringstream, format the embedded player, and add focused tests. --- file_lists/test_files | 1 + src/stim/cmd/command_diagram.test.cc | 1 + .../detector_slice_animation.cc | 764 ++++++++++-------- .../detector_slice/detector_slice_animation.h | 27 + .../detector_slice_animation.test.cc | 163 ++++ .../detector_slice/detector_slice_set.cc | 79 +- .../detector_slice/detector_slice_set.h | 48 +- .../diagram/timeline/timeline_svg_drawer.cc | 6 +- .../diagram/timeline/timeline_svg_drawer.h | 3 +- 9 files changed, 721 insertions(+), 371 deletions(-) create mode 100644 src/stim/diagram/detector_slice/detector_slice_animation.test.cc diff --git a/file_lists/test_files b/file_lists/test_files index e669bc24a..cea609d89 100644 --- a/file_lists/test_files +++ b/file_lists/test_files @@ -17,6 +17,7 @@ src/stim/dem/detector_error_model.test.cc src/stim/diagram/ascii_diagram.test.cc src/stim/diagram/base64.test.cc src/stim/diagram/coord.test.cc +src/stim/diagram/detector_slice/detector_slice_animation.test.cc src/stim/diagram/detector_slice/detector_slice_set.test.cc src/stim/diagram/graph/match_graph_3d_drawer.test.cc src/stim/diagram/graph/match_graph_svg_drawer.test.cc diff --git a/src/stim/cmd/command_diagram.test.cc b/src/stim/cmd/command_diagram.test.cc index e69e7534e..c73a53e76 100644 --- a/src/stim/cmd/command_diagram.test.cc +++ b/src/stim/cmd/command_diagram.test.cc @@ -147,6 +147,7 @@ TEST(command_diagram, run_captured_stim_main_works_various_arguments) { "detector-slice-svg", "time-slice-svg", "time+detector-slice-svg", + "animated-detslice-with-ops", }; for (const auto &type : diagram_types) { auto actual = run_captured_stim_main( diff --git a/src/stim/diagram/detector_slice/detector_slice_animation.cc b/src/stim/diagram/detector_slice/detector_slice_animation.cc index 27ac219ac..7133d3dc5 100644 --- a/src/stim/diagram/detector_slice/detector_slice_animation.cc +++ b/src/stim/diagram/detector_slice/detector_slice_animation.cc @@ -1,15 +1,12 @@ #include "stim/diagram/detector_slice/detector_slice_animation.h" #include -#include #include #include -#include #include #include #include #include -#include #include #include @@ -23,234 +20,6 @@ namespace { constexpr size_t MIN_BOUNDARY_SAMPLES = 64; constexpr size_t MAX_BOUNDARY_SAMPLES = 256; -constexpr size_t ESTIMATED_TRANSITION_BYTES = 1400; - -enum class DetectorSliceSvgPathKind { - CIRCLE, - PATH, -}; - -struct DetectorSliceSvgPathSegment { - bool is_line; - Coord<2> control1; - Coord<2> control2; - Coord<2> end; -}; - -struct DetectorSliceSvgPath { - DetectorSliceSvgPathKind kind; - Coord<2> start; - float radius; - std::vector segments; -}; - -struct DiagramTimelineSvgFrame { - uint64_t tick; - std::string svg; - struct DetectorRegion { - DetectorSliceSvgPath path; - std::string style; - }; - std::map detector_regions; -}; - -struct StringBuffer : std::streambuf { - std::string data; - - explicit StringBuffer(size_t capacity) { - data.reserve(capacity); - } - - std::streamsize xsputn(const char *s, std::streamsize n) override { - data.append(s, n); - return n; - } - - int overflow(int c) override { - if (c != traits_type::eof()) { - data.push_back((char)c); - } - return traits_type::not_eof(c); - } -}; - -std::string_view attribute(std::string_view tag, std::string_view name) { - size_t start = tag.find(name); - while (start != std::string_view::npos) { - size_t equals = start + name.size(); - if (equals + 1 < tag.size() && tag[equals] == '=' && tag[equals + 1] == '"' && - (start == 0 || std::isspace((uint8_t)tag[start - 1]))) { - start = equals + 2; - size_t end = tag.find('"', start); - if (end != std::string_view::npos) { - return tag.substr(start, end - start); - } - } - start = tag.find(name, start + 1); - } - return {}; -} - -float parse_float(std::string_view text) { - std::string copy(text); - char *end; - float result = strtof(copy.c_str(), &end); - if (end != copy.c_str() + copy.size()) { - throw std::invalid_argument("Malformed number in detector slice SVG."); - } - return result; -} - -struct SvgPathReader { - std::string text; - const char *p; - - explicit SvgPathReader(std::string_view text) : text(text), p(this->text.c_str()) { - } - - void skip_separators() { - while (*p == ' ' || *p == ',') { - p++; - } - } - - float read_float() { - skip_separators(); - char *end; - float result = strtof(p, &end); - if (end == p) { - throw std::invalid_argument("Malformed detector slice SVG path."); - } - p = end; - return result; - } - - Coord<2> read_coord() { - return {read_float(), read_float()}; - } -}; - -DetectorSliceSvgPath parse_shape(std::string_view tag) { - if (tag.substr(0, 7) == "', start); - if (end == std::string_view::npos) { - break; - } - auto tag = group.substr(start, end - start + 1); - auto fill = attribute(tag, "fill"); - if (fill.find("grad") != std::string_view::npos) { - out << ':' << attribute(tag, "cx") << ',' << attribute(tag, "cy") << ',' << attribute(tag, "r") << ',' - << fill; - } - start = end + 1; - } - return out.str(); -} - -DiagramTimelineSvgFrame parse_frame(uint64_t tick, std::string svg) { - DiagramTimelineSvgFrame result{tick, std::move(svg), {}}; - size_t start = 0; - while ((start = result.svg.find("", id_end); - if (group_end == std::string::npos) { - throw std::invalid_argument("Malformed detector slice SVG group."); - } - if (id_end > id_start && id_end < result.svg.size() && result.svg[id_end] == ':') { - uint64_t id = std::stoull(result.svg.substr(id_start, id_end - id_start)); - auto group = std::string_view(result.svg).substr(start, group_end + 4 - start); - size_t path_start = group.find("', shape_start); - if (shape_start == std::string_view::npos || shape_end == std::string_view::npos) { - throw std::invalid_argument("Missing detector slice SVG shape."); - } - auto shape_tag = group.substr(shape_start, shape_end - shape_start + 1); - auto inserted = result.detector_regions.insert( - {id, {parse_shape(shape_tag), style_signature(group, shape_tag)}}); - if (!inserted.second) { - throw std::invalid_argument("Duplicate detector slice SVG group."); - } - } - start = group_end + 4; - } - return result; -} - -std::vector make_frames( - const Circuit &circuit, uint64_t tick_slice_start, uint64_t tick_slice_num, SpanRef filter) { - uint64_t circuit_num_ticks = circuit.count_ticks(); - if (tick_slice_start > circuit_num_ticks) { - return {}; - } - tick_slice_num = std::min(tick_slice_num, circuit_num_ticks - tick_slice_start + 1); - if (!circuit.operations.empty() && circuit.operations.back().gate_type == GateType::TICK) { - tick_slice_num = std::min(tick_slice_num, circuit_num_ticks - tick_slice_start); - } - - std::vector result; - result.reserve(tick_slice_num); - for (uint64_t k = 0; k < tick_slice_num; k++) { - std::stringstream out; - DiagramTimelineSvgDrawer::make_diagram_write_to( - circuit, - out, - tick_slice_start + k, - 1, - DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, - filter); - result.push_back(parse_frame(tick_slice_start + k, out.str())); - } - return result; -} bool same_path(const DetectorSliceSvgPath &a, const DetectorSliceSvgPath &b) { if (a.kind != b.kind || a.start != b.start || a.radius != b.radius || a.segments.size() != b.segments.size()) { @@ -373,6 +142,100 @@ std::vector> collapsed(const std::vector> &points) { return std::vector>(points.size(), center); } +} // namespace + +std::vector stim_draw_internal::make_detector_slice_animation_transitions( + const DetectorSliceAnimationFrame &source, const DetectorSliceAnimationFrame &target) { + std::set ids; + for (const auto &entry : source.detector_regions) { + ids.insert(entry.first); + } + for (const auto &entry : target.detector_regions) { + ids.insert(entry.first); + } + + std::vector result; + for (uint64_t id : ids) { + auto s = source.detector_regions.find(id); + auto t = target.detector_regions.find(id); + bool has_source = s != source.detector_regions.end(); + bool has_target = t != target.detector_regions.end(); + bool geometry_changed = !has_source || !has_target || !same_path(s->second.path, t->second.path); + bool style_changed = has_source && has_target && s->second.style != t->second.style; + if (!geometry_changed && !style_changed) { + continue; + } + + std::vector> source_points; + std::vector> target_points; + if (geometry_changed) { + size_t count = + std::max(has_source ? sample_count(s->second.path) : 0, has_target ? sample_count(t->second.path) : 0); + source_points = has_source ? sample_path(s->second.path, count) : sample_path(t->second.path, count); + target_points = has_target ? sample_path(t->second.path, count) : sample_path(s->second.path, count); + if (!has_source) { + source_points = collapsed(target_points); + } else if (!has_target) { + target_points = collapsed(source_points); + } else { + target_points = aligned_to(source_points, target_points); + } + } + result.push_back({ + id, + has_source, + has_target, + geometry_changed, + style_changed, + std::move(source_points), + std::move(target_points), + }); + } + return result; +} + +std::vector stim_draw_internal::make_detector_slice_animation_frames( + const Circuit &circuit, + uint64_t tick_slice_start, + uint64_t tick_slice_num, + SpanRef det_coord_filter) { + std::vector result; + uint64_t circuit_num_ticks = circuit.count_ticks(); + if (tick_slice_start > circuit_num_ticks) { + return result; + } + tick_slice_num = std::min(tick_slice_num, circuit_num_ticks - tick_slice_start + 1); + if (!circuit.operations.empty() && circuit.operations.back().gate_type == GateType::TICK) { + tick_slice_num = std::min(tick_slice_num, circuit_num_ticks - tick_slice_start); + } + + result.reserve(tick_slice_num); + for (uint64_t k = 0; k < tick_slice_num; k++) { + DetectorSliceAnimationFrame frame{tick_slice_start + k, {}, {}}; + DetectorSliceSvgMetadata metadata; + std::stringstream out; + DiagramTimelineSvgDrawer::make_diagram_write_to( + circuit, + out, + frame.tick, + 1, + DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, + det_coord_filter, + 0, + &metadata); + for (auto ®ion : metadata.regions) { + uint64_t detector_id = region.detector_id; + frame.detector_regions.insert({detector_id, std::move(region)}); + } + frame.svg = out.str(); + result.push_back(std::move(frame)); + } + + return result; +} + +namespace { + void write_points(const std::vector> &source, const std::vector> &target, std::ostream &out) { std::string bytes; bytes.reserve((source.size() + target.size()) * 2 * sizeof(float)); @@ -392,60 +255,32 @@ void write_points(const std::vector> &source, const std::vector &frames, std::ostream &out) { +void write_transitions(std::vector &frames, std::ostream &out) { out << '['; for (size_t k = 1; k < frames.size(); k++) { if (k > 1) { out << ','; } + auto transitions = make_detector_slice_animation_transitions(frames[k - 1], frames[k]); out << '['; - bool first = true; - const auto &source = frames[k - 1].detector_regions; - const auto &target = frames[k].detector_regions; - std::set ids; - for (const auto &entry : source) - ids.insert(entry.first); - for (const auto &entry : target) - ids.insert(entry.first); - for (uint64_t id : ids) { - auto s = source.find(id); - auto t = target.find(id); - bool has_source = s != source.end(); - bool has_target = t != target.end(); - bool geometry_changed = !has_source || !has_target || !same_path(s->second.path, t->second.path); - bool style_changed = has_source && has_target && s->second.style != t->second.style; - if (!geometry_changed && !style_changed) { - continue; - } - if (!first) { + for (size_t j = 0; j < transitions.size(); j++) { + if (j) { out << ','; } - first = false; - uint8_t flags = has_source | (has_target << 1) | (geometry_changed << 2) | (style_changed << 3); - size_t count = 0; - if (geometry_changed) { - count = std::max( - has_source ? sample_count(s->second.path) : 0, has_target ? sample_count(t->second.path) : 0); - } - out << "[\"" << id << "\"," << (uint32_t)flags << ',' << count << ','; - if (geometry_changed) { - auto a = has_source ? sample_path(s->second.path, count) : sample_path(t->second.path, count); - auto b = has_target ? sample_path(t->second.path, count) : sample_path(s->second.path, count); - if (!has_source) { - a = collapsed(b); - } else if (!has_target) { - b = collapsed(a); - } else { - b = aligned_to(a, b); - } - write_points(a, b, out); + const auto &transition = transitions[j]; + uint8_t flags = transition.has_source | (transition.has_target << 1) | (transition.geometry_changed << 2) | + (transition.style_changed << 3); + out << "[\"" << transition.detector_id << "\"," << (uint32_t)flags << ',' << transition.source_points.size() + << ','; + if (transition.geometry_changed) { + write_points(transition.source_points, transition.target_points, out); } else { out << "\"\""; } out << ']'; } out << ']'; - std::map().swap(frames[k - 1].detector_regions); + std::map().swap(frames[k - 1].detector_regions); } out << ']'; } @@ -495,7 +330,8 @@ void write_json_string(std::ostream &out, std::string_view text) { out << '"'; } -void write_animation_frames(std::vector frames, uint64_t tick_slice_start, std::ostream &out) { +void write_animation_frames( + std::vector frames, uint64_t tick_slice_start, std::ostream &out) { out << R"HTML(