11#ifndef INCLUDE_UTILITIES_HPP_
12#define INCLUDE_UTILITIES_HPP_
14#include <CGAL/version.h>
56#include <fmt/ostream.h>
59#include <spdlog/sinks/basic_file_sink.h>
60#include <spdlog/sinks/stdout_color_sinks.h>
61#include <spdlog/spdlog.h>
90 default:
return t_os <<
"none";
95namespace cdt::utilities
109 using Counts = std::array<Int_precision, move_tracker::NUMBER_OF_3D_MOVES>;
155 std::optional<long double>
k;
164 std::shared_ptr<std::string const>
171 std::optional<cdt::RandomStream>
173 std::optional<std::uint64_t>
175 std::optional<std::uint64_t>
182 std::filesystem::path
const& payload) -> std::filesystem::path
184 auto metadata = payload;
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};
197 inline constexpr bool PARALLEL_TRIANGULATION_ENABLED{
false};
200 struct Payload_integrity
202 std::uint64_t size{};
203 std::uint64_t digest{};
206 [[nodiscard]]
inline auto artifact_name(ArtifactKind
const artifact)
211 case ArtifactKind::INITIAL_TRIANGULATION:
212 return "initial-triangulation";
213 case ArtifactKind::CHECKPOINT:
return "checkpoint";
214 case ArtifactKind::FINAL_TRIANGULATION:
return "final-triangulation";
219 [[nodiscard]]
inline auto standard_library_name() -> std::string
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);
232 [[nodiscard]]
inline auto payload_integrity(
233 std::filesystem::path
const& filename) -> Payload_integrity
235 std::ifstream input(filename, std::ios::in | std::ios::binary);
236 if (!input.is_open())
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));
243 std::uint64_t digest{14695981039346656037ULL};
244 std::uint64_t size{};
245 std::array<char, 8192> buffer{};
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)
252 digest ^=
static_cast<unsigned char>(
253 buffer[
static_cast<std::size_t
>(index)]);
254 digest *= 1099511628211ULL;
256 size +=
static_cast<std::uint64_t
>(count);
260 throw std::filesystem::filesystem_error(
261 "Could not read payload for integrity validation", filename,
262 std::make_error_code(std::errc::io_error));
264 return {size, digest};
267 [[nodiscard]]
inline auto point_key(
auto const& point) -> std::string
269 return fmt::format(
"{:.17g},{:.17g},{:.17g}", CGAL::to_double(point.x()),
270 CGAL::to_double(point.y()),
271 CGAL::to_double(point.z()));
274 [[nodiscard]]
inline auto point_cell_key(
auto const& cell) -> std::string
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],
285 template <
typename Signature>
286 [[nodiscard]]
auto canonical_colors(
287 std::vector<Signature>
const& signatures) -> std::vector<std::size_t>
289 auto ordered = signatures;
290 std::ranges::sort(ordered);
291 ordered.erase(std::unique(ordered.begin(), ordered.end()), ordered.end());
293 std::vector<std::size_t> colors;
294 colors.reserve(signatures.size());
295 for (
auto const& signature : signatures)
297 colors.push_back(
static_cast<std::size_t
>(
298 std::lower_bound(ordered.begin(), ordered.end(), signature) -
304 inline constexpr std::size_t CANONICAL_INCIDENCE_WORK_BUDGET{100'000};
306 inline void consume_canonical_incidence_work(std::size_t& budget)
310 throw std::runtime_error{
311 "Canonical incidence search exceeded its work budget"};
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>
321 for (std::size_t iteration = 0; iteration < adjacency.size(); ++iteration)
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)
328 std::vector<std::size_t> neighboring_colors;
329 neighboring_colors.reserve(adjacency[node].size());
330 for (
auto const neighbor : adjacency[node])
332 neighboring_colors.push_back(colors.at(neighbor));
334 std::ranges::sort(neighboring_colors);
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));
344 auto refined = canonical_colors(signatures);
345 if (refined == colors) {
break; }
346 colors = std::move(refined);
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>
358 std::vector<std::size_t> nodes_by_color(colors.size());
359 for (std::size_t node = 0; node < colors.size(); ++node)
361 nodes_by_color.at(colors[node]) = node;
364 std::vector<std::string> records;
365 records.reserve(bases.size());
366 for (
auto const node : nodes_by_color)
368 std::vector<std::size_t> neighboring_colors;
369 neighboring_colors.reserve(adjacency[node].size());
370 for (
auto const neighbor : adjacency[node])
372 neighboring_colors.push_back(colors.at(neighbor));
374 std::ranges::sort(neighboring_colors);
377 fmt::format(
"{}:{}:neighbors=", bases[node].size(), bases[node]);
378 for (
auto const color : neighboring_colors)
380 record.append(std::to_string(color));
381 record.push_back(
';');
383 records.push_back(std::move(record));
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>
394 consume_canonical_incidence_work(budget);
395 colors = refine_incidence_colors(adjacency, std::move(colors), budget);
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())
403 return incidence_records_for_coloring(bases, adjacency, colors);
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)
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); }
419 return std::move(*best);
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>
428 if (cell_bases.size() != cell_vertices.size())
430 throw std::invalid_argument{
431 "Cell bases and incidence records must have equal sizes"};
433 for (
auto const& incident_vertices : cell_vertices)
435 for (
auto const vertex : incident_vertices)
437 if (vertex >= vertex_bases.size())
439 throw std::invalid_argument{
440 "Cell incidence records must reference known vertices"};
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)
451 auto const cell_node = vertex_bases.size() + cell;
452 for (
auto const vertex : cell_vertices[cell])
454 adjacency.at(vertex).push_back(cell_node);
455 adjacency[cell_node].push_back(vertex);
458 auto budget = CANONICAL_INCIDENCE_WORK_BUDGET;
459 return canonical_incidence_search(bases, adjacency,
460 canonical_colors(bases), budget);
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();
472 template <
typename TriangulationType>
473 [[nodiscard]]
auto vertex_records(TriangulationType
const& triangulation)
474 -> std::vector<std::string>
476 std::vector<std::string> records;
478 static_cast<std::size_t
>(triangulation.number_of_vertices()));
479 for (
auto const vertex : triangulation.finite_vertex_handles())
481 records.emplace_back(
482 fmt::format(
"v:{}:{}", point_key(vertex->point()), vertex->info()));
484 std::ranges::sort(records);
488 template <
typename TriangulationType>
489 [[nodiscard]]
auto has_coincident_vertices(
490 TriangulationType
const& triangulation) ->
bool
492 std::map<std::string, std::size_t> point_counts;
493 for (
auto const vertex : triangulation.finite_vertex_handles())
495 if (++point_counts[point_key(vertex->point())] > 1) {
return true; }
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)
506 if (has_coincident_vertices(triangulation))
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));
514 template <
typename TriangulationType>
515 [[nodiscard]]
auto incidence_topology_records(
516 TriangulationType
const& triangulation) -> std::vector<std::string>
518 auto const finite_vertices = triangulation.finite_vertex_handles();
520 std::remove_cvref_t<
decltype(*finite_vertices.begin())>;
521 std::vector<Vertex_handle> vertices(finite_vertices.begin(),
522 finite_vertices.end());
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());
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)
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()));
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)
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)
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);
553 return canonical_bipartite_incidence_records(vertex_bases, cell_bases,
557 [[nodiscard]]
inline auto fingerprint_records(
558 std::vector<std::string>
const& records) -> std::uint64_t
560 std::uint64_t digest{14695981039346656037ULL};
561 for (
auto const& record : records)
563 for (
auto const byte : record)
565 digest ^=
static_cast<unsigned char>(byte);
566 digest *= 1099511628211ULL;
569 digest *= 1099511628211ULL;
574 template <
typename TriangulationType>
575 [[nodiscard]]
auto canonical_placement_fingerprint(
576 TriangulationType
const& triangulation) -> std::uint64_t
577 {
return fingerprint_records(vertex_records(triangulation)); }
579 template <
typename TriangulationType>
580 [[nodiscard]]
auto canonical_topology_fingerprint(
581 TriangulationType
const& triangulation) -> std::uint64_t
585 if (has_coincident_vertices(triangulation))
587 return fingerprint_records(incidence_topology_records(triangulation));
590 auto records = vertex_records(triangulation);
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())
596 records.emplace_back(
597 fmt::format(
"c:{}:{}", point_cell_key(cell), cell->info()));
599 std::ranges::sort(records);
600 return fingerprint_records(records);
603 template <
typename TriangulationType>
604 void write_causal_info(std::ostream& output,
605 TriangulationType
const& triangulation)
607 if constexpr (HAS_CAUSAL_INFO<TriangulationType>)
613 std::vector<std::string> vertices;
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())
619 vertices.emplace_back(
620 fmt::format(
"{}|{}", vertex_index, vertex->info()));
624 std::vector<std::string> cells;
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())
630 cells.emplace_back(fmt::format(
"{}|{}", cell_index, cell->info()));
634 output <<
'\n' << CAUSAL_INFO_HEADER <<
'\n';
635 output <<
"vertices=" << vertices.size() <<
'\n';
636 for (
auto const& record : vertices)
638 output <<
"v=" << record <<
'\n';
640 output <<
"cells=" << cells.size() <<
'\n';
641 for (
auto const& record : cells) { output <<
"c=" << record <<
'\n'; }
645 [[nodiscard]]
inline auto metadata_text(
646 Reproducibility_metadata
const& metadata,
647 Payload_integrity
const payload) -> std::string
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)
654 throw std::invalid_argument(
655 "Resumable checkpoint metadata requires cumulative move statistics.");
657 if (metadata.transition_random_state &&
658 metadata.artifact != ArtifactKind::CHECKPOINT)
660 throw std::invalid_argument(
661 "Only checkpoint artifacts may contain resumable PCG state.");
663 auto text = fmt::format(
664 "cdt-plusplus-metadata-v1\n"
666 "payload.fnv1a64={:016x}\n"
668 "resume_supported={}\n"
669 "fresh_topology_replay_supported=false\n"
670 "transition_replay_requires_identical_start=true\n"
672 "source.revision={}\n"
673 "build.compiler_id={}\n"
674 "build.compiler_version={}\n"
675 "build.configuration={}\n"
676 "build.parallel_triangulation={}\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"
684 "random.initialization_stream={}\n"
685 "random.transition_stream={}\n"
688 "desired.simplices={}\n"
689 "desired.timeslices={}\n"
690 "actual.vertices={}\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,
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);
714 auto append_optional = [&text](std::string_view
const name,
716 if (value) { text += fmt::format(
"{}={}\n", name, *value); }
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)
728 text += fmt::format(
"random.transition_state={}\n",
729 *metadata.transition_random_state);
731 if (metadata.transition_trace)
733 text += fmt::format(
"transition_trace.fnv1a64={:016x}\n",
734 *metadata.transition_trace);
736 append_optional(
"transition_trace.count", metadata.transition_count);
737 if (metadata.move_statistics)
739 auto const append_counts = [&text](
740 std::string_view
const name,
742 text += fmt::format(
"{}={}", name, values.front());
743 for (std::size_t index = 1; index < values.size(); ++index)
745 text += fmt::format(
",{}", values[index]);
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);
756 if (metadata.placement_fingerprint)
758 text += fmt::format(
"placement.fnv1a64={:016x}\n",
759 *metadata.placement_fingerprint);
761 if (metadata.topology_fingerprint)
763 text += fmt::format(
"topology.fnv1a64={:016x}\n",
764 *metadata.topology_fingerprint);
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)
774 throw std::invalid_argument(
775 "Input provenance must contain all five starting-artifact fields.");
777 if (input_field_count == 5)
779 if (*metadata.input_artifact != ArtifactKind::INITIAL_TRIANGULATION)
781 throw std::invalid_argument(
782 "Input provenance must identify an initial-triangulation "
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);
799 [[nodiscard]]
inline auto parse_unsigned(std::string_view
const text,
801 std::filesystem::path
const& path)
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())
809 throw std::filesystem::filesystem_error(
810 "Malformed persistence metadata", path,
811 std::make_error_code(std::errc::illegal_byte_sequence));
816 [[nodiscard]]
inline auto parse_info(std::string_view
const text,
817 std::filesystem::path
const& path)
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())
825 throw std::filesystem::filesystem_error(
826 "Malformed causal triangulation metadata", path,
827 std::make_error_code(std::errc::illegal_byte_sequence));
832 [[nodiscard]]
inline auto parse_metadata_integer(
833 std::string_view
const text, std::filesystem::path
const& path)
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())
841 throw std::filesystem::filesystem_error(
842 "Malformed persistence metadata", path,
843 std::make_error_code(std::errc::illegal_byte_sequence));
848 [[nodiscard]]
inline auto parse_move_counts(
849 std::string_view text, std::filesystem::path
const& path)
853 for (std::size_t index = 0; index < counts.size(); ++index)
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))
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));
864 auto const token = is_last ? text : text.substr(0, separator);
865 counts[index] = parse_metadata_integer(token, path);
866 if (counts[index] < 0)
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));
872 if (is_last) { text = {}; }
875 text.remove_prefix(separator + 1);
881 template <std::
floating_po
int Float>
882 [[nodiscard]]
auto parse_metadata_floating(
883 std::string_view
const text, std::filesystem::path
const& path) -> Float
886 std::istringstream input{std::string{text}};
887 input.imbue(std::locale::classic());
891 throw std::filesystem::filesystem_error(
892 "Malformed persistence metadata", path,
893 std::make_error_code(std::errc::illegal_byte_sequence));
896 if (!input.eof() || !std::isfinite(value))
898 throw std::filesystem::filesystem_error(
899 "Malformed persistence metadata", path,
900 std::make_error_code(std::errc::illegal_byte_sequence));
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>
910 if (!line.starts_with(prefix))
912 throw std::filesystem::filesystem_error(
913 "Malformed causal triangulation metadata", path,
914 std::make_error_code(std::errc::illegal_byte_sequence));
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())
921 throw std::filesystem::filesystem_error(
922 "Malformed causal triangulation metadata", path,
923 std::make_error_code(std::errc::illegal_byte_sequence));
925 return {std::string{record.substr(0, separator)},
926 parse_info(record.substr(separator + 1), path)};
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
933 if (!line.starts_with(prefix))
935 throw std::filesystem::filesystem_error(
936 "Malformed causal triangulation metadata", path,
937 std::make_error_code(std::errc::illegal_byte_sequence));
939 return parse_unsigned(std::string_view{line}.substr(prefix.size()), 10,
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>
948 std::vector<std::optional<Int_precision>> indexed(
949 static_cast<std::size_t
>(count));
951 for (std::uint64_t record_index = 0; record_index < count; ++record_index)
953 if (!std::getline(input, line))
955 throw std::filesystem::filesystem_error(
956 "Truncated causal triangulation metadata", path,
957 std::make_error_code(std::errc::illegal_byte_sequence));
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)])
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));
967 indexed[
static_cast<std::size_t
>(index)] = value;
970 std::vector<Int_precision> values;
971 values.reserve(indexed.size());
972 for (
auto const& value : indexed)
976 throw std::filesystem::filesystem_error(
977 "Missing causal metadata index", path,
978 std::make_error_code(std::errc::illegal_byte_sequence));
980 values.push_back(*value);
985 template <
typename TriangulationType>
986 void read_causal_info(std::istream& input, TriangulationType& triangulation,
987 std::filesystem::path
const& path)
990 if (!std::getline(input, line) || line != CAUSAL_INFO_HEADER)
992 throw std::filesystem::filesystem_error(
993 "Unexpected trailing data after triangulation", path,
994 std::make_error_code(std::errc::illegal_byte_sequence));
997 if (!std::getline(input, line))
999 throw std::filesystem::filesystem_error(
1000 "Truncated causal triangulation metadata", path,
1001 std::make_error_code(std::errc::illegal_byte_sequence));
1003 auto const vertex_count = parse_count_line(line,
"vertices=", path);
1005 static_cast<std::uint64_t
>(triangulation.number_of_vertices()))
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));
1012 auto const vertex_info =
1013 read_indexed_info(input,
"v=", vertex_count, path);
1015 if (!std::getline(input, line))
1017 throw std::filesystem::filesystem_error(
1018 "Truncated causal triangulation metadata", path,
1019 std::make_error_code(std::errc::illegal_byte_sequence));
1021 auto const cell_count = parse_count_line(line,
"cells=", path);
1023 static_cast<std::uint64_t
>(triangulation.number_of_finite_cells()))
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));
1030 auto const cell_info = read_indexed_info(input,
"c=", cell_count, path);
1032 std::size_t vertex_index{};
1033 for (
auto const vertex : triangulation.finite_vertex_handles())
1035 vertex->info() = vertex_info.at(vertex_index);
1038 std::size_t cell_index{};
1039 for (
auto const cell : triangulation.finite_cell_handles())
1041 cell->info() = cell_info.at(cell_index);
1046 struct Parsed_persistence_metadata
1048 Payload_integrity payload;
1050 bool resume_supported;
1051 cdt::RandomSeed seed;
1052 cdt::RandomStream initialization_stream;
1053 cdt::RandomStream transition_stream;
1062 std::optional<std::uint64_t> max_threads;
1063 std::uint64_t placement_fingerprint;
1064 std::uint64_t topology_fingerprint;
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;
1087 [[nodiscard]]
inline auto read_persistence_metadata(
1088 std::filesystem::path
const& path) -> Parsed_persistence_metadata
1090 std::ifstream input(path);
1091 if (!input.is_open())
1093 throw std::filesystem::filesystem_error(
1094 "Could not open persistence metadata", path,
1095 std::make_error_code(std::errc::bad_file_descriptor));
1099 if (!std::getline(input, line) || line !=
"cdt-plusplus-metadata-v1")
1101 throw std::filesystem::filesystem_error(
1102 "Unsupported or malformed persistence metadata", path,
1103 std::make_error_code(std::errc::illegal_byte_sequence));
1106 std::map<std::string, std::string> values;
1107 while (std::getline(input, line))
1109 auto const separator = line.find(
'=');
1110 if (separator == std::string::npos || separator == 0 ||
1111 separator + 1 == line.size())
1113 throw std::filesystem::filesystem_error(
1114 "Malformed persistence metadata", path,
1115 std::make_error_code(std::errc::illegal_byte_sequence));
1117 auto [unused, inserted] = values.emplace(line.substr(0, separator),
1118 line.substr(separator + 1));
1121 throw std::filesystem::filesystem_error(
1122 "Duplicate persistence metadata field", path,
1123 std::make_error_code(std::errc::illegal_byte_sequence));
1128 throw std::filesystem::filesystem_error(
1129 "Could not read persistence metadata", path,
1130 std::make_error_code(std::errc::io_error));
1133 for (
auto const required : {
"payload.size",
1137 "fresh_topology_replay_supported",
1138 "transition_replay_requires_identical_start",
1140 "build.compiler_id",
1141 "build.compiler_version",
1142 "build.configuration",
1145 "build.cxx_standard",
1146 "build.standard_library",
1147 "dependency.cgal_version",
1150 "random.initialization_stream",
1151 "random.transition_stream",
1154 "desired.simplices",
1155 "desired.timeslices",
1160 "actual.minimum_timeslice",
1161 "actual.maximum_timeslice",
1163 "foliation_spacing",
1164 "placement.fnv1a64",
1165 "topology.fnv1a64"})
1167 if (!values.contains(required))
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));
1174 bool resume_supported{};
1175 if (values.at(
"resume_supported") ==
"true") { resume_supported =
true; }
1176 else if (values.at(
"resume_supported") !=
"false")
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));
1182 if (values.at(
"fresh_topology_replay_supported") !=
"false" ||
1183 values.at(
"transition_replay_requires_identical_start") !=
"true")
1185 throw std::filesystem::filesystem_error(
1186 "Unsupported persistence replay contract", path,
1187 std::make_error_code(std::errc::not_supported));
1189 if (values.at(
"random.engine") !=
"pcg64" ||
1190 values.at(
"build.cxx_standard") !=
"23")
1192 throw std::filesystem::filesystem_error(
1193 "Unsupported persistence metadata", path,
1194 std::make_error_code(std::errc::not_supported));
1197 auto const parse_artifact = [&path](std::string_view
const value) {
1198 if (value ==
"initial-triangulation")
1200 return ArtifactKind::INITIAL_TRIANGULATION;
1202 if (value ==
"checkpoint") {
return ArtifactKind::CHECKPOINT; }
1203 if (value ==
"final-triangulation")
1205 return ArtifactKind::FINAL_TRIANGULATION;
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));
1211 auto const artifact = parse_artifact(values.at(
"artifact"));
1214 if (values.at(
"topology") ==
"spherical")
1218 else if (values.at(
"topology") ==
"toroidal")
1224 throw std::filesystem::filesystem_error(
1225 "Persistence metadata has an unknown topology", path,
1226 std::make_error_code(std::errc::illegal_byte_sequence));
1229 auto const parse_integer_field = [&](std::string
const& name) {
1230 return parse_metadata_integer(values.at(name), path);
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)
1247 throw std::filesystem::filesystem_error(
1248 "Persistence metadata contains invalid state dimensions", path,
1249 std::make_error_code(std::errc::illegal_byte_sequence));
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)
1258 throw std::filesystem::filesystem_error(
1259 "Persistence metadata contains invalid foliation parameters", path,
1260 std::make_error_code(std::errc::illegal_byte_sequence));
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)
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));
1273 std::optional<long double> alpha;
1274 std::optional<long double> k;
1275 std::optional<long double> lambda;
1276 if (action_field_count == 3)
1278 alpha = parse_metadata_floating<long double>(values.at(
"alpha"), path);
1279 k = parse_metadata_floating<long double>(values.at(
"k"), path);
1281 parse_metadata_floating<long double>(values.at(
"lambda"), path);
1284 throw std::filesystem::filesystem_error(
1285 "Persistence metadata contains an invalid alpha", path,
1286 std::make_error_code(std::errc::illegal_byte_sequence));
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)
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));
1299 std::optional<Int_precision> configured_passes;
1300 std::optional<Int_precision> checkpoint_interval;
1301 if (run_field_count == 2)
1303 configured_passes = parse_integer_field(
"configured_passes");
1304 checkpoint_interval = parse_integer_field(
"checkpoint_interval");
1305 if (*configured_passes <= 0 || *checkpoint_interval <= 0)
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));
1312 std::optional<Int_precision> configured_attempts;
1313 if (values.contains(
"configured_attempts"))
1315 configured_attempts = parse_integer_field(
"configured_attempts");
1316 if (*configured_attempts <= 0)
1318 throw std::filesystem::filesystem_error(
1319 "Persistence metadata contains invalid configured attempts", path,
1320 std::make_error_code(std::errc::illegal_byte_sequence));
1324 if (artifact == ArtifactKind::CHECKPOINT &&
1325 !values.contains(
"completed_passes"))
1327 throw std::filesystem::filesystem_error(
1328 "Checkpoint metadata is missing completed passes", path,
1329 std::make_error_code(std::errc::illegal_byte_sequence));
1331 std::optional<Int_precision> completed_passes;
1332 if (values.contains(
"completed_passes"))
1334 completed_passes = parse_integer_field(
"completed_passes");
1335 if (*completed_passes < 0)
1337 throw std::filesystem::filesystem_error(
1338 "Persistence metadata contains invalid completed passes", path,
1339 std::make_error_code(std::errc::illegal_byte_sequence));
1342 std::optional<std::uint64_t> max_threads;
1343 if (
auto const field = values.find(
"parallel.max_threads");
1344 field != values.end())
1346 max_threads = parse_unsigned(field->second, 10, path);
1347 if (*max_threads == 0)
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));
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)
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));
1364 std::optional<std::uint64_t> transition_trace;
1365 std::optional<std::uint64_t> transition_count;
1366 if (transition_field_count == 2)
1369 parse_unsigned(values.at(
"transition_trace.fnv1a64"), 16, path);
1371 parse_unsigned(values.at(
"transition_trace.count"), 10, path);
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)
1383 throw std::filesystem::filesystem_error(
1384 "Persistence metadata has incomplete move statistics", path,
1385 std::make_error_code(std::errc::illegal_byte_sequence));
1387 std::optional<Move_statistics> move_statistics;
1388 if (move_field_count == 6)
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();
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]);
1412 static_cast<std::uint64_t
>(move_statistics->failed[index]);
1413 if (proposed != accepted + rejected || proposed != attempted ||
1414 attempted != succeeded + failed)
1416 throw std::filesystem::filesystem_error(
1417 "Persistence move statistics violate accounting invariants",
1418 path, std::make_error_code(std::errc::illegal_byte_sequence));
1420 if (proposed_total >
1421 std::numeric_limits<std::uint64_t>::max() - proposed)
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));
1427 proposed_total += proposed;
1429 if (transition_count && proposed_total != *transition_count)
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));
1437 std::optional<std::string> transition_random_state;
1438 if (
auto const field = values.find(
"random.transition_state");
1439 field != values.end())
1441 transition_random_state = field->second;
1443 if (resume_supported)
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)
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));
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)
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));
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));
1486 catch (std::invalid_argument
const&)
1488 throw std::filesystem::filesystem_error(
1489 "Resumable checkpoint contains invalid PCG state", path,
1490 std::make_error_code(std::errc::illegal_byte_sequence));
1493 else if (transition_random_state)
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));
1500 auto const input_field_count =
1501 static_cast<int>(values.contains(
"input.artifact")) +
1502 static_cast<int>(values.contains(
"input.random.seed")) +
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)
1509 throw std::filesystem::filesystem_error(
1510 "Persistence metadata has incomplete input provenance", path,
1511 std::make_error_code(std::errc::illegal_byte_sequence));
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)
1520 input_artifact = parse_artifact(values.at(
"input.artifact"));
1521 if (*input_artifact != ArtifactKind::INITIAL_TRIANGULATION)
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));
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);
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,
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,
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
1585 [[nodiscard]]
inline auto to_reproducibility_metadata(
1586 Parsed_persistence_metadata
const& source) -> Reproducibility_metadata
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,
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)
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};
1629 [[nodiscard]]
inline auto validate_payload_integrity(
1630 std::filesystem::path
const& payload)
1631 -> std::optional<Parsed_persistence_metadata>
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)
1640 throw std::filesystem::filesystem_error(
1641 "Triangulation payload does not match its persistence metadata",
1643 std::make_error_code(std::errc::illegal_byte_sequence));
1648 [[nodiscard]]
inline auto write_file_mutex() -> std::mutex&
1650 static std::mutex mutex;
1654 [[nodiscard]]
inline auto write_file_active() noexcept ->
bool&
1656 static thread_local bool active{
false};
1660 class WriteFileOperation final
1663 WriteFileOperation()
1665 if (write_file_active())
1667 throw std::logic_error(
"write_file is not reentrant.");
1669 write_file_active() =
true;
1672 ~WriteFileOperation() noexcept { write_file_active() =
false; }
1674 WriteFileOperation(WriteFileOperation
const&) =
delete;
1675 auto operator=(WriteFileOperation
const&) -> WriteFileOperation& =
delete;
1676 WriteFileOperation(WriteFileOperation&&) =
delete;
1677 auto operator=(WriteFileOperation&&) -> WriteFileOperation& =
delete;
1680 inline void replace_file(std::filesystem::path
const& temporary,
1681 std::filesystem::path
const& destination)
1684 if (!::MoveFileExW(temporary.c_str(), destination.c_str(),
1685 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
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()));
1693 std::error_code error;
1694 std::filesystem::rename(temporary, destination, error);
1697 throw std::filesystem::filesystem_error(
1698 "Could not atomically replace file", temporary, destination, error);
1703 template <
typename TriangulationType>
1704 [[nodiscard]]
auto parse_payload(std::filesystem::path
const& filename)
1705 -> TriangulationType
1707 std::ifstream file(filename, std::ios::in);
1708 if (!file.is_open())
1710 throw std::filesystem::filesystem_error(
1711 "Could not open file for reading", filename,
1712 std::make_error_code(std::errc::bad_file_descriptor));
1714 TriangulationType triangulation;
1715 file >> triangulation;
1718 throw std::filesystem::filesystem_error(
1719 "Could not parse triangulation", filename,
1720 std::make_error_code(std::errc::illegal_byte_sequence));
1725 if constexpr (HAS_CAUSAL_INFO<TriangulationType>)
1727 read_causal_info(file, triangulation, filename);
1733 throw std::filesystem::filesystem_error(
1734 "Unexpected trailing data after triangulation", filename,
1735 std::make_error_code(std::errc::illegal_byte_sequence));
1737 if constexpr (
requires(TriangulationType
const& value) {
1738 { value.tds().is_valid() } -> std::convertible_to<bool>;
1741 if (!triangulation.tds().is_valid())
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));
1754 !
requires(TriangulationType
const& value) {
1755 { value.tds().is_valid() } -> std::convertible_to<bool>;
1757 requires(TriangulationType
const& value) {
1758 { value.is_valid() } -> std::convertible_to<bool>;
1761 if (!triangulation.is_valid())
1763 throw std::filesystem::filesystem_error(
1764 "Parsed triangulation failed its integrity check", filename,
1765 std::make_error_code(std::errc::illegal_byte_sequence));
1768 return triangulation;
1771 template <
typename TriangulationType>
1772 void reconcile_payload_metadata(Reproducibility_metadata& metadata,
1773 TriangulationType
const& triangulation)
1775 if constexpr (
requires(TriangulationType
const& value) {
1777 value.number_of_vertices();
1778 value.number_of_finite_edges();
1779 value.number_of_finite_facets();
1780 value.number_of_finite_cells();
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());
1794 if constexpr (HAS_CAUSAL_INFO<TriangulationType>)
1796 if (triangulation.number_of_vertices() == 0)
1798 metadata.minimum_timeslice = 0;
1799 metadata.maximum_timeslice = 0;
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)
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);
1813 metadata.minimum_timeslice = minimum_timeslice;
1814 metadata.maximum_timeslice = maximum_timeslice;
1816 metadata.placement_fingerprint =
1817 canonical_placement_fingerprint(triangulation);
1818 metadata.topology_fingerprint =
1819 canonical_topology_fingerprint(triangulation);
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)
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;
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));
1852 template <
typename TriangulationType>
1853 void validate_serialized_payload(std::filesystem::path
const& filename,
1854 TriangulationType
const& original)
1856 if constexpr (
requires(std::istream& input, TriangulationType& value) {
1858 } && std::default_initializable<TriangulationType>)
1860 auto const parsed = parse_payload<TriangulationType>(filename);
1861 if constexpr (
requires(TriangulationType
const& value) {
1863 value.number_of_vertices();
1864 value.number_of_finite_edges();
1865 value.number_of_finite_facets();
1866 value.number_of_finite_cells();
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())
1878 throw std::filesystem::filesystem_error(
1879 "Serialized triangulation changed its incidence counts",
1881 std::make_error_code(std::errc::illegal_byte_sequence));
1883 if constexpr (HAS_CAUSAL_INFO<TriangulationType>)
1885 if (canonical_topology_fingerprint(parsed) !=
1886 canonical_topology_fingerprint(original))
1888 throw std::filesystem::filesystem_error(
1889 "Serialized triangulation changed its causal topology",
1891 std::make_error_code(std::errc::illegal_byte_sequence));
1895 else if constexpr (
requires(TriangulationType
const& left,
1896 TriangulationType
const& right) {
1897 { left == right } -> std::convertible_to<bool>;
1900 if (!(parsed == original))
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));
1910 inline void write_text(std::filesystem::path
const& filename,
1911 std::string_view
const contents)
1913 std::ofstream file(filename, std::ios::out | std::ios::trunc);
1914 if (!file.is_open())
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));
1923 throw std::filesystem::filesystem_error(
1924 "Could not serialize persistence metadata", filename,
1925 std::make_error_code(std::errc::io_error));
1930 throw std::filesystem::filesystem_error(
1931 "Could not flush persistence metadata", filename,
1932 std::make_error_code(std::errc::io_error));
1937 throw std::filesystem::filesystem_error(
1938 "Could not close persistence metadata", filename,
1939 std::make_error_code(std::errc::io_error));
1943 template <
typename TriangulationType>
1944 void write_payload(std::filesystem::path
const& filename,
1945 TriangulationType
const& triangulation,
1946 std::optional<Reproducibility_metadata>
const& metadata)
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";
1954 auto metadata_temporary = metadata_destination;
1955 metadata_temporary +=
".tmp";
1956 auto resolved_metadata = metadata;
1957 if (resolved_metadata)
1959 reconcile_payload_metadata(*resolved_metadata, triangulation);
1962 std::error_code cleanup_error;
1963 std::filesystem::remove(temporary, cleanup_error);
1964 std::filesystem::remove(metadata_temporary, cleanup_error);
1967 std::ofstream file(temporary, std::ios::out | std::ios::trunc);
1968 if (!file.is_open())
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));
1974 file << std::setprecision(std::numeric_limits<double>::max_digits10)
1976 write_causal_info(file, triangulation);
1979 throw std::filesystem::filesystem_error(
1980 "Could not serialize triangulation", filename,
1981 std::make_error_code(std::errc::io_error));
1986 throw std::filesystem::filesystem_error(
1987 "Could not flush serialized triangulation", filename,
1988 std::make_error_code(std::errc::io_error));
1993 throw std::filesystem::filesystem_error(
1994 "Could not close serialized triangulation", filename,
1995 std::make_error_code(std::errc::io_error));
1998 validate_serialized_payload(temporary, triangulation);
1999 if (resolved_metadata)
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)
2008 throw std::filesystem::filesystem_error(
2009 "Persistence metadata did not round-trip exactly",
2011 std::make_error_code(std::errc::illegal_byte_sequence));
2013 validate_persistence_metadata(recorded, triangulation, temporary,
2014 metadata_temporary);
2019 replace_file(metadata_temporary, metadata_destination);
2021 replace_file(temporary, filename);
2022 if (!resolved_metadata)
2024 std::filesystem::remove(metadata_destination, cleanup_error);
2029 std::filesystem::remove(temporary, cleanup_error);
2030 std::filesystem::remove(metadata_temporary, cleanup_error);
2045 std::chrono::system_clock::time_point
const timestamp =
2046 std::chrono::system_clock::now())
2048 auto const time = std::chrono::floor<std::chrono::seconds>(timestamp);
2049 return date::format(
"%Y-%m-%d.%TUTC", time);
2065 double t_initial_radius,
2066 double t_foliation_spacing)
2067 -> std::filesystem::path
2069 std::string filename;
2076 filename += std::to_string(t_dimension);
2080 filename += std::to_string(t_number_of_timeslices);
2084 filename += std::to_string(t_number_of_simplices);
2088 filename += std::to_string(t_initial_radius);
2092 filename += std::to_string(t_foliation_spacing);
2097 std::replace(timestamp.begin(), timestamp.end(),
':',
'-');
2098 filename += timestamp;
2109 template <
typename ManifoldType>
2112 return make_filename(ManifoldType::topology, ManifoldType::dimension,
2113 manifold.N3(), manifold.max_time(),
2114 manifold.initial_radius(),
2115 manifold.foliation_spacing());
2123 template <
typename ManifoldType>
2128 return base.parent_path() /
2129 (base.stem().
string() +
"-seed-" + std::to_string(seed.
value()) +
2130 base.extension().string());
2139 template <
typename ManifoldType>
2145 return base.parent_path() /
2146 (base.stem().
string() +
"-pass-" + std::to_string(completed_passes) +
2147 base.extension().string());
2154 template <
typename TriangulationType>
2158 "Triangulation has {} vertices and {} edges and {} faces and {} "
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());
2177 template <
typename TriangulationType>
2179 TriangulationType
const& triangulation)
2181 detail::write_payload(filename, triangulation, std::nullopt);
2200 template <
typename TriangulationType>
2202 TriangulationType
const& triangulation,
2204 { detail::write_payload(filename, triangulation, metadata); }
2210 template <
typename ManifoldType>
2212 ManifoldType
const& manifold) -> std::uint64_t
2214 auto const triangulation = manifold.delaunay_snapshot();
2215 return detail::canonical_topology_fingerprint(triangulation);
2222 template <
typename ManifoldType>
2224 ManifoldType
const& manifold) -> std::uint64_t
2226 auto const triangulation = manifold.delaunay_snapshot();
2227 return detail::canonical_placement_fingerprint(triangulation);
2236 template <
typename ManifoldType>
2242 return {.artifact = artifact,
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(),
2264 template <
typename ManifoldType>
2266 ManifoldType
const& manifold)
2268 metadata.
topology = ManifoldType::topology;
2269 metadata.
dimension = ManifoldType::dimension;
2290 template <
typename ManifoldType>
2293 std::filesystem::path filename;
2295 write_file(filename, t_universe.delaunay_snapshot());
2304 template <
typename ManifoldType>
2320 template <
typename ManifoldType>
2326 metadata.completed_passes = completed_passes;
2328 t_universe.delaunay_snapshot(), metadata);
2339 template <
typename ManifoldType>
2348 throw std::invalid_argument(
2349 "Checkpoint metadata must record completed passes.");
2354 write_file(filename, universe.delaunay_snapshot(), metadata);
2359 template <
typename TriangulationType>
2369 template <
typename TriangulationType>
2386 template <
typename TriangulationType>
2388 std::filesystem::path
const& filename)
2391 static std::mutex mutex;
2392 fmt::print(
"Reading initial triangulation from file {}\n",
2394 std::scoped_lock
const lock(mutex);
2395 auto const parsed_metadata = detail::validate_payload_integrity(filename);
2397 if (!parsed_metadata)
2399 throw std::filesystem::filesystem_error(
2400 "Initial triangulation requires a persistence metadata sidecar",
2402 std::make_error_code(std::errc::no_such_file_or_directory));
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));
2411 auto triangulation = detail::parse_payload<TriangulationType>(filename);
2412 detail::validate_persistence_metadata(*parsed_metadata, triangulation,
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)};
2423 template <
typename TriangulationType>
2436 template <
typename TriangulationType>
2440 static std::mutex mutex;
2441 fmt::print(
"Reading resumable checkpoint from file {}\n",
2443 std::scoped_lock
const lock(mutex);
2444 auto const parsed_metadata = detail::validate_payload_integrity(filename);
2446 if (!parsed_metadata)
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));
2453 !parsed_metadata->resume_supported)
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));
2460 auto triangulation = detail::parse_payload<TriangulationType>(filename);
2461 detail::validate_persistence_metadata(*parsed_metadata, triangulation,
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)};
2477 template <
typename TriangulationType>
2478 [[nodiscard]]
auto read_file(std::filesystem::path
const& filename)
2479 -> TriangulationType
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);
2488 detail::validate_persistence_metadata(*metadata, triangulation, filename,
2491 return triangulation;
2499 template <std::uniform_random_bit_generator Generator>
2500 [[nodiscard]]
inline auto die_roll(Generator& generator)
2503 std::uniform_int_distribution uniform_dist(1, 6);
2524 template <
typename NumberType,
class Distribution,
2525 std::uniform_random_bit_generator Generator>
2527 NumberType t_min_value,
2528 NumberType t_max_value)
2530 Distribution distribution(t_min_value, t_max_value);
2531 return distribution(generator);
2542 template <std::uniform_random_bit_generator Generator,
2543 std::integral IntegerType>
2545 IntegerType t_min_value,
2546 IntegerType t_max_value)
2548 using int_dist = std::uniform_int_distribution<IntegerType>;
2559 template <std::uniform_random_bit_generator Generator,
2560 std::integral IntegerType>
2562 IntegerType t_max_timeslice)
2577 template <std::uniform_random_bit_generator Generator,
2578 std::floating_point FloatingPointType>
2580 FloatingPointType t_min_value,
2581 FloatingPointType t_max_value)
2583 using real_dist = std::uniform_real_distribution<FloatingPointType>;
2592 template <std::uniform_random_bit_generator Generator>
2595 constexpr auto min = 0.0L;
2596 constexpr auto max = 1.0L;
2618 double const initial_radius,
double const foliation_spacing)
2621 if (points_per_timeslice <= 0 || timeslices <= 0)
2623 throw std::invalid_argument(
2624 "Population and timeslices must both be positive.");
2626 if (!std::isfinite(initial_radius) || initial_radius <= 0.0 ||
2627 !std::isfinite(foliation_spacing) || foliation_spacing <= 0.0)
2629 throw std::invalid_argument(
2630 "Layer radius and spacing must be finite and positive.");
2633 std::uint64_t total{};
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()))
2644 throw std::out_of_range(
2645 "A spherical layer exceeds the supported point-count range.");
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)
2651 throw std::out_of_range(
2652 "The generated input-vertex count exceeds uint64_t.");
2668 std::uint64_t
const vertices)
noexcept -> std::optional<std::uint64_t>
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; }
2679 first > std::numeric_limits<std::uint64_t>::max() / second)
2681 return std::nullopt;
2683 return first * second - 1;
2709 Int_precision t_number_of_timeslices,
double const initial_radius = 1.0,
2710 double const foliation_spacing = 1.0)
2713 spdlog::debug(
"{} simplices on {} timeslices desired.\n",
2714 t_number_of_simplices, t_number_of_timeslices);
2717 if (t_dimension != 3)
2719 throw std::invalid_argument(
2720 "Only three-dimensional triangulations are supported.");
2722 if (t_number_of_simplices <= 0 || t_number_of_timeslices <= 0)
2724 throw std::invalid_argument(
2725 "Simplices and timeslices must both be positive.");
2727 if (!std::isfinite(initial_radius) || initial_radius <= 0.0 ||
2728 !std::isfinite(foliation_spacing) || foliation_spacing <= 0.0)
2730 throw std::invalid_argument(
2731 "Layer radius and spacing must be finite and positive.");
2735 auto const minimum = t_number_of_simplices <= t_number_of_timeslices
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) *
2742 if (!std::isfinite(last_radius))
2744 throw std::out_of_range(
2745 "The final foliation radius exceeds the supported range.");
2747 auto const maximum_by_radius =
2748 static_cast<long double>(std::numeric_limits<Int_precision>::max()) /
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()
2755 if (maximum < minimum)
2757 throw std::out_of_range(
2758 "Foliation radii leave no supported base population.");
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);
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))
2779 throw std::out_of_range(
2780 "Requested simplex estimate exceeds the supported population.");
2782 auto const population =
static_cast<Int_precision>(estimated);
2784 population, t_number_of_timeslices, initial_radius, foliation_spacing));
2815 Int_precision const timeslices,
double const initial_radius,
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;
2824 points_per_timeslice, timeslices, initial_radius, foliation_spacing);
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 =
2844 {
return t_value.to_double(); }
2874 auto const console_sink =
2875 std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
2876 console_sink->set_level(spdlog::level::info);
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);
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);
2886 spdlog::sinks_init_list sink_list = {console_sink, debug_sink, trace_sink};
2888 auto const logger = std::make_shared<spdlog::logger>(
2889 "multi_sink", sink_list.begin(), sink_list.end());
2891 logger->set_level(spdlog::level::trace);
2893 logger->info(
"Multi-sink logger initialized.\n");
2894 logger->debug(
"Debug logger initialized.\n");
2895 logger->trace(
"Trace logger initialized.\n");
2897 "You must build in Debug mode for anything to be recorded in this "
2900 register_logger(logger);
2901 set_default_logger(logger);
2903 catch (spdlog::spdlog_ex
const& ex)
2906 spdlog::error(
"Logger initialization failed: {}\n", ex.what());
2907 spdlog::warn(
"Default logger set.\n");
2915 template <
typename Po
int>
2918 std::stringstream stream;
2920 return stream.str();
2929 std::stringstream stream;
2930 stream << t_topology;
2931 return stream.str();
Vertex_handle_t< 3 > Vertex_handle
Three-dimensional CGAL vertex handle.
Cell_handle_t< 3 > Cell_handle
Three-dimensional CGAL cell handle.
Run-owned random-number generation and reproducible stream splitting.
constexpr RandomStream initialization
Stream reserved for initial triangulation generation.
constexpr RandomStream transitions
Stream reserved for stochastic state transitions.
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.
@ 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.
Root entropy value used to reproduce a random run.
constexpr auto value() const noexcept -> std::uint64_t
PCG sequence selector derived from a root random seed.
clang-15 does not support std::format
Topology
Spatial-topology label stored by configuration and persistence APIs.
@ TOROIDAL
Reserved for toroidal slices; construction is unsupported.
@ SPHERICAL
Supported spherical spatial slices.
auto operator<<(std::ostream &output, RandomSeed const seed) -> std::ostream &
std::int32_t Int_precision
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.
A validated triangulation artifact and its provenance.
Reproducibility_metadata metadata
Validated artifact metadata.
TriangulationType triangulation
Validated causal payload.