https://github.com/tue-alga/CoordinatedSchematization
Tip revision: 9ffc292c946498d56f7938a86628adba534a9085 authored by TUE\bacusters on 01 July 2021, 09:20:10 UTC
Add base schematization library
Add base schematization library
Tip revision: 9ffc292
BidirectionalBruteforceTrajectoryBundler.h
#ifndef SCHEMATLIB_TRAJECTORYSCHEMATIZATION_BIDIRECTIONALBRUTEFORCETRAJECTORYBUNDLER_H
#define SCHEMATLIB_TRAJECTORYSCHEMATIZATION_BIDIRECTIONALBRUTEFORCETRAJECTORYBUNDLER_H
#include <GCpp/DS/BoostEmbeddedGraph.h>
#include <GCpp/DS/Heap.h>
#include <SchematLib/IO/settings_io.h>
#include <optional>
#include <queue>
#include <set>
#include <tuple>
#include <type_traits>
#include <vector>
#include "TrajectoryBundleTraits.h"
namespace SchematLib::TrajectorySchematization {
struct IsSuperSequence {
template <typename It, typename It2>
bool operator()(It seq0Start, It seq0End, It2 seq1Start, It2 seq1End) const {
auto size0 = std::distance(seq0Start, seq0End);
auto size1 = std::distance(seq1Start, seq1End);
// First sequence is smaller, so cannot be super sequence
if (size0 < size1)
return false;
// Find start in first sequence of second
auto start0 = std::find(seq0Start, seq0End, *seq1Start);
// If not found, or subsequence cannot fully match seq1, seq0 cannot be a supersequence.
if (start0 == seq0End || std::distance(start0, seq0End) < size1)
return false;
// Run over the sequences, comparing elements
auto curr1 = seq1Start;
auto curr0 = start0;
for (; curr0 != seq0End && curr1 != seq1End; ++curr0, ++curr1) {
if (*curr0 != *curr1)
return false;
}
return curr1 == seq1End;
}
};
template <typename EmbeddedGraph>
class BidirectionalBruteforceTrajectoryBundler {
public:
enum class WeighingScheme { Length, Support, LengthAndSupport, Custom };
struct Settings {
// std::size_t m_minimum_bundle_element_complexity = 3;
std::size_t m_max_bundle_set_complexity = 10;
Models::NT m_minimum_bundle_length = 1000; // meters assumed
// Minimum match count for first class.
std::size_t m_min_matched_trajectories = 10;
bool m_priotizie_by_length = false;
bool m_verbose = false;
bool m_weigh_edges_by_bundles = false;
bool m_superbundles_only = false;
// Capture overlapping trajectories that differ by at most the given amount
bool m_match_intermediate_to_bundle = false;
Models::NT m_max_intermediate_bundle_difference = 500;
Models::NT m_weight_exponent = 1.0;
WeighingScheme m_weighingScheme = WeighingScheme::Length;
};
private:
using SEF = MapSimplification::SpecialEdgeIdFunctions;
struct TrajectoryEdge {
std::size_t trajectory;
std::size_t edgeNum;
bool operator<(const TrajectoryEdge& other) const {
return trajectory == other.trajectory ? edgeNum < other.edgeNum : trajectory < other.trajectory;
}
bool operator==(const TrajectoryEdge& other) const {
return trajectory == other.trajectory && edgeNum == other.edgeNum;
}
};
using Edge = typename EmbeddedGraph::edge_descriptor;
using GT = boost::graph_traits<EmbeddedGraph>;
using Vertex = typename EmbeddedGraph::vertex_descriptor;
struct PathEdge {
enum class Explore { Source, Target, Both };
Edge edge;
Explore toExplore;
Vertex vertexToExplore(const EmbeddedGraph& graph) const {
if (toExplore == Explore::Source)
return boost::source(edge, graph);
if (toExplore == Explore::Target)
return boost::target(edge, graph);
throw std::runtime_error("Vertex to explore only works for Source/Target");
}
};
// Semantically tag the weight to avoid confusion with IDs of std::size_t
struct EdgeWeight {
std::size_t value;
bool operator<(const EdgeWeight& other) const { return value < other.value; }
bool operator>(const EdgeWeight& other) const { return value > other.value; }
bool operator==(const EdgeWeight& other) const { return value == other.value; }
};
enum class Direction { Forward = 0, Backward = 1 };
/**
* \brief Matcher object for a trajectory, in a particular direction.
*/
struct TrajectoryMatch {
std::size_t trajectoryIndex;
std::size_t startEdgeIndex;
std::size_t endEdgeIndex;
Models::NT pathLength = 0;
bool matches(Direction direction,
std::size_t edgeId,
const std::vector<std::vector<std::size_t>>& trajectories) const {
if (direction == Direction::Backward) {
if (startEdgeIndex == 0)
return false;
return trajectories[trajectoryIndex][startEdgeIndex - 1] == edgeId;
} else {
if (endEdgeIndex == trajectories[trajectoryIndex].size() - 1)
return false;
return trajectories[trajectoryIndex][endEdgeIndex + 1] == edgeId;
}
}
void extend(Direction direction) {
if (direction == Direction::Backward)
--startEdgeIndex;
else
++endEdgeIndex;
}
};
using MatchingMap = std::map<std::size_t, TrajectoryMatch>;
struct ComputationData {
// Edge IDs of the given graph
std::unordered_set<std::size_t> edgeIds;
std::map<std::size_t, EdgeWeight> edgeWeights;
// Indexed by edgeID, then trajectory index.
std::map<std::size_t, std::map<std::size_t, TrajectoryEdge>> edgeIdToTrajectoryEdges;
std::map<std::size_t, std::set<std::size_t>> edgeIdToTrajectoryNum;
std::map<std::size_t, typename GT::edge_descriptor> edgeIdToEdgeMap;
std::map<std::size_t, std::size_t> edgeIdToBundleCount;
std::vector<Models::NT> m_trajectoryLengths;
const EmbeddedGraph& graph;
ComputationData(const EmbeddedGraph& inputGraph) : graph(inputGraph) { GCpp::DS::computeEdgeIdSet(graph, edgeIds); }
Models::NT get_trajectory_length(std::size_t trajectoryNum) const { return m_trajectoryLengths[trajectoryNum]; }
void precomputeComputationData(const std::vector<std::vector<std::size_t>>& trajectories) {
GCpp::DS::computeEdgeIdToEdgeMap(graph, edgeIdToEdgeMap);
std::size_t trajectoryNum = 0;
for (const auto& traj : trajectories) {
std::size_t currentEdge = 0;
Models::NT totalLength = 0;
for (auto e : traj) {
if (edgeIdToTrajectoryNum.find(e) == edgeIdToTrajectoryNum.end()) {
edgeIdToTrajectoryNum[e] = {};
}
edgeIdToTrajectoryNum[e].insert(trajectoryNum);
if (edgeIds.find(e) == edgeIds.end())
throw std::runtime_error("Trajectory off-road. Clean your data first!");
if (edgeWeights.find(e) == edgeWeights.end())
edgeWeights[e] = EdgeWeight{0};
++edgeWeights[e].value;
if (edgeIdToTrajectoryEdges.find(e) == edgeIdToTrajectoryEdges.end())
edgeIdToTrajectoryEdges[e] = {};
edgeIdToTrajectoryEdges[e][trajectoryNum] = TrajectoryEdge{trajectoryNum, currentEdge};
totalLength += getWeightedEdgeLength(e, false);
++currentEdge;
}
m_trajectoryLengths.push_back(totalLength);
++trajectoryNum;
}
}
std::size_t bundleCount(std::size_t edge_id) const {
return edgeIdToBundleCount.find(edge_id) == edgeIdToBundleCount.end() ? 0 : edgeIdToBundleCount.at(edge_id);
}
Models::NT getWeightedEdgeLength(std::size_t edgeId, bool weight_by_bundles = false) const {
auto weight = GCpp::DS::get_linear_edge_length(edgeIdToEdgeMap.at(edgeId), graph);
if (weight_by_bundles) {
return weight / (Models::NT{1} + Models::NT(bundleCount(edgeId)));
}
return weight;
}
void updatePriority(const std::vector<std::size_t>& bundleEdges, const MatchingMap& trajectories) {
for (auto e : bundleEdges) {
edgeWeights[e] = EdgeWeight{edgeWeights[e].value - trajectories.size()};
if (edgeIdToBundleCount.find(e) == edgeIdToBundleCount.end())
edgeIdToBundleCount[e] = 0;
edgeIdToBundleCount[e] += 1;
}
}
/**
* \brief Update edge ID to trajectories mapping by removing the given trajectories for the given edges.
* \param edges The dges
* \param trajectories The trajectories.
*/
void updateEdgeIdToTrajectoryMapping(const std::vector<std::size_t>& edges, const MatchingMap& trajectories) {
for (auto edge : edges) {
auto& trajectorySet = edgeIdToTrajectoryNum[edge];
auto& edgeToTrajectoryEdges = edgeIdToTrajectoryEdges[edge];
for (const auto& [trajId, trajMatching] : trajectories) {
trajectorySet.erase(trajId);
edgeToTrajectoryEdges.erase(trajId);
}
}
}
};
static bool isConnected(typename EmbeddedGraph::vertex_descriptor v,
typename EmbeddedGraph::edge_descriptor e,
const EmbeddedGraph& g) {
return boost::source(e, g) == v || boost::target(e, g) == v;
}
struct ProcessState {
std::vector<PathEdge> currentPath;
MatchingMap activeTrajectories;
typename EmbeddedGraph::edge_descriptor edge;
typename EmbeddedGraph::vertex_descriptor endVertex;
std::size_t edgeId;
Models::NT pathLength;
std::size_t depth;
};
bool isBetter(const ProcessState& s0, const ProcessState& s1) const {
return isBetter(std::make_pair(s0.pathLength, s0.activeTrajectories.size()),
std::make_pair(s1.pathLength, s1.activeTrajectories.size()));
}
bool isBetterBundle(const TrajectoryBundle& b0, const TrajectoryBundle& b1) const {
return isBetter(std::make_pair(b0.bundleLength, b0.matchedTrajectoriesCount),
std::make_pair(b1.bundleLength, b1.matchedTrajectoriesCount));
}
bool isBetter(const std::pair<Models::NT, std::size_t>& lenAndSize0,
const std::pair<Models::NT, std::size_t>& lenAndSize1) const {
auto weight = [this](Models::NT val, bool oneMinus = false) {
return std::pow<Models::NT>(
val, oneMinus ? 1.0 - m_currentSettings.m_weight_exponent : m_currentSettings.m_weight_exponent);
};
switch (m_currentSettings.m_weighingScheme) {
case WeighingScheme::Length: return lenAndSize0.first > lenAndSize1.first;
case WeighingScheme::Support: return lenAndSize0.second > lenAndSize1.second;
case WeighingScheme::LengthAndSupport:
return lenAndSize0.second * lenAndSize0.first > lenAndSize1.second * lenAndSize1.first;
case WeighingScheme::Custom:
return weight(lenAndSize0.first) * weight(lenAndSize0.second, true) >
weight(lenAndSize1.first) * weight(lenAndSize1.second, true);
default: throw std::runtime_error("Invalid scheme!");
}
}
/**
* \brief Search for a bundle starting with the edge, given as an ID. Outputs bundle, its score and matched
* trajectories. Returns whether a bundle was found \param edgeID ID of the edge to start at \param data Computation
* data that is needed \param graph The graph \param outputBundle The output bundle \param outputScore The output
* score \param matchedTrajectories The output matched trajectories. \return Whether a bundle was found satisfying the
* criteria.
*/
bool searchForBundle(std::size_t edgeID,
const ComputationData& data,
const std::vector<std::vector<std::size_t>>& mmTrajectories,
std::size_t min_bundle_count,
TrajectoryBundle& bundle,
MatchingMap& matchedTrajectories) const {
if (data.edgeIdToTrajectoryNum.find(edgeID) == data.edgeIdToTrajectoryNum.end()) {
if (m_currentSettings.m_verbose) {
std::cout << "\tNo trajectories\n";
}
return false;
}
if (m_currentSettings.m_verbose) {
std::cout << "###############################################\n";
std::cout << "############### New edge " << edgeID << " ##########################\n";
std::cout << "###############################################\n";
}
namespace it = GCpp::Helpers::Iterators;
auto searchV = boost::target(data.edgeIdToEdgeMap.at(edgeID), data.graph);
ProcessState initState{std::vector<PathEdge>{PathEdge{data.edgeIdToEdgeMap.at(edgeID), PathEdge::Explore::Both}},
{},
data.edgeIdToEdgeMap.at(edgeID),
searchV,
edgeID,
data.getWeightedEdgeLength(edgeID, m_currentSettings.m_weigh_edges_by_bundles),
0};
// Initial matched trajectories.
for (auto el : data.edgeIdToTrajectoryNum.at(edgeID)) {
const auto edgeIndex = data.edgeIdToTrajectoryEdges.at(edgeID).at(el).edgeNum;
initState.activeTrajectories[el] = TrajectoryMatch{
el, edgeIndex, edgeIndex, data.getWeightedEdgeLength(edgeID, m_currentSettings.m_weigh_edges_by_bundles)};
}
if (m_currentSettings.m_verbose) {
for (auto d = 0; d < initState.depth; ++d) std::cout << "##|";
std::cout << "Init bundle of length " << initState.pathLength
<< ", matches: " << initState.activeTrajectories.size() << "/" << initState.activeTrajectories.size()
<< '\n';
}
std::queue<ProcessState> elementsToProcess;
std::optional<ProcessState> bestState;
if (initState.activeTrajectories.size() >= min_bundle_count &&
initState.pathLength >= m_currentSettings.m_minimum_bundle_length) {
bestState = initState;
}
elementsToProcess.push(initState);
// Keep going while our score improves.
while (!elementsToProcess.empty()) {
auto state = elementsToProcess.front();
elementsToProcess.pop();
for (auto e : GCpp::Helpers::Iterators::range(boost::out_edges(state.endVertex, data.graph))) {
auto eID = GCpp::DS::getEdgeId(data.graph, e);
// Don't go to self
if (eID == state.edgeId || eID == SEF::flipCanonical(state.edgeId))
continue;
// Skip edges without trajectories.
if (data.edgeIdToTrajectoryNum.find(eID) == data.edgeIdToTrajectoryNum.end())
continue;
// Maybe weigh edges.
const auto edgeLength = data.getWeightedEdgeLength(eID, m_currentSettings.m_weigh_edges_by_bundles);
const auto newPathLength =
state.pathLength + data.getWeightedEdgeLength(eID, m_currentSettings.m_weigh_edges_by_bundles);
ProcessState newState = state;
newState.currentPath.push_back(PathEdge{e, PathEdge::Explore::Both});
newState.activeTrajectories = {};
for (const auto& kv : state.activeTrajectories) {
if (!kv.second.matches(Direction::Forward, eID, mmTrajectories)) {
continue;
}
auto copyVal = kv.second;
copyVal.extend(Direction::Forward);
copyVal.pathLength += edgeLength;
newState.activeTrajectories[kv.first] = copyVal;
}
newState.edge = e;
newState.edgeId = eID;
++newState.depth;
newState.endVertex = boost::source(e, data.graph) != state.endVertex ? boost::source(e, data.graph)
: boost::target(e, data.graph);
newState.pathLength += data.getWeightedEdgeLength(eID, m_currentSettings.m_weigh_edges_by_bundles);
if (newState.activeTrajectories.size() >= min_bundle_count) {
if (m_currentSettings.m_verbose) {
for (auto d = 0; d < newState.depth; ++d) std::cout << "##|";
std::cout << "Checking bundle of length " << newState.pathLength
<< ", matches: " << newState.activeTrajectories.size() << "/" << state.activeTrajectories.size()
<< '\n';
}
if (newState.pathLength >= m_currentSettings.m_minimum_bundle_length) {
if (!bestState.has_value() || this->isBetter(newState, bestState.value())) {
bestState = newState;
}
}
elementsToProcess.push(newState);
} else {
if (m_currentSettings.m_verbose) {
for (auto d = 0; d < newState.depth; ++d) std::cout << "##|";
std::cout << "Too litle matches: bundle of length " << newState.pathLength
<< ", matches: " << newState.activeTrajectories.size() << "/" << state.activeTrajectories.size()
<< '\n';
}
}
}
}
if (!bestState.has_value()) {
return false;
}
matchedTrajectories = bestState.value().activeTrajectories;
bundle.matchedTrajectoriesCount = matchedTrajectories.size();
bundle.edges.clear();
bundle.bundleLength = bestState.value().pathLength;
for (auto el : bestState.value().currentPath) {
bundle.edges.push_back(GCpp::DS::getEdgeId(data.graph, el.edge));
}
// std::cout << "\tBundle with " << bundle.matchedTrajectoriesCount << " matched, length " << bundle.bundleLength <<
// '\n';
// Succesful path of required minimum length found.
return bestState.value().pathLength >= m_currentSettings.m_minimum_bundle_length &&
matchedTrajectories.size() >= min_bundle_count;
}
/**
* \brief Check if b0 is a superbundle of b1, that is, b0 contains all edges of b1 (in order).
* \param b0 The first bundle
* \param b1 The second bundle
* \return b0 is a superbundle of b1 (or not)
*/
static bool isSuperBundle(const TrajectoryBundle& b0, const TrajectoryBundle& b1) {
if (b0.edges.size() < b1.edges.size())
return false;
auto start = std::find(b0.edges.begin(), b0.edges.end(), b1.edges.front());
if (start == b0.edges.end() || std::distance(start, b0.edges.end()) < b1.edges.size())
return false;
auto b1It = b1.edges.begin();
for (; start != b0.edges.end() && b1It != b1.edges.end(); ++start, ++b1It) {
if (*b1It != *start)
return false;
}
return b1It == b1.edges.end();
}
// Settings object
Settings m_currentSettings;
public:
void setSettings(const Settings& settings) { m_currentSettings = settings; }
const Settings& settings() const { return m_currentSettings; }
#pragma region Parameters
void set_weigh_edges_by_bundles(bool val) { m_currentSettings.m_weigh_edges_by_bundles = val; }
bool get_weigh_edges_by_bundles() const { return m_currentSettings.m_weigh_edges_by_bundles; }
void set_superbundles_only(bool val) { m_currentSettings.m_superbundles_only = val; }
void set_weighing_scheme(WeighingScheme scheme) { m_currentSettings.m_weighingScheme = scheme; }
WeighingScheme weighing_scheme() const { return m_currentSettings.m_weighingScheme; }
bool get_superbundles_only() const { return m_currentSettings.m_superbundles_only; }
void set_verbose(bool value) { m_currentSettings.m_verbose = value; }
bool get_verbose() const { return m_currentSettings.m_verbose; }
void set_priotizie_by_length(bool val) { m_currentSettings.m_priotizie_by_length = val; }
bool get_priotizie_by_length() const { return m_currentSettings.m_priotizie_by_length; }
void set_minimum_bundle_length(const Models::NT& minimum_bundle_length) {
m_currentSettings.m_minimum_bundle_length = minimum_bundle_length;
}
Models::NT get_minimum_bundle_length() const { return m_currentSettings.m_minimum_bundle_length; }
void set_max_bundle_set_complexity(const std::size_t& max_bundle_complexity) {
m_currentSettings.m_max_bundle_set_complexity = max_bundle_complexity;
}
std::size_t get_max_bundle_set_complexity() const { return m_currentSettings.m_max_bundle_set_complexity; }
void set_weight_exponent(const Models::NT& exponent) { m_currentSettings.m_weight_exponent = exponent; }
Models::NT weight_exponent() const { return m_currentSettings.m_weight_exponent; }
void set_min_matched_trajectories(const std::size_t& min_match) {
m_currentSettings.m_min_matched_trajectories = min_match;
}
std::size_t get_min_matched_trajectories() const { return m_currentSettings.m_min_matched_trajectories; }
#pragma endregion
/**
* \brief
* \param graph Input simplified graph
* \param trajectories Given as list of edge IDs.
* \param output Vector of trajectory bundles, that contain score and bundle as list of edge IDs
*/
void operator()(const EmbeddedGraph& graph,
const std::vector<std::vector<std::size_t>>& trajectories,
std::vector<TrajectoryBundle>& output) const {
// Compute data
ComputationData data(graph);
data.precomputeComputationData(trajectories);
std::cout << "[BruteforceBundling] Starting bundling\n";
print_max_trajectory(data, trajectories);
if (!graph.m_use_canonical_edges_ids)
throw std::runtime_error("Not using canonical edge IDS!");
verify_connectivity(data, trajectories);
// Allow getting smaller, increasing the class
std::size_t current_bundle_count = m_currentSettings.m_min_matched_trajectories;
std::size_t currentClass = 0;
while (output.size() < m_currentSettings.m_max_bundle_set_complexity) {
TrajectoryBundle bundle, bestBundle;
MatchingMap matchedTrajectories, bestMatchedTrajectories;
const std::size_t edgeNum = boost::num_edges(data.graph);
std::size_t currEdge = 0;
std::size_t perc = 5;
const std::size_t percInc = 5;
// Go over all directed edges (2 per undirected)
for (auto e : GCpp::Helpers::Iterators::range(boost::edges(data.graph))) {
bundle = {};
matchedTrajectories = {};
if (searchForBundle(GCpp::DS::getEdgeId(data.graph, e),
data,
trajectories,
current_bundle_count,
bundle,
matchedTrajectories)) {
if (isBetterBundle(bundle, bestBundle)) {
bestBundle = bundle;
bestMatchedTrajectories = matchedTrajectories;
}
}
++currEdge;
if (currEdge * 100 > edgeNum * perc) {
std::cout << "[BruteforceBundling] At " << output.size() << ", " << perc << "%\n";
perc += percInc;
}
}
if (bestBundle.bundleLength > m_currentSettings.m_minimum_bundle_length) {
bestBundle.supportClass = currentClass;
data.updateEdgeIdToTrajectoryMapping(bestBundle.edges, bestMatchedTrajectories);
data.updatePriority(bestBundle.edges, bestMatchedTrajectories);
// TODO: cross class superbundles?
if (m_currentSettings.m_superbundles_only) {
bool shouldBeAdded = true;
// Check if current is a subbundle or superbundle of someone else.
for (std::size_t i = 0; i < output.size(); ++i) {
if (isSuperBundle(output[i], bestBundle)) {
shouldBeAdded = false;
break;
} else if (isSuperBundle(bestBundle, output[i])) {
// Replace in output
output[i] = bestBundle;
shouldBeAdded = false;
break;
}
// TODO: track where they go?
}
if (shouldBeAdded) {
output.push_back(bestBundle);
}
} else {
output.push_back(bestBundle);
}
} else {
++currentClass;
current_bundle_count /= 2;
if (current_bundle_count == 1)
break;
}
}
std::cout << "[BruteforceBundling] Found " << output.size() << "/" << m_currentSettings.m_max_bundle_set_complexity
<< " bundles \n";
for (const auto& bundle : output) {
std::cout << "[BruteforceBundling] Bundle with length " << bundle.bundleLength << " and support "
<< bundle.matchedTrajectoriesCount << '\n';
}
}
private:
void print_max_trajectory(const ComputationData& data,
const std::vector<std::vector<std::size_t>>& trajectories) const {
Models::NT maxLength = 0;
for (const auto& traj : trajectories) {
Models::NT localLength = 0;
for (auto e : traj) {
localLength += GCpp::DS::get_linear_edge_length(data.edgeIdToEdgeMap.at(e), data.graph);
}
maxLength = std::max(maxLength, localLength);
}
std::cout << "[BruteforceBundling] Max length trajectory" << maxLength << "\n";
}
void verify_connectivity(const ComputationData& data,
const std::vector<std::vector<std::size_t>>& trajectories) const { // Verify connectivity
for (const auto& t : trajectories) {
for (std::size_t i = 1; i < t.size(); ++i) {
auto e0 = data.edgeIdToEdgeMap.at(t[i - 1]);
auto e1 = data.edgeIdToEdgeMap.at(t[i]);
if (boost::source(e1, data.graph) != boost::target(e0, data.graph)) {
std::cout << GCpp::DS::getEdgeId(data.graph, e0) << "|" << GCpp::DS::getEdgeId(data.graph, e1) << '\n';
std::cout << "Inv:" << SEF::flipCanonical(GCpp::DS::getEdgeId(data.graph, e0)) << "|"
<< SEF::flipCanonical(GCpp::DS::getEdgeId(data.graph, e1));
throw std::runtime_error("Disconnected path");
}
}
}
}
};
} // namespace SchematLib::TrajectorySchematization
namespace SchematLib::IO {
template <typename EmbeddedGraph>
void read_settings(
boost::property_tree::ptree& data,
typename TrajectorySchematization::BidirectionalBruteforceTrajectoryBundler<EmbeddedGraph>::Settings& settings) {
#define READ_MEM(member) settings.m_member = data.get<decltype(settings.member)>(#member)
READ_MEM(min_matched_trajectories);
READ_MEM(minimum_bundle_length);
READ_MEM(verbose);
}
} // namespace SchematLib::IO
#endif