Spaces:
Configuration error
Configuration error
| namespace { | |
| struct Arguments { | |
| std::string engine; | |
| std::string input; | |
| std::string input_bgr; | |
| std::string dump_prefix; | |
| std::string lanes_json; | |
| std::string bev_json; | |
| std::string report; | |
| std::string frames_jsonl; | |
| bool raw_bgr_stdin{false}; | |
| int source_width{1920}; | |
| int source_height{1080}; | |
| int max_frames{0}; | |
| int timing_warmup{5}; | |
| int warmup{10}; | |
| int iterations{100}; | |
| int threads{8}; | |
| rclane::BevConfig bev_config; | |
| bool bev_mode_explicit{false}; | |
| }; | |
| Arguments parse_arguments(int argc, char** argv) { | |
| Arguments args; | |
| const auto select_bev_mode = [&args]( | |
| rclane::BevMode mode, const std::string& option | |
| ) { | |
| if (args.bev_mode_explicit && args.bev_config.mode != mode) { | |
| throw std::invalid_argument( | |
| "conflicting BEV mode option: " + option | |
| ); | |
| } | |
| args.bev_config.mode = mode; | |
| args.bev_mode_explicit = true; | |
| }; | |
| for (int index = 1; index < argc; ++index) { | |
| const std::string option = argv[index]; | |
| const auto value = [&]() -> std::string { | |
| if (++index >= argc) { | |
| throw std::invalid_argument("missing value for " + option); | |
| } | |
| return argv[index]; | |
| }; | |
| if (option == "--engine") { | |
| args.engine = value(); | |
| } else if (option == "--input-nchw") { | |
| args.input = value(); | |
| } else if (option == "--input-bgr") { | |
| args.input_bgr = value(); | |
| } else if (option == "--raw-bgr-stdin") { | |
| args.raw_bgr_stdin = true; | |
| } else if (option == "--dump-prefix") { | |
| args.dump_prefix = value(); | |
| } else if (option == "--lanes-json") { | |
| args.lanes_json = value(); | |
| } else if (option == "--bev-json") { | |
| args.bev_json = value(); | |
| } else if (option == "--report") { | |
| args.report = value(); | |
| } else if (option == "--frames-jsonl") { | |
| args.frames_jsonl = value(); | |
| } else if (option == "--source-width") { | |
| args.source_width = std::stoi(value()); | |
| } else if (option == "--source-height") { | |
| args.source_height = std::stoi(value()); | |
| } else if (option == "--max-frames") { | |
| args.max_frames = std::stoi(value()); | |
| } else if (option == "--timing-warmup") { | |
| args.timing_warmup = std::stoi(value()); | |
| } else if (option == "--warmup") { | |
| args.warmup = std::stoi(value()); | |
| } else if (option == "--iterations") { | |
| args.iterations = std::stoi(value()); | |
| } else if (option == "--threads") { | |
| args.threads = std::stoi(value()); | |
| } else if (option == "--bev-mode") { | |
| select_bev_mode(rclane::parse_bev_mode(value()), option); | |
| } else if (option == "--disable-parallel-repair") { | |
| select_bev_mode(rclane::BevMode::Raw, option); | |
| } else if (option == "--parallel-repair") { | |
| select_bev_mode(rclane::BevMode::Trigger, option); | |
| } else if (option == "--always-parallel-repair") { | |
| select_bev_mode(rclane::BevMode::AlwaysParallel, option); | |
| } else if (option == "--complete-four-parallel-lanes") { | |
| select_bev_mode(rclane::BevMode::CompleteFour, option); | |
| } else if (option == "--nominal-lane-width") { | |
| args.bev_config.nominal_lane_width = std::stod(value()); | |
| } else if (option == "--repair-trigger-gap-ratio") { | |
| args.bev_config.trigger_gap_ratio = std::stod(value()); | |
| } else if (option == "--repair-minimum-gap") { | |
| args.bev_config.minimum_gap = std::stod(value()); | |
| } else if (option == "--repair-minimum-run") { | |
| args.bev_config.minimum_bad_run = std::stod(value()); | |
| } else if (option == "--repair-max-reference-extrapolation") { | |
| args.bev_config.maximum_reference_extrapolation = std::stod(value()); | |
| } else if (option == "--funnel-margin") { | |
| args.bev_config.funnel_margin = std::stod(value()); | |
| } else { | |
| throw std::invalid_argument("unknown argument: " + option); | |
| } | |
| } | |
| const int input_modes = static_cast<int>(!args.input.empty()) | |
| + static_cast<int>(!args.input_bgr.empty()) | |
| + static_cast<int>(args.raw_bgr_stdin); | |
| if (args.engine.empty() || input_modes != 1 | |
| || args.source_width <= 0 || args.source_height <= 0 | |
| || args.timing_warmup < 0 || args.max_frames < 0 | |
| || args.bev_config.nominal_lane_width <= 0.0 | |
| || args.bev_config.trigger_gap_ratio <= 0.0 | |
| || args.bev_config.trigger_gap_ratio >= 1.0 | |
| || args.bev_config.minimum_gap <= 0.0 | |
| || args.bev_config.minimum_bad_run < 0.0 | |
| || args.bev_config.maximum_reference_extrapolation < 0.0 | |
| || args.bev_config.funnel_margin < 0.0) { | |
| throw std::invalid_argument( | |
| "usage: rclane_runtime --engine model.engine " | |
| "(--input-nchw frame.f32 | --input-bgr frame.bgr | " | |
| "--raw-bgr-stdin) " | |
| "[--bev-mode raw|trigger|always-parallel|complete-four] " | |
| "[--dump-prefix output]" | |
| ); | |
| } | |
| return args; | |
| } | |
| std::vector<float> read_floats( | |
| const std::string& path, std::size_t expected | |
| ) { | |
| std::ifstream stream(path, std::ios::binary | std::ios::ate); | |
| if (!stream) { | |
| throw std::runtime_error("cannot open input: " + path); | |
| } | |
| const auto byte_count = static_cast<std::size_t>(stream.tellg()); | |
| if (byte_count != expected * sizeof(float)) { | |
| throw std::runtime_error( | |
| "input byte count mismatch: expected " | |
| + std::to_string(expected * sizeof(float)) + ", got " | |
| + std::to_string(byte_count) | |
| ); | |
| } | |
| std::vector<float> values(expected); | |
| stream.seekg(0); | |
| stream.read( | |
| reinterpret_cast<char*>(values.data()), | |
| static_cast<std::streamsize>(byte_count) | |
| ); | |
| return values; | |
| } | |
| std::vector<std::uint8_t> read_bytes( | |
| const std::string& path, std::size_t expected | |
| ) { | |
| std::ifstream stream(path, std::ios::binary | std::ios::ate); | |
| if (!stream) { | |
| throw std::runtime_error("cannot open input: " + path); | |
| } | |
| const auto byte_count = static_cast<std::size_t>(stream.tellg()); | |
| if (byte_count != expected) { | |
| throw std::runtime_error( | |
| "BGR byte count mismatch: expected " + std::to_string(expected) | |
| + ", got " + std::to_string(byte_count) | |
| ); | |
| } | |
| std::vector<std::uint8_t> bytes(expected); | |
| stream.seekg(0); | |
| stream.read(reinterpret_cast<char*>(bytes.data()), | |
| static_cast<std::streamsize>(byte_count)); | |
| return bytes; | |
| } | |
| void dump_outputs( | |
| const std::string& prefix, | |
| const std::unordered_map<std::string, rclane::Tensor>& outputs | |
| ) { | |
| if (prefix.empty()) { | |
| return; | |
| } | |
| for (const auto& [name, tensor] : outputs) { | |
| const std::string path = prefix + "." + name + ".f32"; | |
| std::ofstream stream(path, std::ios::binary); | |
| if (!stream) { | |
| throw std::runtime_error("cannot write output: " + path); | |
| } | |
| stream.write( | |
| reinterpret_cast<const char*>(tensor.values.data()), | |
| static_cast<std::streamsize>( | |
| tensor.values.size() * sizeof(float) | |
| ) | |
| ); | |
| } | |
| } | |
| void dump_input(const std::string& prefix, const std::vector<float>& input) { | |
| if (prefix.empty()) { | |
| return; | |
| } | |
| std::ofstream stream(prefix + ".input.f32", std::ios::binary); | |
| if (!stream) { | |
| throw std::runtime_error("cannot write normalized input"); | |
| } | |
| stream.write( | |
| reinterpret_cast<const char*>(input.data()), | |
| static_cast<std::streamsize>(input.size() * sizeof(float)) | |
| ); | |
| } | |
| struct TimingSummary { | |
| double mean{}; | |
| double median{}; | |
| double p95{}; | |
| double minimum{}; | |
| double maximum{}; | |
| }; | |
| TimingSummary summarize(std::vector<double> values) { | |
| if (values.empty()) { | |
| throw std::runtime_error("no timed frames after warmup"); | |
| } | |
| std::sort(values.begin(), values.end()); | |
| const auto percentile = [&values](double fraction) { | |
| const auto position = static_cast<std::size_t>(std::floor( | |
| fraction * static_cast<double>(values.size() - 1) | |
| )); | |
| return values[position]; | |
| }; | |
| return { | |
| std::accumulate(values.begin(), values.end(), 0.0) | |
| / static_cast<double>(values.size()), | |
| percentile(0.5), percentile(0.95), values.front(), values.back(), | |
| }; | |
| } | |
| double milliseconds( | |
| std::chrono::steady_clock::time_point start, | |
| std::chrono::steady_clock::time_point stop | |
| ) { | |
| return std::chrono::duration<double, std::milli>(stop - start).count(); | |
| } | |
| void write_stream_report( | |
| const std::string& path, | |
| std::size_t frames, | |
| std::size_t timed_frames, | |
| int threads, | |
| const TimingSummary& read, | |
| const TimingSummary& preprocess, | |
| const TimingSummary& inference, | |
| const TimingSummary& decode, | |
| const TimingSummary& bev, | |
| const TimingSummary& core, | |
| double mean_lanes, | |
| const rclane::BevConfig& bev_config | |
| ) { | |
| if (path.empty()) { | |
| return; | |
| } | |
| std::ofstream stream(path); | |
| if (!stream) { | |
| throw std::runtime_error("cannot write benchmark report: " + path); | |
| } | |
| const auto timing = [&stream](const char* name, const TimingSummary& value, | |
| bool comma) { | |
| stream << " \"" << name << "\": {\"mean_ms\": " << value.mean | |
| << ", \"median_ms\": " << value.median | |
| << ", \"p95_ms\": " << value.p95 | |
| << ", \"min_ms\": " << value.minimum | |
| << ", \"max_ms\": " << value.maximum << '}' | |
| << (comma ? ",\n" : "\n"); | |
| }; | |
| stream << std::setprecision(12) | |
| << "{\n \"runtime\": \"native_tensorrt_cpp\",\n" | |
| << " \"sequential_per_frame\": true,\n" | |
| << " \"frame_overlap\": false,\n" | |
| << " \"rendering_included\": false,\n" | |
| << " \"video_writing_included\": false,\n" | |
| << " \"bev_mode\": \"" | |
| << rclane::bev_mode_name(bev_config.mode) << "\",\n" | |
| << " \"parallel_assumption\": " | |
| << (bev_config.mode == rclane::BevMode::Raw ? "false" : "true") | |
| << ",\n" | |
| << " \"synthetic_lane_mode\": " | |
| << (bev_config.mode == rclane::BevMode::CompleteFour | |
| ? "true" : "false") << ",\n" | |
| << " \"frames\": " << frames << ",\n" | |
| << " \"timed_frames\": " << timed_frames << ",\n" | |
| << " \"decode_threads\": " << threads << ",\n" | |
| << " \"mean_output_lanes\": " << mean_lanes << ",\n" | |
| << " \"timing\": {\n"; | |
| timing("source_read", read, true); | |
| timing("preprocess", preprocess, true); | |
| timing("inference_with_transfers", inference, true); | |
| timing("decode", decode, true); | |
| timing("bev_cubic_topology_funnel", bev, true); | |
| timing("core_pipeline", core, false); | |
| stream << " },\n \"fps_from_core_median_latency\": " | |
| << 1000.0 / core.median << "\n}\n"; | |
| } | |
| int run_raw_stream( | |
| const Arguments& args, | |
| rclane::TensorRTRunner& runner | |
| ) { | |
| const std::size_t frame_bytes = static_cast<std::size_t>( | |
| args.source_width * args.source_height * 3 | |
| ); | |
| std::vector<std::uint8_t> frame(frame_bytes); | |
| std::vector<float> input(runner.input_elements(), 0.0F); | |
| // Warm only TensorRT with a neutral tensor; actual frame timings discard | |
| // the first timing_warmup frames so OpenMP and allocator startup are absent. | |
| for (int index = 0; index < args.warmup; ++index) { | |
| runner.infer_reuse(input.data()); | |
| } | |
| rclane::DecoderConfig decoder_config; | |
| decoder_config.threads = args.threads; | |
| std::vector<double> read_samples; | |
| std::vector<double> preprocess_samples; | |
| std::vector<double> inference_samples; | |
| std::vector<double> decode_samples; | |
| std::vector<double> bev_samples; | |
| std::vector<double> core_samples; | |
| std::size_t frames = 0; | |
| std::size_t timed_frames = 0; | |
| double lane_sum = 0.0; | |
| std::ofstream frame_results; | |
| if (!args.frames_jsonl.empty()) { | |
| frame_results.open(args.frames_jsonl); | |
| if (!frame_results) { | |
| throw std::runtime_error( | |
| "cannot write frame results: " + args.frames_jsonl | |
| ); | |
| } | |
| frame_results << std::setprecision(9); | |
| } | |
| for (;;) { | |
| if (args.max_frames > 0 && static_cast<int>(frames) >= args.max_frames) { | |
| break; | |
| } | |
| const auto read_start = std::chrono::steady_clock::now(); | |
| std::cin.read( | |
| reinterpret_cast<char*>(frame.data()), | |
| static_cast<std::streamsize>(frame.size()) | |
| ); | |
| const auto read_stop = std::chrono::steady_clock::now(); | |
| if (std::cin.gcount() == 0) { | |
| break; | |
| } | |
| if (std::cin.gcount() != static_cast<std::streamsize>(frame.size())) { | |
| throw std::runtime_error("partial BGR frame on stdin"); | |
| } | |
| const auto core_start = std::chrono::steady_clock::now(); | |
| const auto preprocess_start = core_start; | |
| rclane::normalize_bgr_to_nchw( | |
| frame.data(), args.source_width, args.source_height, input | |
| ); | |
| const auto preprocess_stop = std::chrono::steady_clock::now(); | |
| const auto inference_start = preprocess_stop; | |
| const auto& outputs = runner.infer_reuse(input.data()); | |
| const auto inference_stop = std::chrono::steady_clock::now(); | |
| const auto decode_start = inference_stop; | |
| rclane::DecodeStatistics decode_statistics; | |
| const auto lanes = rclane::decode_outputs( | |
| outputs, decoder_config, &decode_statistics | |
| ); | |
| const auto decode_stop = std::chrono::steady_clock::now(); | |
| rclane::BevTopologyReport topology; | |
| const auto bev_lanes = rclane::project_lanes_to_bev( | |
| lanes, args.bev_config, &topology | |
| ); | |
| const auto bev_stop = std::chrono::steady_clock::now(); | |
| const double frame_preprocess_ms = milliseconds( | |
| preprocess_start, preprocess_stop | |
| ); | |
| const double frame_inference_ms = milliseconds( | |
| inference_start, inference_stop | |
| ); | |
| const double frame_decode_ms = milliseconds(decode_start, decode_stop); | |
| const double frame_bev_ms = milliseconds(decode_stop, bev_stop); | |
| const double frame_core_ms = milliseconds(core_start, bev_stop); | |
| // Serialization is deliberately after bev_stop, outside core latency. | |
| if (frame_results) { | |
| frame_results << "{\"frame_index\":" << frames | |
| << ",\"timing\":{\"preprocess_ms\":" | |
| << frame_preprocess_ms | |
| << ",\"inference_ms\":" << frame_inference_ms | |
| << ",\"decode_ms\":" << frame_decode_ms | |
| << ",\"bev_result_ms\":" << frame_bev_ms | |
| << ",\"core_ms\":" << frame_core_ms << "}" | |
| << ",\"bev_mode\":\"" | |
| << rclane::bev_mode_name(args.bev_config.mode) | |
| << "\",\"bev_topology\":" | |
| << rclane::bev_topology_json(topology) | |
| << ",\"lanes\":["; | |
| for (std::size_t lane_index = 0; lane_index < lanes.size(); | |
| ++lane_index) { | |
| const auto& lane = lanes[lane_index]; | |
| if (lane_index != 0U) { | |
| frame_results << ','; | |
| } | |
| frame_results << "{\"lane_id\":" << lane.lane_id | |
| << ",\"role\":\"" << lane.role | |
| << "\",\"score\":" << lane.score() | |
| << ",\"points\":["; | |
| for (std::size_t point_index = 0; | |
| point_index < lane.points.size(); ++point_index) { | |
| const auto& point = lane.points[point_index]; | |
| if (point_index != 0U) { | |
| frame_results << ','; | |
| } | |
| frame_results << '[' << point.x << ',' << point.y << ',' | |
| << point.score << ']'; | |
| } | |
| frame_results << "]}"; | |
| } | |
| frame_results << "],\"bev_lanes\":["; | |
| for (std::size_t lane_index = 0; | |
| lane_index < bev_lanes.size(); ++lane_index) { | |
| const auto& lane = bev_lanes[lane_index]; | |
| if (lane_index != 0U) { | |
| frame_results << ','; | |
| } | |
| frame_results << "{\"lane_id\":" << lane.lane_id | |
| << ",\"role\":\"" << lane.role | |
| << "\",\"score\":" << lane.score | |
| << ",\"fit_accepted\":" | |
| << (lane.fit_accepted ? "true" : "false") | |
| << ",\"funnel_clipped\":" | |
| << (lane.funnel_clipped ? "true" : "false") | |
| << ",\"synthetic\":" | |
| << (lane.synthetic ? "true" : "false") | |
| << ",\"parallel_repaired\":" | |
| << (lane.parallel_repaired ? "true" : "false") | |
| << ",\"parallel_reference_lane\":"; | |
| if (lane.parallel_reference_lane < 0) { | |
| frame_results << "null"; | |
| } else { | |
| frame_results << lane.parallel_reference_lane; | |
| } | |
| frame_results << ",\"parallel_offset_m\":" | |
| << lane.parallel_offset_m | |
| << ",\"parallel_repair_method\":\"" | |
| << lane.parallel_repair_method << "\"" | |
| << ",\"points\":["; | |
| for (std::size_t point_index = 0; | |
| point_index < lane.points.size(); ++point_index) { | |
| const auto& point = lane.points[point_index]; | |
| if (point_index != 0U) { | |
| frame_results << ','; | |
| } | |
| frame_results << '[' << point.x << ',' << point.y << ',' | |
| << point.score << ']'; | |
| } | |
| frame_results << ']'; | |
| if (lane.fit.valid) { | |
| frame_results << ",\"fit\":{\"coefficients\":[" | |
| << lane.fit.coefficients[0] << ',' | |
| << lane.fit.coefficients[1] << ',' | |
| << lane.fit.coefficients[2] << ',' | |
| << lane.fit.coefficients[3] | |
| << "],\"x_min\":" << lane.fit.x_min | |
| << ",\"x_max\":" << lane.fit.x_max | |
| << ",\"rmse\":" << lane.fit.rmse | |
| << ",\"point_count\":" | |
| << lane.fit.point_count | |
| << ",\"inlier_count\":" | |
| << lane.fit.inlier_count << '}'; | |
| } else { | |
| frame_results << ",\"fit\":null"; | |
| } | |
| frame_results << '}'; | |
| } | |
| frame_results << "]}\n"; | |
| } | |
| lane_sum += static_cast<double>(bev_lanes.size()); | |
| if (static_cast<int>(frames) >= args.timing_warmup) { | |
| read_samples.push_back(milliseconds(read_start, read_stop)); | |
| preprocess_samples.push_back(frame_preprocess_ms); | |
| inference_samples.push_back(frame_inference_ms); | |
| decode_samples.push_back(frame_decode_ms); | |
| bev_samples.push_back(frame_bev_ms); | |
| core_samples.push_back(frame_core_ms); | |
| ++timed_frames; | |
| } | |
| ++frames; | |
| if (frames % 100U == 0U) { | |
| std::cerr << "processed " << frames << " frames\n"; | |
| } | |
| } | |
| if (frames == 0 || timed_frames == 0) { | |
| throw std::runtime_error("raw stream produced no timed frames"); | |
| } | |
| const auto read = summarize(std::move(read_samples)); | |
| const auto preprocess = summarize(std::move(preprocess_samples)); | |
| const auto inference = summarize(std::move(inference_samples)); | |
| const auto decode = summarize(std::move(decode_samples)); | |
| const auto bev = summarize(std::move(bev_samples)); | |
| const auto core = summarize(std::move(core_samples)); | |
| write_stream_report( | |
| args.report, frames, timed_frames, args.threads, read, preprocess, | |
| inference, decode, bev, core, lane_sum / static_cast<double>(frames), | |
| args.bev_config | |
| ); | |
| std::cout << std::fixed << std::setprecision(3) | |
| << "C++ sequential BEV benchmark mode=" | |
| << rclane::bev_mode_name(args.bev_config.mode) | |
| << " (render/write excluded)\n" | |
| << "frames=" << frames << " timed=" << timed_frames | |
| << " threads=" << args.threads << '\n' | |
| << "preprocess median=" << preprocess.median << "ms\n" | |
| << "inference+D2H median=" << inference.median << "ms\n" | |
| << "decode median=" << decode.median << "ms\n" | |
| << "BEV median=" << bev.median << "ms\n" | |
| << "core median=" << core.median << "ms p95=" << core.p95 | |
| << "ms FPS=" << (1000.0 / core.median) << '\n'; | |
| return 0; | |
| } | |
| } // namespace | |
| int main(int argc, char** argv) { | |
| try { | |
| const auto args = parse_arguments(argc, argv); | |
| rclane::TensorRTRunner runner(args.engine); | |
| if (args.raw_bgr_stdin) { | |
| return run_raw_stream(args, runner); | |
| } | |
| std::vector<float> input; | |
| if (!args.input.empty()) { | |
| input = read_floats(args.input, runner.input_elements()); | |
| } else { | |
| const auto bgr = read_bytes( | |
| args.input_bgr, static_cast<std::size_t>(1920 * 1080 * 3) | |
| ); | |
| rclane::normalize_bgr_to_nchw(bgr.data(), 1920, 1080, input); | |
| } | |
| dump_input(args.dump_prefix, input); | |
| const auto outputs = runner.infer(input.data()); | |
| dump_outputs(args.dump_prefix, outputs); | |
| rclane::DecoderConfig decoder_config; | |
| decoder_config.threads = args.threads; | |
| rclane::DecodeStatistics decode_statistics; | |
| const auto lanes = rclane::decode_outputs( | |
| outputs, decoder_config, &decode_statistics | |
| ); | |
| if (!args.lanes_json.empty()) { | |
| rclane::write_lanes_json( | |
| args.lanes_json, lanes, &decode_statistics | |
| ); | |
| } | |
| rclane::BevTopologyReport topology; | |
| const auto bev_lanes = rclane::project_lanes_to_bev( | |
| lanes, args.bev_config, &topology | |
| ); | |
| if (!args.bev_json.empty()) { | |
| rclane::write_bev_json(args.bev_json, bev_lanes, &topology); | |
| } | |
| const auto timing = runner.benchmark( | |
| input.data(), args.warmup, args.iterations | |
| ); | |
| std::cout << std::fixed << std::setprecision(3) | |
| << "TensorRT C++ inference: mean=" << timing.mean_ms | |
| << "ms median=" << timing.median_ms | |
| << "ms p95=" << timing.p95_ms | |
| << "ms min=" << timing.min_ms | |
| << "ms max=" << timing.max_ms | |
| << "ms FPS=" << (1000.0 / timing.median_ms) << '\n'; | |
| for (const auto& [name, tensor] : outputs) { | |
| std::cout << name << " elements=" << tensor.values.size() << '\n'; | |
| } | |
| std::cout << "decode seeds=" << decode_statistics.seeds | |
| << " candidates=" << decode_statistics.crawled_candidates | |
| << " NMS=" << decode_statistics.nms_candidates | |
| << "->" << decode_statistics.nms_survivors | |
| << " output_lanes=" << lanes.size() << '\n'; | |
| std::cout << "BEV mode=" | |
| << rclane::bev_mode_name(args.bev_config.mode) | |
| << " lanes=" << bev_lanes.size() << " valid_cubics=" | |
| << std::count_if( | |
| bev_lanes.begin(), bev_lanes.end(), | |
| [](const rclane::BevLane& lane) { | |
| return lane.fit_accepted; | |
| } | |
| ) << " topology_applied=" | |
| << (topology.applied ? "true" : "false") << '\n'; | |
| return 0; | |
| } catch (const std::exception& error) { | |
| std::cerr << "error: " << error.what() << '\n'; | |
| return 1; | |
| } | |
| } | |