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/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/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/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 new file mode 100644 index 000000000..7133d3dc5 --- /dev/null +++ b/src/stim/diagram/detector_slice/detector_slice_animation.cc @@ -0,0 +1,672 @@ +#include "stim/diagram/detector_slice/detector_slice_animation.h" + +#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; + +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); +} + +} // 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)); + 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 << ','; + } + auto transitions = make_detector_slice_animation_transitions(frames[k - 1], frames[k]); + out << '['; + for (size_t j = 0; j < transitions.size(); j++) { + if (j) { + 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); + } + 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) { + std::stringstream out; + write_animation_frames( + make_detector_slice_animation_frames(circuit, tick_slice_start, tick_slice_num, det_coord_filter), + tick_slice_start, + out); + return out.str(); +} 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..da607f71c --- /dev/null +++ b/src/stim/diagram/detector_slice/detector_slice_animation.h @@ -0,0 +1,47 @@ +#ifndef _STIM_DIAGRAM_DETECTOR_SLICE_DETECTOR_SLICE_ANIMATION_H +#define _STIM_DIAGRAM_DETECTOR_SLICE_DETECTOR_SLICE_ANIMATION_H + +#include +#include +#include +#include + +#include "stim/circuit/circuit.h" +#include "stim/diagram/detector_slice/detector_slice_set.h" + +namespace stim_draw_internal { + +struct DetectorSliceAnimationFrame { + uint64_t tick; + std::string svg; + std::map detector_regions; +}; + +struct DetectorSliceAnimationTransition { + uint64_t detector_id; + bool has_source; + bool has_target; + bool geometry_changed; + bool style_changed; + std::vector> source_points; + std::vector> target_points; +}; + +std::vector make_detector_slice_animation_transitions( + const DetectorSliceAnimationFrame &source, const DetectorSliceAnimationFrame &target); + +std::vector make_detector_slice_animation_frames( + const stim::Circuit &circuit, + uint64_t tick_slice_start, + uint64_t tick_slice_num, + stim::SpanRef det_coord_filter); + +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 diff --git a/src/stim/diagram/detector_slice/detector_slice_animation.test.cc b/src/stim/diagram/detector_slice/detector_slice_animation.test.cc new file mode 100644 index 000000000..39ef67e4e --- /dev/null +++ b/src/stim/diagram/detector_slice/detector_slice_animation.test.cc @@ -0,0 +1,163 @@ +#include "stim/diagram/detector_slice/detector_slice_animation.h" + +#include + +#include "gtest/gtest.h" + +#include "stim/diagram/diagram_util.h" +#include "stim/diagram/timeline/timeline_svg_drawer.h" + +using namespace stim; +using namespace stim_draw_internal; + +TEST(detector_slice_animation, frames_match_existing_renderer) { + Circuit circuit(R"CIRCUIT( + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + QUBIT_COORDS(2, 0) 2 + QUBIT_COORDS(0, 1) 3 + QUBIT_COORDS(1, 1) 4 + QUBIT_COORDS(2, 1) 5 + RX 0 1 3 4 + R 2 5 + TICK + MPP X0 X1*Z2 X3*X4*Z5 + DETECTOR(0) rec[-3] + DETECTOR(1) rec[-2] + DETECTOR(2) rec[-1] + )CIRCUIT"); + CoordFilter all; + auto frames = make_detector_slice_animation_frames(circuit, 0, 2, {&all}); + + ASSERT_EQ(frames.size(), 2); + for (const auto &frame : frames) { + std::stringstream expected; + DiagramTimelineSvgDrawer::make_diagram_write_to( + circuit, expected, frame.tick, 1, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, {&all}); + EXPECT_EQ(frame.svg, expected.str()); + } + + const auto ®ions = frames[0].detector_regions; + ASSERT_EQ(regions.size(), 3); + EXPECT_EQ(regions.at(0).path.kind, DetectorSliceSvgPathKind::CIRCLE); + EXPECT_EQ(regions.at(1).path.kind, DetectorSliceSvgPathKind::PATH); + EXPECT_EQ(regions.at(2).path.kind, DetectorSliceSvgPathKind::PATH); + EXPECT_EQ(regions.at(0).style.fill, X_RED); + EXPECT_EQ(regions.at(1).style.fill, BG_GREY); + EXPECT_EQ(regions.at(2).style.fill, BG_GREY); + EXPECT_TRUE(regions.at(0).style.gradients.empty()); + EXPECT_EQ(regions.at(1).style.gradients.size(), 2); + EXPECT_EQ(regions.at(2).style.gradients.size(), 3); + EXPECT_FLOAT_EQ(regions.at(2).style.fill_opacity, 0.75f); +} + +TEST(detector_slice_animation, transitions) { + DetectorSliceSvgPath circle{DetectorSliceSvgPathKind::CIRCLE, {0, 0}, 1, {}}; + DetectorSliceSvgPath death_circle{DetectorSliceSvgPathKind::CIRCLE, {3, 4}, 1, {}}; + DetectorSliceSvgPath birth_circle{DetectorSliceSvgPathKind::CIRCLE, {-2, 5}, 1, {}}; + DetectorSliceSvgPath lens{ + DetectorSliceSvgPathKind::PATH, + {-1, 0}, + 0, + { + {false, {-1, 1}, {1, 1}, {1, 0}}, + {false, {1, -1}, {-1, -1}, {-1, 0}}, + }, + }; + DetectorSliceSvgPath square{ + DetectorSliceSvgPathKind::PATH, + {0, 0}, + 0, + { + {true, {}, {}, {1, 0}}, + {true, {}, {}, {1, 1}}, + {true, {}, {}, {0, 1}}, + {true, {}, {}, {0, 0}}, + }, + }; + DetectorSliceSvgPath reversed_square{ + DetectorSliceSvgPathKind::PATH, + {1, 1}, + 0, + { + {true, {}, {}, {1, 0}}, + {true, {}, {}, {0, 0}}, + {true, {}, {}, {0, 1}}, + {true, {}, {}, {1, 1}}, + }, + }; + DetectorSliceSvgStyle x_style{X_RED, 1, {}}; + DetectorSliceSvgStyle z_style{Z_BLUE, 1, {}}; + DetectorSliceAnimationFrame source{ + 0, + {}, + { + {0, {circle, x_style, 0}}, + {1, {circle, x_style, 1}}, + {2, {death_circle, x_style, 2}}, + {4, {circle, x_style, 4}}, + {5, {square, x_style, 5}}, + }, + }; + DetectorSliceAnimationFrame target{ + 1, + {}, + { + {0, {circle, x_style, 0}}, + {1, {lens, x_style, 1}}, + {3, {birth_circle, x_style, 3}}, + {4, {circle, z_style, 4}}, + {5, {reversed_square, x_style, 5}}, + }, + }; + + auto transitions = make_detector_slice_animation_transitions(source, target); + ASSERT_EQ(transitions.size(), 5); + + EXPECT_EQ(transitions[0].detector_id, 1); + EXPECT_TRUE(transitions[0].has_source); + EXPECT_TRUE(transitions[0].has_target); + EXPECT_TRUE(transitions[0].geometry_changed); + EXPECT_FALSE(transitions[0].style_changed); + EXPECT_EQ(transitions[0].source_points.size(), transitions[0].target_points.size()); + ASSERT_EQ(transitions[0].source_points.size(), 64); + EXPECT_EQ(transitions[0].source_points[0], (Coord<2>{1, 0})); + + EXPECT_EQ(transitions[1].detector_id, 2); + EXPECT_TRUE(transitions[1].has_source); + EXPECT_FALSE(transitions[1].has_target); + ASSERT_FALSE(transitions[1].target_points.empty()); + for (const auto &point : transitions[1].target_points) { + EXPECT_NEAR(point.xyz[0], 3, 1e-5); + EXPECT_NEAR(point.xyz[1], 4, 1e-5); + } + + EXPECT_EQ(transitions[2].detector_id, 3); + EXPECT_FALSE(transitions[2].has_source); + EXPECT_TRUE(transitions[2].has_target); + ASSERT_FALSE(transitions[2].source_points.empty()); + for (const auto &point : transitions[2].source_points) { + EXPECT_NEAR(point.xyz[0], -2, 1e-5); + EXPECT_NEAR(point.xyz[1], 5, 1e-5); + } + + EXPECT_EQ(transitions[3].detector_id, 4); + EXPECT_TRUE(transitions[3].has_source); + EXPECT_TRUE(transitions[3].has_target); + EXPECT_FALSE(transitions[3].geometry_changed); + EXPECT_TRUE(transitions[3].style_changed); + EXPECT_TRUE(transitions[3].source_points.empty()); + EXPECT_TRUE(transitions[3].target_points.empty()); + + const auto &transition = transitions[4]; + EXPECT_EQ(transition.detector_id, 5); + EXPECT_TRUE(transition.geometry_changed); + EXPECT_FALSE(transition.style_changed); + ASSERT_EQ(transition.source_points.size(), 64); + ASSERT_EQ(transition.target_points.size(), 64); + for (size_t k = 0; k < 64; k++) { + EXPECT_NEAR((transition.source_points[(k + 1) % 64] - transition.source_points[k]).norm(), 0.0625, 1e-6); + EXPECT_NEAR(transition.source_points[k].xyz[0], transition.target_points[k].xyz[0], 1e-6); + EXPECT_NEAR(transition.source_points[k].xyz[1], transition.target_points[k].xyz[1], 1e-6); + } +} diff --git a/src/stim/diagram/detector_slice/detector_slice_set.cc b/src/stim/diagram/detector_slice/detector_slice_set.cc index b0d926f11..9e014508f 100644 --- a/src/stim/diagram/detector_slice/detector_slice_set.cc +++ b/src/stim/diagram/detector_slice/detector_slice_set.cc @@ -1,5 +1,8 @@ #include "stim/diagram/detector_slice/detector_slice_set.h" +#include +#include + #include "stim/dem/detector_error_model.h" #include "stim/diagram/coord.h" #include "stim/diagram/diagram_util.h" @@ -720,7 +723,8 @@ void _start_many_body_svg_path( const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, SpanRef terms, - std::vector> &pts_workspace) { + std::vector> &pts_workspace, + DetectorSliceSvgPath *path) { pts_workspace.clear(); for (const auto &term : terms) { pts_workspace.push_back(coords(tick, term.qubit_value())); @@ -730,6 +734,9 @@ void _start_many_body_svg_path( return offset_angle_from_to(center, a) < offset_angle_from_to(center, b); }); + if (path != nullptr) { + *path = {DetectorSliceSvgPathKind::PATH, pts_workspace[0], 0, {}}; + } out << "segments.push_back({false, d, d, b}); + } } else { out << " L" << b.xyz[0] << "," << b.xyz[1]; + if (path != nullptr) { + path->segments.push_back({true, {}, {}, b}); + } } } out << '"'; @@ -758,7 +771,8 @@ void _start_two_body_svg_path( std::ostream &out, const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, - SpanRef terms) { + SpanRef terms, + DetectorSliceSvgPath *path) { auto a = coords(tick, terms[0].qubit_value()); auto b = coords(tick, terms[1].qubit_value()); auto dif = b - a; @@ -772,6 +786,17 @@ void _start_two_body_svg_path( auto bc1 = average + perp * -0.2f + dif * 0.2f; auto bc2 = average + perp * -0.2f - dif * 0.2f; + if (path != nullptr) { + *path = { + DetectorSliceSvgPathKind::PATH, + a, + 0, + { + {false, ac1, ac2, b}, + {false, bc1, bc2, a}, + }, + }; + } out << "(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, SpanRef terms, - size_t scale) { + size_t scale, + DetectorSliceSvgPath *path) { auto c = coords(tick, terms[0].qubit_value()); + if (path != nullptr) { + *path = {DetectorSliceSvgPathKind::CIRCLE, c, (float)scale, {}}; + } out << " terms, std::vector> &pts_workspace, - size_t scale) { + size_t scale, + DetectorSliceSvgPath *path) { if (terms.size() > 2) { - _start_many_body_svg_path(out, coords, tick, terms, pts_workspace); + _start_many_body_svg_path(out, coords, tick, terms, pts_workspace, path); } else if (terms.size() == 2) { - _start_two_body_svg_path(out, coords, tick, terms); + _start_two_body_svg_path(out, coords, tick, terms, path); } else if (terms.size() == 1) { - _start_one_body_svg_path(out, coords, tick, terms, scale); + _start_one_body_svg_path(out, coords, tick, terms, scale, path); } } @@ -913,8 +943,12 @@ void DetectorSliceSet::write_svg_contents_to( const std::function(uint32_t qubit)> &unscaled_coords, const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t end_tick, - size_t scale) const { + size_t scale, + DetectorSliceSvgMetadata *metadata) const { size_t clip_id = 0; + if (metadata != nullptr) { + *metadata = {}; + } std::vector> pts_workspace; @@ -967,6 +1001,11 @@ void DetectorSliceSet::write_svg_contents_to( drawCorners = true; color = BG_GREY; } + float fill_opacity = terms.size() > 2 ? 0.75f : 1.0f; + std::optional region; + if (metadata != nullptr && target.is_relative_detector_id()) { + region = DetectorSliceSvgRegion{{}, {color, fill_opacity, {}}, target.val()}; + } // Open the group element for this slice out << "\n"; - _start_slice_shape_command(out, coords, tick, terms, pts_workspace, scale); + _start_slice_shape_command( + out, coords, tick, terms, pts_workspace, scale, region.has_value() ? ®ion->path : nullptr); write_key_val(out, "stroke", "none"); if (terms.size() > 2) { - write_key_val(out, "fill-opacity", 0.75); + write_key_val(out, "fill-opacity", fill_opacity); } write_key_val(out, "fill", color); out << "/>\n"; @@ -988,7 +1028,7 @@ void DetectorSliceSet::write_svg_contents_to( out << R"SVG("; - _start_slice_shape_command(out, coords, tick, terms, pts_workspace, scale); + _start_slice_shape_command(out, coords, tick, terms, pts_workspace, scale, nullptr); out << "/>\n"; size_t blur_radius = scale == 6 ? 20 : scale * 1.8f; @@ -1001,12 +1041,17 @@ void DetectorSliceSet::write_svg_contents_to( write_key_val(out, "cy", c.xyz[1]); write_key_val(out, "r", blur_radius); write_key_val(out, "stroke", "none"); + const char *gradient_fill; if (t.is_x_target()) { - write_key_val(out, "fill", "url('#xgrad')"); + gradient_fill = "url('#xgrad')"; } else if (t.is_y_target()) { - write_key_val(out, "fill", "url('#ygrad')"); + gradient_fill = "url('#ygrad')"; } else { - write_key_val(out, "fill", "url('#zgrad')"); + gradient_fill = "url('#zgrad')"; + } + write_key_val(out, "fill", gradient_fill); + if (region.has_value()) { + region->style.gradients.push_back({c, blur_radius, gradient_fill}); } out << "/>\n"; } @@ -1015,13 +1060,17 @@ void DetectorSliceSet::write_svg_contents_to( } // Draw outline - _start_slice_shape_command(out, coords, tick, terms, pts_workspace, scale); + _start_slice_shape_command(out, coords, tick, terms, pts_workspace, scale, nullptr); write_key_val(out, "stroke", "black"); write_key_val(out, "fill", "none"); out << "/>\n"; // Close the group element for this slice out << "\n"; + + if (region.has_value()) { + metadata->regions.push_back(std::move(*region)); + } } if (haveDrawnCorners) { // write out the universal radialGradients that all corners reference diff --git a/src/stim/diagram/detector_slice/detector_slice_set.h b/src/stim/diagram/detector_slice/detector_slice_set.h index 1fdeffa98..673f53cac 100644 --- a/src/stim/diagram/detector_slice/detector_slice_set.h +++ b/src/stim/diagram/detector_slice/detector_slice_set.h @@ -35,6 +35,51 @@ struct CoordFilter { static CoordFilter parse_from(std::string_view data); }; +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 DetectorSliceSvgGradient { + Coord<2> center; + size_t radius; + std::string fill; + + bool operator==(const DetectorSliceSvgGradient &other) const = default; +}; + +struct DetectorSliceSvgStyle { + std::string fill; + float fill_opacity; + std::vector gradients; + + bool operator==(const DetectorSliceSvgStyle &other) const = default; +}; + +struct DetectorSliceSvgRegion { + DetectorSliceSvgPath path; + DetectorSliceSvgStyle style; + uint64_t detector_id; +}; + +struct DetectorSliceSvgMetadata { + std::vector regions; +}; + struct DetectorSliceSet { uint64_t num_qubits; uint64_t min_tick; @@ -71,7 +116,8 @@ struct DetectorSliceSet { const std::function(uint32_t qubit)> &unscaled_coords, const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t end_tick, - size_t scale) const; + size_t scale, + DetectorSliceSvgMetadata *metadata = nullptr) const; }; double inv_space_fill_transform(Coord<2> a); diff --git a/src/stim/diagram/timeline/timeline_svg_drawer.cc b/src/stim/diagram/timeline/timeline_svg_drawer.cc index 022154ad8..75cf8be06 100644 --- a/src/stim/diagram/timeline/timeline_svg_drawer.cc +++ b/src/stim/diagram/timeline/timeline_svg_drawer.cc @@ -849,7 +849,8 @@ void DiagramTimelineSvgDrawer::make_diagram_write_to( uint64_t tick_slice_num, DiagramTimelineSvgDrawerMode mode, SpanRef filter, - size_t num_rows) { + size_t num_rows, + DetectorSliceSvgMetadata *detector_slice_metadata) { uint64_t circuit_num_ticks = circuit.count_ticks(); auto circuit_has_ticks = circuit_num_ticks > 0; auto num_qubits = circuit.count_qubits(); @@ -924,7 +925,8 @@ void DiagramTimelineSvgDrawer::make_diagram_write_to( return obj.qt2xy(tick - 1, 0, qubit); }, obj.max_tick + 2, - 24); + 24, + detector_slice_metadata); } // Make sure qubit lines/points are drawn first, so they are in the background. diff --git a/src/stim/diagram/timeline/timeline_svg_drawer.h b/src/stim/diagram/timeline/timeline_svg_drawer.h index cbc35bbe2..b1ee01706 100644 --- a/src/stim/diagram/timeline/timeline_svg_drawer.h +++ b/src/stim/diagram/timeline/timeline_svg_drawer.h @@ -65,7 +65,8 @@ struct DiagramTimelineSvgDrawer { uint64_t tick_slice_num, DiagramTimelineSvgDrawerMode mode, stim::SpanRef det_coord_filter, - size_t num_rows = 0); + size_t num_rows = 0, + DetectorSliceSvgMetadata *detector_slice_metadata = nullptr); void do_start_repeat(const CircuitTimelineLoopData &loop_data); void do_end_repeat(const CircuitTimelineLoopData &loop_data);