CDT++ 1.0.0
Causal Dynamical Triangulations in C++
Loading...
Searching...
No Matches
Utilities.hpp
Go to the documentation of this file.
1/*******************************************************************************
2 Causal Dynamical Triangulations in C++ using CGAL
3
4 Copyright © 2017 Adam Getchell
5 ******************************************************************************/
6
10
11#ifndef INCLUDE_UTILITIES_HPP_
12#define INCLUDE_UTILITIES_HPP_
13
14#include <CGAL/version.h>
15
16#include <algorithm>
17#include <array>
18#include <charconv>
19#include <cmath>
20#include <concepts>
21#include <cstdint>
22#include <filesystem>
23#include <fstream>
24#include <gsl/gsl>
25#include <iomanip>
26#include <limits>
27#include <locale>
28#include <map>
29#include <memory>
30#include <mutex>
31#include <optional>
32#include <random>
33#include <span>
34#include <sstream>
35#include <stdexcept>
36#include <string>
37#include <string_view>
38#include <type_traits>
39#include <utility>
40#include <vector>
41#include <version>
42// H. Hinnant date and time library
43#include <date/date.h>
44
45#ifdef _WIN32
46#ifndef NOMINMAX
47#define NOMINMAX
48#endif
49#include <windows.h>
50#endif
51
53// #include <format>
54
55// V. Zverovich {fmt} library
56#include <fmt/ostream.h>
57
58// G. Melman spdlog library
59#include <spdlog/sinks/basic_file_sink.h>
60#include <spdlog/sinks/stdout_color_sinks.h>
61#include <spdlog/spdlog.h>
62
63// Global project settings
64#include "Move_tracker.hpp"
65#include "Random.hpp"
66#include "Settings.hpp"
67#include "Version.hpp"
68
69namespace cdt
70{
73 enum class Topology
74 {
77 };
78
83 inline auto operator<<(std::ostream& t_os, Topology const& t_topology)
84 -> std::ostream&
85 {
86 switch (t_topology)
87 {
88 case Topology::SPHERICAL: return t_os << "spherical";
89 case Topology::TOROIDAL: return t_os << "toroidal";
90 default: return t_os << "none";
91 }
92 } // operator<<
93} // namespace cdt
94
95namespace cdt::utilities
96{
104
107 {
109 using Counts = std::array<Int_precision, move_tracker::NUMBER_OF_3D_MOVES>;
110
117
119 [[nodiscard]] auto operator==(Move_statistics const&) const noexcept
120 -> bool = default;
121 };
122
134 {
152 double initial_radius{};
154 std::optional<long double> alpha;
155 std::optional<long double> k;
156 std::optional<long double> lambda;
157 std::optional<Int_precision> configured_passes;
158 std::optional<Int_precision> configured_attempts;
159 std::optional<Int_precision> checkpoint_interval;
160 std::optional<Int_precision> completed_passes;
161 std::optional<std::uint64_t> max_threads;
162 std::optional<std::uint64_t> transition_trace;
163 std::optional<std::uint64_t> transition_count;
164 std::shared_ptr<std::string const>
166 std::optional<Move_statistics> move_statistics;
167 std::optional<std::uint64_t> placement_fingerprint;
168 std::optional<std::uint64_t> topology_fingerprint;
169 std::optional<ArtifactKind> input_artifact;
170 std::optional<cdt::RandomSeed> input_seed;
171 std::optional<cdt::RandomStream>
173 std::optional<std::uint64_t>
175 std::optional<std::uint64_t>
177 };
178
181 [[nodiscard]] inline auto metadata_filename(
182 std::filesystem::path const& payload) -> std::filesystem::path
183 {
184 auto metadata = payload;
185 metadata += ".meta";
186 return metadata;
187 }
188
189 namespace detail
190 {
191 inline constexpr std::string_view CAUSAL_INFO_HEADER{
192 "cdt-plusplus-causal-info-v2"};
193#if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \
194 CDT_ENABLE_PARALLEL_TRIANGULATION
195 inline constexpr bool PARALLEL_TRIANGULATION_ENABLED{true};
196#else
197 inline constexpr bool PARALLEL_TRIANGULATION_ENABLED{false};
198#endif
199
200 struct Payload_integrity
201 {
202 std::uint64_t size{};
203 std::uint64_t digest{};
204 };
205
206 [[nodiscard]] inline auto artifact_name(ArtifactKind const artifact)
207 -> std::string_view
208 {
209 switch (artifact)
210 {
211 case ArtifactKind::INITIAL_TRIANGULATION:
212 return "initial-triangulation";
213 case ArtifactKind::CHECKPOINT: return "checkpoint";
214 case ArtifactKind::FINAL_TRIANGULATION: return "final-triangulation";
215 }
216 return "unknown";
217 }
218
219 [[nodiscard]] inline auto standard_library_name() -> std::string
220 {
221#if defined(_LIBCPP_VERSION)
222 return fmt::format("libc++-{}", _LIBCPP_VERSION);
223#elif defined(__GLIBCXX__)
224 return fmt::format("libstdc++-{}", __GLIBCXX__);
225#elif defined(_MSVC_STL_VERSION)
226 return fmt::format("msvc-stl-{}", _MSVC_STL_VERSION);
227#else
228 return "unknown";
229#endif
230 }
231
232 [[nodiscard]] inline auto payload_integrity(
233 std::filesystem::path const& filename) -> Payload_integrity
234 {
235 std::ifstream input(filename, std::ios::in | std::ios::binary);
236 if (!input.is_open())
237 {
238 throw std::filesystem::filesystem_error(
239 "Could not open payload for integrity validation", filename,
240 std::make_error_code(std::errc::bad_file_descriptor));
241 }
242
243 std::uint64_t digest{14695981039346656037ULL};
244 std::uint64_t size{};
245 std::array<char, 8192> buffer{};
246 while (input)
247 {
248 input.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
249 auto const count = input.gcount();
250 for (std::streamsize index = 0; index < count; ++index)
251 {
252 digest ^= static_cast<unsigned char>(
253 buffer[static_cast<std::size_t>(index)]);
254 digest *= 1099511628211ULL;
255 }
256 size += static_cast<std::uint64_t>(count);
257 }
258 if (!input.eof())
259 {
260 throw std::filesystem::filesystem_error(
261 "Could not read payload for integrity validation", filename,
262 std::make_error_code(std::errc::io_error));
263 }
264 return {size, digest};
265 }
266
267 [[nodiscard]] inline auto point_key(auto const& point) -> std::string
268 {
269 return fmt::format("{:.17g},{:.17g},{:.17g}", CGAL::to_double(point.x()),
270 CGAL::to_double(point.y()),
271 CGAL::to_double(point.z()));
272 }
273
274 [[nodiscard]] inline auto point_cell_key(auto const& cell) -> std::string
275 {
276 std::array points{point_key(cell->vertex(0)->point()),
277 point_key(cell->vertex(1)->point()),
278 point_key(cell->vertex(2)->point()),
279 point_key(cell->vertex(3)->point())};
280 std::ranges::sort(points);
281 return fmt::format("{};{};{};{}", points[0], points[1], points[2],
282 points[3]);
283 }
284
285 template <typename Signature>
286 [[nodiscard]] auto canonical_colors(
287 std::vector<Signature> const& signatures) -> std::vector<std::size_t>
288 {
289 auto ordered = signatures;
290 std::ranges::sort(ordered);
291 ordered.erase(std::unique(ordered.begin(), ordered.end()), ordered.end());
292
293 std::vector<std::size_t> colors;
294 colors.reserve(signatures.size());
295 for (auto const& signature : signatures)
296 {
297 colors.push_back(static_cast<std::size_t>(
298 std::lower_bound(ordered.begin(), ordered.end(), signature) -
299 ordered.begin()));
300 }
301 return colors;
302 }
303
304 inline constexpr std::size_t CANONICAL_INCIDENCE_WORK_BUDGET{100'000};
305
306 inline void consume_canonical_incidence_work(std::size_t& budget)
307 {
308 if (budget == 0)
309 {
310 throw std::runtime_error{
311 "Canonical incidence search exceeded its work budget"};
312 }
313 --budget;
314 }
315
316 [[nodiscard]] inline auto refine_incidence_colors(
317 std::vector<std::vector<std::size_t>> const& adjacency,
318 std::vector<std::size_t> colors, std::size_t& budget)
319 -> std::vector<std::size_t>
320 {
321 for (std::size_t iteration = 0; iteration < adjacency.size(); ++iteration)
322 {
323 consume_canonical_incidence_work(budget);
324 std::vector<std::vector<std::size_t>> signatures;
325 signatures.reserve(adjacency.size());
326 for (std::size_t node = 0; node < adjacency.size(); ++node)
327 {
328 std::vector<std::size_t> neighboring_colors;
329 neighboring_colors.reserve(adjacency[node].size());
330 for (auto const neighbor : adjacency[node])
331 {
332 neighboring_colors.push_back(colors.at(neighbor));
333 }
334 std::ranges::sort(neighboring_colors);
335
336 std::vector<std::size_t> signature;
337 signature.reserve(neighboring_colors.size() + 1);
338 signature.push_back(colors[node]);
339 signature.insert(signature.end(), neighboring_colors.begin(),
340 neighboring_colors.end());
341 signatures.push_back(std::move(signature));
342 }
343
344 auto refined = canonical_colors(signatures);
345 if (refined == colors) { break; }
346 colors = std::move(refined);
347 }
348 return colors;
349 }
350
353 [[nodiscard]] inline auto incidence_records_for_coloring(
354 std::vector<std::string> const& bases,
355 std::vector<std::vector<std::size_t>> const& adjacency,
356 std::vector<std::size_t> const& colors) -> std::vector<std::string>
357 {
358 std::vector<std::size_t> nodes_by_color(colors.size());
359 for (std::size_t node = 0; node < colors.size(); ++node)
360 {
361 nodes_by_color.at(colors[node]) = node;
362 }
363
364 std::vector<std::string> records;
365 records.reserve(bases.size());
366 for (auto const node : nodes_by_color)
367 {
368 std::vector<std::size_t> neighboring_colors;
369 neighboring_colors.reserve(adjacency[node].size());
370 for (auto const neighbor : adjacency[node])
371 {
372 neighboring_colors.push_back(colors.at(neighbor));
373 }
374 std::ranges::sort(neighboring_colors);
375
376 auto record =
377 fmt::format("{}:{}:neighbors=", bases[node].size(), bases[node]);
378 for (auto const color : neighboring_colors)
379 {
380 record.append(std::to_string(color));
381 record.push_back(';');
382 }
383 records.push_back(std::move(record));
384 }
385 return records;
386 }
387
388 [[nodiscard]] inline auto canonical_incidence_search(
389 std::vector<std::string> const& bases,
390 std::vector<std::vector<std::size_t>> const& adjacency,
391 std::vector<std::size_t> colors, std::size_t& budget)
392 -> std::vector<std::string>
393 {
394 consume_canonical_incidence_work(budget);
395 colors = refine_incidence_colors(adjacency, std::move(colors), budget);
396
397 std::vector<std::size_t> color_counts(colors.size());
398 for (auto const color : colors) { ++color_counts.at(color); }
399 auto const ambiguous = std::ranges::find_if(
400 color_counts, [](std::size_t const count) { return count > 1; });
401 if (ambiguous == color_counts.end())
402 {
403 return incidence_records_for_coloring(bases, adjacency, colors);
404 }
405
406 auto const ambiguous_color =
407 static_cast<std::size_t>(ambiguous - color_counts.begin());
408 auto const individualized_color = *std::ranges::max_element(colors) + 1;
409 std::optional<std::vector<std::string>> best;
410 for (std::size_t node = 0; node < colors.size(); ++node)
411 {
412 if (colors[node] != ambiguous_color) { continue; }
413 auto individualized = colors;
414 individualized[node] = individualized_color;
415 auto candidate = canonical_incidence_search(
416 bases, adjacency, std::move(individualized), budget);
417 if (!best || candidate < *best) { best = std::move(candidate); }
418 }
419 return std::move(*best);
420 }
421
422 [[nodiscard]] inline auto canonical_bipartite_incidence_records(
423 std::vector<std::string> const& vertex_bases,
424 std::vector<std::string> const& cell_bases,
425 std::vector<std::vector<std::size_t>> const& cell_vertices)
426 -> std::vector<std::string>
427 {
428 if (cell_bases.size() != cell_vertices.size())
429 {
430 throw std::invalid_argument{
431 "Cell bases and incidence records must have equal sizes"};
432 }
433 for (auto const& incident_vertices : cell_vertices)
434 {
435 for (auto const vertex : incident_vertices)
436 {
437 if (vertex >= vertex_bases.size())
438 {
439 throw std::invalid_argument{
440 "Cell incidence records must reference known vertices"};
441 }
442 }
443 }
444
445 auto bases = vertex_bases;
446 bases.insert(bases.end(), cell_bases.begin(), cell_bases.end());
447 if (bases.empty()) { return {}; }
448 std::vector<std::vector<std::size_t>> adjacency(bases.size());
449 for (std::size_t cell = 0; cell < cell_vertices.size(); ++cell)
450 {
451 auto const cell_node = vertex_bases.size() + cell;
452 for (auto const vertex : cell_vertices[cell])
453 {
454 adjacency.at(vertex).push_back(cell_node);
455 adjacency[cell_node].push_back(vertex);
456 }
457 }
458 auto budget = CANONICAL_INCIDENCE_WORK_BUDGET;
459 return canonical_incidence_search(bases, adjacency,
460 canonical_colors(bases), budget);
461 }
462
463 template <typename TriangulationType>
464 inline constexpr bool HAS_CAUSAL_INFO =
465 requires(TriangulationType const& triangulation) {
466 triangulation.finite_vertex_handles();
467 triangulation.finite_cell_handles();
468 triangulation.number_of_vertices();
469 triangulation.number_of_finite_cells();
470 };
471
472 template <typename TriangulationType>
473 [[nodiscard]] auto vertex_records(TriangulationType const& triangulation)
474 -> std::vector<std::string>
475 {
476 std::vector<std::string> records;
477 records.reserve(
478 static_cast<std::size_t>(triangulation.number_of_vertices()));
479 for (auto const vertex : triangulation.finite_vertex_handles())
480 {
481 records.emplace_back(
482 fmt::format("v:{}:{}", point_key(vertex->point()), vertex->info()));
483 }
484 std::ranges::sort(records);
485 return records;
486 }
487
488 template <typename TriangulationType>
489 [[nodiscard]] auto has_coincident_vertices(
490 TriangulationType const& triangulation) -> bool
491 {
492 std::map<std::string, std::size_t> point_counts;
493 for (auto const vertex : triangulation.finite_vertex_handles())
494 {
495 if (++point_counts[point_key(vertex->point())] > 1) { return true; }
496 }
497 return false;
498 }
499
500 template <typename TriangulationType>
501 void require_distinct_evolution_coordinates(
502 TriangulationType const& triangulation,
503 std::filesystem::path const& payload,
504 std::filesystem::path const& sidecar, std::string_view const operation)
505 {
506 if (has_coincident_vertices(triangulation))
507 {
508 throw std::filesystem::filesystem_error(
509 fmt::format("{} requires distinct vertex coordinates", operation),
510 payload, sidecar, std::make_error_code(std::errc::not_supported));
511 }
512 }
513
514 template <typename TriangulationType>
515 [[nodiscard]] auto incidence_topology_records(
516 TriangulationType const& triangulation) -> std::vector<std::string>
517 {
518 auto const finite_vertices = triangulation.finite_vertex_handles();
519 using Vertex_handle =
520 std::remove_cvref_t<decltype(*finite_vertices.begin())>;
521 std::vector<Vertex_handle> vertices(finite_vertices.begin(),
522 finite_vertices.end());
523
524 auto const finite_cells = triangulation.finite_cell_handles();
525 using Cell_handle = std::remove_cvref_t<decltype(*finite_cells.begin())>;
526 std::vector<Cell_handle> cells(finite_cells.begin(), finite_cells.end());
527
528 std::map<Vertex_handle, std::size_t> vertex_indices;
529 std::vector<std::string> vertex_bases;
530 vertex_bases.reserve(vertices.size());
531 for (std::size_t index = 0; index < vertices.size(); ++index)
532 {
533 vertex_indices.emplace(vertices[index], index);
534 vertex_bases.emplace_back(
535 fmt::format("v:{}:{}", point_key(vertices[index]->point()),
536 vertices[index]->info()));
537 }
538
539 std::vector<std::string> cell_bases;
540 cell_bases.reserve(cells.size());
541 std::vector<std::vector<std::size_t>> cell_vertices(cells.size());
542 for (std::size_t cell_index = 0; cell_index < cells.size(); ++cell_index)
543 {
544 cell_bases.emplace_back(fmt::format("c:{}", cells[cell_index]->info()));
545 cell_vertices[cell_index].reserve(4);
546 for (std::size_t local_index = 0; local_index < 4; ++local_index)
547 {
548 auto const vertex_index = vertex_indices.at(
549 cells[cell_index]->vertex(static_cast<int>(local_index)));
550 cell_vertices[cell_index].push_back(vertex_index);
551 }
552 }
553 return canonical_bipartite_incidence_records(vertex_bases, cell_bases,
554 cell_vertices);
555 }
556
557 [[nodiscard]] inline auto fingerprint_records(
558 std::vector<std::string> const& records) -> std::uint64_t
559 {
560 std::uint64_t digest{14695981039346656037ULL};
561 for (auto const& record : records)
562 {
563 for (auto const byte : record)
564 {
565 digest ^= static_cast<unsigned char>(byte);
566 digest *= 1099511628211ULL;
567 }
568 digest ^= 0xFFU;
569 digest *= 1099511628211ULL;
570 }
571 return digest;
572 }
573
574 template <typename TriangulationType>
575 [[nodiscard]] auto canonical_placement_fingerprint(
576 TriangulationType const& triangulation) -> std::uint64_t
577 { return fingerprint_records(vertex_records(triangulation)); }
578
579 template <typename TriangulationType>
580 [[nodiscard]] auto canonical_topology_fingerprint(
581 TriangulationType const& triangulation) -> std::uint64_t
582 {
583 // topology.fnv1a64 uses different record schemes in these branches, so
584 // coincident-coordinate and point-keyed digests are not comparable.
585 if (has_coincident_vertices(triangulation))
586 {
587 return fingerprint_records(incidence_topology_records(triangulation));
588 }
589
590 auto records = vertex_records(triangulation);
591 records.reserve(
592 static_cast<std::size_t>(triangulation.number_of_vertices() +
593 triangulation.number_of_finite_cells()));
594 for (auto const cell : triangulation.finite_cell_handles())
595 {
596 records.emplace_back(
597 fmt::format("c:{}:{}", point_cell_key(cell), cell->info()));
598 }
599 std::ranges::sort(records);
600 return fingerprint_records(records);
601 }
602
603 template <typename TriangulationType>
604 void write_causal_info(std::ostream& output,
605 TriangulationType const& triangulation)
606 {
607 if constexpr (HAS_CAUSAL_INFO<TriangulationType>)
608 {
609 // CGAL writes and recreates vertices and cells in container order.
610 // Persist those payload indices so generic persistence can retain
611 // legacy or manually constructed geometrically coincident TDS state.
612 // CDT evolution boundaries reject that ambiguous locator state.
613 std::vector<std::string> vertices;
614 vertices.reserve(
615 static_cast<std::size_t>(triangulation.number_of_vertices()));
616 std::uint64_t vertex_index{};
617 for (auto const vertex : triangulation.finite_vertex_handles())
618 {
619 vertices.emplace_back(
620 fmt::format("{}|{}", vertex_index, vertex->info()));
621 ++vertex_index;
622 }
623
624 std::vector<std::string> cells;
625 cells.reserve(
626 static_cast<std::size_t>(triangulation.number_of_finite_cells()));
627 std::uint64_t cell_index{};
628 for (auto const cell : triangulation.finite_cell_handles())
629 {
630 cells.emplace_back(fmt::format("{}|{}", cell_index, cell->info()));
631 ++cell_index;
632 }
633
634 output << '\n' << CAUSAL_INFO_HEADER << '\n';
635 output << "vertices=" << vertices.size() << '\n';
636 for (auto const& record : vertices)
637 {
638 output << "v=" << record << '\n';
639 }
640 output << "cells=" << cells.size() << '\n';
641 for (auto const& record : cells) { output << "c=" << record << '\n'; }
642 }
643 }
644
645 [[nodiscard]] inline auto metadata_text(
646 Reproducibility_metadata const& metadata,
647 Payload_integrity const payload) -> std::string
648 {
649 auto const resume_supported =
650 metadata.artifact == ArtifactKind::CHECKPOINT &&
651 static_cast<bool>(metadata.transition_random_state);
652 if (metadata.transition_random_state && !metadata.move_statistics)
653 {
654 throw std::invalid_argument(
655 "Resumable checkpoint metadata requires cumulative move statistics.");
656 }
657 if (metadata.transition_random_state &&
658 metadata.artifact != ArtifactKind::CHECKPOINT)
659 {
660 throw std::invalid_argument(
661 "Only checkpoint artifacts may contain resumable PCG state.");
662 }
663 auto text = fmt::format(
664 "cdt-plusplus-metadata-v1\n"
665 "payload.size={}\n"
666 "payload.fnv1a64={:016x}\n"
667 "artifact={}\n"
668 "resume_supported={}\n"
669 "fresh_topology_replay_supported=false\n"
670 "transition_replay_requires_identical_start=true\n"
671 "cdt.version={}\n"
672 "source.revision={}\n"
673 "build.compiler_id={}\n"
674 "build.compiler_version={}\n"
675 "build.configuration={}\n"
676 "build.parallel_triangulation={}\n"
677 "build.system={}\n"
678 "build.processor={}\n"
679 "build.cxx_standard=23\n"
680 "build.standard_library={}\n"
681 "dependency.cgal_version={}\n"
682 "random.engine=pcg64\n"
683 "random.seed={}\n"
684 "random.initialization_stream={}\n"
685 "random.transition_stream={}\n"
686 "topology={}\n"
687 "dimension={}\n"
688 "desired.simplices={}\n"
689 "desired.timeslices={}\n"
690 "actual.vertices={}\n"
691 "actual.edges={}\n"
692 "actual.faces={}\n"
693 "actual.simplices={}\n"
694 "actual.minimum_timeslice={}\n"
695 "actual.maximum_timeslice={}\n"
696 "initial_radius={}\n"
697 "foliation_spacing={}\n",
698 payload.size, payload.digest, artifact_name(metadata.artifact),
699 resume_supported ? "true" : "false", cdt::VERSION,
700 cdt::SOURCE_REVISION, cdt::BUILD_COMPILER_ID,
701 cdt::BUILD_COMPILER_VERSION, cdt::BUILD_CONFIGURATION,
702 PARALLEL_TRIANGULATION_ENABLED ? "true" : "false",
703 cdt::BUILD_SYSTEM_NAME, cdt::BUILD_SYSTEM_PROCESSOR,
704 standard_library_name(), CGAL_VERSION_STR, metadata.seed,
705 metadata.initialization_stream, metadata.transition_stream,
706 metadata.topology == Topology::SPHERICAL ? "spherical" : "toroidal",
707 metadata.dimension, metadata.desired_simplices,
708 metadata.desired_timeslices, metadata.actual_vertices,
709 metadata.actual_edges, metadata.actual_faces,
710 metadata.actual_simplices, metadata.minimum_timeslice,
711 metadata.maximum_timeslice, metadata.initial_radius,
712 metadata.foliation_spacing);
713
714 auto append_optional = [&text](std::string_view const name,
715 auto const& value) {
716 if (value) { text += fmt::format("{}={}\n", name, *value); }
717 };
718 append_optional("alpha", metadata.alpha);
719 append_optional("k", metadata.k);
720 append_optional("lambda", metadata.lambda);
721 append_optional("configured_passes", metadata.configured_passes);
722 append_optional("configured_attempts", metadata.configured_attempts);
723 append_optional("checkpoint_interval", metadata.checkpoint_interval);
724 append_optional("completed_passes", metadata.completed_passes);
725 append_optional("parallel.max_threads", metadata.max_threads);
726 if (metadata.transition_random_state)
727 {
728 text += fmt::format("random.transition_state={}\n",
729 *metadata.transition_random_state);
730 }
731 if (metadata.transition_trace)
732 {
733 text += fmt::format("transition_trace.fnv1a64={:016x}\n",
734 *metadata.transition_trace);
735 }
736 append_optional("transition_trace.count", metadata.transition_count);
737 if (metadata.move_statistics)
738 {
739 auto const append_counts = [&text](
740 std::string_view const name,
741 Move_statistics::Counts const& values) {
742 text += fmt::format("{}={}", name, values.front());
743 for (std::size_t index = 1; index < values.size(); ++index)
744 {
745 text += fmt::format(",{}", values[index]);
746 }
747 text += '\n';
748 };
749 append_counts("moves.proposed", metadata.move_statistics->proposed);
750 append_counts("moves.accepted", metadata.move_statistics->accepted);
751 append_counts("moves.rejected", metadata.move_statistics->rejected);
752 append_counts("moves.attempted", metadata.move_statistics->attempted);
753 append_counts("moves.succeeded", metadata.move_statistics->succeeded);
754 append_counts("moves.failed", metadata.move_statistics->failed);
755 }
756 if (metadata.placement_fingerprint)
757 {
758 text += fmt::format("placement.fnv1a64={:016x}\n",
759 *metadata.placement_fingerprint);
760 }
761 if (metadata.topology_fingerprint)
762 {
763 text += fmt::format("topology.fnv1a64={:016x}\n",
764 *metadata.topology_fingerprint);
765 }
766 auto const input_field_count =
767 static_cast<int>(metadata.input_artifact.has_value()) +
768 static_cast<int>(metadata.input_seed.has_value()) +
769 static_cast<int>(metadata.input_initialization_stream.has_value()) +
770 static_cast<int>(metadata.input_placement_fingerprint.has_value()) +
771 static_cast<int>(metadata.input_topology_fingerprint.has_value());
772 if (input_field_count != 0 && input_field_count != 5)
773 {
774 throw std::invalid_argument(
775 "Input provenance must contain all five starting-artifact fields.");
776 }
777 if (input_field_count == 5)
778 {
779 if (*metadata.input_artifact != ArtifactKind::INITIAL_TRIANGULATION)
780 {
781 throw std::invalid_argument(
782 "Input provenance must identify an initial-triangulation "
783 "artifact.");
784 }
785 text += fmt::format(
786 "input.artifact={}\n"
787 "input.random.seed={}\n"
788 "input.random.initialization_stream={}\n"
789 "input.placement.fnv1a64={:016x}\n"
790 "input.topology.fnv1a64={:016x}\n",
791 artifact_name(*metadata.input_artifact), *metadata.input_seed,
792 *metadata.input_initialization_stream,
793 *metadata.input_placement_fingerprint,
794 *metadata.input_topology_fingerprint);
795 }
796 return text;
797 }
798
799 [[nodiscard]] inline auto parse_unsigned(std::string_view const text,
800 int const base,
801 std::filesystem::path const& path)
802 -> std::uint64_t
803 {
804 std::uint64_t value{};
805 auto const [end, error] =
806 std::from_chars(text.data(), text.data() + text.size(), value, base);
807 if (error != std::errc{} || end != text.data() + text.size())
808 {
809 throw std::filesystem::filesystem_error(
810 "Malformed persistence metadata", path,
811 std::make_error_code(std::errc::illegal_byte_sequence));
812 }
813 return value;
814 }
815
816 [[nodiscard]] inline auto parse_info(std::string_view const text,
817 std::filesystem::path const& path)
819 {
820 Int_precision value{};
821 auto const [end, error] =
822 std::from_chars(text.data(), text.data() + text.size(), value);
823 if (error != std::errc{} || end != text.data() + text.size())
824 {
825 throw std::filesystem::filesystem_error(
826 "Malformed causal triangulation metadata", path,
827 std::make_error_code(std::errc::illegal_byte_sequence));
828 }
829 return value;
830 }
831
832 [[nodiscard]] inline auto parse_metadata_integer(
833 std::string_view const text, std::filesystem::path const& path)
835 {
836 Int_precision value{};
837 auto const [end, error] =
838 std::from_chars(text.data(), text.data() + text.size(), value);
839 if (error != std::errc{} || end != text.data() + text.size())
840 {
841 throw std::filesystem::filesystem_error(
842 "Malformed persistence metadata", path,
843 std::make_error_code(std::errc::illegal_byte_sequence));
844 }
845 return value;
846 }
847
848 [[nodiscard]] inline auto parse_move_counts(
849 std::string_view text, std::filesystem::path const& path)
851 {
853 for (std::size_t index = 0; index < counts.size(); ++index)
854 {
855 auto const separator = text.find(',');
856 auto const is_last = index + 1 == counts.size();
857 if (text.empty() || (is_last && separator != std::string_view::npos) ||
858 (!is_last && separator == std::string_view::npos))
859 {
860 throw std::filesystem::filesystem_error(
861 "Persistence metadata has the wrong number of move counts", path,
862 std::make_error_code(std::errc::illegal_byte_sequence));
863 }
864 auto const token = is_last ? text : text.substr(0, separator);
865 counts[index] = parse_metadata_integer(token, path);
866 if (counts[index] < 0)
867 {
868 throw std::filesystem::filesystem_error(
869 "Persistence metadata contains a negative move count", path,
870 std::make_error_code(std::errc::illegal_byte_sequence));
871 }
872 if (is_last) { text = {}; }
873 else
874 {
875 text.remove_prefix(separator + 1);
876 }
877 }
878 return counts;
879 }
880
881 template <std::floating_point Float>
882 [[nodiscard]] auto parse_metadata_floating(
883 std::string_view const text, std::filesystem::path const& path) -> Float
884 {
885 Float value{};
886 std::istringstream input{std::string{text}};
887 input.imbue(std::locale::classic());
888 input >> value;
889 if (input.fail())
890 {
891 throw std::filesystem::filesystem_error(
892 "Malformed persistence metadata", path,
893 std::make_error_code(std::errc::illegal_byte_sequence));
894 }
895 input >> std::ws;
896 if (!input.eof() || !std::isfinite(value))
897 {
898 throw std::filesystem::filesystem_error(
899 "Malformed persistence metadata", path,
900 std::make_error_code(std::errc::illegal_byte_sequence));
901 }
902 return value;
903 }
904
905 [[nodiscard]] inline auto parse_record(std::string const& line,
906 std::string_view const prefix,
907 std::filesystem::path const& path)
908 -> std::pair<std::string, Int_precision>
909 {
910 if (!line.starts_with(prefix))
911 {
912 throw std::filesystem::filesystem_error(
913 "Malformed causal triangulation metadata", path,
914 std::make_error_code(std::errc::illegal_byte_sequence));
915 }
916 auto const record = std::string_view{line}.substr(prefix.size());
917 auto const separator = record.rfind('|');
918 if (separator == std::string_view::npos || separator == 0 ||
919 separator + 1 == record.size())
920 {
921 throw std::filesystem::filesystem_error(
922 "Malformed causal triangulation metadata", path,
923 std::make_error_code(std::errc::illegal_byte_sequence));
924 }
925 return {std::string{record.substr(0, separator)},
926 parse_info(record.substr(separator + 1), path)};
927 }
928
929 [[nodiscard]] inline auto parse_count_line(
930 std::string const& line, std::string_view const prefix,
931 std::filesystem::path const& path) -> std::uint64_t
932 {
933 if (!line.starts_with(prefix))
934 {
935 throw std::filesystem::filesystem_error(
936 "Malformed causal triangulation metadata", path,
937 std::make_error_code(std::errc::illegal_byte_sequence));
938 }
939 return parse_unsigned(std::string_view{line}.substr(prefix.size()), 10,
940 path);
941 }
942
943 [[nodiscard]] inline auto read_indexed_info(
944 std::istream& input, std::string_view const prefix,
945 std::uint64_t const count, std::filesystem::path const& path)
946 -> std::vector<Int_precision>
947 {
948 std::vector<std::optional<Int_precision>> indexed(
949 static_cast<std::size_t>(count));
950 std::string line;
951 for (std::uint64_t record_index = 0; record_index < count; ++record_index)
952 {
953 if (!std::getline(input, line))
954 {
955 throw std::filesystem::filesystem_error(
956 "Truncated causal triangulation metadata", path,
957 std::make_error_code(std::errc::illegal_byte_sequence));
958 }
959 auto const [key, value] = parse_record(line, prefix, path);
960 auto const index = parse_unsigned(key, 10, path);
961 if (index >= count || indexed[static_cast<std::size_t>(index)])
962 {
963 throw std::filesystem::filesystem_error(
964 "Duplicate or out-of-range causal metadata index", path,
965 std::make_error_code(std::errc::illegal_byte_sequence));
966 }
967 indexed[static_cast<std::size_t>(index)] = value;
968 }
969
970 std::vector<Int_precision> values;
971 values.reserve(indexed.size());
972 for (auto const& value : indexed)
973 {
974 if (!value)
975 {
976 throw std::filesystem::filesystem_error(
977 "Missing causal metadata index", path,
978 std::make_error_code(std::errc::illegal_byte_sequence));
979 }
980 values.push_back(*value);
981 }
982 return values;
983 }
984
985 template <typename TriangulationType>
986 void read_causal_info(std::istream& input, TriangulationType& triangulation,
987 std::filesystem::path const& path)
988 {
989 std::string line;
990 if (!std::getline(input, line) || line != CAUSAL_INFO_HEADER)
991 {
992 throw std::filesystem::filesystem_error(
993 "Unexpected trailing data after triangulation", path,
994 std::make_error_code(std::errc::illegal_byte_sequence));
995 }
996
997 if (!std::getline(input, line))
998 {
999 throw std::filesystem::filesystem_error(
1000 "Truncated causal triangulation metadata", path,
1001 std::make_error_code(std::errc::illegal_byte_sequence));
1002 }
1003 auto const vertex_count = parse_count_line(line, "vertices=", path);
1004 if (vertex_count !=
1005 static_cast<std::uint64_t>(triangulation.number_of_vertices()))
1006 {
1007 throw std::filesystem::filesystem_error(
1008 "Causal vertex metadata count does not match triangulation", path,
1009 std::make_error_code(std::errc::illegal_byte_sequence));
1010 }
1011
1012 auto const vertex_info =
1013 read_indexed_info(input, "v=", vertex_count, path);
1014
1015 if (!std::getline(input, line))
1016 {
1017 throw std::filesystem::filesystem_error(
1018 "Truncated causal triangulation metadata", path,
1019 std::make_error_code(std::errc::illegal_byte_sequence));
1020 }
1021 auto const cell_count = parse_count_line(line, "cells=", path);
1022 if (cell_count !=
1023 static_cast<std::uint64_t>(triangulation.number_of_finite_cells()))
1024 {
1025 throw std::filesystem::filesystem_error(
1026 "Causal cell metadata count does not match triangulation", path,
1027 std::make_error_code(std::errc::illegal_byte_sequence));
1028 }
1029
1030 auto const cell_info = read_indexed_info(input, "c=", cell_count, path);
1031
1032 std::size_t vertex_index{};
1033 for (auto const vertex : triangulation.finite_vertex_handles())
1034 {
1035 vertex->info() = vertex_info.at(vertex_index);
1036 ++vertex_index;
1037 }
1038 std::size_t cell_index{};
1039 for (auto const cell : triangulation.finite_cell_handles())
1040 {
1041 cell->info() = cell_info.at(cell_index);
1042 ++cell_index;
1043 }
1044 }
1045
1046 struct Parsed_persistence_metadata
1047 {
1048 Payload_integrity payload;
1049 ArtifactKind artifact;
1050 bool resume_supported;
1051 cdt::RandomSeed seed;
1052 cdt::RandomStream initialization_stream;
1053 cdt::RandomStream transition_stream;
1054 Topology topology;
1055 Int_precision dimension;
1056 Int_precision actual_vertices;
1057 Int_precision actual_edges;
1058 Int_precision actual_faces;
1059 Int_precision actual_simplices;
1060 Int_precision minimum_timeslice;
1061 Int_precision maximum_timeslice;
1062 std::optional<std::uint64_t> max_threads;
1063 std::uint64_t placement_fingerprint;
1064 std::uint64_t topology_fingerprint;
1065 Int_precision desired_simplices;
1066 Int_precision desired_timeslices;
1067 double initial_radius;
1068 double foliation_spacing;
1069 std::optional<long double> alpha;
1070 std::optional<long double> k;
1071 std::optional<long double> lambda;
1072 std::optional<Int_precision> configured_passes;
1073 std::optional<Int_precision> configured_attempts;
1074 std::optional<Int_precision> checkpoint_interval;
1075 std::optional<Int_precision> completed_passes;
1076 std::optional<std::uint64_t> transition_trace;
1077 std::optional<std::uint64_t> transition_count;
1078 std::optional<std::string> transition_random_state;
1079 std::optional<Move_statistics> move_statistics;
1080 std::optional<ArtifactKind> input_artifact;
1081 std::optional<cdt::RandomSeed> input_seed;
1082 std::optional<cdt::RandomStream> input_initialization_stream;
1083 std::optional<std::uint64_t> input_placement_fingerprint;
1084 std::optional<std::uint64_t> input_topology_fingerprint;
1085 };
1086
1087 [[nodiscard]] inline auto read_persistence_metadata(
1088 std::filesystem::path const& path) -> Parsed_persistence_metadata
1089 {
1090 std::ifstream input(path);
1091 if (!input.is_open())
1092 {
1093 throw std::filesystem::filesystem_error(
1094 "Could not open persistence metadata", path,
1095 std::make_error_code(std::errc::bad_file_descriptor));
1096 }
1097
1098 std::string line;
1099 if (!std::getline(input, line) || line != "cdt-plusplus-metadata-v1")
1100 {
1101 throw std::filesystem::filesystem_error(
1102 "Unsupported or malformed persistence metadata", path,
1103 std::make_error_code(std::errc::illegal_byte_sequence));
1104 }
1105
1106 std::map<std::string, std::string> values;
1107 while (std::getline(input, line))
1108 {
1109 auto const separator = line.find('=');
1110 if (separator == std::string::npos || separator == 0 ||
1111 separator + 1 == line.size())
1112 {
1113 throw std::filesystem::filesystem_error(
1114 "Malformed persistence metadata", path,
1115 std::make_error_code(std::errc::illegal_byte_sequence));
1116 }
1117 auto [unused, inserted] = values.emplace(line.substr(0, separator),
1118 line.substr(separator + 1));
1119 if (!inserted)
1120 {
1121 throw std::filesystem::filesystem_error(
1122 "Duplicate persistence metadata field", path,
1123 std::make_error_code(std::errc::illegal_byte_sequence));
1124 }
1125 }
1126 if (!input.eof())
1127 {
1128 throw std::filesystem::filesystem_error(
1129 "Could not read persistence metadata", path,
1130 std::make_error_code(std::errc::io_error));
1131 }
1132
1133 for (auto const required : {"payload.size",
1134 "payload.fnv1a64",
1135 "artifact",
1136 "resume_supported",
1137 "fresh_topology_replay_supported",
1138 "transition_replay_requires_identical_start",
1139 "cdt.version",
1140 "build.compiler_id",
1141 "build.compiler_version",
1142 "build.configuration",
1143 "build.system",
1144 "build.processor",
1145 "build.cxx_standard",
1146 "build.standard_library",
1147 "dependency.cgal_version",
1148 "random.engine",
1149 "random.seed",
1150 "random.initialization_stream",
1151 "random.transition_stream",
1152 "topology",
1153 "dimension",
1154 "desired.simplices",
1155 "desired.timeslices",
1156 "actual.vertices",
1157 "actual.edges",
1158 "actual.faces",
1159 "actual.simplices",
1160 "actual.minimum_timeslice",
1161 "actual.maximum_timeslice",
1162 "initial_radius",
1163 "foliation_spacing",
1164 "placement.fnv1a64",
1165 "topology.fnv1a64"})
1166 {
1167 if (!values.contains(required))
1168 {
1169 throw std::filesystem::filesystem_error(
1170 "Persistence metadata is missing a required field", path,
1171 std::make_error_code(std::errc::illegal_byte_sequence));
1172 }
1173 }
1174 bool resume_supported{};
1175 if (values.at("resume_supported") == "true") { resume_supported = true; }
1176 else if (values.at("resume_supported") != "false")
1177 {
1178 throw std::filesystem::filesystem_error(
1179 "Persistence metadata has an invalid resume contract", path,
1180 std::make_error_code(std::errc::illegal_byte_sequence));
1181 }
1182 if (values.at("fresh_topology_replay_supported") != "false" ||
1183 values.at("transition_replay_requires_identical_start") != "true")
1184 {
1185 throw std::filesystem::filesystem_error(
1186 "Unsupported persistence replay contract", path,
1187 std::make_error_code(std::errc::not_supported));
1188 }
1189 if (values.at("random.engine") != "pcg64" ||
1190 values.at("build.cxx_standard") != "23")
1191 {
1192 throw std::filesystem::filesystem_error(
1193 "Unsupported persistence metadata", path,
1194 std::make_error_code(std::errc::not_supported));
1195 }
1196
1197 auto const parse_artifact = [&path](std::string_view const value) {
1198 if (value == "initial-triangulation")
1199 {
1200 return ArtifactKind::INITIAL_TRIANGULATION;
1201 }
1202 if (value == "checkpoint") { return ArtifactKind::CHECKPOINT; }
1203 if (value == "final-triangulation")
1204 {
1205 return ArtifactKind::FINAL_TRIANGULATION;
1206 }
1207 throw std::filesystem::filesystem_error(
1208 "Persistence metadata has an unknown artifact kind", path,
1209 std::make_error_code(std::errc::illegal_byte_sequence));
1210 };
1211 auto const artifact = parse_artifact(values.at("artifact"));
1212
1213 Topology topology{};
1214 if (values.at("topology") == "spherical")
1215 {
1216 topology = Topology::SPHERICAL;
1217 }
1218 else if (values.at("topology") == "toroidal")
1219 {
1220 topology = Topology::TOROIDAL;
1221 }
1222 else
1223 {
1224 throw std::filesystem::filesystem_error(
1225 "Persistence metadata has an unknown topology", path,
1226 std::make_error_code(std::errc::illegal_byte_sequence));
1227 }
1228
1229 auto const parse_integer_field = [&](std::string const& name) {
1230 return parse_metadata_integer(values.at(name), path);
1231 };
1232 auto const dimension = parse_integer_field("dimension");
1233 auto const desired_simplices = parse_integer_field("desired.simplices");
1234 auto const desired_timeslices = parse_integer_field("desired.timeslices");
1235 auto const actual_vertices = parse_integer_field("actual.vertices");
1236 auto const actual_edges = parse_integer_field("actual.edges");
1237 auto const actual_faces = parse_integer_field("actual.faces");
1238 auto const actual_simplices = parse_integer_field("actual.simplices");
1239 auto const minimum_timeslice =
1240 parse_integer_field("actual.minimum_timeslice");
1241 auto const maximum_timeslice =
1242 parse_integer_field("actual.maximum_timeslice");
1243 if (dimension <= 0 || desired_simplices < 0 || desired_timeslices < 0 ||
1244 actual_vertices < 0 || actual_edges < 0 || actual_faces < 0 ||
1245 actual_simplices < 0 || minimum_timeslice > maximum_timeslice)
1246 {
1247 throw std::filesystem::filesystem_error(
1248 "Persistence metadata contains invalid state dimensions", path,
1249 std::make_error_code(std::errc::illegal_byte_sequence));
1250 }
1251
1252 auto const initial_radius =
1253 parse_metadata_floating<double>(values.at("initial_radius"), path);
1254 auto const foliation_spacing =
1255 parse_metadata_floating<double>(values.at("foliation_spacing"), path);
1256 if (initial_radius < 0.0 || foliation_spacing <= 0.0)
1257 {
1258 throw std::filesystem::filesystem_error(
1259 "Persistence metadata contains invalid foliation parameters", path,
1260 std::make_error_code(std::errc::illegal_byte_sequence));
1261 }
1262
1263 auto const action_field_count =
1264 static_cast<int>(values.contains("alpha")) +
1265 static_cast<int>(values.contains("k")) +
1266 static_cast<int>(values.contains("lambda"));
1267 if (action_field_count != 0 && action_field_count != 3)
1268 {
1269 throw std::filesystem::filesystem_error(
1270 "Persistence metadata has an incomplete action parameter set", path,
1271 std::make_error_code(std::errc::illegal_byte_sequence));
1272 }
1273 std::optional<long double> alpha;
1274 std::optional<long double> k;
1275 std::optional<long double> lambda;
1276 if (action_field_count == 3)
1277 {
1278 alpha = parse_metadata_floating<long double>(values.at("alpha"), path);
1279 k = parse_metadata_floating<long double>(values.at("k"), path);
1280 lambda =
1281 parse_metadata_floating<long double>(values.at("lambda"), path);
1282 if (*alpha <= 0.5L)
1283 {
1284 throw std::filesystem::filesystem_error(
1285 "Persistence metadata contains an invalid alpha", path,
1286 std::make_error_code(std::errc::illegal_byte_sequence));
1287 }
1288 }
1289
1290 auto const run_field_count =
1291 static_cast<int>(values.contains("configured_passes")) +
1292 static_cast<int>(values.contains("checkpoint_interval"));
1293 if (run_field_count != 0 && run_field_count != 2)
1294 {
1295 throw std::filesystem::filesystem_error(
1296 "Persistence metadata has an incomplete run configuration", path,
1297 std::make_error_code(std::errc::illegal_byte_sequence));
1298 }
1299 std::optional<Int_precision> configured_passes;
1300 std::optional<Int_precision> checkpoint_interval;
1301 if (run_field_count == 2)
1302 {
1303 configured_passes = parse_integer_field("configured_passes");
1304 checkpoint_interval = parse_integer_field("checkpoint_interval");
1305 if (*configured_passes <= 0 || *checkpoint_interval <= 0)
1306 {
1307 throw std::filesystem::filesystem_error(
1308 "Persistence metadata contains an invalid run configuration",
1309 path, std::make_error_code(std::errc::illegal_byte_sequence));
1310 }
1311 }
1312 std::optional<Int_precision> configured_attempts;
1313 if (values.contains("configured_attempts"))
1314 {
1315 configured_attempts = parse_integer_field("configured_attempts");
1316 if (*configured_attempts <= 0)
1317 {
1318 throw std::filesystem::filesystem_error(
1319 "Persistence metadata contains invalid configured attempts", path,
1320 std::make_error_code(std::errc::illegal_byte_sequence));
1321 }
1322 }
1323
1324 if (artifact == ArtifactKind::CHECKPOINT &&
1325 !values.contains("completed_passes"))
1326 {
1327 throw std::filesystem::filesystem_error(
1328 "Checkpoint metadata is missing completed passes", path,
1329 std::make_error_code(std::errc::illegal_byte_sequence));
1330 }
1331 std::optional<Int_precision> completed_passes;
1332 if (values.contains("completed_passes"))
1333 {
1334 completed_passes = parse_integer_field("completed_passes");
1335 if (*completed_passes < 0)
1336 {
1337 throw std::filesystem::filesystem_error(
1338 "Persistence metadata contains invalid completed passes", path,
1339 std::make_error_code(std::errc::illegal_byte_sequence));
1340 }
1341 }
1342 std::optional<std::uint64_t> max_threads;
1343 if (auto const field = values.find("parallel.max_threads");
1344 field != values.end())
1345 {
1346 max_threads = parse_unsigned(field->second, 10, path);
1347 if (*max_threads == 0)
1348 {
1349 throw std::filesystem::filesystem_error(
1350 "Persistence metadata contains an invalid thread limit", path,
1351 std::make_error_code(std::errc::illegal_byte_sequence));
1352 }
1353 }
1354
1355 auto const transition_field_count =
1356 static_cast<int>(values.contains("transition_trace.fnv1a64")) +
1357 static_cast<int>(values.contains("transition_trace.count"));
1358 if (transition_field_count != 0 && transition_field_count != 2)
1359 {
1360 throw std::filesystem::filesystem_error(
1361 "Persistence metadata has an incomplete transition trace", path,
1362 std::make_error_code(std::errc::illegal_byte_sequence));
1363 }
1364 std::optional<std::uint64_t> transition_trace;
1365 std::optional<std::uint64_t> transition_count;
1366 if (transition_field_count == 2)
1367 {
1368 transition_trace =
1369 parse_unsigned(values.at("transition_trace.fnv1a64"), 16, path);
1370 transition_count =
1371 parse_unsigned(values.at("transition_trace.count"), 10, path);
1372 }
1373
1374 auto const move_field_count =
1375 static_cast<int>(values.contains("moves.proposed")) +
1376 static_cast<int>(values.contains("moves.accepted")) +
1377 static_cast<int>(values.contains("moves.rejected")) +
1378 static_cast<int>(values.contains("moves.attempted")) +
1379 static_cast<int>(values.contains("moves.succeeded")) +
1380 static_cast<int>(values.contains("moves.failed"));
1381 if (move_field_count != 0 && move_field_count != 6)
1382 {
1383 throw std::filesystem::filesystem_error(
1384 "Persistence metadata has incomplete move statistics", path,
1385 std::make_error_code(std::errc::illegal_byte_sequence));
1386 }
1387 std::optional<Move_statistics> move_statistics;
1388 if (move_field_count == 6)
1389 {
1390 move_statistics = Move_statistics{
1391 .proposed = parse_move_counts(values.at("moves.proposed"), path),
1392 .accepted = parse_move_counts(values.at("moves.accepted"), path),
1393 .rejected = parse_move_counts(values.at("moves.rejected"), path),
1394 .attempted = parse_move_counts(values.at("moves.attempted"), path),
1395 .succeeded = parse_move_counts(values.at("moves.succeeded"), path),
1396 .failed = parse_move_counts(values.at("moves.failed"), path)};
1397 std::uint64_t proposed_total{};
1398 for (std::size_t index = 0; index < move_statistics->proposed.size();
1399 ++index)
1400 {
1401 auto const proposed =
1402 static_cast<std::uint64_t>(move_statistics->proposed[index]);
1403 auto const accepted =
1404 static_cast<std::uint64_t>(move_statistics->accepted[index]);
1405 auto const rejected =
1406 static_cast<std::uint64_t>(move_statistics->rejected[index]);
1407 auto const attempted =
1408 static_cast<std::uint64_t>(move_statistics->attempted[index]);
1409 auto const succeeded =
1410 static_cast<std::uint64_t>(move_statistics->succeeded[index]);
1411 auto const failed =
1412 static_cast<std::uint64_t>(move_statistics->failed[index]);
1413 if (proposed != accepted + rejected || proposed != attempted ||
1414 attempted != succeeded + failed)
1415 {
1416 throw std::filesystem::filesystem_error(
1417 "Persistence move statistics violate accounting invariants",
1418 path, std::make_error_code(std::errc::illegal_byte_sequence));
1419 }
1420 if (proposed_total >
1421 std::numeric_limits<std::uint64_t>::max() - proposed)
1422 {
1423 throw std::filesystem::filesystem_error(
1424 "Persistence transition count exceeds the supported range",
1425 path, std::make_error_code(std::errc::value_too_large));
1426 }
1427 proposed_total += proposed;
1428 }
1429 if (transition_count && proposed_total != *transition_count)
1430 {
1431 throw std::filesystem::filesystem_error(
1432 "Persistence move statistics do not match the transition count",
1433 path, std::make_error_code(std::errc::illegal_byte_sequence));
1434 }
1435 }
1436
1437 std::optional<std::string> transition_random_state;
1438 if (auto const field = values.find("random.transition_state");
1439 field != values.end())
1440 {
1441 transition_random_state = field->second;
1442 }
1443 if (resume_supported)
1444 {
1445 auto const complete_resume_state =
1446 artifact == ArtifactKind::CHECKPOINT && alpha && k && lambda &&
1447 configured_passes && checkpoint_interval && completed_passes &&
1448 max_threads && transition_trace && transition_count &&
1449 transition_random_state && move_statistics;
1450 if (!complete_resume_state || *completed_passes > *configured_passes)
1451 {
1452 throw std::filesystem::filesystem_error(
1453 "Resumable checkpoint metadata is incomplete or inconsistent",
1454 path, std::make_error_code(std::errc::illegal_byte_sequence));
1455 }
1456 auto const producer_matches =
1457 values.at("cdt.version") == cdt::VERSION &&
1458 values.contains("source.revision") &&
1459 values.at("source.revision") == cdt::SOURCE_REVISION &&
1460 values.at("build.compiler_id") == cdt::BUILD_COMPILER_ID &&
1461 values.at("build.compiler_version") ==
1462 cdt::BUILD_COMPILER_VERSION &&
1463 values.at("build.configuration") == cdt::BUILD_CONFIGURATION &&
1464 values.contains("build.parallel_triangulation") &&
1465 values.at("build.parallel_triangulation") ==
1466 (PARALLEL_TRIANGULATION_ENABLED ? "true" : "false") &&
1467 values.at("build.system") == cdt::BUILD_SYSTEM_NAME &&
1468 values.at("build.processor") == cdt::BUILD_SYSTEM_PROCESSOR &&
1469 values.at("build.standard_library") == standard_library_name() &&
1470 values.at("dependency.cgal_version") == CGAL_VERSION_STR;
1471 if (!producer_matches)
1472 {
1473 throw std::filesystem::filesystem_error(
1474 "Exact checkpoint resume requires the recorded producer toolchain",
1475 path, std::make_error_code(std::errc::not_supported));
1476 }
1477 try
1478 {
1479 static_cast<void>(cdt::Random::from_serialized_state(
1480 cdt::RandomSeed{
1481 parse_unsigned(values.at("random.seed"), 10, path)},
1482 cdt::RandomStream{parse_unsigned(
1483 values.at("random.transition_stream"), 10, path)},
1484 *transition_random_state));
1485 }
1486 catch (std::invalid_argument const&)
1487 {
1488 throw std::filesystem::filesystem_error(
1489 "Resumable checkpoint contains invalid PCG state", path,
1490 std::make_error_code(std::errc::illegal_byte_sequence));
1491 }
1492 }
1493 else if (transition_random_state)
1494 {
1495 throw std::filesystem::filesystem_error(
1496 "Snapshot metadata contains PCG state without resume support", path,
1497 std::make_error_code(std::errc::illegal_byte_sequence));
1498 }
1499
1500 auto const input_field_count =
1501 static_cast<int>(values.contains("input.artifact")) +
1502 static_cast<int>(values.contains("input.random.seed")) +
1503 static_cast<int>(
1504 values.contains("input.random.initialization_stream")) +
1505 static_cast<int>(values.contains("input.placement.fnv1a64")) +
1506 static_cast<int>(values.contains("input.topology.fnv1a64"));
1507 if (input_field_count != 0 && input_field_count != 5)
1508 {
1509 throw std::filesystem::filesystem_error(
1510 "Persistence metadata has incomplete input provenance", path,
1511 std::make_error_code(std::errc::illegal_byte_sequence));
1512 }
1513 std::optional<ArtifactKind> input_artifact;
1514 std::optional<cdt::RandomSeed> input_seed;
1515 std::optional<cdt::RandomStream> input_initialization_stream;
1516 std::optional<std::uint64_t> input_placement_fingerprint;
1517 std::optional<std::uint64_t> input_topology_fingerprint;
1518 if (input_field_count == 5)
1519 {
1520 input_artifact = parse_artifact(values.at("input.artifact"));
1521 if (*input_artifact != ArtifactKind::INITIAL_TRIANGULATION)
1522 {
1523 throw std::filesystem::filesystem_error(
1524 "Persistence input provenance must identify an "
1525 "initial-triangulation artifact",
1526 path, std::make_error_code(std::errc::illegal_byte_sequence));
1527 }
1528 input_seed = cdt::RandomSeed{
1529 parse_unsigned(values.at("input.random.seed"), 10, path)};
1530 input_initialization_stream = cdt::RandomStream{parse_unsigned(
1531 values.at("input.random.initialization_stream"), 10, path)};
1532 input_placement_fingerprint =
1533 parse_unsigned(values.at("input.placement.fnv1a64"), 16, path);
1534 input_topology_fingerprint =
1535 parse_unsigned(values.at("input.topology.fnv1a64"), 16, path);
1536 }
1537
1538 return {
1539 .payload = {parse_unsigned(values.at("payload.size"), 10, path),
1540 parse_unsigned(values.at("payload.fnv1a64"), 16, path)},
1541 .artifact = artifact,
1542 .resume_supported = resume_supported,
1543 .seed = cdt::RandomSeed{parse_unsigned(values.at("random.seed"), 10,
1544 path)},
1545 .initialization_stream = cdt::RandomStream{parse_unsigned(
1546 values.at("random.initialization_stream"), 10, path)},
1547 .transition_stream = cdt::RandomStream{parse_unsigned(
1548 values.at("random.transition_stream"), 10, path)},
1549 .topology = topology,
1550 .dimension = dimension,
1551 .actual_vertices = actual_vertices,
1552 .actual_edges = actual_edges,
1553 .actual_faces = actual_faces,
1554 .actual_simplices = actual_simplices,
1555 .minimum_timeslice = minimum_timeslice,
1556 .maximum_timeslice = maximum_timeslice,
1557 .max_threads = max_threads,
1558 .placement_fingerprint =
1559 parse_unsigned(values.at("placement.fnv1a64"), 16, path),
1560 .topology_fingerprint =
1561 parse_unsigned(values.at("topology.fnv1a64"), 16, path),
1562 .desired_simplices = desired_simplices,
1563 .desired_timeslices = desired_timeslices,
1564 .initial_radius = initial_radius,
1565 .foliation_spacing = foliation_spacing,
1566 .alpha = alpha,
1567 .k = k,
1568 .lambda = lambda,
1569 .configured_passes = configured_passes,
1570 .configured_attempts = configured_attempts,
1571 .checkpoint_interval = checkpoint_interval,
1572 .completed_passes = completed_passes,
1573 .transition_trace = transition_trace,
1574 .transition_count = transition_count,
1575 .transition_random_state = transition_random_state,
1576 .move_statistics = move_statistics,
1577 .input_artifact = input_artifact,
1578 .input_seed = input_seed,
1579 .input_initialization_stream = input_initialization_stream,
1580 .input_placement_fingerprint = input_placement_fingerprint,
1581 .input_topology_fingerprint = input_topology_fingerprint
1582 };
1583 }
1584
1585 [[nodiscard]] inline auto to_reproducibility_metadata(
1586 Parsed_persistence_metadata const& source) -> Reproducibility_metadata
1587 {
1588 return {
1589 .artifact = source.artifact,
1590 .seed = source.seed,
1591 .initialization_stream = source.initialization_stream,
1592 .transition_stream = source.transition_stream,
1593 .topology = source.topology,
1594 .dimension = source.dimension,
1595 .desired_simplices = source.desired_simplices,
1596 .desired_timeslices = source.desired_timeslices,
1597 .actual_vertices = source.actual_vertices,
1598 .actual_edges = source.actual_edges,
1599 .actual_faces = source.actual_faces,
1600 .actual_simplices = source.actual_simplices,
1601 .minimum_timeslice = source.minimum_timeslice,
1602 .maximum_timeslice = source.maximum_timeslice,
1603 .initial_radius = source.initial_radius,
1604 .foliation_spacing = source.foliation_spacing,
1605 .alpha = source.alpha,
1606 .k = source.k,
1607 .lambda = source.lambda,
1608 .configured_passes = source.configured_passes,
1609 .configured_attempts = source.configured_attempts,
1610 .checkpoint_interval = source.checkpoint_interval,
1611 .completed_passes = source.completed_passes,
1612 .max_threads = source.max_threads,
1613 .transition_trace = source.transition_trace,
1614 .transition_count = source.transition_count,
1615 .transition_random_state = source.transition_random_state
1616 ? std::make_shared<std::string const>(
1617 *source.transition_random_state)
1618 : nullptr,
1619 .move_statistics = source.move_statistics,
1620 .placement_fingerprint = source.placement_fingerprint,
1621 .topology_fingerprint = source.topology_fingerprint,
1622 .input_artifact = source.input_artifact,
1623 .input_seed = source.input_seed,
1624 .input_initialization_stream = source.input_initialization_stream,
1625 .input_placement_fingerprint = source.input_placement_fingerprint,
1626 .input_topology_fingerprint = source.input_topology_fingerprint};
1627 }
1628
1629 [[nodiscard]] inline auto validate_payload_integrity(
1630 std::filesystem::path const& payload)
1631 -> std::optional<Parsed_persistence_metadata>
1632 {
1633 auto const metadata = metadata_filename(payload);
1634 if (!std::filesystem::exists(metadata)) { return std::nullopt; }
1635 auto const expected = read_persistence_metadata(metadata);
1636 auto const actual = payload_integrity(payload);
1637 if (expected.payload.size != actual.size ||
1638 expected.payload.digest != actual.digest)
1639 {
1640 throw std::filesystem::filesystem_error(
1641 "Triangulation payload does not match its persistence metadata",
1642 payload, metadata,
1643 std::make_error_code(std::errc::illegal_byte_sequence));
1644 }
1645 return expected;
1646 }
1647
1648 [[nodiscard]] inline auto write_file_mutex() -> std::mutex&
1649 {
1650 static std::mutex mutex;
1651 return mutex;
1652 }
1653
1654 [[nodiscard]] inline auto write_file_active() noexcept -> bool&
1655 {
1656 static thread_local bool active{false};
1657 return active;
1658 }
1659
1660 class WriteFileOperation final
1661 {
1662 public:
1663 WriteFileOperation()
1664 {
1665 if (write_file_active())
1666 {
1667 throw std::logic_error("write_file is not reentrant.");
1668 }
1669 write_file_active() = true;
1670 }
1671
1672 ~WriteFileOperation() noexcept { write_file_active() = false; }
1673
1674 WriteFileOperation(WriteFileOperation const&) = delete;
1675 auto operator=(WriteFileOperation const&) -> WriteFileOperation& = delete;
1676 WriteFileOperation(WriteFileOperation&&) = delete;
1677 auto operator=(WriteFileOperation&&) -> WriteFileOperation& = delete;
1678 };
1679
1680 inline void replace_file(std::filesystem::path const& temporary,
1681 std::filesystem::path const& destination)
1682 {
1683#ifdef _WIN32
1684 if (!::MoveFileExW(temporary.c_str(), destination.c_str(),
1685 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
1686 {
1687 throw std::filesystem::filesystem_error(
1688 "Could not atomically replace file", temporary, destination,
1689 std::error_code(static_cast<int>(::GetLastError()),
1690 std::system_category()));
1691 }
1692#else
1693 std::error_code error;
1694 std::filesystem::rename(temporary, destination, error);
1695 if (error)
1696 {
1697 throw std::filesystem::filesystem_error(
1698 "Could not atomically replace file", temporary, destination, error);
1699 }
1700#endif
1701 }
1702
1703 template <typename TriangulationType>
1704 [[nodiscard]] auto parse_payload(std::filesystem::path const& filename)
1705 -> TriangulationType
1706 {
1707 std::ifstream file(filename, std::ios::in);
1708 if (!file.is_open())
1709 {
1710 throw std::filesystem::filesystem_error(
1711 "Could not open file for reading", filename,
1712 std::make_error_code(std::errc::bad_file_descriptor));
1713 }
1714 TriangulationType triangulation;
1715 file >> triangulation;
1716 if (!file)
1717 {
1718 throw std::filesystem::filesystem_error(
1719 "Could not parse triangulation", filename,
1720 std::make_error_code(std::errc::illegal_byte_sequence));
1721 }
1722 file >> std::ws;
1723 if (!file.eof())
1724 {
1725 if constexpr (HAS_CAUSAL_INFO<TriangulationType>)
1726 {
1727 read_causal_info(file, triangulation, filename);
1728 file >> std::ws;
1729 }
1730 }
1731 if (!file.eof())
1732 {
1733 throw std::filesystem::filesystem_error(
1734 "Unexpected trailing data after triangulation", filename,
1735 std::make_error_code(std::errc::illegal_byte_sequence));
1736 }
1737 if constexpr (requires(TriangulationType const& value) {
1738 { value.tds().is_valid() } -> std::convertible_to<bool>;
1739 })
1740 {
1741 if (!triangulation.tds().is_valid())
1742 {
1743 throw std::filesystem::filesystem_error(
1744 "Parsed triangulation data structure failed its integrity check",
1745 filename, std::make_error_code(std::errc::illegal_byte_sequence));
1746 }
1747 }
1748 // Evolved CDT states are valid abstract causal triangulations but need
1749 // not satisfy the Euclidean Delaunay empty-sphere property. For CGAL
1750 // triangulations the TDS check above is therefore the authoritative
1751 // integrity check. Use a type's broader validator only when it does not
1752 // expose a distinct triangulation data structure.
1753 if constexpr (
1754 !requires(TriangulationType const& value) {
1755 { value.tds().is_valid() } -> std::convertible_to<bool>;
1756 } &&
1757 requires(TriangulationType const& value) {
1758 { value.is_valid() } -> std::convertible_to<bool>;
1759 })
1760 {
1761 if (!triangulation.is_valid())
1762 {
1763 throw std::filesystem::filesystem_error(
1764 "Parsed triangulation failed its integrity check", filename,
1765 std::make_error_code(std::errc::illegal_byte_sequence));
1766 }
1767 }
1768 return triangulation;
1769 }
1770
1771 template <typename TriangulationType>
1772 void reconcile_payload_metadata(Reproducibility_metadata& metadata,
1773 TriangulationType const& triangulation)
1774 {
1775 if constexpr (requires(TriangulationType const& value) {
1776 value.dimension();
1777 value.number_of_vertices();
1778 value.number_of_finite_edges();
1779 value.number_of_finite_facets();
1780 value.number_of_finite_cells();
1781 })
1782 {
1783 metadata.dimension =
1784 gsl::narrow<Int_precision>(triangulation.dimension());
1785 metadata.actual_vertices =
1786 gsl::narrow<Int_precision>(triangulation.number_of_vertices());
1787 metadata.actual_edges =
1788 gsl::narrow<Int_precision>(triangulation.number_of_finite_edges());
1789 metadata.actual_faces =
1790 gsl::narrow<Int_precision>(triangulation.number_of_finite_facets());
1791 metadata.actual_simplices =
1792 gsl::narrow<Int_precision>(triangulation.number_of_finite_cells());
1793 }
1794 if constexpr (HAS_CAUSAL_INFO<TriangulationType>)
1795 {
1796 if (triangulation.number_of_vertices() == 0)
1797 {
1798 metadata.minimum_timeslice = 0;
1799 metadata.maximum_timeslice = 0;
1800 }
1801 else
1802 {
1803 auto const vertices = triangulation.finite_vertex_handles();
1804 auto const first = vertices.begin();
1805 auto minimum_timeslice = static_cast<Int_precision>((*first)->info());
1806 auto maximum_timeslice = minimum_timeslice;
1807 for (auto const vertex : vertices)
1808 {
1809 auto const time = static_cast<Int_precision>(vertex->info());
1810 minimum_timeslice = std::min(minimum_timeslice, time);
1811 maximum_timeslice = std::max(maximum_timeslice, time);
1812 }
1813 metadata.minimum_timeslice = minimum_timeslice;
1814 metadata.maximum_timeslice = maximum_timeslice;
1815 }
1816 metadata.placement_fingerprint =
1817 canonical_placement_fingerprint(triangulation);
1818 metadata.topology_fingerprint =
1819 canonical_topology_fingerprint(triangulation);
1820 }
1821 }
1822
1823 template <typename TriangulationType>
1824 void validate_persistence_metadata(
1825 Parsed_persistence_metadata const& metadata,
1826 TriangulationType const& triangulation,
1827 std::filesystem::path const& payload_path,
1828 std::filesystem::path const& metadata_path)
1829 {
1830 Reproducibility_metadata derived;
1831 reconcile_payload_metadata(derived, triangulation);
1832 auto const state_matches =
1833 metadata.dimension == derived.dimension &&
1834 metadata.actual_vertices == derived.actual_vertices &&
1835 metadata.actual_edges == derived.actual_edges &&
1836 metadata.actual_faces == derived.actual_faces &&
1837 metadata.actual_simplices == derived.actual_simplices &&
1838 metadata.minimum_timeslice == derived.minimum_timeslice &&
1839 metadata.maximum_timeslice == derived.maximum_timeslice &&
1840 derived.placement_fingerprint && derived.topology_fingerprint &&
1841 metadata.placement_fingerprint == *derived.placement_fingerprint &&
1842 metadata.topology_fingerprint == *derived.topology_fingerprint;
1843 if (!state_matches)
1844 {
1845 throw std::filesystem::filesystem_error(
1846 "Triangulation state does not match its persistence metadata",
1847 payload_path, metadata_path,
1848 std::make_error_code(std::errc::illegal_byte_sequence));
1849 }
1850 }
1851
1852 template <typename TriangulationType>
1853 void validate_serialized_payload(std::filesystem::path const& filename,
1854 TriangulationType const& original)
1855 {
1856 if constexpr (requires(std::istream& input, TriangulationType& value) {
1857 input >> value;
1858 } && std::default_initializable<TriangulationType>)
1859 {
1860 auto const parsed = parse_payload<TriangulationType>(filename);
1861 if constexpr (requires(TriangulationType const& value) {
1862 value.dimension();
1863 value.number_of_vertices();
1864 value.number_of_finite_edges();
1865 value.number_of_finite_facets();
1866 value.number_of_finite_cells();
1867 })
1868 {
1869 if (parsed.dimension() != original.dimension() ||
1870 parsed.number_of_vertices() != original.number_of_vertices() ||
1871 parsed.number_of_finite_edges() !=
1872 original.number_of_finite_edges() ||
1873 parsed.number_of_finite_facets() !=
1874 original.number_of_finite_facets() ||
1875 parsed.number_of_finite_cells() !=
1876 original.number_of_finite_cells())
1877 {
1878 throw std::filesystem::filesystem_error(
1879 "Serialized triangulation changed its incidence counts",
1880 filename,
1881 std::make_error_code(std::errc::illegal_byte_sequence));
1882 }
1883 if constexpr (HAS_CAUSAL_INFO<TriangulationType>)
1884 {
1885 if (canonical_topology_fingerprint(parsed) !=
1886 canonical_topology_fingerprint(original))
1887 {
1888 throw std::filesystem::filesystem_error(
1889 "Serialized triangulation changed its causal topology",
1890 filename,
1891 std::make_error_code(std::errc::illegal_byte_sequence));
1892 }
1893 }
1894 }
1895 else if constexpr (requires(TriangulationType const& left,
1896 TriangulationType const& right) {
1897 { left == right } -> std::convertible_to<bool>;
1898 })
1899 {
1900 if (!(parsed == original))
1901 {
1902 throw std::filesystem::filesystem_error(
1903 "Serialized triangulation did not round-trip exactly", filename,
1904 std::make_error_code(std::errc::illegal_byte_sequence));
1905 }
1906 }
1907 }
1908 }
1909
1910 inline void write_text(std::filesystem::path const& filename,
1911 std::string_view const contents)
1912 {
1913 std::ofstream file(filename, std::ios::out | std::ios::trunc);
1914 if (!file.is_open())
1915 {
1916 throw std::filesystem::filesystem_error(
1917 "Could not open temporary metadata file for writing", filename,
1918 std::make_error_code(std::errc::bad_file_descriptor));
1919 }
1920 file << contents;
1921 if (!file)
1922 {
1923 throw std::filesystem::filesystem_error(
1924 "Could not serialize persistence metadata", filename,
1925 std::make_error_code(std::errc::io_error));
1926 }
1927 file.flush();
1928 if (!file)
1929 {
1930 throw std::filesystem::filesystem_error(
1931 "Could not flush persistence metadata", filename,
1932 std::make_error_code(std::errc::io_error));
1933 }
1934 file.close();
1935 if (!file)
1936 {
1937 throw std::filesystem::filesystem_error(
1938 "Could not close persistence metadata", filename,
1939 std::make_error_code(std::errc::io_error));
1940 }
1941 }
1942
1943 template <typename TriangulationType>
1944 void write_payload(std::filesystem::path const& filename,
1945 TriangulationType const& triangulation,
1946 std::optional<Reproducibility_metadata> const& metadata)
1947 {
1948 WriteFileOperation const operation;
1949 fmt::print("Writing to file {}\n", filename.string());
1950 std::scoped_lock const lock(write_file_mutex());
1951 auto temporary = filename;
1952 temporary += ".tmp";
1953 auto const metadata_destination = metadata_filename(filename);
1954 auto metadata_temporary = metadata_destination;
1955 metadata_temporary += ".tmp";
1956 auto resolved_metadata = metadata;
1957 if (resolved_metadata)
1958 {
1959 reconcile_payload_metadata(*resolved_metadata, triangulation);
1960 }
1961
1962 std::error_code cleanup_error;
1963 std::filesystem::remove(temporary, cleanup_error);
1964 std::filesystem::remove(metadata_temporary, cleanup_error);
1965 try
1966 {
1967 std::ofstream file(temporary, std::ios::out | std::ios::trunc);
1968 if (!file.is_open())
1969 {
1970 throw std::filesystem::filesystem_error(
1971 "Could not open temporary file for writing", filename,
1972 std::make_error_code(std::errc::bad_file_descriptor));
1973 }
1974 file << std::setprecision(std::numeric_limits<double>::max_digits10)
1975 << triangulation;
1976 write_causal_info(file, triangulation);
1977 if (!file)
1978 {
1979 throw std::filesystem::filesystem_error(
1980 "Could not serialize triangulation", filename,
1981 std::make_error_code(std::errc::io_error));
1982 }
1983 file.flush();
1984 if (!file)
1985 {
1986 throw std::filesystem::filesystem_error(
1987 "Could not flush serialized triangulation", filename,
1988 std::make_error_code(std::errc::io_error));
1989 }
1990 file.close();
1991 if (!file)
1992 {
1993 throw std::filesystem::filesystem_error(
1994 "Could not close serialized triangulation", filename,
1995 std::make_error_code(std::errc::io_error));
1996 }
1997
1998 validate_serialized_payload(temporary, triangulation);
1999 if (resolved_metadata)
2000 {
2001 auto const integrity = payload_integrity(temporary);
2002 write_text(metadata_temporary,
2003 metadata_text(*resolved_metadata, integrity));
2004 auto const recorded = read_persistence_metadata(metadata_temporary);
2005 if (recorded.payload.size != integrity.size ||
2006 recorded.payload.digest != integrity.digest)
2007 {
2008 throw std::filesystem::filesystem_error(
2009 "Persistence metadata did not round-trip exactly",
2010 metadata_temporary,
2011 std::make_error_code(std::errc::illegal_byte_sequence));
2012 }
2013 validate_persistence_metadata(recorded, triangulation, temporary,
2014 metadata_temporary);
2015
2016 // Publish the manifest first. A process interrupted between these
2017 // replacements observes a detectable checksum mismatch, never a
2018 // payload silently paired with stale provenance.
2019 replace_file(metadata_temporary, metadata_destination);
2020 }
2021 replace_file(temporary, filename);
2022 if (!resolved_metadata)
2023 {
2024 std::filesystem::remove(metadata_destination, cleanup_error);
2025 }
2026 }
2027 catch (...)
2028 {
2029 std::filesystem::remove(temporary, cleanup_error);
2030 std::filesystem::remove(metadata_temporary, cleanup_error);
2031 throw;
2032 }
2033 }
2034 } // namespace detail
2035
2044 [[nodiscard]] inline auto current_date_time(
2045 std::chrono::system_clock::time_point const timestamp =
2046 std::chrono::system_clock::now())
2047 {
2048 auto const time = std::chrono::floor<std::chrono::seconds>(timestamp);
2049 return date::format("%Y-%m-%d.%TUTC", time);
2050 } // current_date_time
2051
2061 [[nodiscard]] inline auto make_filename(Topology const& t_topology,
2062 Int_precision t_dimension,
2063 Int_precision t_number_of_simplices,
2064 Int_precision t_number_of_timeslices,
2065 double t_initial_radius,
2066 double t_foliation_spacing)
2067 -> std::filesystem::path
2068 {
2069 std::string filename;
2070 if (t_topology == Topology::SPHERICAL) { filename += "S"; }
2071 else
2072 {
2073 filename += "T";
2074 }
2075 // std::to_string() works in C++11, but not earlier
2076 filename += std::to_string(t_dimension);
2077
2078 filename += "-";
2079
2080 filename += std::to_string(t_number_of_timeslices);
2081
2082 filename += "-";
2083
2084 filename += std::to_string(t_number_of_simplices);
2085
2086 filename += "-I";
2087
2088 filename += std::to_string(t_initial_radius);
2089
2090 filename += "-R";
2091
2092 filename += std::to_string(t_foliation_spacing);
2093
2094 // Append current time
2095 filename += "-";
2096 auto timestamp = current_date_time();
2097 std::replace(timestamp.begin(), timestamp.end(), ':', '-');
2098 filename += timestamp;
2099
2100 // Append .off file extension
2101 filename += ".off";
2102 return filename;
2103 } // make_filename
2104
2109 template <typename ManifoldType>
2110 [[nodiscard]] auto make_filename(ManifoldType const& manifold)
2111 {
2112 return make_filename(ManifoldType::topology, ManifoldType::dimension,
2113 manifold.N3(), manifold.max_time(),
2114 manifold.initial_radius(),
2115 manifold.foliation_spacing());
2116 } // make_filename
2117
2123 template <typename ManifoldType>
2124 [[nodiscard]] auto make_filename(ManifoldType const& manifold,
2125 cdt::RandomSeed const seed)
2126 {
2127 auto const base = make_filename(manifold);
2128 return base.parent_path() /
2129 (base.stem().string() + "-seed-" + std::to_string(seed.value()) +
2130 base.extension().string());
2131 }
2132
2139 template <typename ManifoldType>
2140 [[nodiscard]] auto make_filename(ManifoldType const& manifold,
2141 cdt::RandomSeed const seed,
2142 Int_precision const completed_passes)
2143 {
2144 auto const base = make_filename(manifold, seed);
2145 return base.parent_path() /
2146 (base.stem().string() + "-pass-" + std::to_string(completed_passes) +
2147 base.extension().string());
2148 }
2149
2154 template <typename TriangulationType>
2155 void print_delaunay(TriangulationType const& t_triangulation)
2156 {
2157 fmt::print(
2158 "Triangulation has {} vertices and {} edges and {} faces and {} "
2159 "simplices.\n",
2160 t_triangulation.number_of_vertices(),
2161 t_triangulation.number_of_finite_edges(),
2162 t_triangulation.number_of_finite_facets(),
2163 t_triangulation.number_of_finite_cells());
2164 } // print_delaunay
2165
2177 template <typename TriangulationType>
2178 void write_file(std::filesystem::path const& filename,
2179 TriangulationType const& triangulation)
2180 {
2181 detail::write_payload(filename, triangulation, std::nullopt);
2182 } // write_file
2183
2200 template <typename TriangulationType>
2201 void write_file(std::filesystem::path const& filename,
2202 TriangulationType const& triangulation,
2203 Reproducibility_metadata const& metadata)
2204 { detail::write_payload(filename, triangulation, metadata); }
2205
2210 template <typename ManifoldType>
2212 ManifoldType const& manifold) -> std::uint64_t
2213 {
2214 auto const triangulation = manifold.delaunay_snapshot();
2215 return detail::canonical_topology_fingerprint(triangulation);
2216 }
2217
2222 template <typename ManifoldType>
2224 ManifoldType const& manifold) -> std::uint64_t
2225 {
2226 auto const triangulation = manifold.delaunay_snapshot();
2227 return detail::canonical_placement_fingerprint(triangulation);
2228 }
2229
2236 template <typename ManifoldType>
2237 [[nodiscard]] auto make_reproducibility_metadata(ManifoldType const& manifold,
2238 cdt::RandomSeed const seed,
2239 ArtifactKind const artifact)
2241 {
2242 return {.artifact = artifact,
2243 .seed = seed,
2244 .topology = ManifoldType::topology,
2245 .dimension = ManifoldType::dimension,
2246 .desired_simplices = manifold.N3(),
2247 .desired_timeslices = manifold.max_time(),
2248 .actual_vertices = manifold.N0(),
2249 .actual_edges = manifold.N1(),
2250 .actual_faces = manifold.N2(),
2251 .actual_simplices = manifold.N3(),
2252 .minimum_timeslice = manifold.min_time(),
2253 .maximum_timeslice = manifold.max_time(),
2254 .initial_radius = manifold.initial_radius(),
2255 .foliation_spacing = manifold.foliation_spacing(),
2256 .placement_fingerprint = canonical_placement_fingerprint(manifold),
2257 .topology_fingerprint = canonical_topology_fingerprint(manifold)};
2258 }
2259
2264 template <typename ManifoldType>
2266 ManifoldType const& manifold)
2267 {
2268 metadata.topology = ManifoldType::topology;
2269 metadata.dimension = ManifoldType::dimension;
2270 metadata.actual_vertices = manifold.N0();
2271 metadata.actual_edges = manifold.N1();
2272 metadata.actual_faces = manifold.N2();
2273 metadata.actual_simplices = manifold.N3();
2274 metadata.minimum_timeslice = manifold.min_time();
2275 metadata.maximum_timeslice = manifold.max_time();
2276 metadata.initial_radius = manifold.initial_radius();
2277 metadata.foliation_spacing = manifold.foliation_spacing();
2280 }
2281
2290 template <typename ManifoldType>
2291 void write_file(ManifoldType const& t_universe)
2292 {
2293 std::filesystem::path filename;
2294 filename.assign(make_filename(t_universe));
2295 write_file(filename, t_universe.delaunay_snapshot());
2296 } // write_file
2297
2304 template <typename ManifoldType>
2305 void write_file(ManifoldType const& t_universe, cdt::RandomSeed const seed)
2306 {
2307 auto const metadata = make_reproducibility_metadata(
2308 t_universe, seed, ArtifactKind::FINAL_TRIANGULATION);
2309 write_file(make_filename(t_universe, seed), t_universe.delaunay_snapshot(),
2310 metadata);
2311 }
2312
2320 template <typename ManifoldType>
2321 void write_file(ManifoldType const& t_universe, cdt::RandomSeed const seed,
2322 Int_precision const completed_passes)
2323 {
2324 auto metadata = make_reproducibility_metadata(t_universe, seed,
2326 metadata.completed_passes = completed_passes;
2327 write_file(make_filename(t_universe, seed, completed_passes),
2328 t_universe.delaunay_snapshot(), metadata);
2329 }
2330
2339 template <typename ManifoldType>
2340 void write_file(ManifoldType const& universe,
2341 Reproducibility_metadata const& metadata)
2342 {
2343 auto filename = make_filename(universe, metadata.seed);
2344 if (metadata.artifact == ArtifactKind::CHECKPOINT)
2345 {
2346 if (!metadata.completed_passes)
2347 {
2348 throw std::invalid_argument(
2349 "Checkpoint metadata must record completed passes.");
2350 }
2351 filename =
2352 make_filename(universe, metadata.seed, *metadata.completed_passes);
2353 }
2354 write_file(filename, universe.delaunay_snapshot(), metadata);
2355 }
2356
2359 template <typename TriangulationType>
2365
2369 template <typename TriangulationType>
2372
2386 template <typename TriangulationType>
2387 [[nodiscard]] auto read_initial_triangulation(
2388 std::filesystem::path const& filename)
2390 {
2391 static std::mutex mutex;
2392 fmt::print("Reading initial triangulation from file {}\n",
2393 filename.string());
2394 std::scoped_lock const lock(mutex);
2395 auto const parsed_metadata = detail::validate_payload_integrity(filename);
2396 auto const sidecar = metadata_filename(filename);
2397 if (!parsed_metadata)
2398 {
2399 throw std::filesystem::filesystem_error(
2400 "Initial triangulation requires a persistence metadata sidecar",
2401 filename, sidecar,
2402 std::make_error_code(std::errc::no_such_file_or_directory));
2403 }
2404 if (parsed_metadata->artifact != ArtifactKind::INITIAL_TRIANGULATION)
2405 {
2406 throw std::filesystem::filesystem_error(
2407 "CDT input must be an initial-triangulation artifact", filename,
2408 sidecar, std::make_error_code(std::errc::not_supported));
2409 }
2410
2411 auto triangulation = detail::parse_payload<TriangulationType>(filename);
2412 detail::validate_persistence_metadata(*parsed_metadata, triangulation,
2413 filename, sidecar);
2414 detail::require_distinct_evolution_coordinates(triangulation, filename,
2415 sidecar, "CDT input");
2416 auto metadata = detail::to_reproducibility_metadata(*parsed_metadata);
2417 return {.triangulation = std::move(triangulation),
2418 .metadata = std::move(metadata)};
2419 }
2420
2423 template <typename TriangulationType>
2425
2436 template <typename TriangulationType>
2437 [[nodiscard]] auto read_checkpoint(std::filesystem::path const& filename)
2439 {
2440 static std::mutex mutex;
2441 fmt::print("Reading resumable checkpoint from file {}\n",
2442 filename.string());
2443 std::scoped_lock const lock(mutex);
2444 auto const parsed_metadata = detail::validate_payload_integrity(filename);
2445 auto const sidecar = metadata_filename(filename);
2446 if (!parsed_metadata)
2447 {
2448 throw std::filesystem::filesystem_error(
2449 "Checkpoint resume requires a persistence metadata sidecar", filename,
2450 sidecar, std::make_error_code(std::errc::no_such_file_or_directory));
2451 }
2452 if (parsed_metadata->artifact != ArtifactKind::CHECKPOINT ||
2453 !parsed_metadata->resume_supported)
2454 {
2455 throw std::filesystem::filesystem_error(
2456 "CDT resume requires a resumable checkpoint artifact", filename,
2457 sidecar, std::make_error_code(std::errc::not_supported));
2458 }
2459
2460 auto triangulation = detail::parse_payload<TriangulationType>(filename);
2461 detail::validate_persistence_metadata(*parsed_metadata, triangulation,
2462 filename, sidecar);
2463 detail::require_distinct_evolution_coordinates(triangulation, filename,
2464 sidecar, "CDT resume");
2465 auto metadata = detail::to_reproducibility_metadata(*parsed_metadata);
2466 return {.triangulation = std::move(triangulation),
2467 .metadata = std::move(metadata)};
2468 }
2469
2477 template <typename TriangulationType>
2478 [[nodiscard]] auto read_file(std::filesystem::path const& filename)
2479 -> TriangulationType
2480 {
2481 static std::mutex mutex;
2482 fmt::print("Reading from file {}\n", filename.string());
2483 std::scoped_lock const lock(mutex);
2484 auto const metadata = detail::validate_payload_integrity(filename);
2485 auto triangulation = detail::parse_payload<TriangulationType>(filename);
2486 if (metadata)
2487 {
2488 detail::validate_persistence_metadata(*metadata, triangulation, filename,
2489 metadata_filename(filename));
2490 }
2491 return triangulation;
2492 } // read_file
2493
2499 template <std::uniform_random_bit_generator Generator>
2500 [[nodiscard]] inline auto die_roll(Generator& generator)
2501 {
2502 // Choose random number from 1 to 6
2503 std::uniform_int_distribution uniform_dist(1, 6); // NOLINT
2504 Int_precision const roll = uniform_dist(generator);
2505 return roll;
2506 } // die_roll()
2507
2524 template <typename NumberType, class Distribution,
2525 std::uniform_random_bit_generator Generator>
2526 [[nodiscard]] auto generate_random(Generator& generator,
2527 NumberType t_min_value,
2528 NumberType t_max_value)
2529 {
2530 Distribution distribution(t_min_value, t_max_value);
2531 return distribution(generator);
2532 } // generate_random()
2533
2542 template <std::uniform_random_bit_generator Generator,
2543 std::integral IntegerType>
2544 [[nodiscard]] auto generate_random_int(Generator& generator,
2545 IntegerType t_min_value,
2546 IntegerType t_max_value)
2547 {
2548 using int_dist = std::uniform_int_distribution<IntegerType>;
2549 return generate_random<IntegerType, int_dist>(generator, t_min_value,
2550 t_max_value);
2551 } // generate_random_int()
2552
2559 template <std::uniform_random_bit_generator Generator,
2560 std::integral IntegerType>
2561 [[nodiscard]] auto generate_random_timeslice(Generator& generator,
2562 IntegerType t_max_timeslice)
2563 -> decltype(auto)
2564 {
2565 return generate_random_int(generator, static_cast<IntegerType>(1),
2566 t_max_timeslice);
2567 } // generate_random_timeslice()
2568
2577 template <std::uniform_random_bit_generator Generator,
2578 std::floating_point FloatingPointType>
2579 [[nodiscard]] auto generate_random_real(Generator& generator,
2580 FloatingPointType t_min_value,
2581 FloatingPointType t_max_value)
2582 {
2583 using real_dist = std::uniform_real_distribution<FloatingPointType>;
2584 return generate_random<FloatingPointType, real_dist>(generator, t_min_value,
2585 t_max_value);
2586 } // generate_random_real()
2587
2592 template <std::uniform_random_bit_generator Generator>
2593 [[nodiscard]] inline auto generate_probability(Generator& generator)
2594 {
2595 constexpr auto min = 0.0L;
2596 constexpr auto max = 1.0L;
2597 return generate_random_real(generator, min, max);
2598 } // generate_probability()
2599
2616 [[nodiscard]] inline auto generated_input_vertex_count(
2617 Int_precision const points_per_timeslice, Int_precision const timeslices,
2618 double const initial_radius, double const foliation_spacing)
2619 -> std::uint64_t
2620 {
2621 if (points_per_timeslice <= 0 || timeslices <= 0)
2622 {
2623 throw std::invalid_argument(
2624 "Population and timeslices must both be positive.");
2625 }
2626 if (!std::isfinite(initial_radius) || initial_radius <= 0.0 ||
2627 !std::isfinite(foliation_spacing) || foliation_spacing <= 0.0)
2628 {
2629 throw std::invalid_argument(
2630 "Layer radius and spacing must be finite and positive.");
2631 }
2632
2633 std::uint64_t total{};
2634 for (Int_precision layer = 0; layer < timeslices; ++layer)
2635 {
2636 auto const radius =
2637 initial_radius + static_cast<double>(layer) * foliation_spacing;
2638 auto const layer_points =
2639 static_cast<long double>(points_per_timeslice) * radius;
2640 if (!std::isfinite(layer_points) || layer_points < 0.0L ||
2641 layer_points > static_cast<long double>(
2642 std::numeric_limits<Int_precision>::max()))
2643 {
2644 throw std::out_of_range(
2645 "A spherical layer exceeds the supported point-count range.");
2646 }
2647 auto const narrowed =
2648 static_cast<std::uint64_t>(static_cast<Int_precision>(layer_points));
2649 if (narrowed > std::numeric_limits<std::uint64_t>::max() - total)
2650 {
2651 throw std::out_of_range(
2652 "The generated input-vertex count exceeds uint64_t.");
2653 }
2654 total += narrowed;
2655 }
2656 return total;
2657 }
2658
2667 [[nodiscard]] inline auto delaunay_tetrahedron_upper_bound(
2668 std::uint64_t const vertices) noexcept -> std::optional<std::uint64_t>
2669 {
2670 if (vertices < 4) { return std::uint64_t{0}; }
2671 auto first = vertices;
2672 auto second = vertices - 3;
2673 if (first % 2 == 0) { first /= 2; }
2674 else
2675 {
2676 second /= 2;
2677 }
2678 if (second != 0 &&
2679 first > std::numeric_limits<std::uint64_t>::max() / second)
2680 {
2681 return std::nullopt;
2682 }
2683 return first * second - 1;
2684 }
2685
2707 [[nodiscard]] inline auto expected_points_per_timeslice(
2708 Int_precision const t_dimension, Int_precision t_number_of_simplices,
2709 Int_precision t_number_of_timeslices, double const initial_radius = 1.0,
2710 double const foliation_spacing = 1.0)
2711 {
2712#ifndef NDEBUG
2713 spdlog::debug("{} simplices on {} timeslices desired.\n",
2714 t_number_of_simplices, t_number_of_timeslices);
2715#endif
2716
2717 if (t_dimension != 3)
2718 {
2719 throw std::invalid_argument(
2720 "Only three-dimensional triangulations are supported.");
2721 }
2722 if (t_number_of_simplices <= 0 || t_number_of_timeslices <= 0)
2723 {
2724 throw std::invalid_argument(
2725 "Simplices and timeslices must both be positive.");
2726 }
2727 if (!std::isfinite(initial_radius) || initial_radius <= 0.0 ||
2728 !std::isfinite(foliation_spacing) || foliation_spacing <= 0.0)
2729 {
2730 throw std::invalid_argument(
2731 "Layer radius and spacing must be finite and positive.");
2732 }
2733
2734 constexpr auto minimum_population = Int_precision{8};
2735 auto const minimum = t_number_of_simplices <= t_number_of_timeslices
2736 ? Int_precision{2}
2737 : minimum_population;
2738 auto const last_radius =
2739 static_cast<long double>(initial_radius) +
2740 static_cast<long double>(t_number_of_timeslices - 1) *
2741 foliation_spacing;
2742 if (!std::isfinite(last_radius))
2743 {
2744 throw std::out_of_range(
2745 "The final foliation radius exceeds the supported range.");
2746 }
2747 auto const maximum_by_radius =
2748 static_cast<long double>(std::numeric_limits<Int_precision>::max()) /
2749 last_radius;
2750 auto const maximum =
2751 maximum_by_radius >= static_cast<long double>(
2752 std::numeric_limits<Int_precision>::max())
2753 ? std::numeric_limits<Int_precision>::max()
2754 : static_cast<Int_precision>(maximum_by_radius);
2755 if (maximum < minimum)
2756 {
2757 throw std::out_of_range(
2758 "Foliation radii leave no supported base population.");
2759 }
2760
2761 auto const simplices = static_cast<std::uint64_t>(t_number_of_simplices);
2762 auto const timeslices = static_cast<std::uint64_t>(t_number_of_timeslices);
2763 auto const scaled_floor = [timeslices](
2764 std::uint64_t const bounded_simplices,
2765 std::uint64_t const numerator,
2766 std::uint64_t const denominator) {
2767 return bounded_simplices * numerator / (timeslices * denominator);
2768 };
2769 auto const historical_upper_envelope = std::max(
2770 {scaled_floor(std::min(simplices, std::uint64_t{1'000}), 2, 5),
2771 scaled_floor(std::min(simplices, std::uint64_t{10'000}), 1, 5),
2772 scaled_floor(std::min(simplices, std::uint64_t{100'000}), 3, 20),
2773 scaled_floor(simplices, 1, 10)});
2774 auto const construction_margin = (5 * historical_upper_envelope + 3) / 4;
2775 auto const estimated =
2776 std::max(static_cast<std::uint64_t>(minimum), construction_margin);
2777 if (estimated > static_cast<std::uint64_t>(maximum))
2778 {
2779 throw std::out_of_range(
2780 "Requested simplex estimate exceeds the supported population.");
2781 }
2782 auto const population = static_cast<Int_precision>(estimated);
2783 static_cast<void>(generated_input_vertex_count(
2784 population, t_number_of_timeslices, initial_radius, foliation_spacing));
2785 return population;
2786 } // expected_points_per_timeslice
2787
2800
2813 [[nodiscard]] inline auto generated_population_bounds(
2814 Int_precision const dimension, Int_precision const simplices,
2815 Int_precision const timeslices, double const initial_radius,
2816 double const foliation_spacing) -> Generated_population_bounds
2817 {
2818 auto const points_per_timeslice = expected_points_per_timeslice(
2819 dimension, simplices, timeslices, initial_radius, foliation_spacing);
2820 auto const last_radius =
2821 static_cast<long double>(initial_radius) +
2822 static_cast<long double>(timeslices - 1) * foliation_spacing;
2823 auto const input_vertices = generated_input_vertex_count(
2824 points_per_timeslice, timeslices, initial_radius, foliation_spacing);
2825 return {
2826 .points_per_timeslice = points_per_timeslice,
2827 .last_layer_points =
2828 static_cast<long double>(points_per_timeslice) * last_radius,
2829 .input_vertices = input_vertices,
2830 .tetrahedra_upper_bound =
2831 delaunay_tetrahedron_upper_bound(input_vertices),
2832 };
2833 }
2834
2843 [[nodiscard]] inline auto gmpzf_to_double(Gmpzf const& t_value) -> double
2844 { return t_value.to_double(); } // gmpzf_to_double
2845
2871 inline void create_logger()
2872 try
2873 {
2874 auto const console_sink =
2875 std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
2876 console_sink->set_level(spdlog::level::info);
2877
2878 auto const debug_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
2879 "logs/debug-log.txt", true);
2880 debug_sink->set_level(spdlog::level::debug);
2881
2882 auto const trace_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
2883 "logs/trace-log.txt", true);
2884 trace_sink->set_level(spdlog::level::trace);
2885
2886 spdlog::sinks_init_list sink_list = {console_sink, debug_sink, trace_sink};
2887
2888 auto const logger = std::make_shared<spdlog::logger>(
2889 "multi_sink", sink_list.begin(), sink_list.end());
2890 // This allows the logger to capture all events
2891 logger->set_level(spdlog::level::trace);
2892 // The sinks will filter items further via should_log()
2893 logger->info("Multi-sink logger initialized.\n");
2894 logger->debug("Debug logger initialized.\n");
2895 logger->trace("Trace logger initialized.\n");
2896 logger->debug(
2897 "You must build in Debug mode for anything to be recorded in this "
2898 "file.\n");
2899
2900 register_logger(logger);
2901 set_default_logger(logger);
2902 }
2903 catch (spdlog::spdlog_ex const& ex)
2904 {
2905 // Use default logger
2906 spdlog::error("Logger initialization failed: {}\n", ex.what());
2907 spdlog::warn("Default logger set.\n");
2908
2909 } // create_logger
2910
2915 template <typename Point>
2916 [[nodiscard]] auto point_to_str(Point const& t_point) -> std::string
2917 {
2918 std::stringstream stream;
2919 stream << t_point;
2920 return stream.str();
2921 } // point_to_str
2922
2926 [[nodiscard]] inline auto topology_to_str(Topology const& t_topology)
2927 -> std::string
2928 {
2929 std::stringstream stream;
2930 stream << t_topology;
2931 return stream.str();
2932 } // topology_to_str
2933} // namespace cdt::utilities
2934#endif // INCLUDE_UTILITIES_HPP_
Vertex_handle_t< 3 > Vertex_handle
Three-dimensional CGAL vertex handle.
Cell_handle_t< 3 > Cell_handle
Three-dimensional CGAL cell handle.
Track ergodic moves.
Run-owned random-number generation and reproducible stream splitting.
constexpr RandomStream initialization
Stream reserved for initial triangulation generation.
Definition Random.hpp:120
constexpr RandomStream transitions
Stream reserved for stochastic state transitions.
Definition Random.hpp:122
Global integer and precision settings.
auto generate_random_int(Generator &generator, IntegerType t_min_value, IntegerType t_max_value)
Generate random integers by calling generate_random, preserves template argument deduction.
auto generate_random_real(Generator &generator, FloatingPointType t_min_value, FloatingPointType t_max_value)
Generate random real numbers by calling generate_random, preserves template argument deduction.
auto current_date_time(std::chrono::system_clock::time_point const timestamp=std::chrono::system_clock::now())
Return current date and time.
auto read_checkpoint(std::filesystem::path const &filename) -> Triangulation_artifact< TriangulationType >
Read a checkpoint that can continue the identical Markov chain.
auto generate_random(Generator &generator, NumberType t_min_value, NumberType t_max_value)
Generate random numbers with a caller-supplied generator.
void update_reproducibility_state(Reproducibility_metadata &metadata, ManifoldType const &manifold)
Refresh state-dependent provenance after a transition sequence.
ArtifactKind
Persistence artifact represented by a triangulation payload.
Definition Utilities.hpp:99
@ CHECKPOINT
Intermediate snapshot during a move run.
@ FINAL_TRIANGULATION
Final state after the configured move run.
@ INITIAL_TRIANGULATION
Initial state before stochastic transitions.
void print_delaunay(TriangulationType const &t_triangulation)
Print triangulation statistics.
void create_logger()
Create console and file loggers.
Triangulation_artifact< TriangulationType > Checkpoint_artifact
A validated resumable checkpoint and its complete run state.
auto generate_probability(Generator &generator)
Generate a probability.
auto canonical_topology_fingerprint(ManifoldType const &manifold) -> std::uint64_t
Fingerprint vertices, causal metadata, and abstract finite cells.
auto read_initial_triangulation(std::filesystem::path const &filename) -> Initial_triangulation_artifact< TriangulationType >
Read a manifested initial triangulation for a new CDT run.
auto canonical_placement_fingerprint(ManifoldType const &manifold) -> std::uint64_t
Fingerprint finite vertex coordinates and timeslice metadata.
auto gmpzf_to_double(Gmpzf const &t_value) -> double
Convert Gmpzf into a double.
void write_file(std::filesystem::path const &filename, TriangulationType const &triangulation)
Write triangulation to file.
auto generated_input_vertex_count(Int_precision const points_per_timeslice, Int_precision const timeslices, double const initial_radius, double const foliation_spacing) -> std::uint64_t
Calculate the exact number of vertices generated on spherical layers.
auto point_to_str(Point const &t_point) -> std::string
Covert a CGAL point to a string.
auto make_filename(Topology const &t_topology, Int_precision t_dimension, Int_precision t_number_of_simplices, Int_precision t_number_of_timeslices, double t_initial_radius, double t_foliation_spacing) -> std::filesystem::path
Generate useful filenames.
auto generate_random_timeslice(Generator &generator, IntegerType t_max_timeslice) -> decltype(auto)
Generate a random timeslice.
auto metadata_filename(std::filesystem::path const &payload) -> std::filesystem::path
auto expected_points_per_timeslice(Int_precision const t_dimension, Int_precision t_number_of_simplices, Int_precision t_number_of_timeslices, double const initial_radius=1.0, double const foliation_spacing=1.0)
Estimate the base population for the layered spherical generator.
auto die_roll(Generator &generator)
Roll a die using a caller-supplied std::uniform_random_bit_generator.
auto incidence_records_for_coloring(std::vector< std::string > const &bases, std::vector< std::vector< std::size_t > > const &adjacency, std::vector< std::size_t > const &colors) -> std::vector< std::string >
auto make_reproducibility_metadata(ManifoldType const &manifold, cdt::RandomSeed const seed, ArtifactKind const artifact) -> Reproducibility_metadata
Build provenance from a canonical manifold state.
auto delaunay_tetrahedron_upper_bound(std::uint64_t const vertices) noexcept -> std::optional< std::uint64_t >
Return the rigorous finite 3D Delaunay tetrahedron upper bound.
auto read_file(std::filesystem::path const &filename) -> TriangulationType
Read triangulation from file.
auto topology_to_str(Topology const &t_topology) -> std::string
Convert a topology to a string using it's << operator.
auto generated_population_bounds(Int_precision const dimension, Int_precision const simplices, Int_precision const timeslices, double const initial_radius, double const foliation_spacing) -> Generated_population_bounds
Calculate the generated point count and its upper bound.
Triangulation_artifact< TriangulationType > Initial_triangulation_artifact
A validated initial triangulation and its initialization provenance.
static auto from_serialized_state(RandomSeed const seed, RandomStream const stream, std::string_view const state) -> Random
Restore an exact PCG continuation point.
Definition Random.hpp:218
Root entropy value used to reproduce a random run.
Definition Random.hpp:49
constexpr auto value() const noexcept -> std::uint64_t
Definition Random.hpp:60
PCG sequence selector derived from a root random seed.
Definition Random.hpp:71
clang-15 does not support std::format
Topology
Spatial-topology label stored by configuration and persistence APIs.
Definition Utilities.hpp:74
@ TOROIDAL
Reserved for toroidal slices; construction is unsupported.
Definition Utilities.hpp:75
@ SPHERICAL
Supported spherical spatial slices.
Definition Utilities.hpp:76
auto operator<<(std::ostream &output, RandomSeed const seed) -> std::ostream &
Definition Random.hpp:106
std::int32_t Int_precision
Definition Settings.hpp:30
CGAL::Gmpzf Gmpzf
Definition Settings.hpp:23
Validated simplex-population bounds for generated triangulations.
Int_precision points_per_timeslice
Base population used to construct the spherical layers.
long double last_layer_points
Untruncated point-count expression for the final spherical layer.
std::optional< std::uint64_t > tetrahedra_upper_bound
Rigorous finite 3D Delaunay bound, if representable by uint64_t.
std::uint64_t input_vertices
Exact total number of vertices supplied to CGAL.
Cumulative counters needed to preserve observable run state.
Counts rejected
Explicit self-transitions by move kind.
Counts succeeded
Successful candidate constructions by move kind.
Counts accepted
Accepted proposals by move kind.
auto operator==(Move_statistics const &) const noexcept -> bool=default
Counts failed
Failed candidate constructions by move kind.
Counts attempted
Candidate constructions by move kind.
Counts proposed
Raw proposals by move kind.
std::array< Int_precision, move_tracker::NUMBER_OF_3D_MOVES > Counts
Per-move counts in stable MoveType index order.
Provenance recorded next to every stochastic triangulation.
Int_precision desired_timeslices
Requested timeslice count.
ArtifactKind artifact
Artifact role.
std::shared_ptr< std::string const > transition_random_state
Exact PCG state.
std::optional< std::uint64_t > placement_fingerprint
Coordinate hash.
cdt::RandomStream transition_stream
Transition stream.
std::optional< Int_precision > configured_passes
Requested move passes.
Int_precision actual_edges
Persisted finite edges.
double foliation_spacing
Radius increment per slice.
std::optional< std::uint64_t > max_threads
Configured concurrency.
Int_precision desired_simplices
Requested simplex target.
std::optional< Int_precision > configured_attempts
Explicit attempts.
Topology topology
Spatial-topology label.
double initial_radius
Initial spherical radius.
cdt::RandomStream initialization_stream
Initialization stream.
std::optional< cdt::RandomSeed > input_seed
Starting artifact seed.
std::optional< Int_precision > checkpoint_interval
Checkpoint cadence.
std::optional< std::uint64_t > input_placement_fingerprint
Starting coordinate hash.
std::optional< std::uint64_t > input_topology_fingerprint
Starting incidence hash.
Int_precision minimum_timeslice
Minimum persisted time label.
std::optional< long double > lambda
Optional cosmological coupling.
std::optional< std::uint64_t > transition_count
Hashed transitions.
std::optional< long double > k
Optional inverse Newton coupling.
Int_precision maximum_timeslice
Maximum persisted time label.
std::optional< ArtifactKind > input_artifact
Starting artifact role.
std::optional< long double > alpha
Optional Wick-rotation parameter.
Int_precision dimension
Spatial dimension.
std::optional< std::uint64_t > topology_fingerprint
Incidence hash.
Int_precision actual_vertices
Persisted finite vertices.
std::optional< cdt::RandomStream > input_initialization_stream
Starting initialization stream.
std::optional< Int_precision > completed_passes
Passes before snapshot.
std::optional< std::uint64_t > transition_trace
Ordered trace hash.
std::optional< Move_statistics > move_statistics
Cumulative counters.
cdt::RandomSeed seed
Root seed for the recorded run.
Int_precision actual_simplices
Persisted finite simplices.
Int_precision actual_faces
Persisted finite faces.
A validated triangulation artifact and its provenance.
Reproducibility_metadata metadata
Validated artifact metadata.
TriangulationType triangulation
Validated causal payload.