From 3d947f85ffa11688b0c09b0b1d45aa7a0e2c2e31 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Sat, 23 Nov 2024 11:26:36 -0800 Subject: [PATCH 01/14] catchup --- CMakeLists.txt | 2 +- test/CMakeLists.txt | 1 + test/TestGraph/CMakeLists.txt | 15 ++ test/TestGraph/__init__.cpp | 31 +++ test/TestGraph/__str__.cpp | 39 +++ test/TestGraph/add.cpp | 57 +++++ test/TestGraph/add_edge.cpp | 35 +++ test/TestGraph/add_node.cpp | 33 +++ test/TestGraph/add_series.cpp | 86 +++++++ test/TestGraph/cc.cpp | 34 +++ test/TestGraph/connected_components.cpp | 123 ++++++++++ test/TestGraph/degree.cpp | 87 +++++++ test/TestGraph/drop_series.cpp | 74 ++++++ test/TestGraph/foo.cpp | 18 ++ test/TestGraph/for_all_edges.cpp | 51 ++++ test/TestGraph/meta.json | 7 + test/TestGraph/mvmap.hpp | 305 ++++++++++++++++++++++++ test/TestGraph/ne.cpp | 29 +++ test/TestGraph/nv.cpp | 29 +++ test/TestGraph/remove.cpp | 36 +++ test/TestGraph/remove_if.cpp | 50 ++++ test/TestGraph/selector.hpp | 26 ++ test/TestGraph/testconst.cpp | 8 + test/TestGraph/testgraph.cpp | 45 ++++ test/TestGraph/testgraph.hpp | 186 +++++++++++++++ test/TestGraph/testlocator.cpp | 7 + test/TestGraph/testmvmap.cpp | 71 ++++++ test/TestGraph/where.cpp | 50 ++++ test/TestSelector/selector.hpp | 26 ++ 29 files changed, 1560 insertions(+), 1 deletion(-) create mode 100644 test/TestGraph/CMakeLists.txt create mode 100644 test/TestGraph/__init__.cpp create mode 100644 test/TestGraph/__str__.cpp create mode 100644 test/TestGraph/add.cpp create mode 100644 test/TestGraph/add_edge.cpp create mode 100644 test/TestGraph/add_node.cpp create mode 100644 test/TestGraph/add_series.cpp create mode 100644 test/TestGraph/cc.cpp create mode 100644 test/TestGraph/connected_components.cpp create mode 100644 test/TestGraph/degree.cpp create mode 100644 test/TestGraph/drop_series.cpp create mode 100644 test/TestGraph/foo.cpp create mode 100644 test/TestGraph/for_all_edges.cpp create mode 100644 test/TestGraph/meta.json create mode 100644 test/TestGraph/mvmap.hpp create mode 100644 test/TestGraph/ne.cpp create mode 100644 test/TestGraph/nv.cpp create mode 100644 test/TestGraph/remove.cpp create mode 100644 test/TestGraph/remove_if.cpp create mode 100644 test/TestGraph/selector.hpp create mode 100644 test/TestGraph/testconst.cpp create mode 100644 test/TestGraph/testgraph.cpp create mode 100644 test/TestGraph/testgraph.hpp create mode 100644 test/TestGraph/testlocator.cpp create mode 100644 test/TestGraph/testmvmap.cpp create mode 100644 test/TestGraph/where.cpp create mode 100644 test/TestSelector/selector.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 932a91c..18b0e11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Only do these if this is the main project, and not if it is included through add_subdirectory if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) - set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Let's ensure -std=c++xx instead of -std=g++xx diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9f56e11..f2f8d05 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -21,3 +21,4 @@ add_subdirectory(TestBag) add_subdirectory(TestSet) add_subdirectory(TestFunctions) add_subdirectory(TestSelector) +add_subdirectory(TestGraph) diff --git a/test/TestGraph/CMakeLists.txt b/test/TestGraph/CMakeLists.txt new file mode 100644 index 0000000..0f78239 --- /dev/null +++ b/test/TestGraph/CMakeLists.txt @@ -0,0 +1,15 @@ +add_test(TestGraph __init__) +add_test(TestGraph __str__) +add_test(TestGraph add_edge) +add_test(TestGraph add_node) +add_test(TestGraph nv) +add_test(TestGraph ne) +add_test(TestGraph degree) +add_test(TestGraph add_series) +add_test(TestGraph connected_components) +add_test(TestGraph drop_series) +add_custom_command( + TARGET TestGraph_nv POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/meta.json + ${CMAKE_CURRENT_BINARY_DIR}/meta.json) diff --git a/test/TestGraph/__init__.cpp b/test/TestGraph/__init__.cpp new file mode 100644 index 0000000..34fc260 --- /dev/null +++ b/test/TestGraph/__init__.cpp @@ -0,0 +1,31 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__init__"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes a TestGraph"}; + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + // testgraph needs to be convertible to json + testgraph::testgraph the_graph; + clip.set_state(state_name, the_graph); + std::map selectors; + clip.set_state(sel_state_name, selectors); + + return 0; +} diff --git a/test/TestGraph/__str__.cpp b/test/TestGraph/__str__.cpp new file mode 100644 index 0000000..a0cd317 --- /dev/null +++ b/test/TestGraph/__str__.cpp @@ -0,0 +1,39 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__str__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Str method for TestGraph"}; + + clip.add_required_state(state_name, + "Internal container"); + + clip.returns("String of data."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_graph = clip.get_state(state_name); + clip.set_state(state_name, the_graph); + + std::stringstream sstr; + sstr << "Graph with " << the_graph.nv() << " nodes and " << the_graph.ne() + << " edges"; + + clip.to_return(sstr.str()); + + return 0; +} diff --git a/test/TestGraph/add.cpp b/test/TestGraph/add.cpp new file mode 100644 index 0000000..481b290 --- /dev/null +++ b/test/TestGraph/add.cpp @@ -0,0 +1,57 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Adds a subselector"}; + clip.add_required("selector", "Parent Selector"); + clip.add_required("subname", "Description of new selector"); + clip.add_optional("desc", "Description", "EMPTY DESCRIPTION"); + clip.add_required_state>( + "selector_state", "Internal container"); + + if (clip.parse(argc, argv)) { + return 0; + } + + std::map sstate; + if (clip.has_state("selector_state")) { + sstate = + clip.get_state>("selector_state"); + } + + auto jo = clip.get("selector"); + std::string subname = clip.get("subname"); + std::string desc = clip.get("desc"); + + std::string parentname; + try { + if (jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + parentname = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + sstate[parentname + "." + subname] = desc; + + clip.set_state("selector_state", sstate); + clip.update_selectors(sstate); + clip.return_self(); + + return 0; +} diff --git a/test/TestGraph/add_edge.cpp b/test/TestGraph/add_edge.cpp new file mode 100644 index 0000000..cc00d9f --- /dev/null +++ b/test/TestGraph/add_edge.cpp @@ -0,0 +1,35 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add_edge"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts (src, dst) into a TestGraph"}; + clip.add_required("src", "source node"); + clip.add_required("dst", "dest node"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto src = clip.get("src"); + auto dst = clip.get("dst"); + auto the_graph = clip.get_state(state_name); + the_graph.add_edge(src, dst); + clip.set_state(state_name, the_graph); + clip.return_self(); + return 0; +} diff --git a/test/TestGraph/add_node.cpp b/test/TestGraph/add_node.cpp new file mode 100644 index 0000000..b043660 --- /dev/null +++ b/test/TestGraph/add_node.cpp @@ -0,0 +1,33 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add_node"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts a node into a TestGraph"}; + clip.add_required("node", "node to insert"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto node = clip.get("node"); + auto the_graph = clip.get_state(state_name); + the_graph.add_node(node); + clip.set_state(state_name, the_graph); + clip.return_self(); + return 0; +} diff --git a/test/TestGraph/add_series.cpp b/test/TestGraph/add_series.cpp new file mode 100644 index 0000000..bc5c462 --- /dev/null +++ b/test/TestGraph/add_series.cpp @@ -0,0 +1,86 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add_series"; +static const std::string graph_state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Adds a subselector"}; + clip.add_required("parent_sel", "Parent Selector"); + clip.add_required("sub_sel", "Name of new selector"); + clip.add_optional("desc", "Description of new selector", ""); + + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.add_required_state(graph_state_name, + "Internal state for the graph"); + + if (clip.parse(argc, argv)) { + return 0; + } + + auto jo = clip.get("parent_sel"); + auto subsel = clip.get("sub_sel"); + auto desc = clip.get("desc"); + + std::string parentname; + try { + if (jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + parentname = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + // std::map selectors; + auto the_graph = clip.get_state(graph_state_name); + auto fullname = parentname + "." + subsel; + if (parentname == "edge") { + if (the_graph.has_edge_series(subsel)) { + std::cerr << "!! ERROR: Selector name already exists in edge table !!" + << std::endl; + exit(-1); + } + } else if (parentname == "node") { + if (the_graph.has_node_series(subsel)) { + std::cerr << "!! ERROR: Selector name already exists in node table !!" + << std::endl; + exit(-1); + } + } else { + std::cerr + << "((!! ERROR: Parent must be either \"edge\" or \"node\" (received " + << parentname << ") !!)"; + exit(-1); + } + + if (clip.has_state(sel_state_name)) { + auto selectors = + clip.get_state>(sel_state_name); + if (selectors.contains(fullname)) { + std::cerr << "Warning: Selector name already exists; ignoring" + << std::endl; + } else { + selectors[fullname] = desc; + clip.set_state(sel_state_name, selectors); + clip.update_selectors(selectors); + clip.return_self(); + } + } + + return 0; +} diff --git a/test/TestGraph/cc.cpp b/test/TestGraph/cc.cpp new file mode 100644 index 0000000..5b29f3e --- /dev/null +++ b/test/TestGraph/cc.cpp @@ -0,0 +1,34 @@ +#include +#include +#include + +int main() { + std::vector> adj{{1}, {0, 2}, {1}, {4}, {3}}; + std::vector components(adj.size()); + std::iota(components.begin(), components.end(), 0); + + std::vector curr_level{0}; + std::vector next_level{}; + std::vector visited(adj.size(), false); + + long int u{}; + + while (!curr_level.empty()) { + u = curr_level.back(); + std::cout << "u = " << u << "\n"; + curr_level.pop_back(); + for (long int v : adj[u]) { + std::cout << " testing " << v << "\n"; + if (!visited[v]) { + std::cout << " visiting " << v << "\n"; + visited[v] = true; + components[v] = components[u]; + std::cout << " components[" << v << "] = " << components[u] << "\n"; + next_level.push_back(v); + } + std::cout << "swapping\n"; + std::swap(next_level, curr_level); + next_level.clear(); + } + } +} diff --git a/test/TestGraph/connected_components.cpp b/test/TestGraph/connected_components.cpp new file mode 100644 index 0000000..26c8e02 --- /dev/null +++ b/test/TestGraph/connected_components.cpp @@ -0,0 +1,123 @@ + + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy-eval.hpp" +#include "testgraph.hpp" +#include +#include +#include +#include + +static const std::string method_name = "connected_components"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{ + method_name, + "Populates a column containing the component id of each node in a graph"}; + clip.add_required( + "selector", + "Existing selector name into which the component id will be written"); + clip.add_required_state(state_name, + "Internal container"); + clip.add_required_state>( + sel_state_name, "Internal container for selectors"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto sel_json = clip.get("selector"); + + std::string sel; + try { + if (sel_json["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + sel = sel_json["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + if (!sel.starts_with("node.")) { + std::cerr << "Selector must be a node subselector" << std::endl; + return 1; + } + auto the_graph = clip.get_state(state_name); + + auto selectors = + clip.get_state>(sel_state_name); + if (!selectors.contains(sel)) { + std::cerr << "Selector not found" << std::endl; + return 1; + } + auto subsel = sel.substr(5); + if (the_graph.has_node_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto cc_o = the_graph.add_node_series(subsel, selectors.at(sel)); + if (!cc_o) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + + auto cc = cc_o.value(); + std::map ccmap; + + long int i = 0; + for (auto &node : the_graph.nodes()) { + ccmap[node] = i++; + } + std::vector> adj(the_graph.nv()); + the_graph.for_all_edges([&adj, &ccmap](auto edge, mvmap::locator /*unused*/) { + long i = ccmap[edge.first]; + long j = ccmap[edge.second]; + adj[i].push_back(j); + adj[j].push_back(i); + }); + + std::vector visited(the_graph.nv(), false); + std::vector components(the_graph.nv()); + std::iota(components.begin(), components.end(), 0); + + for (long int i = 0; i < the_graph.nv(); ++i) { + if (!visited[i]) { + std::queue q; + q.push(i); + while (!q.empty()) { + long int v = q.front(); + q.pop(); + visited[v] = true; + for (long int u : adj[v]) { + if (!visited[u]) { + q.push(u); + components[u] = components[i]; + } + } + } + } + } + + the_graph.for_all_nodes( + [&components, &ccmap, &cc](auto node, mvmap::locator /*unused*/) { + long int i = ccmap[node]; + cc[node] = components[i]; + }); + + clip.set_state(state_name, the_graph); + // clip.set_state(sel_state_name, selectors); + // clip.update_selectors(selectors); + + clip.return_self(); + return 0; +} diff --git a/test/TestGraph/degree.cpp b/test/TestGraph/degree.cpp new file mode 100644 index 0000000..2a844f4 --- /dev/null +++ b/test/TestGraph/degree.cpp @@ -0,0 +1,87 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy-eval.hpp" +#include "testgraph.hpp" +#include +#include +#include + +static const std::string method_name = "degree"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{ + method_name, + "Populates a column containing the degree of each node in a graph"}; + clip.add_required( + "selector", + "Existing selector name into which the degree will be written"); + clip.add_required_state(state_name, + "Internal container"); + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto sel_json = clip.get("selector"); + + std::string sel; + try { + if (sel_json["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + sel = sel_json["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + if (!sel.starts_with("node.")) { + std::cerr << "Selector must be a node subselector" << std::endl; + return 1; + } + auto the_graph = clip.get_state(state_name); + + auto selectors = + clip.get_state>(sel_state_name); + if (!selectors.contains(sel)) { + std::cerr << "Selector not found" << std::endl; + return 1; + } + auto subsel = sel.substr(5); + if (the_graph.has_node_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto deg_o = the_graph.add_node_series(subsel, "Degree"); + if (!deg_o) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + + auto deg = deg_o.value(); + + the_graph.for_all_edges([°](auto edge, mvmap::locator /*unused*/) { + deg[edge.first]++; + if (edge.first != edge.second) { + deg[edge.second]++; + } + }); + + clip.set_state(state_name, the_graph); + clip.set_state(sel_state_name, selectors); + clip.update_selectors(selectors); + + clip.return_self(); + return 0; +} diff --git a/test/TestGraph/drop_series.cpp b/test/TestGraph/drop_series.cpp new file mode 100644 index 0000000..b0af31b --- /dev/null +++ b/test/TestGraph/drop_series.cpp @@ -0,0 +1,74 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "drop_series"; +static const std::string graph_state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; +static const std::string sel_name = "selector"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Drops a subselector"}; + clip.add_required(sel_name, "Selector to drop"); + + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.add_required_state(graph_state_name, + "Internal state for the graph"); + + if (clip.parse(argc, argv)) { + return 0; + } + + auto jo = clip.get(sel_name); + + std::string sel; + try { + if (jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + sel = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + auto sel_state = + clip.get_state>(sel_state_name); + if (!sel_state.contains(sel)) { + std::cerr << "Selector name not found!" << std::endl; + exit(-1); + } + auto the_graph = clip.get_state(graph_state_name); + auto subsel = sel.substr(5); + if (sel.starts_with("edge.")) { + if (the_graph.has_edge_series(subsel)) { + the_graph.drop_edge_series(sel); + } + } else if (sel.starts_with("node.")) { + if (the_graph.has_node_series(subsel)) { + the_graph.drop_node_series(subsel); + } + } else { + std::cerr << "Selector name must start with either \"edge.\" or \"node.\"" + << std::endl; + exit(-1); + } + sel_state.erase(sel); + clip.set_state(graph_state_name, the_graph); + clip.set_state(sel_state_name, sel_state); + clip.update_selectors(sel_state); + + return 0; +} diff --git a/test/TestGraph/foo.cpp b/test/TestGraph/foo.cpp new file mode 100644 index 0000000..e8063f6 --- /dev/null +++ b/test/TestGraph/foo.cpp @@ -0,0 +1,18 @@ +#include + +struct Foo { + int x; + void print() { std::cout << "non-const" << x << "\n"; } + void print() const { std::cout << "const" << x << "\n"; } +}; + +int main() { + Foo f{10}; + f.print(); + + const Foo &g = f; + g.print(); + Foo h = g; + h.x = 20; + g.print(); +} diff --git a/test/TestGraph/for_all_edges.cpp b/test/TestGraph/for_all_edges.cpp new file mode 100644 index 0000000..80ff892 --- /dev/null +++ b/test/TestGraph/for_all_edges.cpp @@ -0,0 +1,51 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy-eval.hpp" +#include "testgraph.hpp" +#include +#include +#include +#include +#include + +static const std::string method_name = "add_node"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "Adds a new column to a graph based on a lambda"}; + clip.add_required("name", "New column name"); + clip.add_required("expression", "Lambda Expression"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto name = clip.get("name"); + auto expression = clip.get("expression"); + auto the_graph = clip.get_state(state_name); + + // + // Expression here + auto apply_jl = [&expression](const testgraph::edge_t &value, + mvmap::locator loc) { + boost::json::object data; + data["src"] = value.first; + data["dst"] = value.second; + data["loc"] = boost::json::value_from(loc); + json_logic::ValueExpr res = json_logic::apply(expression["rule"], data); + return json_logic::unpackValue(res); + }; + + the_graph.for_all_edges(apply_jl); + + clip.set_state(state_name, the_graph); + clip.return_self(); + return 0; +} diff --git a/test/TestGraph/meta.json b/test/TestGraph/meta.json new file mode 100644 index 0000000..0af2980 --- /dev/null +++ b/test/TestGraph/meta.json @@ -0,0 +1,7 @@ +{ + "__doc__" : "A graph data structure", + "initial_selectors" : { + "edge" : "The edges of the graph", + "node": "The nodes of the graph" + } +} diff --git a/test/TestGraph/mvmap.hpp b/test/TestGraph/mvmap.hpp new file mode 100644 index 0000000..04e2a70 --- /dev/null +++ b/test/TestGraph/mvmap.hpp @@ -0,0 +1,305 @@ +#pragma once +// #include +// #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mvmap { +using index = uint64_t; +class locator { + static const index INVALID_LOC = std::numeric_limits::max(); + index loc; + + locator(index loc) : loc(loc) {}; + +public: + template friend class mvmap; + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, locator l); + friend locator tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v); + locator() : loc(INVALID_LOC) {}; + [[nodiscard]] bool is_valid() const { return loc != INVALID_LOC; } +}; +void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, + locator l) { + v = l.loc; +} + +locator tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + return boost::json::value_to(v); +} +template class mvmap { + template using series = std::map; + using key_to_idx = std::map; + using idx_to_key = std::map; + using variants = std::variant; + + // A locator is an opaque handle to a key in a series. + + idx_to_key itk; + key_to_idx kti; + std::map...>> data; + std::map series_desc; + +public: + // A series_proxy is a reference to a series in an mvmap. + template class series_proxy { + std::string id; + std::string_view desc; + key_to_idx &kti_r; + idx_to_key &itk_r; + series &series_r; + + // returns true if there is an index assigned to a given key + bool has_idx(K k) { return kti_r.count(k) > 0; } + // returns true if there is a key assigned to a given locator + bool has_key(locator l) { return itk_r.count(l) > 0; } + + // returns or creates the index for a key. + index get_idx(K k) { + if (!has_idx(k)) { + index i{kti_r.size()}; + kti_r[k] = i; + itk_r[i] = k; + return i; + } + return kti_r[k]; + } + + public: + series_proxy(std::string id, series &ser, mvmap &m) + : id(std::move(id)), kti_r(m.kti), itk_r(m.itk), series_r(ser) {} + + series_proxy(std::string id, const std::string &desc, series &ser, + mvmap &m) + : id(std::move(id)), desc(desc), kti_r(m.kti), itk_r(m.itk), + series_r(ser) {} + + V &operator[](K k) { return series_r[get_idx(k)]; } + const V &operator[](K k) const { return series_r[get_idx(k)]; } + + // this assumes the key exists. + V &operator[](locator l) { return series_r[l.loc]; } + const V &operator[](locator l) const { return series_r[l.loc]; } + + std::optional> at(locator l) { + if (!has_key(l)) { + return {}; + } + return series_r[l.loc]; + }; + std::optional> at(locator l) const { + if (!has_key(l)) { + return {}; + } + return series_r[l.loc]; + }; + + std::optional> at(K k) { + if (!has_idx(k)) { + return {}; + } + return series_r[get_idx(k)]; + }; + + std::optional> at(K k) const { + if (!has_idx(k)) { + return {}; + } + return series_r[get_idx(k)]; + }; + + // this will create the key/index if it doesn't exist. + locator get_loc(K k) { return locator(get_idx(k)); } + + // F takes (K key, locator, V value) + template void for_all(F f) { + for (auto el : series_r) { + f(itk_r[el.first], locator(el.first), el.second); + } + }; + + template void for_all(F f) const { + for (auto el : series_r) { + f(itk_r[el.first], locator(el.first), el.second); + } + }; + + // F takes (K key, locator, V value) + template void remove_if(F f) { + auto indices_to_delete = std::vector{}; + for (auto el : series_r) { + if (f(itk_r[el.first], locator(el.first), el.second)) { + indices_to_delete.emplace_back(el.first); + } + } + + for (auto ltd : indices_to_delete) { + erase(locator(ltd)); + } + }; + + void erase(const locator &l) { + auto i = l.loc; + kti_r.erase(itk_r[i]); + itk_r.erase(i); + series_r.erase(i); + } + + // if the key doesn't exist, do nothing. + void erase(const K &k) { + if (!has_idx(k)) { + return; + } + auto i = kti_r[k]; + erase(locator(i)); + } + + // this returns the key for a given locator in a series, or nullopt if the + // locator is invalid. + std::optional> + get_key(const locator &l) const { + if (!has_idx(l.loc)) { + return {}; + } + return itk_r[l.loc]; + } + }; // end of series + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + mvmap(const idx_to_key &itk, const key_to_idx &kti, + const std::map...>> &data) + : itk(itk), kti(kti), data(data) {} + + mvmap() = default; + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, const mvmap &m) { + v = {{"itk", boost::json::value_from(m.itk)}, + {"kti", boost::json::value_from(m.kti)}, + {"data", boost::json::value_from(m.data)}}; + } + + friend mvmap + tag_invoke(boost::json::value_to_tag> /*unused*/, + const boost::json::value &v) { + const auto &obj = v.as_object(); + using index = uint64_t; + // template using series = std::map; + using key_to_idx = std::map; + using idx_to_key = std::map; + return {boost::json::value_to(obj.at("itk")), + boost::json::value_to(obj.at("kti")), + boost::json::value_to< + std::map...>>>( + obj.at("data"))}; + } + + [[nodiscard]] size_t size() const { return kti.size(); } + bool add_key(const K &k) { + if (kti.count(k) > 0) { + return false; + } + auto i = kti.size(); + kti[k] = i; + itk[i] = k; + return true; + } + + [[nodiscard]] std::vector list_series() const { + return std::views::keys(data); + } + + [[nodiscard]] bool has_series(const std::string &id) const { + return data.contains(id); + } + + template + [[nodiscard]] bool has_series(const std::string &id) const { + return data.contains(id) && std::holds_alternative>(data[id]); + } + + bool contains(const K &k) { return kti.contains(k); } + auto keys() const { return std::views::keys(kti); } + + // adds a new column (series) to the mvmap and returns true. If already + // exists, return false + template + std::optional> add_series(const std::string &name, + const std::string &desc = "") { + if (has_series(name)) { + return std::nullopt; + } + data[name] = series{}; + series_desc[name] = desc; + return series_proxy(name, desc, std::get>(data[name]), *this); + } + + template + std::optional> get_series(const std::string &name) { + if (!has_series(name)) { + // series doesn't exist or is of the wrong type. + return std::nullopt; + } + return series_proxy(name, std::get>(data[name]), *this); + } + + void drop_series(const std::string &name) { + if (!has_series(name)) { + return; + } + data.erase(name); + series_desc.erase(name); + } + + // returns a series_proxy for the given string. If the series doesn't exist, + // create it. + // template + // series_proxy get_or_create_series(const std::string &id, + // const std::string &desc = "") { + // if (data.count(id) == 0) { + // add_series(id, desc); + // } else { + // if (!std::holds_alternative>(data[id])) { + // throw std::runtime_error( + // "series id already exists with different type"); + // } + // } + // return series_proxy(id, desc, std::get>(data[id]), *this); + // } + + // F is a function that takes a key and a locator. + // Users will need to close over series_proxies that they want to use. + template void for_all(F f) { + for (auto &idx : kti) { + f(idx.first, locator(idx.second)); + } + } + + template void remove_if(F f) { + std::vector indices_to_delete; + for (auto &idx : kti) { + if (f(idx.first, locator(idx.second))) { + indices_to_delete.emplace_back(idx.second); + } + } + + for (auto &idx : indices_to_delete) { + kti.erase(itk[idx]); + itk.erase(idx); + for (auto &id_ser : data) { + std::visit([&idx](auto &ser) { ser.erase(idx); }, id_ser.second); + } + } + } +}; +}; // namespace mvmap diff --git a/test/TestGraph/ne.cpp b/test/TestGraph/ne.cpp new file mode 100644 index 0000000..cb2ded3 --- /dev/null +++ b/test/TestGraph/ne.cpp @@ -0,0 +1,29 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "ne"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the number of edges in the graph"}; + + clip.returns("Number of edges."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_graph = clip.get_state(state_name); + + clip.to_return(the_graph.ne()); + return 0; +} diff --git a/test/TestGraph/nv.cpp b/test/TestGraph/nv.cpp new file mode 100644 index 0000000..fafd0d5 --- /dev/null +++ b/test/TestGraph/nv.cpp @@ -0,0 +1,29 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "nv"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the number of nodes in the graph"}; + + clip.returns("Number of nodes."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_graph = clip.get_state(state_name); + + clip.to_return(the_graph.nv()); + return 0; +} diff --git a/test/TestGraph/remove.cpp b/test/TestGraph/remove.cpp new file mode 100644 index 0000000..f24422b --- /dev/null +++ b/test/TestGraph/remove.cpp @@ -0,0 +1,36 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove_edge"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + + clippy::clippy clip{method_name, "Removes a string from a TestSet"}; + + clip.add_required("item", "Item to remove"); + clip.add_required_state>(state_name, "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + auto the_set = clip.get_state>(state_name); + the_set.erase(item); + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/test/TestGraph/remove_if.cpp b/test/TestGraph/remove_if.cpp new file mode 100644 index 0000000..628b061 --- /dev/null +++ b/test/TestGraph/remove_if.cpp @@ -0,0 +1,50 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy-eval.hpp" +#include +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove_if"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Removes a string from a TestSet"}; + clip.add_required("expression", "Remove If Expression"); + clip.add_required_state>(state_name, "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto expression = clip.get("expression"); + auto the_set = clip.get_state>(state_name); + + // + // Expression here + auto apply_jl = [&expression](int value) { + boostjsn::object data; + data["value"] = value; + json_logic::ValueExpr res = json_logic::apply(expression["rule"], data); + return json_logic::unpackValue(res); + }; + + for (auto first = the_set.begin(), last = the_set.end(); first != last;) { + if (apply_jl(*first)) + first = the_set.erase(first); + else + ++first; + } + + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/test/TestGraph/selector.hpp b/test/TestGraph/selector.hpp new file mode 100644 index 0000000..2da089f --- /dev/null +++ b/test/TestGraph/selector.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +namespace testgraph { +class selector { + std::string name; + +public: + selector(boost::json::object &jo) { + try { + if (jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY\n"; + exit(-1); + } + name = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!\n"; + exit(-1); + } + } + [[nodiscard]] std::string to_string() const { return name; } +}; + +} // namespace testgraph diff --git a/test/TestGraph/testconst.cpp b/test/TestGraph/testconst.cpp new file mode 100644 index 0000000..badd1f1 --- /dev/null +++ b/test/TestGraph/testconst.cpp @@ -0,0 +1,8 @@ +class Foo { + int x; + +public: + int get_x() const { return x; } + + int get_x() { return x + 1; } +}; diff --git a/test/TestGraph/testgraph.cpp b/test/TestGraph/testgraph.cpp new file mode 100644 index 0000000..e267c4d --- /dev/null +++ b/test/TestGraph/testgraph.cpp @@ -0,0 +1,45 @@ +#include + +#include "testgraph.hpp" + +int main() { + auto g = testgraph::testgraph{}; + + g.add_node("a"); + g.add_node("b"); + g.add_edge("a", "b"); + g.add_edge("b", "c"); + + assert(g.has_node("a")); + assert(g.has_node("b")); + assert(g.has_node("c")); + assert(!g.has_node("d")); + + assert(g.has_edge({"a", "b"})); + assert(g.has_edge({"b", "c"})); + assert(!g.has_edge({"a", "c"})); + + assert(g.out_degree("a") == 1); + assert(g.in_degree("a") == 0); + assert(g.in_degree("b") == 1); + assert(g.out_degree("b") == 1); + + auto colref = + g.add_node_series("color", "The color of the nodes"); + colref.value()["a"] = "blue"; + colref.value()["c"] = "red"; + + auto weightref = g.add_edge_series("weight", "edge weights"); + + weightref.value()[std::pair("a", "b")] = 5.5; + weightref.value()[std::pair("b", "c")] = 3.3; + + std::cout << "g.nv: " << g.nv() << ", g.ne: " << g.ne() << "\n"; + std::cout << boost::json::value_from(g) << "\n"; + + auto val = boost::json::value_from(g); + auto str = boost::json::serialize(val); + std::cout << "here it is: " << str << "\n"; + + auto g2 = boost::json::value_to(val); +} diff --git a/test/TestGraph/testgraph.hpp b/test/TestGraph/testgraph.hpp new file mode 100644 index 0000000..05cdee7 --- /dev/null +++ b/test/TestGraph/testgraph.hpp @@ -0,0 +1,186 @@ +#pragma once +#include "boost/json.hpp" +#include "mvmap.hpp" +#include "selector.hpp" +#include +#include +#include +#include +#include +#include + +namespace testgraph { +// map of (src, dst) : weight +using node_t = std::string; +using edge_t = std::pair; + +template using sparsevec = std::map; + +class testgraph { + + using edge_mvmap = mvmap::mvmap; + using node_mvmap = mvmap::mvmap; + template using edge_series_proxy = edge_mvmap::series_proxy; + template using node_series_proxy = node_mvmap::series_proxy; + node_mvmap node_table; + edge_mvmap edge_table; + +public: + static bool is_edge_selector(const std::string &name) { + return name.starts_with("edge."); + } + + static bool is_node_selector(const std::string &name) { + return name.starts_with("node."); + } + + static bool is_valid_selector(const std::string &name) { + return is_edge_selector(name) || is_node_selector(name); + } + + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, testgraph const &g) { + v = {{"node_table", boost::json::value_from(g.node_table)}, + {"edge_table", boost::json::value_from(g.edge_table)}}; + } + + friend testgraph tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + const auto &obj = v.as_object(); + auto nt = boost::json::value_to(obj.at("node_table")); + auto et = boost::json::value_to(obj.at("edge_table")); + return {nt, et}; + } + testgraph() = default; + testgraph(node_mvmap nt, edge_mvmap et) + : node_table(std::move(nt)), edge_table(std::move(et)) {}; + + // template + // std::optional> get_edge_series(const std::string + // &name) { + // return edge_table.get_series(name); + // } + // edge_series_proxy get_or_create_edge_col(const std::string &name, + // const std::string &desc = "") { + // return edge_table.get_or_create_series(name, desc); + // } + + // this function requires that the "edge." prefix be removed from the name. + template + std::optional> + add_edge_series(const std::string &name, const std::string &desc = "") { + return edge_table.add_series(name, desc); + } + + void drop_edge_series(const std::string &name) { + edge_table.drop_series(name); + } + + void drop_node_series(const std::string &name) { + node_table.drop_series(name); + } + // this function requires that the "node." prefix be removed from the name. + template + std::optional> + add_node_series(const std::string &name, const std::string &desc = "") { + return node_table.add_series(name, desc); + } + + template + std::optional> get_edge_series(const std::string &name) { + return edge_table.get_series(name); + } + template + std::optional> get_node_series(const std::string &name) { + return node_table.get_series(name); + } + + // template + // void get_or_create_edge_col(const std::string &name, + // std::map data) { + // auto proxy = get_or_create_edge_col(name); + // for (const auto &[k, v] : data) { + // proxy[k] = v; + // } + // } + // template + // node_series_proxy get_or_create_node_col(const std::string &name, + // const std::string &desc = "") { + // return node_table.get_or_create_series(name, desc); + // } + + // template + // void get_or_create_node_col(const std::string &name, + // std::map data) { + // auto proxy = get_or_create_node_col(name); + // for (const auto &[k, v] : data) { + // proxy[k] = v; + // } + // } + + [[nodiscard]] size_t nv() const { return node_table.size(); } + [[nodiscard]] size_t ne() const { return edge_table.size(); } + + template void for_all_edges(F f) { edge_table.for_all(f); } + template void for_all_nodes(F f) { node_table.for_all(f); } + + [[nodiscard]] std::vector edges() const { + auto kv = edge_table.keys(); + return {kv.begin(), kv.end()}; + } + [[nodiscard]] std::vector nodes() const { + auto kv = node_table.keys(); + return {kv.begin(), kv.end()}; + } + + bool add_node(const node_t &node) { return node_table.add_key(node); }; + bool add_edge(const node_t &src, const node_t &dst) { + node_table.add_key(src); + node_table.add_key(dst); + return edge_table.add_key({src, dst}); + } + + bool has_node(const node_t &node) { return node_table.contains(node); }; + bool has_edge(const edge_t &edge) { return edge_table.contains(edge); }; + bool has_edge(const node_t &src, const node_t &dst) { + return edge_table.contains({src, dst}); + }; + + [[nodiscard]] bool has_node_series(const std::string &name) const { + return node_table.has_series(name); + } + + [[nodiscard]] bool has_edge_series(const std::string &name) const { + return edge_table.has_series(name); + } + + [[nodiscard]] std::vector out_neighbors(const node_t &node) const { + std::vector neighbors; + for (const auto &[src, dst] : edge_table.keys()) { + if (src == node) { + neighbors.emplace_back(dst); + } + } + return neighbors; + } + + [[nodiscard]] std::vector in_neighbors(const node_t &node) const { + std::vector neighbors; + for (const auto &[src, dst] : edge_table.keys()) { + if (dst == node) { + neighbors.emplace_back(src); + } + } + return neighbors; + } + + [[nodiscard]] size_t in_degree(const node_t &node) const { + return in_neighbors(node).size(); + } + [[nodiscard]] size_t out_degree(const node_t &node) const { + return out_neighbors(node).size(); + } + +}; // class testgraph + +} // namespace testgraph diff --git a/test/TestGraph/testlocator.cpp b/test/TestGraph/testlocator.cpp new file mode 100644 index 0000000..a91b02c --- /dev/null +++ b/test/TestGraph/testlocator.cpp @@ -0,0 +1,7 @@ +class locator { + int loc; +} + +int main() { + auto l = locator{5}; +} diff --git a/test/TestGraph/testmvmap.cpp b/test/TestGraph/testmvmap.cpp new file mode 100644 index 0000000..c040fd1 --- /dev/null +++ b/test/TestGraph/testmvmap.cpp @@ -0,0 +1,71 @@ +#include "mvmap.hpp" +#include +// #include +#include +#include +#include + +using mymap_t = mvmap::mvmap; +int main() { + + mymap_t m{}; + + m.add_series("weight"); + std::cout << "added series weight\n"; + m.add_series("name"); + std::cout << "added series name\n"; + + auto hmap = m.get_or_create_series("age"); + std::cout << "created hmap\n"; + + hmap["seth"] = 5; + std::cout << "added seth\n"; + hmap["roger"] = 8; + std::cout << "added roger\n"; + + assert(hmap["seth"] == 5); + assert(hmap["roger"] == 8); + + auto v = boost::json::value_from(m); + std::string j = boost::json::serialize(v); + std::cout << "j = " << j << '\n'; + auto jv = boost::json::parse(j); + std::cout << boost::json::serialize(jv) << "\n"; + auto n = boost::json::value_to(jv); + + std::cout << "created n\n"; + auto hmap2 = n.get_or_create_series("age"); + std::cout << "created hmap2\n"; + assert(hmap2["seth"] == 5); + assert(hmap2["roger"] == 8); + + size_t age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 13); + + hmap2.remove_if([](const auto &k, auto, auto &v) { return v > 6; }); + + assert(hmap2.at("roger") == std::nullopt); + assert(hmap2.at("seth") == 5); + + age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 5); + hmap2["roger"] = 8; + + age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 13); + n.remove_if([&hmap2](const auto &k, auto) { return hmap2[k] == 5; }); + + age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 8); +} diff --git a/test/TestGraph/where.cpp b/test/TestGraph/where.cpp new file mode 100644 index 0000000..628b061 --- /dev/null +++ b/test/TestGraph/where.cpp @@ -0,0 +1,50 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy-eval.hpp" +#include +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove_if"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Removes a string from a TestSet"}; + clip.add_required("expression", "Remove If Expression"); + clip.add_required_state>(state_name, "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto expression = clip.get("expression"); + auto the_set = clip.get_state>(state_name); + + // + // Expression here + auto apply_jl = [&expression](int value) { + boostjsn::object data; + data["value"] = value; + json_logic::ValueExpr res = json_logic::apply(expression["rule"], data); + return json_logic::unpackValue(res); + }; + + for (auto first = the_set.begin(), last = the_set.end(); first != last;) { + if (apply_jl(*first)) + first = the_set.erase(first); + else + ++first; + } + + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/test/TestSelector/selector.hpp b/test/TestSelector/selector.hpp new file mode 100644 index 0000000..2da089f --- /dev/null +++ b/test/TestSelector/selector.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +namespace testgraph { +class selector { + std::string name; + +public: + selector(boost::json::object &jo) { + try { + if (jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY\n"; + exit(-1); + } + name = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!\n"; + exit(-1); + } + } + [[nodiscard]] std::string to_string() const { return name; } +}; + +} // namespace testgraph From f971e7fe31dc9f624e2612ae2e956c02cd67c4f5 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Sun, 24 Nov 2024 11:48:02 -0800 Subject: [PATCH 02/14] cmake --- .gitignore | 1 + CM-broken.txt | 106 ++++++++++++++++++++++++++++ CM-working.txt | 109 +++++++++++++++++++++++++++++ CMakeLists.txt | 20 +++++- cmake/setup_boost.cmake | 83 ++++++++++++++++++++++ test/CMakeLists.txt | 4 +- test/TestGraph/{cc.cpp => bfs.cpp} | 0 test/TestGraph/mvmap.hpp | 2 +- 8 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 CM-broken.txt create mode 100644 CM-working.txt create mode 100644 cmake/setup_boost.cmake rename test/TestGraph/{cc.cpp => bfs.cpp} (100%) diff --git a/.gitignore b/.gitignore index f4a71f8..d5f38ab 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ CMakeDoxyfile.in CMakeDoxygenDefaults.cmake DartConfiguration.tcl __pycache__ +/.vscode diff --git a/CM-broken.txt b/CM-broken.txt new file mode 100644 index 0000000..583fb48 --- /dev/null +++ b/CM-broken.txt @@ -0,0 +1,106 @@ +# Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +# Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: MIT + +# Works with 3.11 and tested through 3.15 (not tested yet) +cmake_minimum_required(VERSION 3.14) +set(ALLOW_DUPLICATE_CUSTOM_TARGETS TRUE) + +project(CLIPPy + VERSION 0.2 + DESCRIPTION "Command Line Interface Plus Python" + LANGUAGES CXX) + +include(FetchContent) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Only do these if this is the main project, and not if it is included through add_subdirectory +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + + # Let's ensure -std=c++xx instead of -std=g++xx + set(CMAKE_CXX_EXTENSIONS OFF) + + # Let's nicely support folders in IDE's + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + + # Testing only available if this is the main app + # Note this needs to be done in the main CMakeLists + # since it calls enable_testing, which must be in the + # main CMakeLists. + include(CTest) + + # Docs only available if this is the main app + find_package(Doxygen) + if(Doxygen_FOUND) + #add_subdirectory(docs) + else() + message(STATUS "Doxygen not found, not building docs") + endif() +endif() + +# +# Metall +# find_package(Metall QUIET) +# if (NOT Metall_FOUND) +# #set(METALL_WORK_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-work) +# set(METALL_SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-src) +# set(METALL_BUILD_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-build) +# set(METALL_INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-install) +# FetchContent_Declare(Metall +# GIT_REPOSITORY https://github.com/LLNL/metall.git +# GIT_TAG master +# ) +# # SOURCE_DIR ${METALL_SOURCE_DIR} +# # BINARY_DIR ${METALL_BUILD_DIR} +# # CMAKE_ARGS -DINSTALL_HEADER_ONLY=ON -DCMAKE_INSTALL_PREFIX=${METALL_INSTALL_DIR} +# # ) +# # set(METALL_INCLUDE_DIR ${METALL_INSTALL_DIR}/include) +# FetchContent_MakeAvailable(Metall) +# endif () + +# find_package(Threads REQUIRED) + + +# +# Boost +# find_package(Boost 1.83 REQUIRED COMPONENTS) + +include(FetchContent) + +# Fetch Boost +FetchContent_Declare( + boost + GIT_REPOSITORY https://github.com/boostorg/boost.git + GIT_TAG boost-1.83.0 # Replace with the desired Boost version + GIT_PROGRESS TRUE +) +FetchContent_MakeAvailable(boost) + +# Ensure submodules for Boost.JSON are initialized +add_subdirectory(${boost_SOURCE_DIR}/libs/json ${boost_BINARY_DIR}/boost-json) + + +### Require out-of-source builds +file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH) +if(EXISTS "${LOC_PATH}") + message(FATAL_ERROR "You cannot build in a source directory (or any directory with a CMakeLists.txt file). Please make a build subdirectory. Feel free to remove CMakeCache.txt and CMakeFiles.") +endif() + +include_directories("${PROJECT_SOURCE_DIR}/include") + +option(TEST_WITH_SLURM "Run tests with Slurm" OFF) + +# Header-only library, so likely not have src dir +# add_subdirectory(src) + +# Testing & examples are only available if this is the main app +# Emergency override MODERN_CMAKE_BUILD_TESTING provided as well +if((CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME OR MODERN_CMAKE_BUILD_TESTING) AND BUILD_TESTING) + add_subdirectory(test) + # Example codes are here. + #add_subdirectory(examples) +endif() diff --git a/CM-working.txt b/CM-working.txt new file mode 100644 index 0000000..421b8cf --- /dev/null +++ b/CM-working.txt @@ -0,0 +1,109 @@ +# Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +# Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: MIT + +# Works with 3.11 and tested through 3.15 (not tested yet) +cmake_minimum_required(VERSION 3.14) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +set(ALLOW_DUPLICATE_CUSTOM_TARGETS TRUE) + +project(CLIPPy + VERSION 0.2 + DESCRIPTION "Command Line Interface Plus Python" + LANGUAGES CXX) + +include(FetchContent) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Only do these if this is the main project, and not if it is included through add_subdirectory +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + + # Let's ensure -std=c++xx instead of -std=g++xx + set(CMAKE_CXX_EXTENSIONS OFF) + + # Let's nicely support folders in IDE's + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + + # Testing only available if this is the main app + # Note this needs to be done in the main CMakeLists + # since it calls enable_testing, which must be in the + # main CMakeLists. + include(CTest) + + # Docs only available if this is the main app + find_package(Doxygen) + if(Doxygen_FOUND) + #add_subdirectory(docs) + else() + message(STATUS "Doxygen not found, not building docs") + endif() +endif() + +# +# Metall +# find_package(Metall QUIET) +# if (NOT Metall_FOUND) +# #set(METALL_WORK_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-work) +# set(METALL_SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-src) +# set(METALL_BUILD_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-build) +# set(METALL_INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-install) +# FetchContent_Declare(Metall +# GIT_REPOSITORY https://github.com/LLNL/metall.git +# GIT_TAG master +# ) +# # SOURCE_DIR ${METALL_SOURCE_DIR} +# # BINARY_DIR ${METALL_BUILD_DIR} +# # CMAKE_ARGS -DINSTALL_HEADER_ONLY=ON -DCMAKE_INSTALL_PREFIX=${METALL_INSTALL_DIR} +# # ) +# # set(METALL_INCLUDE_DIR ${METALL_INSTALL_DIR}/include) +# FetchContent_MakeAvailable(Metall) +# endif () + +# find_package(Threads REQUIRED) + + +# +# Boost +# find_package(Boost 1.83 REQUIRED COMPONENTS) + +# +# Boost +include(setup_boost) +prepare_fetchcontent_boost() +set(FETCHCONTENT_QUIET FALSE) +FetchContent_Declare( + Boost + GIT_REPOSITORY https://github.com/boostorg/boost.git + GIT_TAG boost-1.83.0 + GIT_SUBMODULES ${BOOST_REQD_SUBMODULES} + GIT_PROGRESS TRUE + CONFIGURE_COMMAND "" # tell CMake it's not a cmake project +) +FetchContent_MakeAvailable(Boost) +get_boost_include_dirs() + + +### Require out-of-source builds +file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH) +if(EXISTS "${LOC_PATH}") + message(FATAL_ERROR "You cannot build in a source directory (or any directory with a CMakeLists.txt file). Please make a build subdirectory. Feel free to remove CMakeCache.txt and CMakeFiles.") +endif() + +include_directories("${PROJECT_SOURCE_DIR}/include") + +option(TEST_WITH_SLURM "Run tests with Slurm" OFF) + +# Header-only library, so likely not have src dir +# add_subdirectory(src) + +# Testing & examples are only available if this is the main app +# Emergency override MODERN_CMAKE_BUILD_TESTING provided as well +if((CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME OR MODERN_CMAKE_BUILD_TESTING) AND BUILD_TESTING) + add_subdirectory(test) + # Example codes are here. + #add_subdirectory(examples) +endif() diff --git a/CMakeLists.txt b/CMakeLists.txt index 18b0e11..421b8cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ # Works with 3.11 and tested through 3.15 (not tested yet) cmake_minimum_required(VERSION 3.14) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") set(ALLOW_DUPLICATE_CUSTOM_TARGETS TRUE) project(CLIPPy @@ -67,7 +68,24 @@ endif() # # Boost -find_package(Boost 1.75 REQUIRED COMPONENTS) +# find_package(Boost 1.83 REQUIRED COMPONENTS) + +# +# Boost +include(setup_boost) +prepare_fetchcontent_boost() +set(FETCHCONTENT_QUIET FALSE) +FetchContent_Declare( + Boost + GIT_REPOSITORY https://github.com/boostorg/boost.git + GIT_TAG boost-1.83.0 + GIT_SUBMODULES ${BOOST_REQD_SUBMODULES} + GIT_PROGRESS TRUE + CONFIGURE_COMMAND "" # tell CMake it's not a cmake project +) +FetchContent_MakeAvailable(Boost) +get_boost_include_dirs() + ### Require out-of-source builds file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH) diff --git a/cmake/setup_boost.cmake b/cmake/setup_boost.cmake new file mode 100644 index 0000000..5b6b2e7 --- /dev/null +++ b/cmake/setup_boost.cmake @@ -0,0 +1,83 @@ +function(prepare_fetchcontent_boost) + set(BOOST_INCLUDE_LIBRARIES json PARENT_SCOPE) + set(BOOST_ENABLE_CMAKE ON PARENT_SCOPE) + + set(BOOST_REQD_SUBMODULES + "tools/cmake;" + "libs/assert;" + "libs/exception;" + "libs/throw_exception;" + "libs/static_assert;" + "libs/config;" + "libs/container;" + "libs/container_hash;" + "libs/utility;" + "libs/type_traits;" + "libs/move;" + "libs/tuple;" + "libs/variant2;" + "libs/detail;" + "libs/smart_ptr;" + "libs/integer;" + "libs/intrusive;" + "libs/io;" + # "libs/iostreams;" + "libs/describe;" + "libs/core;" + "libs/align;" + "libs/predef;" + "libs/preprocessor;" + "libs/system;" + "libs/winapi;" + "libs/mp11;" + "libs/json;" + # "libs/property_tree;" + # "libs/interprocess;" + # "libs/optional;" + # "libs/any;" + # "libs/type_index;" + # "libs/mpl;" + # "libs/multi_index;" + # "libs/serialization;" + # "libs/bind;" + # "libs/foreach;" + # "libs/iterator;" + # "libs/headers;" + # "libs/format;" + # "libs/range;" + # "libs/concept_check;" + # "libs/uuid;" + # "libs/random;" + # "libs/tti;" + # "libs/function_types;" + PARENT_SCOPE) +endfunction() + +function(get_boost_include_dirs) + list(APPEND BOOST_INCLUDE_DIRS + # ${Boost_SOURCE_DIR}/libs/interprocess/include + # ${Boost_SOURCE_DIR}/libs/property_tree/include + # ${Boost_SOURCE_DIR}/libs/optional/include + # ${Boost_SOURCE_DIR}/libs/any/include + # ${Boost_SOURCE_DIR}/libs/type_index/include + # ${Boost_SOURCE_DIR}/libs/mpl/include + # ${Boost_SOURCE_DIR}/libs/bind/include + # ${Boost_SOURCE_DIR}/libs/multi_index/include + # ${Boost_SOURCE_DIR}/libs/serialization/include + # ${Boost_SOURCE_DIR}/libs/foreach/include + # ${Boost_SOURCE_DIR}/libs/iterator/include + ${Boost_SOURCE_DIR}/libs/container/include + # ${Boost_SOURCE_DIR}/libs/unordered/include + # ${Boost_SOURCE_DIR}/libs/iostreams/include + ${Boost_SOURCE_DIR}/libs/system/include + ${Boost_SOURCE_DIR}/libs/describe/include + # ${Boost_SOURCE_DIR}/libs/format/include + # ${Boost_SOURCE_DIR}/libs/range/include + # ${Boost_SOURCE_DIR}/libs/concept_check/include + # ${Boost_SOURCE_DIR}/libs/uuid/include + # ${Boost_SOURCE_DIR}/libs/random/include + # ${Boost_SOURCE_DIR}/libs/tti/include + # ${Boost_SOURCE_DIR}/libs/function_types/include + ) + set(BOOST_INCLUDE_DIRS ${BOOST_INCLUDE_DIRS} PARENT_SCOPE) +endfunction() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f2f8d05..52a280c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -10,9 +10,11 @@ function ( add_test class_name method_name ) set(source "${method_name}.cpp") set(target "${class_name}_${method_name}") add_executable(${target} ${source}) - target_include_directories(${target} PRIVATE ${Boost_INCLUDE_DIRS}) + # target_include_directories(${target} PRIVATE ${Boost_INCLUDE_DIRS}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}) set_target_properties(${target} PROPERTIES OUTPUT_NAME "${method_name}" ) + target_include_directories(${target} PRIVATE ${PROJECT_SOURCE_DIR}/include ${BOOST_INCLUDE_DIRS}) + target_link_libraries(${target} PRIVATE Boost::json) endfunction() diff --git a/test/TestGraph/cc.cpp b/test/TestGraph/bfs.cpp similarity index 100% rename from test/TestGraph/cc.cpp rename to test/TestGraph/bfs.cpp diff --git a/test/TestGraph/mvmap.hpp b/test/TestGraph/mvmap.hpp index 04e2a70..128d40a 100644 --- a/test/TestGraph/mvmap.hpp +++ b/test/TestGraph/mvmap.hpp @@ -1,5 +1,5 @@ #pragma once -// #include +#include // #include #include #include From 7ed19c97b3143e88edabe57f6d35ad1d4b08694c Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Wed, 4 Dec 2024 18:50:03 +0000 Subject: [PATCH 03/14] copy_series --- .devcontainer/Dockerfile | 18 ++++++ .devcontainer/devcontainer.json | 23 +++++++ .devcontainer/reinstall-cmake.sh | 59 ++++++++++++++++++ test/TestGraph/CMakeLists.txt | 1 + test/TestGraph/bfs | Bin 0 -> 204440 bytes test/TestGraph/copy_series.cpp | 101 +++++++++++++++++++++++++++++++ test/TestGraph/foo.cpp | 18 ------ test/TestGraph/mvmap.hpp | 78 ++++++++++++++++++------ test/TestGraph/testgraph.hpp | 70 +++++++++++++++------ 9 files changed, 312 insertions(+), 56 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/reinstall-cmake.sh create mode 100755 test/TestGraph/bfs create mode 100644 test/TestGraph/copy_series.cpp delete mode 100644 test/TestGraph/foo.cpp diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..60fc29d --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04 + +ARG REINSTALL_CMAKE_VERSION_FROM_SOURCE="3.22.2" + +# Optionally install the cmake for vcpkg +COPY ./reinstall-cmake.sh /tmp/ + +RUN if [ "${REINSTALL_CMAKE_VERSION_FROM_SOURCE}" != "none" ]; then \ + chmod +x /tmp/reinstall-cmake.sh && /tmp/reinstall-cmake.sh ${REINSTALL_CMAKE_VERSION_FROM_SOURCE}; \ + fi \ + && rm -f /tmp/reinstall-cmake.sh + +# [Optional] Uncomment this section to install additional vcpkg ports. +# RUN su vscode -c "${VCPKG_ROOT}/vcpkg install " + +# [Optional] Uncomment this section to install additional packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b519e25 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/cpp +{ + "name": "C++", + "build": { + "dockerfile": "Dockerfile" + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "gcc -v", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.devcontainer/reinstall-cmake.sh b/.devcontainer/reinstall-cmake.sh new file mode 100644 index 0000000..408b81d --- /dev/null +++ b/.devcontainer/reinstall-cmake.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +set -e + +CMAKE_VERSION=${1:-"none"} + +if [ "${CMAKE_VERSION}" = "none" ]; then + echo "No CMake version specified, skipping CMake reinstallation" + exit 0 +fi + +# Cleanup temporary directory and associated files when exiting the script. +cleanup() { + EXIT_CODE=$? + set +e + if [[ -n "${TMP_DIR}" ]]; then + echo "Executing cleanup of tmp files" + rm -Rf "${TMP_DIR}" + fi + exit $EXIT_CODE +} +trap cleanup EXIT + + +echo "Installing CMake..." +apt-get -y purge --auto-remove cmake +mkdir -p /opt/cmake + +architecture=$(dpkg --print-architecture) +case "${architecture}" in + arm64) + ARCH=aarch64 ;; + amd64) + ARCH=x86_64 ;; + *) + echo "Unsupported architecture ${architecture}." + exit 1 + ;; +esac + +CMAKE_BINARY_NAME="cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" +CMAKE_CHECKSUM_NAME="cmake-${CMAKE_VERSION}-SHA-256.txt" +TMP_DIR=$(mktemp -d -t cmake-XXXXXXXXXX) + +echo "${TMP_DIR}" +cd "${TMP_DIR}" + +curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_BINARY_NAME}" -O +curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_CHECKSUM_NAME}" -O + +sha256sum -c --ignore-missing "${CMAKE_CHECKSUM_NAME}" +sh "${TMP_DIR}/${CMAKE_BINARY_NAME}" --prefix=/opt/cmake --skip-license + +ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake +ln -s /opt/cmake/bin/ctest /usr/local/bin/ctest diff --git a/test/TestGraph/CMakeLists.txt b/test/TestGraph/CMakeLists.txt index 0f78239..6957a5b 100644 --- a/test/TestGraph/CMakeLists.txt +++ b/test/TestGraph/CMakeLists.txt @@ -8,6 +8,7 @@ add_test(TestGraph degree) add_test(TestGraph add_series) add_test(TestGraph connected_components) add_test(TestGraph drop_series) +add_test(TestGraph copy_series) add_custom_command( TARGET TestGraph_nv POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy diff --git a/test/TestGraph/bfs b/test/TestGraph/bfs new file mode 100755 index 0000000000000000000000000000000000000000..fad136a1f092e45cec454a7b12bbc79c25b9b33d GIT binary patch literal 204440 zcmeFZ33OFO_C8v5?!7sI6GCJR^JP#*1sNnDN+8Ifs0cwLq6q{fC}RkUf`S5yf-M0Q zTR>4!0a0nC6>SGv98gg~P*G9CpiQ^fqM$+x=Kp;)oO|viY5o1)d+WXR*41^gtLode zt7_M-UAyX>%gplz_I0Eb_P>yLUZ`9(U6O5p`)Y?Qi^vvPq6+@D5Y0sjO2c@v4Vp-g z8qt=f8a_&6y(~u;B+>?k&r;h%(ulz-BA<@ZrIrdk8ugcbEjzC9vg2B!rCDZp+5Rv! z&hD|^Aye;=sb^`_^xslDK8}q~uE7&&A7K2?Qu6Y@L0^}QVmkADl|6~(n!#IY>s^9+ z?B^f-FT*enHtkiWm#EQ)rFI_Lj~hF({kT!5j~hE->a^2Gj3_QFI;-;;Qzo8qrXF`K z$Y<4l7Y!Bd?#sA&h;#F{wat3pQPk$;?iXZU{V4m*K2RQc_#gRQmD1(JY=TcUMyWEaaS0ft~> zaYzj*IkT(fA30)F{)lnoCKg_Y-V{z7kv|fxPslGEQBqi>SUa9McH)%$kt3#z&d0JX z9#MQl{@4j)OUOQE+O+&hql>3ZoG@bC*peIauj`;|WW@qPAMrKJ!1TT!rc7xhIGl-Lz!?r2BLad zJX#G`$?9A%I5B?o`0*33 zBYFTi@)s3UcIAwH+7%_5UFn<8|4taQT^EB+ zy8#}ts&I_^BhSGAzO?jSY#$L()yE^^4TBe;5512r1yV)S#$U?3Vf)jNmnvG=yuw{^ zrpfPk`daK%VUca}C9M}>YYd4Ckmr9AJ>F~NGrvK#pCT?Z{JKAm-ygeAhs=hTk;2i?xMn$StR-il>H)RLP?VigD{R8DT zmSOpzKzUFPd4ck(WHkR31j>V3aZ#W=%`$^d3Y42EROqxoxtT&$J}XdOJs@XZpgg!g zEew>`4DjChuOaW88*Bef z}JYR>z z+C8D{V|$y6(r+*?WtazXpx?0&urQt%%FCDT#G}fO_B)mhx3#~#ye{X>&JP^hLQx<} zv2GVO1??%sx0`Sb*aKNjkMuh>0(B0BFV83ItP)D^9&OSC5ecy#A zDrOBX7%93P_Og$inEslwJKO)=7|)66FKU@XNKrZ^MQksBL_oi@J}i4&94iY&O3P}Rd|l*KUCfo&dreuW z*4o{ux5U)L*gh;<3Z6&MHgux$)q{0da!DW#HM&{+c>&uuitB^BU^RG3^wv z&V%6D7cTt|`hPa_wqK$87*_)|roW;N^->1?90lJCOz#VomUWHkVh{6$4)j4?WTI}p zwLhXB`L2wJCDhqr)St3fmQv14M|`^yHZ^6W*j^T@xpo_P|ATpB`+LHgpYqy;#FBl~ zE#;q`(B5XJRO>#pwmtaB10z>$l>zI6vcs&4^c6@uuo*p1HP;p+U;YHfj=ua1p00LG zJrmk!&N=3o>R?QD*A|;GZ8Yta&rDxaK3(g%kQn-04!R#Q%bPph%9?Y$F`0Xj-_zVF zWnW{q6JEQ3{Rh?~MfblPcDhiAT4iW|3orpYEkl{WI+80-<+|P`4)r_s-f!hg_q5GE z_C4rcw5jk-z+snfqKx%FL4ESDZ~tUF?a|-%*~gBvzs+^JCd*rjWANj(%0jZ##rRa4 zO-!3*u{PJ6w#u4hAJh7D)_zby`;W1G%Y*U4c9xXgp1P(C<3Ei3LNrvzm%)bCXd=X9 zgk!mC4AIm4F)pc~y=NO{-Nx9Rc6Lp5Np4r zg7y~&+g}M@==&O!(cWwupF!3@SE6jsKKSX`q0&zkKjwE+tnH!1F^9xc zFejwxq?n^6;NJtAVR{zpYJDm_?)jlSmvdh)RvxG4Uf%)V7b>j+oH7qdQ1Jho6uLlM zt!%jqY((gF(?;1GeCED?WFK=du0r8_i#7FOSyhpX+mBk%samHa%-Z-Fe(ea>*Albl zA2e$oer`K`-VU_EHGc$a%dz#*7wAi^;a45;Lzxu0^ld`ad{^rc`_9psxH6FQ#i5&a zJnM-0Go9LTzT{=d{S|WeppDAabtzu~L{ccx71ov+~*T@+pV*F1fCl#(o_Y;!dpVAIjVZ>(f3H+uqYebi*3# zCUBL(^Y0lFJ$cpcvIAA8r^h% z*(1W=>n}sQWw-ZU!~KqXvu$TTd^y{iBjmSiD;sP4>*HMG4)*WR+7r|JtU({unh5HE zwr=f!@g>t%2jwA-DYWJUbmyTO0{UzAOOswOXfprYs!!OmKUswAF=DR8E+{DUlXh(m7d~I*q zetdf~c4^nymGGX@mtAE??Nnv2r&k=_@o)rNDe{5$Mt2+DA#*LwrL?AGc4U zUx~W<{#UMRx%Twm(bd=B``_s5E%c!xUDY=6Id#De z@|l96_xu>0A(q)a>BRJx^xV-8VSe^BfUkw!9cFBbcDQa=qb=Hkj)7{f{gOIDIp^;X z(zx%_wuby@`XIy^-*D{MyH8AiUC$%-u@lpsH790kUuJDg`Dd&-<;Uosb42O)kXeSh zsy$IU=$#Hd=HzU!!P+~~tT z_J?)@fBORJS{vC&?vn|7G}>S9#~p$8 zhpD|C`aye)E4rCJl%Wq7&=z=pF?IJ<(DsMAZNw3QJ=(;Ev}eYj(#Bwjk@s*dnQ|Rt zpl#TaihrQTpv_R9{`$#@k2k3U6<@&L1=P(eaxblLE+lo2I7y8Q^YIwY zI<$Ke~5(f`C)X|kFF8_xyiAwg%X$1@OP>u8*w%)MZOp41rw?I9*T))V)g(J<~i zkymSm=L+~_`ew$hW!yipcMik8sceRR82c&L6z^#Ev`4xv=M48Ta@L3UbHAo-wa-3w zA?i@K1x~yviI^BULbKB$Mam{Xi3bRX^f2I~i7<~fzrO|%c|JFSg!d_Q4LD%~JG7jigX(9Qff zyWGq;II`vhVx7aCpj+%!Y@g>-#B1H+Yz*_jeav5HT!WlD+CSEqnnTVP=FrAJun*;T z6^^xY82X@o+E|}}zHPB-_{S0Gr`4NU8{k#;20s5R$Ffwk3#!&1XdTEve-V4vT&sKo z=4oV5mN{43@$7A>JtVGagXm$NFu8vrpO52kX~Fe89c?QxgkeKGmJkEZ&EheFk?EYApuW z%>JY0O9f*J(X#YAq-jq|w%zMdCo#_WgnZERVfgX*b8p45x{A3lH@)DWZOqjjV+h7q z{drcw+85L**1nC)6aAvyyOrO7%v7ukwO*(v)bC++1B`nGbr$C@gL@%#t$Y*4OkYLb zL?1zY;9g0^Dor3y<>9-6yeFpX^QDa~_R@#Yr{X?J>7RBP+#g|&+!N`;te;(J?u98g zcDAf&pQ+eSEsv{jVwQy15A#}K%&`|^ETi4A-#M^5#7FE0ALf-k;GRgGdyu~+(3Wpo z)Hn9bplw|UI|;^v!99{|^52Z9LEk;ex5e&ues8T%$IzX+7iGM3f-bJTYsydt;+nu z^N?v{`VB}^hbpG8tMAz3e8C@FAF9us3)mFx3HR6ULhsmD#IZfFAD)r^1;5KRuja-2 zPRvE3-sbsZIXOOOJHG!3>>2ic)2=nHv*4Yv@?q!)?RXf^0Js0{y{0}xnEfDlCtRTV zr_T_H_nPIK9oSJ7#F}_VIi#w-7c{YET;KX>n-bVpoU)ecJW0D9#`#7J>DxAAPue-} zg6JQSet_%Co_%vf>HVz3GX*f7Io?K0pw1I5Q3reJ5tQ@1q1HLdI+=Ce9rcd1N4(0I zxGSIRV|-ty=BGXCoQ<+hpgl2%IY{@^b0=^nnaVwgXOf)kW35n!HiZ4u_lbP}Oux7Y ze#^$MSL2MP;#c%R-OnM8p3c6L7yG23&6|320`>B2J@D+od~gpNZ|d<*iQ`uHAdm-} zi`{`RPRG6d2-+QZ_MlzvVJZz--23PYtX`D=Q?m3pFkcIK53si%>&5%+uYSh(hn$Z) z&H;#{9rI4$GXh1k7?Qj_E65f(ksT#clY#r_R(wIo&(Jtu!lN)p-cI zrN51u@z^`!?+~LX`vE`4!#?amANH^hn7iJvO}1_O9v^?``(k@HWzT7xFBR`$Pf}}x zvZ*hOnQhpykneMhn|T`>#hn4LhOFLl(x5Lk`L|cM;F82f@ zw;~;u8Xf+(cd2$Q)Wsg4BAHKw|xg&(b}|Ya#oxk|MzPR z9}1YeGP~Adve-}RM){YJj!%2obGyNO-^zU*Wvt_5fI|N^p326)y@#;@eIMrqzK`cs zbza2$Tw~TI>rOeTKPUV4!<&y$>egA+t2SuEw-uuG8THDp_!5)q=##D9y`Lqk# zqiqLN-8%-`uCOf8H_?{quU*KmU|mqBXG5oI&qg{M=@E{2m$ugv_q}B;b?hvH-=Wy} zp#uN&!4d6qJ|%^`b^+O~kyiUJ)>YXfq7=VZvhme?HTU3;qD-v~wGYd0@ok#YV~vFJ zJ4|^R%BfSn3;ew`x|)6ZFMiJ6zuJ3-df0RAd7=)|v~~Dl6^8}S5*4jO?veYT!{9zC z(O0`qR`O0Dwtrwf@NC9db2sB+P@dCVr3ctC(qW`kIeimsM<7p|vGZKfJMuBmLHxPg ztmnY~&X}z1SJ6$;a?vd~hDNM1OA(_iao}2Uwp<2ZzNo zE7z9&_}~0@nc-nSlnw$uygmHDAMxQ)V;}K8{AB% zqzH>Ao5HV3BhR<@3jEO%hiUJ@yOvmd%KrTZJsiT?j`b<#XM^9@Ma>x@(I30rOx1-q4K1F&T=3y@|_5CK!iY(LLqnx}R zX%oS{MCEzbW?va&5a&B~=1;oQJlW>$nS^l?V+^%7;O<$S0UKs7!aD!KzB{#h1?DA} zyh0pbME(KD7h*on$7A>ZF-U8hu}{W0aVQ`DVr&-1n&6@vu)0{L-q;`@Y)k@n{SK41_r#2GO;fp|D5hkd|(h5A0Y zW%jX&^r?`Y$9ZI*xF2y0z*L%d8z`qAqCMlTDVG?(AG!+T*n>2E!cv@1Z$+LyYaYjk zKK+S#)u)qVYU@7}?Uid(;Qs9%>@(0|+&DTy*uN#LgMCGPKZic4^^SgI zQ#a~+y?FmfpBL*##r~K&fxq(g-WO@>i?4(3f_}IUFxBQGqI6(Fo58;G_g1$5>Kiun zQG{lFn1}vTb{EE77*8)N58tBZAWfa~w>T^99RU3*>hiY&7o%<&ekX7yb#A_ekJ-Y> zzI0y>-Y>u>(@sawPN6THlWN&@!uufFrSiFGlQ?2+e3PW@+2VE$SG;CUZa3(|Z-KnT z`q1Ao?n!oDDw10s;vKa4-eE|}nxD=_9h_@ecN2flXTPb2?J{mWjCrO^5BH>M?vQpc zcgm+Bp5PkPu|DKre8Ian&O=#qZHKfi`!F5}&i!kAABwUYad%E0w)Y6`Ms0g9BF*oL zX>;l<0s0wu_n}OE@4#=3F^~Q1oCD|nOj&kJiQhXQ{>g2OIa6^0+P=cHO`EWF_O(3u zo~P&s+KOEJ7^td?wxZZ4ey`pzQ|~^|%UE|vU+A7b2j5zR>>crcpL6_MFXkS>`u_jU ze`>%+{Jkr|2M2BHoB%)HdD(kKb?(JD8JF?wX=8EPu{3w)!t8S@b};AOU@Q)K<{tdy zn4!{rAdGnEllb_Eaf0>#%7)Mf?h9u?r>bA@IXEZ%fc2rju>!_#V{Dzu_6&8V#{CNK zW>uVD$=QwfA=Wq8^<~#xFjl1P*>zZv?t^*{1@!LwI_!_u&+&U*_8?@RI9^SC{% z3bDW*X8ZO>{WN|H?#ssS$bCPZhIJoYtDhl{@vi0F!(OC=dqdoQR47VU;ml2&vF8Hn z#Qvt+{+^rv&H?|M8a*dU3O}v>i$?ZP8n-UfJX}_j-P~oJY+)2 zlqwbTfTqIGR)a=C zGj^1h_XnK@S_HZubRK9fegwV_bOGpA(A}ViK>L4$@nd3&K(jy(faZaY`nbG&8t6vQ zg`fvPSA%x=1oc7lK=*@A0zFO~v>}5qeALw$^Z;lcXvU|I1KI=hKF~p+>p`c1ZUbEi zdH{3_Xj&TTeO6xH0rWU%f6$E2F@De}NZH|Q$Rk9TH-R1n4ex^<@Z*LBpgllWg64vD#*gl2fOhz%ynGetAY|w$Ad7ukGr-5z&T?o1b zbT#O1&`qEj-$M@QAkb8Nd_M=Y6=>Qaj03bkXc1@u=q%6$p!b2c!v*q2mV@pBT?cv; zbU$cDEy(>5_5hjL7}fzALu3c3rlA^tH8+<}#M z23-!C3%Uig1T>6~A(wzI0o?$)33MmuPSAs(M?llCS>^p)UfvFL9_T>O@Gn^ZpqZd^ zK-+;X2VDaC66kKwGSEp!p^v)I2WS*@J!m%QQP5GKX~$p}pyNPSf*t_f209QwlsyDm z0GgT!eSx+DT?LvAx)C%F6z|c>=Yh@vT@AV(bPH%1=mF5I6Id7ZFka9oX!tkO2Wu4Rj^wa?lN+8$fH7V?IFnAIwYrCvO}q!qXbd25C)H8N`c{m!Vgpz1+s+Ylmk& z$~v$N|FJ9^&sLP-9g6xdiodydj-sqTOM0b8SA-u3^ju!?lOX`n4$fnBly z|54TgCfb1goKo))RB?VCaCg1`;4y;Um8Sqr# zGj#oIRe!jy--2Zb2Sx#3uJM9iYO%-w&(n#Nun z-T{Z#E4^Sv+5=v{^!4GzRhLw`C-q);Y07<(KZoxREptNaWO@Ob{`}L=KH2rr$2)ea zF;ag;cs5{+oWt;oN)Gg`jdzKh_{EoomwH=*ClBKpPTpmjcSY3)s`Or*y2OQI$V!;%*D^eeDdoND8 z)ULgTz;MnJn+zXqRoTR4F`KFthRwVK9|&F_&(nAmMV*1?0-vOD+eTinjXdzI1W%Uc zQT8&>%r|B5-vZkRd=S#%Ss83c*~DPO!|@yi&wkU7aDn2nZCw^@>vgo%3Nc$0b*!AA zZS6PsERE|maAj<~VZ?;ir-5fUcxWT&b4Gt7b0NydpO=-XA=ur`8L`?C+UT6t4qMCe997iDMoR8}l~swA&UxX4XA`@>VE6fHdSA zKflyi!IBoTVCq=ZKb}_uw02{-Y1@L%=!jRv&iGS$`Dp zc006RCZ6bT=KwDOKF-uvc);JT0=@}&W&VV1YynRhc;;ytdYt*XkJQ-#;Qe>T>yLBJ z3xGZ-U!v<7yWsv`;P3zBYX!db;8Xv+VXyS{(k{3q(t#?}&KL#XAn?t>d2$od7_0R+ zrsaTUfp4IerEC#~2#eAF_a)%#fCtwS>+Lk< z+*|B?*nNfg5#YN~uR{P2#$2^vyA?yB6DOMvGA5B58X zdJ9bX0i>xrt75xGDVyi(hM&f-z2Mk*?j8udvauZkJ{)*uW6Qu27zO-3-3M*!g)v*_ z`F%I=0*x!(Ah=L-!u|~c-taSDzFLpLcy|);7QhGU`f+p0`U`;%0&dqU}eiZn6 z;G2M(xwUo^=X-X7=OB0}4|>cv&+G>WQGN*Ji9RF@S4aMA&9BA;$Lre}?;O%_7h!bf z`!e1^G(>q&9@Duf&rB$fqI?|6YoWZe_TT>eI{md1CeK3fwAk(U#qI+Lo%G(vF>L@( zHh4NCZSDJlz#QxZo(p`Q#^Zbe>mLFB67U`dr;ZVv_~WgG)Gpvb`{cOVneukJp6|PP zCOZiJmAYKp%;=cSi~`O(8`EDk=fVEY0X_@(KwV$yKe*0U0bd6^%d|TXd5+^H(5)yp z{W;0^?*_gLctvZ0<2ere0C1*hCw5P%g*%)5C^!98{R#HBGw`FpOLRRw-eIxv@=j>r zmlemOY^%iJiT1Pr<-<`gIL;{ed8af3<->J*c28asG3N}*PO zNBN9E-75pO!h?4(rqrpdMT=ffw0*qSQ#xCG3aIe=PfCqj2QQ)0{SELvAB@@S| zg}{UTV!8v$7bKK(oc&Qg59LL=JvB!-5UKt|QC0+e8*no}b`Jgcj{D*Pa32BB5~RaR zaY&2pizDp%XCF2|RsrrPgMDKkwxWDE%FSFVo!M9@3j84OBH#&qSQ23`a9*Xn4BYcA zNRZ9Cyf>VO@&csMFWdhm5d>+h%QjcT)?Nn>bsENx?<`-=;*=$kd&2jIhJE>kJ#e$X zGkCX%yVZ*3kbPMWoOiH^EoX6fN$4KOLBRNt3UQR*>5Mzf*Kzk-nQi3)-wC{;{&CzTz{@JB&%WUYVd4<* zJfxw+4!LG6uS0nU+%+%MfVa}P+Q-T4^s&CDh1&pu^jKDnW?ZK zjf2V#_ZPgVFifc(!UM(dK)zkLv*;7ctG+C&Evt11kCKy8r-?aift$G!kxFqzn*Ed| zGQ7|cyJdK*BepndMigK2Pf`O(4J0*?)Id@LNev`5kkmj@14#`eHIURmQUgg1BsGxK zKvDxq4J0*?)Id@LNev`5kkmj@14#`eHIURmQUgg1BsGxKKvDxq4J0*?)Id@LNev`5 zkkmj@14#`eHIURmQUgg1BsGxKKvDxq4J0*?)Id@LNev`5kkmj@14#`eHIURmQUgg1 zBsGxKKvDxq4J0*?)Id@LNev`5kkmj@14#`eHIURmQUgg1BsGxKKvDxq4J0*?)Id@L zNev`5kkmj@14#`eHIURmQUgg1BsGxKKvDxq4J0*?)Id@LNev`5kkmj@14#`eHIURm zQUgg1BsGxKKvDxq4J0*?)Id@LNev`5kkmj@14#`eHIURmQUgg1BsGxKKvDxq4J0*? z)Id@LNev`5kkmj@14#`eHIURmQUgg1BsGxKKvDxq4J0*?)Id@L|9{j#M!Fu=rfQl> zGsrhg!sd4-1JsfN}!^nk$|Cg4B)Rz8tM z_c)lTg>=s8Qe9ruA%Fz=!7j_ zZ3bk=m1^o6HoyJGroQF-wOo%6?5gNDliy_omz#Xp)H`JI4;%bg0&dv;A14cqV*kHJ z4;eeM3aDl5-RA!&O%>J>bd7#FIo+eJhK`&%p=4_G%uZ)?Jfp+u9Xm}``Od9Pi5L#^ z)AjWH+SU(RGd1RCXFt{Ct#w8d@>wQtt#F{pcQ(VAm5|?H^3x37u8vMv{sNQVX7CQi zoLiZEp2>GG`FSS4&g46rj#~cVCO^aAyG(w$$(NYrpKJ14Oun7TA2Io;$=5ROz~(!f zyzOt;xR2q6zrf@z-odyJi!U&FJN`{3Uu5dHYNQi3ztH5z8T`1(Pcr#&X8C5B{3??l zWb#K$-qtT@qJgNvXPNvglNSb%iqrAcN%%3c|6|A&?f(puA8y3$GI`tnA(QWK@T}8xBFnTtsbzfob4>n#;U8xj$~E~-CU56EqqPQ1 zvB)xc+uuVzGC*fd;Z@Np(T&g6HQ zytSW(r)yxI!B?5Q?cYX|Ut;iqXXu1=vi&>g{3e5^o~aX7-a?ZfXz<-8Z|CQT$y8KfvSW#l~R=>+l-pV^*@>ajuopgOGKhNY3nD(x6A@p~>6!HkrI_?|{kQXX?-D zq7wxsUuN=_KlLmP*#703{6fRO-{fukox5tFp}_~4d@Yk7W%Ai3zslrods`Fu%M$WO zOunF=O^ zI$`Ip%;f#~H3M&D`1_c=l|RYk4G|kn!j5mN$rqUV?FQ&X)Z`P1`<@UHAyZjCt>h6Q znx1A@(h^uRcA<^hAJ?VBIX%_=YpD<}(;3gs6{1e71{WcH!?y)OM6<+QNMCeKu22ba z(Ui_Qm3q--00?GWlnu_ik;=U2ZEOilH550dP7g=MPACyB^BLLegs5Aq?&C-RwXRFw&*}+btOx9^#>zUEAlr0 zVR3m1rjySwjvypXm#IC#?6N51H^|Sd9eN5#hQRh%FQs-3l)H@yzJjLeN9vu8bi+>{ z6Jl3`GgDu4(AcXjg=o;pH1;s2v%y(aQm(-n%=-*;*PyG)WO%hNMkY&TGQHv(k?E!~ zQSX9mWV)+NE3Yo>tHIeS)4@B0J*hztmC5ot4MgS~mC5!_#qQZ4TV?ut=b+yWda2AH z@7b%7>8&z(-r>o}^rOF1{;#RL2)Y{7>f0u-UoB)trY?nm*G0w`n1+U~nv&BUwFwF_kp7K zdEY4dXYa70_j~nh)6eId8b4&wJM^y3t#t z=nG!{wd8-%o2uyFyn7UV$@{CKFMA&;`idvUk?&Qnfueu+aunU{U9ITr-ffD$;XSYD zo8E3k-|~(ty2YzGp7r1MIxG5)H&oGgz3UX+>Md3DJ@08n-}l}%^k0g8;8mHx`X6|$ z6y4_aGxTajw|jF9U8U%U-eyA&fHrUB$vRg|VTTYrl}y*wTg!A^y^Ty~^_zP@ysN@9=koO0bdIvKtWSU9_KUzG zuQ?kfjn7Sc4VjFau+AFyIz1C>ChPshNavh#+XX^g4eU-l3-LHD0E(yVPr+Qn=+=aluCLhJ!5Z+JQ_W@HH_^ouw@*+4MK-jVS4PA-eqy z>C`48njr19gP&+pSnVzhQOqRJWOT+8QTPe8wwNvEqd!u?E!z(OJSOe+`9eGn=t&EU zmj7uEq$usuc|!ahrEmF2RzoPh$u-pmRn_~;S3tfdI*P;0R1Frpx2zQ67)s0idKxKl zUz>|7RlMGOgEY5UY;$) z6b^4H@L4{OYC-cXaNmT7RXl$NzRKrOaxx!U1v!h|PS?T{0@`e0hufscJQwEJ^hQfv> z0*o{{T)!P?mwzt+9KA%QU4yyn-t!g&5c$%bdA$%%P{vK{(XIF+YG2(E*bv#2yR=L2 z+R>B6HL}w~dR(88ot&Ee+Hoafg8XKb27ieM)8D{89=%Swubgj!k1D8lJ1eU{T)`r} zdOtATA>DfIgu4sHMqS19#di@#lOL3f6Qv5#{ zc=OrbRJiL%^EuvXMep>|q4G%cyS#RaF7)~-TIyY?=pt{5qKmyd6urmWpy++xc17>^ z4k)_93*SUJ4|q{UAM|=D`j9tL(N*4TMIZN;D!Rs7rRZAkIYpoFK2~&{_a8-{^cvqx zc~5&?6kYFKs_2nApR0Z!t21>X@&D8*R^`X*{7KOhbq*@}Tb)&kme<(=+Pt}tbzH=3 zkrpF8#>|lxh2G;TKiYd)(W|`=6dmJzt!R;VT+y*!%~|BX#%rzUwO+QO~InxrOzw^I9r8%{y1o8@!>4-t3K3bhcNb=p64(Mdx~tDtf#3 zyrT2G_Z6M*9Z~cyPrz?STHNi`0i7eCuLe69Dtkpv?e2wO>vQF#@x`KB7jJz2r5#1r z!7_5%VDDU3IcF=U{oFRlQo75@)rYZAHzKZuoH&g$Jbmf@*ijYjcFi77bergdW0!tDcLU-MghR z7;;bf5{edHl?Ra-T0I9qYD=<(^CDlIf&ITal#IvO2R2DwNgknjhDZKXD8!|}hg%+T zr&QJRBcImMDMjbUu)j>skJG)qIkE#p~+ zc5e@sKSict-FGEKCBR)%rx{-5sQQLY&%{vEXEwVROc`%?!0g>tpLXawOFyXW z*ZNP`rU3Pueje&z#`^78BE(-PxZyV?oStW@4mt&1{9?$`bM#j;M-ySF;pb{_VpCf# z7=z4KesJ!_(ELL5S=fG{Tmh@L&x7A;dpfM-i&#uGx+0$$(p zh?aeZ1KAn5Zh#PHptQ4(4DB2SQWi;HD?}e#D!Fq(BOG{_f1-aG;WK2_2xh($i|Vl` z+)YOIwtyL+(ri|&&u(g{hm15@h0`&LS6e91&)S!CNBy&8qoDv@aI3O&TwBD>Gi1QDFMr2ViutJ7B(Vk zV&e=?l98qu7)7iC{8GFf$REjwu}3O_C*1F>j2Vk)!nIGrFx)Kkz}894?U zyAZ`!S|~7Sn&~MS=?Pma2E$E0Qx=8jDSTE&7JEXl5BCCJPM&iJ*Y3f!`haZ4wXUad zqm0ZwL-V`_zAZ7{6YT0L*{mvx^n`7akt%TNy&>w~C_F-5)preVk&&0NWGSK=2AqjU z!NK|5E+g%RX%S}v?`nC>e10k;?_aJ(3<7k8g@g0CS4L7sYY`>DXZk#W`TUoRoO7q< zxexdw2|Q{(e}t;d$X^2f7I~;8GoQ-ZS7=q~$$eNh>qjlR)JV5)u^KYcPTvj-Zp5XO z*10}WZxCE>T2_t25GNWQz=@V$H1P_+b)Ng~)gY`p4}bnd_!9QD$pdV4fO`jQ zL5Y4G(33tqeDs7-;?{>X_8N+}_)u#8d1?z0?msY;hTY*4WcovCr_{l)l^b5$g$mE0 z-KRg%h07s>I!j;Ig+3#6?`my(!J(2kZ5c?tb_ZC1f)g-nL9a z;}=Rd1Go~h57@sgZiF=EsIHK1P8-9Nin*$ZM`=JZB^uFQyhpn3cV(ESZ^{S??0>fA`QLl!Hp4*!tT@R+IB+UlHpAsZ-se3qa4_RQe zz8VbA#!HIXs>{Y2{#1lEVCyHqR*|jm#DcK4PPd`z(*41BB~o(KNbFP7Ru(^a>oE~q-JfB7 zPXK(*XDy)|YwK0825&&Y`+z@!WYOT0wW@oxtv~c!I5a zaJU?wd>RaYwM;QvfAI+;$t}FPqe(kwN>iJ+8N~W096l?3Lt&OCKV7M_slC||kHyEui0mFmwl47>%atPgq z^hM88dtTTGRyMgKwx&FHQX1wTwRW=RY|gDlhP!-Sa{#4>Eh6tCtc)jhRA!BMsSAV@TEQfKBF0eblj><83chC@^ffIE~$!!H1w5ppIxb;?S#6( zd5SCF0z+h zUAF%PbzSmm0f(i#v<$@^+x7h`c>Vj1D&ZAm%lYE^Mc|+Fa@zo`pu6()U7D4NhKDWrP zOv^RH8|O@g3C#d>yASgs*3_TmxRXys{S^S8^ch%RUQDifs*BSdw?3r228K_3CQ4DA z(*~kBXE|;w?2re+C8KsMF-|o@y61}>_p=+pRTpe6<5^Y9n(HygefA=7odb>weXh}c zOw*#;Yn;O`9QUDfQB(+y36@Jc4)wO9`iGGz1F@}fShoXTWO=00R0`v`$Zw8Yc$Z;X z2ZrZ;CZ#XUR4e3W3^h#K!LZwBVxL4c-kL{Rh1`$f6XcHsn_4aSGital3%Ls~16L+E zn)_TzOqAz^$UE|!mnXw}FhY-v4Y~P0gg=OZ_T2KUsm?g6rlsIq&F-X6+(L^tp(kQe;AQHITt_hpCN^U%;k5oAh3 zZkyXRk1PY@H@>K74w>JRT`u>TmxbI@b2N{v21ip9kM32<7puYiXvlpiY=oT-tP^q7 zFO5GPa{rQN>R$+KNUZ)?1f7u=LvH7Gy5n*@uqiS8w=CeVhul{e8T>9_i(~jo4(9!k zdmCH<#~_~s_G}E_&Hn8Qxuxq2{vNO$G2ANvUKVnf7U{JwzXNtShP%YS3AqonGEcF#aJ z$5^jt58r)?gZ_BQ8{2Die-A|DrqdWtD zy0Hd(r@W!4#_xC{v%_vjELZ1ZfWzV$&K^-BBKL%_?Up>n#VCNFoOF`j3gxzsbTvg3M|MGxc z<=3xPi|xyZ%g+*J3Cd^My2eNpLyd@AG6M|v09@%amA(ygSBF)}&gE(WNaRyh*YPqVY3I8}3+b9Ti23468T zYz?k%@tn#lYR(lAxAl#Na|pPK;yGhpHsu{eVUgb=Zl9Tk{Z_Ev6~`_#Q~eb8!&?l~ z<6w9`j!6wmRO74YNXHcS?4=mwd*ImRb8%x4)n*L`!+9z0^i(kX8{iSkAgbN@5U|xL zZntI7Kvj5w`gmf7X{M%G9=M)OaepP)0e{Yp;}X^C+yEt}%g8@*sB|s?IKpR8Z)LR` zxSEr6iW3=J0eiI&)qD{hK zUTXWVru!|v7A^!BnqN{+s3n8dc`&T{^b7AczJFS9o7l=Wk~EY z5FwAOvQDNAz|hnsY?Z&tYMjP&9^9(zk?wF*z8sZr!y|V9k@q1bYBPqf^0cf*IWEE4 zjL)mQE~{}R>FS51`whHu=m&}UV9BY+VWHp`uyYtXjCG5B@qp&ugn{{=I>23iS z&UV~S&<^`h6MdGusEYK9=L5RHaqoq8*_ShXs5S%tD$mGj)PC*fpvo#cWGXu?8Wurd zW@Knu_4dfD0$*lic=~Us>WSu<@vEwa5Dp3N(i+I*t4ykQ6mu3SP??ODtP?48YCeK( z5Y<-UasI&$X=JSPDHhO1ly0_&{6WEgn}&*$own%HCxG_)@Xv&YJ`9tH+~|yb66?W- zekCN_OHrWigd@YM44H(E-@J$&pPSB)Ry%QEoygLX#gRLlA&)_|K(g^Tg66G}MGjB> zxhT!Ih^!ie?{~Icg9s4N%|2W`20!TJ7U38GXoU~giovU#mTT}{5YXS_V5vl0brZTa z4uQ)17Bn`Qq0X*(wdY`1E{D@1L`9q~-T$14qZH(au{l@8<1}Vt8t#a%TGCOBlIDO~ z6K+LVnP{Z7tfNenjmd5+x!uZcZtOfR0ALnu~IPQ9{S%tL#?6Wvr3(@TlmhM70-0#8g zAD^jXtdYy`O%eK6{S0h^cvSzSQoim4?uxVWglG?}r;oFKu%~0Cd%uhM2g4|zi6fD^ zrTmWk~ovOv0#voWwAz;WE?Qfl-sZbEdcY8AV5T@A}~ zAfxg8w60jn8Bxqp;|0nyh2~+^D87q-8pd_9P8LD7y5v~|*($-Vssu-=60|Xu_!fOr ziL+6X&8A|PJp7`ZmpseRX2K;;YbZk9{}56;kAKN?Dg+VoFL^k8|B~lLX!0#o3Uxq5 zCIWxDa(dKW=U?)0k+|gbFL@@x{#dV1Wdc-h{7asdt2O(CO4$8Noy_NqfmdU+{INLXFCEv`3b;(_zeDx z#x8lz!_>$V;7ILc+friuOP*De5J1^Wo`dITM%fB{#3dBCshU-FFafnM25o>`@uN4^2ZcYRU*B~P)> zj7y&UQ#6nK5sXKD$^Iozvsp%14J@QgJbFF)mpuJ3jch}91eQfyjVi@n^6Y-njCwGz zycljTd2-eoycpQD7;Z0lR&+P=7XiC3hTBV?%~u%w8DJY@xK{w@j!T|4aPn%NKLoZb zhP%XZ$x}Yh;6DO8>f`<;&qBzT>0Pky@WdQJ?2_kIZU*fE_KahY^c1m6o~hS?YbZFb zuw1cAp6!SeRTrlMn@(J-&|dNschyc@-UDozkK0S0!#F!UjnWs1&{--^CHzaCd>Wy> zTl6E_qJP)~!qfIy+v5f5~$>^u$5R6<~P8iiutFJUGhql;5pvwnAc;JX5Ne z`dYo4>g3`@A*h`-M=bP6`jes>LuC}HWd&zS?iqx6+93*mxt9kS< zdHP@t%ga&DAK7XZXd|(gJjIBlK3{9*~;F#>$ZGWeG~uU}?d(TjlI zv9P35(x&KN^6cnuIQN5#zt`1WQJktd|B`3vU4}C~3sa9Lz^S~Vf5|iaPQ#f6t_$Ni zV_w$3d=_ShxQMTp%L(*iIok7H89ie2)ITMALnfa7JKiyKSqk|#vH z?Ev_xWr$t!oQW$$=X;cwTP(KIi)!|gr)wIxYIj4Vfk&%>85l$w zH()P$F1!$2!@)7x=TbwA>Bzt2nTkV=b351;`K+o%|B~m_dfF{G>%j1gWr|($@C}37 z#kK<5ZEuw&cL z(kRA$mUbt2D*P<%5di*YY4aE1cL*pb!{bcjd@&`-tUgP74Qe8GIg0arh@YiV5yGWE>AUA1{8@!f zxZ5eP0({7DwdX3_Q-GKqXWDhYWv1&|yDh;9Z@r zJJH<%ztf5^aFK>H!X45)%_%e%qxAo)3N$yDVZG8`C<@87`jfzPi* zu#*2FLk)~mNjZShf0L1e@IPd@Xu5_|p#C&G8qNF=Yb^C;XI3?EqO?CA^S z1*Wu;Im(QGIm(QGIof=@5Pw5Y{*K4_hGVstqdRc{^$|+TEFupn*j$cw`V(Ry zK;=IC6JdKfx)Kv#9Std&@;`)xI|aPu7mmv{q#4fk=@5B3z7qw}xE{Da{ZpNS(eE!oS_>FDmgVBoi5swaA?{L9gHn5Ed| z=s&QXa%F4*H$N-Web)-Gm!mBw;rl0GM}3?%5-vxdAC1cCXuBSsfE53ole!$`ybAZ~ zY%GGsI3@9NG#C8#a+I*T994W7Cd@e(94^_q-8Ni>IlO(cVGg%V8|F*~PlaL5odEnW zXC#7?=TYz$9_K15kSR$XKg^+28|J(?2J;W_J3NZt4|AR?M)ZkNnSu_b;c*Hf&JS}| zA+1E83aG6Qt1#zzEEr-7m}CH=#+)nLtNxeTs( zXB+-xRQ-bh=^wRaCH>9v$8FV5U%BU`f6|uTRe!U*t1W{K(QWH(kd)f9A8xs9z*Kw% z<{yIJXFul81(yw&S|R-2i_-5bBJUw=1Ey0R65@UoMEar?JgTy0u>sSAtzm3{+FMv# zqYaq&b1fS%jmLIMHkpe*`Q(2PvL#b~!1Nc4!3IqI{JIm}Vq894%eo8kXPIA@DL-Jk zl>Fw)&$M>nMP6)uv1EvvRIYf!jo#s~}Sji8VMq^)AQtE-J2^l#EKVaIF zrs1wY&hb&TMtJ=$TuyMe4{t*^Qf~bS4B0I3gBiYhDMsluR$WEGHkj#*m9YS&OD!Uw zB5Z@1H!sGQPbhdA@Qd**CYad*?zb&syt_LP1zoOdi5@VRc^3hbAIxy*elYX!_3#kj ztql&j5$(v9NNIP@*Rho260A6w*}Ga-zdWwGWVVuEW<1>G?LIe~)-C$MOtV64J79R2 zOfORv{_EK2b-JT3`;Z^MaD<}U>KUk$a_fu%Q2#CXGaN|mL{KEbiwe7F_ zHVesy#1}}uGYYqNoXc>SX^JBG5IEMwbLj>;I!;X-oBs}MtB;>XDfMo<1VXYL=c}st z88oni33xZG#&9pkDW~5-mt8!%gLg@OB@yoHI9I|T>e+J1u7Iy51l690kSkov>GTYy zs>^$IsH2OVboU}m94EIEnd-4TJlJuXOvg1a#9TvBZB_3SbcN%%2-s($_)ZJSE-}0i zqszqM9N1bP?;gX89OrY?e-+pVKHd|!R7@@S&q9YMI?g3y;03Vh9stw7$=HW7uG)(3 zJvdH?Uu|8xV{}D9?P3fge2UZT>+3M)Gz_do)%hry;kajFd%Oq|x}xwLJkAh`)9@{h zdpo9;gSZsXaKikz2DI&mr#W@hHo|gsDhS`^xJObnqCEku8Z2Fjmc+G? zdS|lz6^?rw?7A5kPPa^2P5Jy#A^eb2o7zg@cM9Q09d}qw{78oS%msJ8tzp7!#^T|> zI_^}MmopXMbju*BDRX_@aZ64ER*LfF7E^r^gPLKl4!`3Jn1#>Hb5O54cvXU3RtYw*5}aX`XpBTQTX571 z`0#(6`f4s);ZT>t&tJm$PJj9BkcA$}$3fk%32bOU{b*x?|}0WLPVp_C7q9uzEQkJ}cyIZIAs8*yVA!Y9#P- zK74k_-G#L=0bH{y;EcVT51$`$AHiaie+Ju{c-A}FvhMjMA@`+OX{L=KH(G3%ZUnLhjyl%_t9p`83>_N>t#LvNyl4GaezNmQ)^^$tIL z%;~@%tEzYS;a@^-r!ksG-T}tD$*e@FclhC>PM0jqkY+v}a(6b>Jn}g(zT`_*@94ua z?7mvl2>Te=ZsJOq##6%X;m1t$7P*dI1@Yu=R5rHDPi}m z1qMG082_P6gYzAJxK7wTb)u1fDX`%&obT|%4Z`l9+8F#gU^8O4R{*?e*qydSuX1@W zu;nq_C4O4iozdOk&jEWWhVxex;Z9+<&MgN27})L@&KK?(2>~6Okyc0D^0oWGoLlS;kQ}~^Q+wTUS zoS}ni?FP4fLE~>gTAPnAugG0|EYb)%_AbO< zWN>X6Kg)W4_i{IQ4(z)9WhCD21JJZsU$D92-#y?4mn8$@dq}l~5ne2LPwn2~29K%H zjr|n>yyKH#tF*<4D9HQW;OH-E$na<2so(T#XHfuB-}QL^Vx zwio;dglbnIer7C-h&gAAqe~j0Nz@^r2LY@9rS|qmILDvKh30xzom6lUh<#ec+a302rK}^ zX+9K7qBKHr^dyut?PkaO6CpZq*_8+?P)U62?tb6}8^&rc=NdrWDk!G)t4}NB3klat z3O+Il^d17l_Y>$@N%DonBaXNFCN1;d0^seDfSz)^`rR5R3C>^+eoFVH5K<>s*j)4C zj!8LE#PJm?q35FID6^Ohsk?y<)4a6swaeh3I{XT3PO1g zyO3AvZ&L0?eFGQp;i1?fmYPqV>o`V0yAR`6?iMu=46qGCOlHWhFq?FFww) zG7kp+eF>avZb8Pc8*Kjq=-(sK&NwgS20-U$Uujtx7loS8fCzIH;$-_YzKxHeGi1w! zK(UR`ENjN?RY+nP9tWyl27XyC49W+yGk!Z0Kn`XnO3)}%ADRu?_u7HQXwgy`{$qiFT+Yr{7g>2k{B-!nN0f#&LaNU6HDrcp( z!fZOZ0dGgUD$^SPj%~@999^v7lSs&Hhh0}{ntDA9qmv!*ZthoU?*Q<>;Xn1v zyFdV&t=)+nS*+}+&dDz*_Cz`cac8*R1*IyJwS?L8Wfkx@92`>MV^I#NDVr~Nto+f? z(bT_q=T_qlM*}(l(x^*{E0I;d<^~v(PVal@-@b}uAxN#4wHAjFvq&1avM=(Ox&kv0 zYze=erpphg|Em1|fFWT^SP$1m%D({wDe1WKUx@-ZdtER2R+Y&b!1x;RiRHKIt5mrz zx6$GS6}whbrF%09OhxAZVmXwe#=9GR25+~7}|F$?IIVyOK~4?&;v`y(k3PwJj@WW3JxE+YoeR|*MT2!B~bTmuy? zRShgm-GP3ck1++K`+*zNWX4$auMMEyuk>JCj4x$q5+^R*t0Dy4{B zRC(rQ0@k*@B9x00_DeuS6jk({F*Yw&YQrFMdm!MwS*9}h9CI^P%~u*FG~!zMbvFaz z_uj&#it?eOP|iQF2$T?LM@BORQ3yppQ zBJ$$v5-as3TrxtAHwP?|xBCF&v*e3vB>N(DQvVS$%Dmru3JUVPLG^tQu@+Y9S1<>{ z-r`qiL`<8|GLqDMlcW3(-wCn#4XBlW1R@yK;`Q8yB~>W-Qqs5t0*!er@}vyjKWB@< z+l}h^E`U1;OoJyN6F|bM-?K^>-*dTT4KUfLN31DXsYm80#xF&6Q#ijQAgeo};$p(_ z?Uo`U63YMOCBpcfcVOZHyg04+k?|O?JSoPDa2_V<09-;~GAbD zFWvJj-PPVW4Cq+87Y*se(pht+&%!8YezJGbe0V%8 zyG$1=a>kG03D|i7=MU&~WLTB;L6K#Z&9KuB0Pqh46ehAZV@OkWnIsVQ%(x;dl)Sj zMTirBSs7UX_(ebfc&v!vX*Nd@`9+n+qefI3DJ-*6D|aAqypu8ocT)e!5P+<97UXut z77r#-95vmCLYCd5qO)#>;L4WUX|U*4_GZ9vWw*_c&7c+HNhw)(9R$<@1;wLGlt_kv zW>rF2%Ia+?$!Ub*1&aYm;da=jvd=0C=lT@nDK0Zo%=!uoeZS(5=cB|+lSgxuJ*3jU zZPKi)2hmfPy`kj9BUBWBLRbsyN#h4m4k{Q6jCWpR6evY#o+JaxsU#^^ds zAxD2!Pa6!@$wEDdRe!V@+R&i@VmtHtaq!BeuAnA9!g}(MKo5` zICtnT-d_i(ozez)hK%Bs^0JDjoP?x(Qp*dNiZkvpx1Cqp(X5y2wVD8H{3c>9| z2;N14KNl6`c{FiYSuaqfvRp}fz(+dSQ9rJ5d*Kyu{z5CSO@NQwL%Hh=mk38Y7M!^}=kl|Xi+=pqEv;Zb8F zOC}+ZE-87D_hJ5~XGowRvI#0PJv)WoZE57WFao2~sL_>H@L}*-{6DabW&e<}Yz322 z0;~AZ2x89>!F-XiVr@jGET&bwwG8POh)lm}uvFny`j4pDn*{$CBj9KKbOq9TRDFL) z0I6?zA_3jZnPG7g+raa}AP@M7uZXu{jLWXH(`F)`Kb1BYPdEU~{ZRz9K*qEFO}H=F zsZpfx7Z2yUAD~)_mvsZ=U16BV1;rIu$fdq~GSc6(Gk%FXWhnz-5eU?(7G6=uj+Qg;4y1WdOQS%ErTp;&K%+X6>YY&Q`{YrxZ#1x3VfO zL-cqR%|nY~G7%$bWi?(2fQhygho>10BiykK0FxEKwvhm?qmGvWk+AJJX)PJ7ak8lR}RPQ~{;iV+ced zqo9#77F5L!*f#oA`!jZAJ?`Fw59K~-f@?>1BVy77&yMiyr;aP~F*GQSc$1!vsFN^W zjHnYZ-i)XdFy4x&lP`WB;mH@?q(4T~i5PE3cmT$gc1!yt|y4_BrhTKxPgx zZ(gjtd*ckXObvkaH~o8Y{qH%*vv3S6Az zw(8JLjo8N_utJ4oDQo(S8Qdi$csv-ECG+XW&fsP%GH;uH+>GBK({#Q(ORt;pDxUK$ zg;lWA&q=+W2)nMpW%q*PK*9H?)sR-cojT#1{dn2*H(bVCKV) zh|okZxh08$;l~%j^~k{phTm`)!Mun*#|Y-Pc+Q&yYevC*g9utM{{n(P8if2jFX~t$~-h!7Zpe0@|~2nMaIW0?f5=kz?HID+nxm1Fv+0Ke|Q1 z*lhsp@PXCdzo)vvSBfzV0J?o4(k^4VFs9PaaDy|EE<=c0k#?_1S4Lg!bKQ(B*_LS{ zPXXXr17xZ6EpBk_QcdKKNc&%tu8Ht->Bw1ba8bH0Ryrg&9zQ9T%_sG0BU0+lIG#31hq!&jSP7)2?-4FI@RLU zN^b$+Z$l^WPgvAc}bb~I{QSF=!h=a9@rL^aK66Z ztM6QITT_gSDcSB*;evtcDL_<9$4$u|DT3)^EZ2|AMVD+XdLk){bSr;u(;?|rZfw#a zDVY=2At_l2I!WT0UW6-tWgdt5Td9{$f-N}K^U6=HEI9+25`fRl{zT^ zj32n(X`PC2861V-b#I?|4={MKdTtH@;|13nouM#3mcEs>3Uyd^ot?cMg!xlGnHoQ& zyGJ76;V0Jv)vF9)n0nD`X#KLIR%F_Nl<;$Y&hL8Y#; z3(`m5VW*EnZ_jhM`aZWCui5NTB%f^cJqMTPE<24prKek0+v5Q?`aawFB_qCQ|7jsw zz8!dUH2OzMJj5eBLzPL4O|DskTYB)LE{2k3Fck3icQewvFrT6HeW=svZu%ssX=pvL zLKBlQVa7{2gJFs5XB^M%3qOEMNG=lm8-$z}M%;#qnq(f77ja-jE7SqPR%BBJ0_p(a z?0`H{y_mWrhkSR8ruFapbxIZLoI@&p6RC2&fl;Z_7rlEA7+D_TRvN(rovT(}s4 zRT5Yec@4f`#Tp5m8kvk~j*9gXI4zP5dt7n41U?g4Kx$D5G)9_8tw{oFBl}3LSpw@K zFOyoU1lC6iNUcKxr$^FBty2PLSjDG*2BEW4Ir!yCgQ6m84zvI6vygO7jsxdA#8U~= zb6_Vbr)Hy(S;1t)rcYjnh-=4jw3+_ug?PShJfB0yBJmn~&AXRiiSVolzoh<>T{LGb z&l|+~)KzuP568h*Ms(!d@s?FpA0kO>V{dDxHCLWpEBz^eRL%2;C@08svXy=(K&npk&Q&NUIs8VILkE@1$yqXRkX%;EGf+MRNYyFceSjMMW#~}V zeNNhY2!!6p?JkqEM^)nn9jrXIy;r#c-3oxOx{_l6Mcx{ zG0?ZFHkm3ogRz_gv5NB;+b;7MJi$=MzJphsRUH{DB+oNcu~|)DLF}VW>g7P>@rE(B z)YK&E+_K*U=hpQye5!iINu`*11fr^05p>o?&7k8sQtLbxQIpR&3DK`QX%sV0NtDy^ z)Dd}E+%uu7#1%eya-yKQzN7_ZXNLxSz?lfgh`Q#!|#=6LCMwSecgP zlO-)SfBinBKbNrzH;S3ZImTdA7Woq?kzeQL;0E2Yc+}%in5-AC1k3?7lg%?A1;EPs zC=byylu&piq?%t6yUl8{vc~TPz;q=Pj*Mdyj#gGW^L@j~r}lER95?VSu8bn>tjSbn z4xm+b`E?5r`{$e*+?c{4^-xapk6!|qzfI&u(iNmi%GvFKK$qXmIcX_+vKXl}2bhES?Q^g){&S&W~S z+I$#>JQPJ#h#UOLx$`_8eH|r^$kXuL{31ruq4W5(ZWNv_$CCi^Y)k>nveLf^CTdR2 zBo9HxhaN)%TRyH4v8OvcG}C?n7pwS9l)L5u(PSQ>sbaIT!CK9ksv3EsrjG8q1JN5) zbk<%IU3}FlM87RGruwm2f8T)Ec}gSa$D+R%MRdKQak`1N`DBI2XjWDe0KO2A_PCD# zG8f6Vs=4^qBmgAasVu+^1Q2Bw1CsUH45Yn^4~zi*1k-38;u???Ix8E}tVvT8en%*u zg3jW*VE{xFlS$_SfOyIRD5J5P0$Qj5ihKY*S)p&R6t`UkfDA{B7S9gVg#UK~qUS52 zoa#q6?PY!^=+-;!5(K)OekMz)icdq=Qged{nCFb@*m30Hpv3aj(O4m@<-e;(QjWtz zNL9wmp9NDjC)qrXR27C&wX(hiPrGKdn!0>1Ax)V`%F*g?gYMKkCITSg%k$9sN z;PS^&5{iRY`Lt=~5=uW-@SqNlHwAB&`D(#J<;$4bvkF^XSqr zE!&!%H^O|SMP35Hm+e_01TrF>+p4T%vogWcLX7W2T;3hzQDt)4XNX6B2H$0n2_fGqV^3cc-4{Hpj^NC*(1atYsQyhbn@f7B}$q>I7kdqxlJSi~D6ZAq{5EtSa-w=g;!h8zxDa4(dgFL||%yWIh42HNIB*<6FA;tx7Vk+Z8 zT>BT~EA=p+LhOoz+~7HU0_rx*V3>Orhp*y=({bUGa0zi2-ynArF6UF2`x1xw6iO>Y z0=M!Fzk#Q62Ftk-XqcDK5~6X>+7P!j4j(%msf-JAo6r#VGzxMDr|^9UhNo2FviUfG zFd@7XPhmcV7#HM5Qsvx6Gn`Ib?gtd&zDGfBF%;t7FG21p6XupcVFp92-ymnRLW~P? z2b&NVF$OtR5$02fOJ0K99VVPS4;Kc*i}`daFPwA>Z^9+iHy?1^FQlALVeUK<=2M8f zDg?P%N7y+YsSK8LKa4Ohp*Es%SBelfE(vn4i|}f~F(I6vkIPSZIesGISUuqlfGg*T zFyU!P2yr`r5Vt)Da)*HsXJ3MxFA5(#A3;vr1vxV$^O+%jGA`ezh540#n86UALh{WO zKN}D8)ABHb;n8r2!$r7+_=Tz*U>`16hByX8Y@8tmgZ$bxEWvujg?S0dx2XIMI?QjS z!wiNu5VsSTP}Xvw@w3e^zyAy~Sk6m`U%dtSEn|2by6|!a%lVaHh~Gd4`GH|rg8X1C z$glQ7{G2r0PGH7``0ZJU3xb3EG$A}=B`%CBXI%I!JcaobV(SiGO#+;E2y&(*d>;kI z39BIIM8ZsHsXiT-<9JzwOZ}M5n8LUy>8EGo`v<_NzJiMktQ+jIoAKeJodYfo+Ed!` z(IH}!F492QlWP`GcjwYHFr|Pgf|)(HcEMXe{0yTHH> zD~GFEpN4q(w?VGN{vNQ0EZQT6zc?-J!6cB+4UExr(n`Sq!jo0FCv9(sge%!N0t1`j zP9dKMbUxrg{_MdI4VT0d=CJZFPZPCrxCpjKBk3EMTYE7%8M>lYvD6btO<<`fFg}5$ zp1>5tQciv<)-P(ZWj;&yfx0Y%A)defr;r8C7%0)i3t8Tbfw`J?A&Z-FWv(Gz^**|b zJWaaPCmpzijK{Ciqlrj+coH(Y+|E|mJJMb#mheOxk-$KYmBV$uubimJ)cMpt2Wm%S z^w1Q=*=EJ)p?NB}K{0xGyvnMvj^BD_+>^=9ATY4IZqFJ7zo~AYv-%*2Bj(Y+OnZYV4$)3 zgM%p6H|*>yAVO+r71t~O^8}Smqn}$Ba`S7)7pltb2B$#Rgv#v(XO0SQNXv!i=-lXJ zaXB1w+j}FzH#r&PS=s}aIv>%wp%;)pd+;~RjccPQ$D?G6wQSRrn0H9F2rpD&)f_Uq z?XjAz3a$YQ3~aEbzS4u)PY|COjW*%+SiNi%mcxb9gPMslpNaLKAJ!--#37CHul86a zTEzvHQWvYRZj^tut27r@4%b$93@hxnk7 z%xLyZ#$@joU@!Xs60IVFyrMx&lO~8tw!(L#iB)m+`a_FUZZVr#4*M?(PXQhijrL=` z=PU|JS_{{%AJ#Z<=j6{G+yWebF0_JgVgQ^rdM{#E+2y(htsE|D-=UGqeB_@2c}Qn4 zg5G5Z67?=Skf?Xr0blQ`xF)`3SizFNpqV6f=?t(;IZrFImBV%LmPTsvncGgJqpy5d zT6>184Z@Bw(P$Ue47wF`IAC0r)}G1((t?*CM+sNIY>#d=8h%pTAJ$zsqOi zWk5=$(8rS1@sA-!t&T?>^)>h-H1wWOxpqMjMLp| zsIu@5!H@Nl_d6^9;~myy&u19LT0bR#mYI93V5A;%+|h+G~fC0 z+qY;_#?^u8x?aV^B3!4M^@dfYnvm(|s;AwEVq6=TPkK`1(DnwXHn)rY2eYuzw1iuw zWc)zo?RiQF1?mP_%m9#YaSO-H27xQHO!KMYS1ZqJoS15~$K#mB&9&v^BIgYBxp!-v zm^pa@I7e&T7nP{iyhAnai{`3ek8-mwmg9#x`pNxD9`<_&p*&3<21EYr!8u>lRmziF z0BaQKm%xZuq_KcYR3uuRS(={lUZ_a48Y0!HTzqv3@ZfSa@hGVhIXx=(RgmsM^eb}} z6Q*A5!2^lR`1+;9>g$){4f@3so$mln-~n8u##jnHHW`t+LqUE@b+tx2U16hS=L1c2lughV zod4aFYN1=*LTv=q@MNnyTZQ%TWScu$w_s&?w;9X3!Ky0QXj!N0Y-jn|{s7rZ6Fe4C zX!cF~4lA1vSmMG@9!)-^skZ_V+vqK05S!?Y$9a;8y72+yXnn9Twt-m zr>##Z-iepN%yJ$Pj5tuaPd11tCCQk$57|X zxF5nVn?P3XU8tqU@KmbF#(JzjAvUhf`4&ZMp6Rhv6Wokl4xwlt9Va|lF~h*oS4^#^ zAvbEAQtJl-W|B#DJP!nT0ths4!_9Wy9mxIhK#rQ<*kfgK=XMU&0`0g+xo8X2X6=>C zrRjLfIve*3@XID-Hb;0?6~7gHLi06Cu?L|YL7LwHP!=?R-y8i$5c>anU;U>(2wVL! z5PWN_CQ~ZbW6Y%~LyhFm9xOVbOH9)%7BKVR1l_xd16qHh#6#YOYutrM2n?(& zqO)uOQZztlZeX-aml^(82|G;#;CGte8m~Le4MdsBwaYhyKUFF;e_blo{JCgY6%ox} zikNxnG|T!L5CRY4B3kt}>u&Dk%SWfRx~uO@!=!;bK*-ieUa$=P{PWz@I(19#DS6snF(M-!ZYs4^<~? zq&kHIq@NRM*!PU88$L8e^}GJWq6+@E6N{y23;#qmzT$A7dS7wY(W{xth0wV-Xc?rp z&Su{Ud2Ga5@e`yU&Hc}O>`n@Z!P$!Ui~SPHu=e)Q}ju1SGn)p6R-+nv(~ z0#F-kD!;wm0i3umX>+3an8neQW37rS)5P$2|Fp;R>{oPk(w`z+royT}MYv9dH%ou& zy>E{`4i*`P$qMtmN)FfZmFf(@i2T`uQ^1-(eDRmXr8PiXv3hPE^JkWvk&VpsW~p$G zm0MVfCFsEAc{=>7wbk7oh7u>{=JOC-&T}E6&q726Uu!x8x9_Q*bTS0g z8O-!1DhXw#%!PAI*7yyvKci8N}sRmSC{rvUEhVVRtX zSP{k7(LW1eE+6hdY~Vaxgx(<}K^5DrNUeK}tQ?+4w686|N1wPK)^Jy;xj#MvpWgNrzLo8(O9Kdcw%IoYI@wZalz{|H0D*|a0UK$ndEo$3BG>s*! zNcT3ifhv6FBD!BGLs&(6r8^S|$jm)P?rgi%E=EPiA^=OdI8U)#P zS?;$3S*j=4P(&~7Aq`}7#(gj_$^D|v_F|Q-`#HIjPUdG(11BPR*iW@hu zp@buB1L$f%P8xHJO;O3v9J&Htxhm*yq@Q$1^Gfe8=Dk&tldr;9j6wPY{IUscWi|q@ z7Qw(mT>L$prLZKR)-qQT;kNTE>kQmC#{*^)H@hAeYbw7?xeiatCZpTaHd%Fh{3Jvf zXmopGpmVr!z$-IUxniy1pCI9PIsPPpcuAZNQchdQsn(s5C>*vLSU`cI;pkT*7&G%w zfH8+HYp>2?gUSg?%XxcI+9K5$ga*!0El>)N$yT1p!nylEBQ7G1$Ip-Trt*@dl2mTJ z1oI!b_v4pMgltazTRA)#{H!z02P zYZS~|ehz;@4vK1DaMEv3Say1dXtHoT{M-|eEng6=pu;t&kxk8bTZ3bN0(6&t7kmkE z!b&*VVIWNvv8i*q19j|j?jta;7;k=NK8A|YG<|gd&Flswpc!%-4DLnYZ)4HGSI;dn z)6~FsZY>QIt=hvU%t7$fr9hcOl*G*}%6NR%TEx%S*%?vUgKdbF7PJpVl`i6b)W!vf zeXCjtsnM6!>jqZU2D^wJ3r8DI;7PtHo08)`d#Y-Ei;L*n7w9arH5uc|uGsfe(-$g!wa{hZ~D5(j=gI@@EguzXK$OdEGDxMQ!FRanpA zjPQy%_p1x>4HbyLSEaIA>1TN>V&m-S@OH+LK!N7eD37$WKOlka>^E|UV5V{tl%KN* zW)l;wwq#GMEgzNIPIX!_G9q;1E+%C>2_U>3n09vpKa0_}G5=Vdl&vPo=da+v>N1UHO6|=^3Va9T++HN2zj8a}zS1h<5bqrg z?aOj@(s*~2Kx}qxxo;@p)T{wCWYgQmyH6mVh4_}-d&qp19>rLZ*<^zTCT%(C?}Aj2X479 ztU?U19+<8&zFOU2c)yNV5%abS$@%SjuoX(5Wj%}N!y4Fh0`}lb2KZdvil;6FgOPjU z7*re`6UdEmbWC8r4D53G4eBuf&eMc1_POBaU&4hVOvUZ8A{V%Y^2sb@5$4Lg9DZ~5 ze3`;pt$IMC%-1Mt z{C29B!R5y&1RBsjQ11qv`;JjC@A=dpji>HU6kzAuiQJ@?69uc@(n$Mb^v|-~C0>^7 zzKSWkKcQgmGB1t0w2H%%$#V>T49bDu;v#mMW6jvmrj*(CMS%7-*cMH{)MsiGVqL-K zJizTIVHkp#Ks7EtG0_=!e_)!Nv`|Wv)fgU~iu*?}7yR3=Zcx}~5$g(#fyM4*1khAg zOgMz0n$9~}gSrEO(7goa_y~4Q21_NJ0ymJxR3&|VhAY$M0~_Gsaj3RZ(@Ke>rG{+Q zEjEw4;K>}XGxLAoA_d{#G)^jH^Ayy4?!8zTr3;~T2Zd-rtSk5!lQYBDHHe=f=({qa zWTj`Op8$~nC(wqAWVatJFqYjs66_j_@11aeNYje7+ba?43O+00z6MO?A|`MrE$1FWBu!BrKgPH`Rj>q5fu*$ABju#@<6@uWf0s(O7*2x_&W|jf|d~nOZx6}<>&b%*f9HnLx z3?p^rWd* z&`sqgdA*?T4}bk3>jDm_uveZ8gcoo~g@6o$)vhCDe}o>h%ZfW#~@YKKc?V;o&`VZ6Kifw<>rD9Fbgv$Adddk6Ue$a z`d823qm|3!m!AXRJH?7JDoy_E!PgNh0x_e{vSPqG_oG2-SQsJ=n~vB7*vyu4dTq<`VnBu-u7MS&;?H*^c;Rdm?PRaE{Qg3BffmZy;Kv8 z*`LYq)?K2blP}?vL7fKiRsDigmoo;^r)TD%_NwXNH{c?xMyop z*?tkuLaZzJl*xnRv4jCJflF}liCL9|x@innMB&b1M>c`1soXd6*Ld>Bzy6rjZ)o)1 z{D#JW+W)mI87Axl5*fx(@Bv-zdWz4=Jqsqa08gQ#=5?T}x(`;u+?7 z1!6@YX3$*5vqU=(yI;eO^Qjhz!#MQ!xNp`( zjkN5+pCZ;38fY8)5n$WMd>O6%6$bu>ixB2p_&9APS`f`V=Rq_$O*E#h6A|kQKDG5c z5Sxyez)W0xVpb)gmTtfx7x$C!%O;SO`#j8DC!R_**;tc18?nCjuvz7B+fp7b$G=#a zjsVVAaeuv!8Y$0Do~W0TA-vPV)~2&n7^8(Vi*ed3aLbAm#h9-R5IluEf+IdEaO&9(-6B@!X7Q-gFkuS|U}JBXV9EueO3 z+%-P#@ee1|d}9#@$Pa0-WDVw(dt=>yNzHC{-;w)MmG(CYBmq6DYrb zpSY8K3@qSlVk`Hh$?yaKY!%>#zg_BvN`DfuaZPElm3s}8>vhELov48J`mldBuxhBh z*vkC-DNqQ+1~UIgb4uNbnTJxMeBnGScS8CGO|ZZxI2W<5;L|)j0X18Wn7}#l#KcR+ zfox-`g$wvm5CT(A15N4r-8^T4ob*;oVqdum{T=S_<*Pg>D*oAn-$kq|)FJRw5unSw zQSO`FZnW%-Q@6_xJJ3CbX4#Ch&rf051arKylu*xkcpHv;ho%whZ$3n zy3ydILz7)G=HLP5zXs~~5lYHGYBK-q!P6dvf)3l#S!UhCjpO+REA(Gx-NTJDRIsOz zbsTP7a0Ms?20E^g3x|VK31vw5F4kwn7gS2>`jO*)vz>|{Idt|d2|SD=Fd^$ zhY%Zh1s4$9Ud7$jrHDy&!sCb)d`Oi}^(+mNAl0MJ ztWbF&tr4VPE}04ouuw~d-;95(`{<`EH*?a;ph99+}~B#vl|$}as2)Yv-IcG<fTS=ZS>7JXN3i_agt8cbcs^*=oqxo}$3 z8F+@dutTd+FTR}tLzl+ zM64@lbQ-oF4~GyjfyZzW8uUP{QbL7m@csk$_r@vY7>z$8))h3+*!I!_%$t0KsI()R z1|5$$drI&#wqUvg=|?oNGM}N-5$h72^ClGAiyFiznumE4F<|sCa(FsKzlK=j6XCHA z@Zz!XttM%resQA4Ej3Bg^co|C@#du_X@+|9(rtym35$_)AqYM{R#6)C34Wvq0^tIC zBB|@;CRpQkfn9q#DxH%P@-e!5k)6&xTxfSOb)E_P{R4b^p-DpJ55I6kLv2fZJnmF;;y899#vci^nLWyM5fJ z3@-B}lSQXLg?C9BPG@Dgi|z65XOV!x==MIjFGpv29>~4`0)ceobkDZOfhv_lK2144Q+-Ln1-;(-F72;0nIrU>cr$y7%?Lq?Au zDM`%_D`s+=Yct0AD_g{%XX!LJr0LH_4*dVMM|% zSXH;8mCn*Q*?w{MAQ?>o--mzTaFc5m343zNrY-l4%DMwusBugM+JoN!PQ~R4R^M@w zm@T`t+*5dKpW3Y@c^KZi#8ak3eG?P7XMrNqW8H1@@EA+uB>VhXwr4FtKM~-q7FNrt zz~{k?aXqUlD*&#%WNS|3xXA3tSi~lk+w;absfh4qN4fx59jP1FlEsZTgC#-l1^B zx>BnPZ6lCzeu~J-MCU42;)#xrCyK#ImhGfQB1%py>V)``AqTvy@M%k~rVtZ{M#rRB z5MMB%QK$t73QB-Q6YMG}Y-41t_ZigAB&m9E&#Is7)7DjQ3RLVT2qjfJC2_POq%|cT zsw>qXmzE>XuGE$UAy1`<3JM*T{TMA9pUY|GmZ)H1I+U_-Je9Bx>Qfdk*0ZIqrcbsa zk+~5EescNoMg=RV=~RhIlQVOviq*5it*#xST{U^Yn+0#&s!c2!(P#KI2H7y;*hU|Q3qSQ%Z zZek=FF)4iBF0>UV^NeJajw*r7*X6H9krrsp0b6V&$NTiW*!V{J4$;ad3IFiD5jJqW$V?qmD8VBoVAA?^;%ep)sW|R`c zinw}&x|73`XrE}AXpm0!D+I-o8?Po7K`rqU@ho!cCHG;aHFn@E2Lb9CM0<&i z0(7Z-8Bv%ara%hZturZ2SrKRA_=%^E z$*|Mmr0(>smT33c=&+H`=d{&?ldx-yuezjl4is0nJO+2WV77DI+(-mW8ZP$u1s1)n z8)xjmUOO>Gxeg}v+!0f$W&3O?x6apC`}4p+tdGPp5?fatBqp_W0V8zvVYF$pEvrht z$c{B3lA8yuz^KG_?f88 z_YshYGkWafL?f@UOJGI6WY3B71+TS7Yg==jO~>;3SY9B>GLz_mgaY_&aM0uhHQ^?v zF`I~~P*vq(Xt;r)QvBL+x|tl*6loAt9yi%&2%0l|XKN~0xabh0aflFXDq1dwV(((`VqBcdaB&atatALD@KJ9*iIOVY)>4B!&Ho|i#hZs+A*Uhd@O5nP<_@$yq#+*f&dhrxg1 zl59D6O6DbvmodCd;bkH(CA?JN;?Ch^0GH5WT!L#DY-O+lRKi_++RlW1e7Y2u@O6B; z8JEa6aB&{R#d-=C*K@2;z>JxkLD%-m#(K`=fDN&`0nbLN8HG9b;I!ayotIV+2&mMB zNHt?S>^*3E4RryBNsd>FBr{ZW-VfSdRk?SpMsz(7sb=u$Jcj{VaG94`=M`#n1+Z6- z_;Vf% z*mW4&16C!N=~m}T*CYhJ6kwP)B02j!+nLblCVN%Ru{R)x66Yzazzss%!I|@)-33xs z%Fq2JE=fmlN#-Sd#9Nf)v=j*HPp!r{+(k&Xo!5332*+Qy+dxmn{djjB(!3-oY+W7G zlbo;H%&^?~$?iHK_G|(%f(D$|HL-i`M!=>x&j=wKpQjq7Czq8 zsP_mTlbxUKuJg*5F)2y%OLCsJ$cZVr%JBCZ>u$SWGW;d5oFZ>*Ap{Q8a<;-eJ~euU zxCQFGvCF)?3Lz`?SqEjS6A-ks>}B3DOltFlrveEDmU^SfM<5AJMG(<;crW*I>b2f_ zQyEbpA;4!Y^KyvM=oQy{W0+LujaCp00V1c+t7Jr*H%-Meg!mJ1bCkN(dCb*Ct`nAd z*>k=0wceO|;WXfEW$Epta<6j~C{19TX&!h_2L{a3W8^a>7Omq?VJMYwUfjX6MJd z37o7zVpUv+MBDkpZptP_64UnKF|}MP!8P`S8shapK}>qX}l&+hj!*LkDo0t+>V zrk=DeFg^xpUW_^~dbXFg+{;_$hF}zajtcVX*&Lj&>@El-#SqN<;7=xP-eiAj(&k;d z^D?Y`X@z%uxmR88t!PwcRy9rYp5D0&9Zkh1Db-S#lw}@Pva*N;jm8kLiJOH3&Ra>a zcT?7S=>?u>o2V;HTXj-FASG5hvnav&$iSB?#W3(S20koZkXa+4x*#Xi=A{*Rut%fF zOS$J3Dm9-i`iBT_r?E2uaA($WI2Xh8(3YIs@3$xqJIjNHv!%duE8>e}7LYi9hV@OG z>t$4U)62a{=#r*;!R3m)tLz)zEQx))J_+65=LkHy-V3e`gjiq&V^;Av?*pbd!3vrc zL(U_~klpCDUOpK@i^UBMcOq`iQm-gMepZ3b@|h5DQrCMK3MWLIrCv61po4|+GT&uz z?g)d;D)zJ!>%Flwkr~dl+xI~s1J2ul0@22Qdj;{rc*fxVF4^|R&GU-Ny(#Eeb#D}r zIxTfda2gcMuT~pjqtZ{2bKXN^%y40DbDzFtd2h z`5|CX7Zb{jSHM)WEYEp387&Gj)YgX`B4n7adqSn#X8oTNR~QkNI7Q_nLELXrkH8n3QVzdQNmQlYZc?8{>M={b8ggFeO;;tX<*mS*=_?7F znOV5(Tr&#Rc9b{4+1tP0P4yNyFQC&N1&`u`-An0$9pjW!%c!Ncvk&gxxFT;6t_&*! zP=^CDp~6W^z1j+|zTBG(m$T9vSLqd&d3mi~pjvr0s;;bJUCg^t%$y2uqV`^ue9U6- zA@ZU7VpgSpy!*1okRKvNmAP4eKio`w#y1m6qGsv8GorlBM+E5ZhsnFn6L~KkB5(6W z#;1|@(*IK4Z670V^VNtmMBYju=TFhK@op+ynq==p=l5sbE#~%VXo06%rt_mw1=4{B zq=R)H?MwFLV#p0bNHhsW85_&38S{(^TqBUT(ctI8v8nzBqEeKFanc zuZQ>JO`h)MqE@O@(^Iy`e8}W{19l};?~T`fJh{QZXsRv@M>cMSmtGFTGo+(1Uwt{} zI<^;B?`7Ac^^bA>W3_pcSgN42Z;x&#N+ZqJX9*hVibMU3$?r$5a;X2Xs}Vg&s&kE_ z+pg+PJ~D7uc(8Y&wbHA7|8sP6XF9)voL!Gyf$hBPC^z@ET}u%+)_DPRq#ssv%-4v{ zAl#C?dXN6JU52D^VO&GQ`pvwok>#kTLb7i<|MVLFQPG})V!oBucF?|XM6LUmv-)R(=i17@!86Q z`9-9_bg>kw*_yz)EDh#homXD&rPq1r!?Naj<0?QQ+qrp1zjDU&otJ{JJM#=aErHow z!NCdTV%3&D7-!Tt>`Cw;mB}}|JUCCle3W3cue(@PY_ng5^FAzD<_aadIycw)K8dQcm6NZHJb#DEM2n;(MZyzs+oP2oDWlB zebUZ=x>P%F_3lHxo<#34$GHer+4IswX`T#22Mg6mx=wd@aU&(OIh6Bx^e$z~m6Np8 zJEp?R#jvfyJFeU-g1OW~7o{Ap%+P|r7=w+nbzYr62gk}cn}9jLLLbBa`P^MGRd#j% zd>^I-%y>w36=st&=RR~5V`QSPtkOH4WvJ6*JvEeqw^rz-!vJ8kbR3F1#(8RYBfCNu zC4T@zfna5P_hPnea}I-U)jF?|BFb~V16L6(z?ETPzq)62>8i z4bBjML3tBqL%yMgT`^=X7{h z^>CcLz%u11C_OeicRDx2jAc5J#u;8(b)2aSAs@3PsB;%!X8YqNW@2_Em1*gtIB$pN zXhss->VCFWj^aLLTh&JSI8Ds(JH<V7{2xbCmX!@mVP|l~>!Pq$fO1Vxt zM)7Oo)6K@oL!48xZZ>#!Zbfr(>%DYx4u_c4Ul(UB2y3>Xb{@rqw3o6ru8{QUiyGr+ zCd?k#&hIcGg&tj|vCF;Mgyvy3)gCfyrF&E*CbNULvlqSM(ZzIiy{@PC;PV%rExgH8 zEiafi@sifZ_m*ZCbLSm2Teu6O;Lbq!-W&JS=d1Um)myn4-!vT)`^f+6Nsc_nYp(f5EOBp3)RnC<8I`t~(E*i$@d7Sr6hCLQ*oYV$mT;v;LCF~x9a};GhXwv`RSBA3D z=1dLeUMN`HRD)8#$_2Uzd?%Dz>1AsL*4DwKz5uN&`mC41Zrhvcq+pU9lOM46xZ)*H zg{nVA(wunW`~__;h>1^DnR&0^VHQaSYAOSBZj0#Y*mlpYg7Yo#ur5i{y{UD^xI~s? z>;OWQUIwO7G1WG~^G;TzR6SmNye>&%x2fvUoJixm4HX~d;6-Cl#*NTr4`X%xUXk-! zM+}+&pvV)p$Xw7Lchupn;{@l6aIw903OZYiT>3(t=}YX6D}3=ONth#qoa>M}adS)# zs?j3sVlF+YT2WM-o^L2>PNB?-Q&D=di?WlFOtNCO$xB)rZ+5LknxhxNf3Sn|10~g7 zHd@XmZ!GLYLRFdbp5%>STDuk3+O<34k5WQjx^ozgXS%o0xn>s~P&TZ9^S+)U_zk8SJa3|N ztD~kJec(v$vi%NHooS-E=WIUs=&FOf~mq9yY1>d7a)T^$*amtFJF8vOZ{m;r1 zHNCGPzaqSU#UyZrH&#v2!gZNl!RChPBfMe4>#S_pW>{ohpNb`OAmPZ~ha>Yt<_JXR zGGv1}>(RQ<%A}dI7o8!dTNTQj`}pxqTQv=Hu%t6$(|CHZF6^gj8p^XX2WmP`!?S}s z&$kWe%*QLmi>^Cqb$`{dO&uNG%}xE?z0v;OruP26C99j3bTl-~Y-#Q5@9o~n=WVN2 zS4LN^iCPM!I(k}bGZ8j6^|dbPQ1ArOTT+XHg|XR^*43(FWIDS8wAzV@o|iU)stgon-x92z26tVv>pJg?EI_+ zdab?NTCJg_svAkjTJ(Mye%RtXy4}E z_O2~xywSGye$fLKnqkq~R;b+yJT~JFh>nhEQxuwzsvb1*{n*)Y&D9F2HS6K zMcMnCMb&6umA&8C)S`%NL$lu4-qoU{7vIN7BaJJ(?pkKFcJ}n|)cN+uDoeWAVMPKn ztEsJN{=e5;`BCUmoEjY(=US~hTAR1Bm8;B+VU&GW$9q-Zoxz2`$s>+sD-DR{kh(FgUeoUN;P|NNN&WhhxDHoWfluRX$NB~1G*dl3g@=JZU4so+;=wg9)Sgq~tEv;f?tu`q7 zcJvY}qTOv!*?twFjplH9bTrHsL#syttCpb-5=R1b=c^P0rXF;eKy5`7E<4&o6Z}1R zN~#IOISJOr_LlZu48vf+;syh>il0t<=-??glYMXE2xNF0(47=UMAGb|YkG$CuuApR zs6aY885P7_q{Ix2FFCv~llDuOON0&!ca-)U8#Y2qH*VeZacwfrdgy_N_yDMV)fM`E znkF(*yM?_FWnLC%Xg}pxtVPBfVlqaSf*-57B?A+MhVYSg_TbLSY<3+~FG;zZ^ zm|<2ft_;h3yA#*Y;+r44CZouvQ#4EuR}$#h>S*)k*5)lDf+##-aX*e?+|1}$`58kT zZzT)mx3IhKl$jXeEl%f0(rWm#!^;fMAi8=M$ZhR{|J71GV&$nk={3OrCd7yWjaPzY zS__5_8!$mADIeRWWO7c;Sy_wVe{o}iJj=|MOfq2@5uYy_M-J$4=m5+k#Yre^f2 zTX!^k3Sn_Z^JFs=AKtDzIB0`VmHiyC=?|B8t)74~LueVoD4|o;;=1^ro(4V1Gd#gK zRwEAc63LKBJ&!ny8zgaJOWM{^?P(?FHQ}?uQmJ_#HBu-m@Mk6G%o#Q->3jd5G|?FA zC@`)!*K~Q;iT~jxooB+O3HdR$Cz7 zfc^Jfcuxmv98TqEN4Edc-NzEi7TmX~-C? zdD})Jz*X$RTm{T8q|@D|CR9+HF`qZyaK&PMzd>1{xE5(lHpf!RYKg&Gxr~z}9F7gM zrI@zY1D{y2hReoGy0dH}_ubGnT`J}?QWMtG1ik1#{@W&O$PH&~I&N%|!r=0VXC^-CViNT`WoH4~}ZdZO+QYn1TAI78A98$!m$Se!BY zBxN2(umKZo4a1vN{40lToE_@K%pK-V`n&P2(846*_NLyJk8`9;MByQ!Pg|g)3{Ite z%j6Qe%^6W^TRYx#wzg<#4YQ2H%ct7Cl1(ht*xV6c;E21sQFRdTX-D>O;Gl2@_0w8K zNMaG z)e7S|EGoIDl)YH_A&ObKh8sZ|WE(AKVxqB-*#rUpdc!P&WI-)4x7s2mQuI{Hd zk(m4Ki;ee(wqGi--+uXKtG&Cw=@Z{&NO#^tC@L&8i$tj>lDXQqA+&2eem@UZUx6Mvm^h6{B+!Abj}>mw4In-V_rg|A#!@vTk%Huyct_-T6JgatG9CDq#>>R3Hb9Cz3;O$V-j? zr?#&Tvh29(yQ`IbtNqAk^5(`! z)@N;T7DQRZccp1t5W9^@PWJFoF?r;cba6m;xjGB%fEbl^mATi|P?c@borS5ThJ?5U zIf-?Bk{eqtu^wE70EzH+V>rMRz*l8Wmj#cTIp05WyyaOJ4MROhCile2-j0hX@S-bl zM-Hb!VWN`FY5*2EF*bijF0DmG46X}zm!0aIzaL9y4qT4(u|z;bB*~py+JQ*DIbE(X ztKz`1jER3p*Ne6fDb{Sf?xBu5nY!5mojobSL1lHt)Zf~0yQu)@p;@9`X!FgML2MoX z_f-iC1Jgi5IrlI;j$nafgI)Q<#=jk~aIeK{K=OZ*kr1MDxs(`RQ!^0=7oXrt+X63X zC3uez-lo=iQFpk|iEZ?P^nhicE(C8@*6AMS2>0F-+be*D7q1$ynhgddi#w{V8$=w# zkcGC5U&;u4gkIIu~H%pMrn)Cgm!1>`G!bpe*Sd}JIzCo8Z3TG7it}S*6 z0knfMW&WKI3tnev#P7a%-qrPRFE@5~6TB(rI~M*$3=j~0DE>Oq(%dqFgec6egf_VQ z!}>pw*6PLtFSbIS)JRrA2Z`7}a@bAE#+nw_s%fxAEgGRkd2q(O%5UYV$BMzW0e&qu z)YVVi54om>r)^S(i1Ng;{ev+ z;Im=YpoLYKeX#`c(azSw>u@cuu84ku$q5(Mg5aBY5(Wj+sLgqyWg>8bys0*}x0Ver z7U9(&X#=9m+TkauW#F%NPpH_u7|m?WweY*tfj`Pv{^l(7&Qz_j z@d&E5;vgADt-lWN-}ozCbnw1sE$T^nsq{=}&%8G=!H=1~fyvXYmU=4!T)5*BI(;cj zSAC=~J^H{BwxDEhi7fJVLHdSCsZ&y>a@@~8lG-Stto3V}y5$Ivbdi4Z9C^5M>D350VTV0Ej7gpAv@gJDcCh_%4KKmgy^q_qDBT zo}(XopwR)3RcdscjfAT||12g9jSB245r4b%+OZV0CrZ-8}kQ5`^Cj`Zd_m8wr*Yl9-ALC7aS z1auF*F4_Q~n^(5`)y|a-?8)iF>ld%^`6FP^H7m71+&bHWlEKIeQNq332bB z26<2_W($qvR+qaxJ}%x<39uchpZB1ji*oK^My4kX?HvI_qab~79uR6qwCbG_x@K#c z>yKebOuRSZ-XSlMZ(Kr|H10hf85*7JsLF*i;D*~>DOoejnzH>D` ze5-8P3_$-0=F-~rc|tV90~3yX$OyG~uHav7_}lw4GTzvY{!3(QhM2apa-(tbje#%Y z2KkhWx?VKnI5Y$735mne4I-+8j{^*u06>T!Ry5hpAod3~pIhU zeR;^e>lEKXxPBITnsJsj}dnHD)9%QWsjEm(B z##>nI_C)6F_t5AMf|rK|MDKnu#Da|lV9sJT0AU$x4_hqYbGbHDLoLtcMsqDb2#s<7 zn8~a07bg>4I-6xTiN-1IY7wbAXWb8{U=F6T?O6zU7U=vLWB`?R2zrhd<_n2Og4zvy zJ?(u_J#*>Ft>Z6jgT__dNbS9y*CCdDMi*baSr+_-W@e7w9fvZnA!*4PvWw_KsQRIR zVc6*C^IkN}Wvox}PW>dFKsNW(knpxK)z} zK!wkO>dpRY*$%ubAEjfScf%!uasAkKy1bueJy4||#Hw-rE~J9d@txmZ#Rk^(E1uWrI@WuGza9XF*zOIt zu_5(TR9#=qJC!Ov3-%?TlmrN@tR(>gBLf=?5mF9K3|*7BQYu)g%0=R&ySux-x-qwj zP}zsO5N$3-2MT&8T`da!=rg|*0YKYC7d%g@=ZgzPY`U#w067zJZU$)xv|<6A#jx5?pW>Aj z1gBa$Dc{k=;c&qjucpx;6l_nihH~4}te&uKCi2P#cFFT9h4{eXPs~>wu_MbC*-*&? zPjm|h9El!M0tI0W4uaP>xSXMN!1pT)HwIjhaA6?pK#KmFKkY zikw^PtS+vKXV(TOLFk$X`ZNjt>cR$ManRW`5)~{Cg-|7CAoS8!Z3w!Y@89hqOtX@F z;!&`f_|u^uxBbw%eP>z(_DmH52sF$m!dhVybWoe+iE6W=zt}Fj&ywXygNj;xL)8ER zHGYi=q!-G!WC7i}!%7zX5Xw^d#FIB@rU7P=dzgGzlnhe9F8uv;Thf4CcM=Q{Wt~A^ zDVq}FR2wWXu7bw*T2z5_>jSfABoT!!c;%kWcce))< zd3njqhb25S0+iC_I2*xGxi>5a$g$BNPdS6f6(!nK!IKWojC?&Id&vN7p#>e6`ifYN ziq%~47#{g0yCxzxltxTpG1*2E&#>BS;-=QZ;@l2tj3ujQq8IG6F~2E$ui>T3Qg?tJnjWCAVc$e` zmyp83;;okL5)v3yh`F6xm1L9%YC(z&rpG|Mu&A&Y&6bjG8;_foG=@g8=jOe0mXHYv z+iIayNz_ux}qu5|*oM4hIY)sl3JuGi5{n^8p>|la4D$z)k zbbKUE-c&MfP=>nScJ;K0)&NMaM2RIP9U*<_Bm;xK_qq7=1@(O_wtv9sE= z#IUcL9bkYu!_Uf|!1lu%2e&L4LA^C|Iddw+fO~l=83^McNV`S1GhfnfE3g1BUFEX2 z$IJ0x4sNuNovkP+Xeg70{S#}8SNlXYnx@ka#Ik?&5BlqSiNgChv$<+c+rT_)EUUF}m&ZrZg+=-VWC zVbJQi0oq;I`rVNoD9ls$5#(|Q?7qFBza}VUSS!=y=&)h|eT22Ku%%-PE9$VVi!<2L zpw`Oj?86AZ0HCL*EMPTP=g=smK@0@M5)27x8D;Kzit+{}8*@O%^eA1!Gkr$;#l4d#caHdtwdtFI@3p$R}L3sY>`%gu(!BF)8$3IcP=SIT~# z4Hn^Miy`}Vl8;31f!zgE7e-Chc`z6>MERH$2tThWbQ9}&#YPyS>^kr8tDBVEK~I$Q zStcnOCXk7p<6gm9Hq7vF zi^$)lw4gSw-mPUuiweas?+TSIpviUW!P4K9a=S=PN@N4AiK)HZe+RcNH$gOQ=Ifm>tgLt*!0V zMbBY|a8nQ3OiVJ{e6Ax}LZwt-j3M$0_z$<6v0En=kRk+LWCS}8+G0L+G+h*JF1DF7 zV;@R}X23;Kqv>={M)Te|d+bpR5t9k#tPrFGo;M3l97(8VuSHXEL1F?EtZazfRPb5y zoOPG6NtlQ5%x-<8ao`kZyTD7Xj9c1$F-|eb3fr<{{w!g31L8Khkvkkz60!NX)hI+@ z(HL?xi&*ozwdne)+NnMn><@H!;|r!6m@IbZohE*1{%}V5d_y{d{t_$3%rLQ)6lRFquq?^r|skB??@-YNGTnrN6?=1Oec|p_sRJSV)(}FYaEj$ zmyliSvVcWLDh2aW%`*BTqs1Dm^d(788g; zxdY$OhHxZT)j6jJb=;@tbRcj=Pp9(X97mXXhl5>?=kY^=-%7RvJy`tsdcl01~Jk&)W@?LAB4Dh z;v%uY7me*2Au$WQV7541Iu=laDdMbO(Aa!=Ki|h=`~WW94Ii zD@Z>8f5hCT9z=q50b_?W>N_*oNvtk>2$SOi)59e_H6>e9Y>3&I@*GGp7ttXD@1}+#G&#+T#MkW#kC~6oOdA<|KhW; zDsy_9^amRkx4hZ5V;meP>}OG+B{3HQ^HLcr95v|mBPFpJA8)whj{}C@`LsR8jl6S* zLU|!@c_5wY*eolQ zhxshvs5bAVLPR}U5@vJmooLvQ64YX6TE~LubU+y3re|y0Le@A_-zbeLnwIreb`GGi9$}>c5yr;4bVvpf zN!Qm{rH&p1wJa0ffLV*7fr&xW%D}^>=Y4{=C~^=QECvzrY*yf$O{t=b>Tu0$tU=k; zX*eS_ggES{S1mpv3y^Du$#yaopCJYjmkF4VoL@U+4h#c464au!U<|uG`)#)sFa?l6 zZ+NM7JA*4ykju(vLscxvTrG|DCVMeP;oi9%6xCqlK8skqxsKfgE`dke$bxXDgeoI* z-CAE>FluAjpG5UB=;Q&XYgQI6r&;gw2U%yAy1jJ`;w&)y7AdK$q4U<7IpV2z`8 z)D)#_XB41BkCMIhv9(6qi zQ6IBqWewi4$mfPs$g2qWUz5}i*Kpo9mV!+^Kpc_*c5CCZz|=-e3@n;^5;XG&E`zC$ zR0>yCnsc9J7Xc1u-Du5;T_F=&Z|Y+uab%WP*1D?;F#R`8lOruwj{w@miIC=ldJ7 zeR5-b`3D?H!*!*HKn*qmFDm0sAoJUL1jtjs&fP!=OTV?`4snxgOTl<>4; z(->%k$Mn}1Dn%bnsZxri)hoRdOf@|$>WUxi(TG2vYLVOD9gu$Hvb{2vv}DneSMt>= zIcvZ?RPLfebf09qY0d61SdyA5*w9=blOa`ENBf1pQG&|GgLXvfERF|2?AgrDs8z<) z6-h6VHqR^Hg=-227Dzkv2oik_tn!1>8ENmd9C%T3BGD(>x`B3cF_9_+f_VWED}{PIMW_{0*%Ug4}D*n!sF;RW1|ODZD5k~p=4>F6vKi2U=ioA z2lo_+WGv)vbN(ocu0&Z-Oe)@{H82s035UEf*^m~{Vt?WGf}rAc#0nq@oGo<)MMn9p zN~0+OZCDH-PgJFljo^vtVSudezu!%tIdo;A9+J^K7-r4NYK-f^RXXlC<7Q1XjP^YC zPMVhVp8mG$y)7hl&vpu^r&5n{P}Sc?CV5B%oG@#45nqJ>ZLR5=i-!SqtkcD0BcI!6 z<@ixK?RMm1Y1dOnmfs9<#DF4>hRlAO*T;$MPH1ThiHw77si5XMfYhIbM!&;Nya|t zlk5FuoWi05z)SL>)|oyk@1Pp(x|EeCbt0-5MkJPwp&K#!je^-<-N1(2N;y0;*)nKPHIchPzo@2XHz%98bVExj8}ggZO5!cs{DO;96B>@_*o2076qA{klWB5o5AbKgvSR## zi0lcXpAKi*mMxJtS(-558Y)30pVUQK6>_PY#gnC?g6evoj;eOumB;E20MJ&cX5rI~O9=)Sz# z2M~#tK^!1GsmShN?T(tf^0eXeubdVyQUkBU>W>aS!4jG~MT3SwaRujy>{%Lr1AXdI zAymAZ(Fq)#sB}|PQ4|5}&K1m`@~BDtPQdwqa}Xv^V?(Bn#79|4l@&7yv_>;q)uIKn zYN5AP=h5vxd?V3*2TXwym>Ca$$-h_%M#FmN)@;}ol+EISoIfy20(@TZ?8WCx;cN@v zui>Ez%_XJv99uv%M6tif6XF#mV--DlP+BdE!wC3MTp{`42SF%}xEqpY2QEY10o@(}O5W>8zxc@Zs`_A& zAai9En^-i1velliLd>l=NH!eN%_z-g;*=4=UUp&-x~grDz(KwYAF?oDvUGssEVEx{ z2`5NuM@NgmxKe;8XhyddEGpfin{9lJU|j{~bOfWUqvT>j=2Pt#H4B@C6vH3@;&&-M<-6*dFRyK z6H`Ygk4zq(IJnwd>Gyk!2Z2|w%&p)IL|`Ri*qMD*bH z+7*N-9_)*=dka5mR=tBb(WI}Dwg(}c*Vp7O`=N`x)yw=DSgY*;NjUVA#ZL~W%G zY_lt-qL|MX+#!+m(I}^p&W!I}RsJs6RGCPL)lRI$5N)4qg>-0Z1*#lTQPDd~RvHYBmRX9A5aqR_VO}_^ZxO*-me*tqt}nzPdCDM+h4#^!#7sePh)-WHzopI=8GwyW zoW32c&=-Hqk}MfWkz#lp=*B__LA368j8|T*v8aO{HbU+>;C^p9iA7Qivn`xFGu5vG*-wxzcmv`Su}Vf_arx6-)Q! z+wtZSKz?3Om@X1D%cSs1#$6M_bs=2!Xh#nd1}?eDor{z;g_jzT88YLXAqV83H~DVS7}mI)$d@iI(g zx3;!KWv7Z^BQf}4n2QAB8yj3E8GN0J!9b+q@FmY}-Hu7yS&CBXp|dk60w<`ua6&S| zp#7Gv1ZZKn#9(8eC|sepH8RH2Q9tjlcLhY+3piLH&f?&8*=E>3~oxOT9+v2m!sxYa*&0snh& zb8cg~8tbY<^GjQkxVbvCy|sA=GNZS>uy|-r`rz2~!QRT+_SJ(3KswaD^Ugz4M-L%_ zbE|!+{ZOI}y!a3jTiOl1mH9)LyWPU`@GNgkyXw4U(xgMO9lqGGLl!v|zuh<$wy*J* zR|EiMz#&U3mnWAiIrSE(0K)q&uZ!LUlKeSuI=256{%8Re_%V+1^=sFVO=xqa>wgiZ z!Jp|2)aUSb1t&X8+iTseIZ^vLIi5#7(YhWq7e2+$o)e+(_NU4QZbNW1apg z$~evlb>Jwfh0gkh^rJmfuabwMz|_=6TUp1qJ@KHvQ1}yt#jA_mZ8^nCz*QX;b#B|S zfz^rm?Ui2t;L6&)6=J^EH?K)nmG$k-uJrdmcw<$)p}MJ(PrdK$`d$6`+v@6_R=lac z$xFA@6QOk6OZ)0GUYf2y6-r<9($)IFcUmpe^(8O8w5LA&E+g)*FM8>4{gju!u&18B zjXyu`r6=pJdFk0bb^mYK&ll>icnwKW(eebqkC+cZ0;ej`Pmk}SYFL_B?aXX1W z?WO7ZMK3Ma`@h?MU9O+@Qm_7|mp1C&_t>w;>*u|6wtme^z53Gk+OHG!r@Zv3J#{r< z#3$<~y>y{|$xGAqjqkBvPuCB5X@9-prQ7PKy!2#!<9qG8{q+Ga{XVLGABp?DbfCWA zrG52s`1NTo-B!QgrQ`L>UV83^x_ZA=v#;J4O8dQp3ca+ye$q>i*I)C}ZS|{OdZ<4B z0juRpH`E*7#|l5~rC0XUz5PbKP`~JRB&FTJ*>e&J3dK3?B=z)H(?&r7}fYhF5C-+s`3eQHnrychS?U-Hst_ta-5 z?e}l)tsgpU#TWO~H%*1Rp7!FI`j5PHTm71s&eo@o*oBAdzL)mb&wA-h{fd_^)+dkJ zb+^?Qy@Z}RIc>zV_0wKDT)*h0{q-wedS*}ksk`jLOZCfM+F##v%xGuo3tmE3{TDBu zt*7s{pKq_vdTC$1;H8uG^IqCt|4#VzgMoUX5W=}i4)FCDMH>7@hp{u6fH;rgPN_SHQvP1nzP zsbBB=uw4f!;HCZb<6b&mf7VNn*ROgBY4A(v$V=_t~%e>cgRQ+Dpsz7rgXP{hF7+fKT1eTb}jO zf%+?6dZ@nn6o0+LOPA_Td+Eu#I&Hr$)C*qPS6}rKy5CE;)xQybeaTDD?yY+d*v*IQ zXT5|62iiBhbg@4Bpk0TqdD)BTuZN7dzdr4y1NDZN9;(0OrG549c2IjMRHVPLI{AjubL;AJb%}mc^|d1XP1OrG zm9Bqt^~-N6(ce;CeM^b{*6QgZ{cTl+f78$2>h0AT@8kHlz14SAmx}b8tJ{7kzS7Ej zt6Qp#M@#fut9=V4`a7zZzqu#-VQ;msdZkEzXZ324{+-p$ub1k17wXOjY;ScN{wdL2 zRY+R2w|aL%u|M}#-!*`K`vCfP51_kxko>y0`rd?Mf9|a&GP=C|jn&s*Pk%7B$@%=P zxNuL^&?o#x)5LNgS2<^}7ynDDu*pu^gKHfg^|d1-%2zt8EPO0Ktldl!`JHXmN_>t9T+|2XJ-BEBWJ zTRr)F+0pBB`p> z8W<^^=0-}V@sZMLillTJCMlg}N=m2ElG16yq;%RdDV^3$N~f)p(rNglbecgaokmeg zr-hW#X)mR8T2CpRwiM~c58qKO&yr%_97kG@{FQWCVkwomOB9lxLI_=+-PHQ-&(>6}&G?Y_1&E}L& zBRZwiq)zEHuv0qC?UYXAJEhYUPwBMFQ#!5mluna9rPF{<=``n4I*t33PE$Xn)9_E} zgaJ}IL4lM`gdn98FG%SG4^ldzgp^KTA*B;$Na+L}QaTZclukS%r4yV;=|n10I$?{H zP7EWZ6VXWN#5Ynp(T?5TU2}$Y1MN&FZl9W!&B&8ETN$G@BQaZtvluoE6r4x5a z=|o{tIx(4)PJ||<6Rt_=1aDG0p`4UXU?-&$=1J)UeNs9hpp;HPD5Vn}O6kOkQaX{N zluj5ar4v+2>4cb4IsvDYPV_0I6N^geM5a{I`OoWPIN7$6KYH81m03QVYrk|P%fntqD$$|og~G+ zsTUEx%uDHn_EI{5zLZYbk95@el!BzNNVuQS%bRvZ*ow#92CyJQTi7BRZ zB8(}WcwT=@pRee1Z>dtcyhjOVxS}tTT2n`0D3z z{d#BfPLh8$_L_If?o@reETf!m(NF?PQBuiQmg7GozC^~ zDW`wM>C^)Wo8Gb4>gW2x=icvh>OXA%&p4gy#eT27Q{r_1lYH&mTBF?YIBo zbm{}_hY!Bd+RycF!s~g&>9qTfJ70a?>BO(tKW_%KJ%RU4@$=vH>$yI1y#ANdsn?zK z=U#xH)aG#c`i^I_*S0_c^B%e<1z$oKD=pV<@T{-fZomeL;Rc=yc**$E`Wlk2#$< z)r8agPN!YG?DWro4k--Yf`E!XKMQ(^AHF<*{#CD^b_)CLoxt~Rxgp2Z-wV2q`)PY9 z+Cr)sueHhSP~79dP-ibKp7zCr)Bl;%X&;?&`d>Jmcs1L96Z{aj-jMro-VVC7hxkd)>-mse@8$UGLBF2% z7y0vVJDvLTXZ@yUNO#v8>;IRam-zFy{Ce7J9N)imI&oCa$9H^3xt$MyF7HKLeZt$b z-jOKQ$OeX{&T0(uf%cxJ*U%-IN4= zxx_0t?`EA&KgF`wbJ^*%i=TG-KXE#720r(9oK8HM?Y{wxA@55&o%8WSPEW7Dt0Q%H;QFhX=` z_W8#H=)VNIyf6J|?1wKqowyMD?M+bhOZ9&b=u!`HDz@h%PEXgPpLRO^TrYWxUvxTg zZ$9_0oKAZd-J+j+po+GY@Yy*>l_tvBR;^JhSp^)Zcqc;4$F-h0NM`&FmY zUwPQ+Uw1nFHNWrlePCc|XR5#5<8=DDdVc+nI-UMU_QO-4m-_$b{CfJK_`ZMObo%L7 z|Er*5%4eT9B0xdfnc5dW;Ph0!U2{5dd&;XXJDvV1w&%~BPX7YwZ$T)DJeN59i{7#i zfiCM#Ixffkdg5}&{rX3oPTY+1=%<`c{DJb}kDX3@_;#=7Cd@CfYf?S`ouErQiNkPQ z?$hgY{p|t2o;Wc1_DQD`KOz7839o;jHDto=k!dtcpZonr=X0+)oqplRz5SnYI&o<} z_un`@wNL-j>GWTKJ@j)cLUT&|b|>i49^x$z`SsVFPTaNc^e=*58pjv?diovC`t|?U z>Gb1#*6HttULwy;?SiAE<2~f_xP4VW;`G$6-*$Q`e?ISY`uo^#e=(r`TM>#R?SbO1 zpC|pflTN33`vT{2b_|_WX)pPyfZV^XDHqJ(ZJhe^0rd4}vc3q#uX#@v#Bd@A&n^6)#ry`9r4@ zZzmtV8%BiGpXv$6oxbezi|_lm(}_PXd;R~~>GZeqx&Pbg>H2;v>P1&%pGnZAJ;diZ ze?JX+sh@w&uczPhDX;OXPEYOb*PNcpfw$dJuK#_YOMBAw<%gY4zXs>gPdGibXPg7y3v=o5Z#{Pov8akk;Ph0le!=O)|Je_J>U73ku;1SP zefC`9(roi3r!zi>eEZ{0XFM3|`AN`uZ~VT`u^#Y|e8``_>2&&$S-T-o+QawUFLx&V2%zLWH&^w9Woje|u?ZvRf@6 zMZjgG@0C5Zs~v29TEpHp&8v2~hka(f&H@gt+>%X;l_VUI6Wvn zHmnXxzxC}qjsUI8_H5(GO-;8nd|;YwwEjXPvO!1tYW;?A{EhB#&YHojPnQ1S6mw|J z_I}7VlFN|_A-7OrOE^ZYImWP^Y$W~3c4ZDbs!lmTvZx4y$R_7xw{~}Do{ZJXkec<9 zWlSBNfC@Xz@sz?=>h_6s2{UGoW73e*Q`)>j9^Si<>9>DPeq9tEvjM_p0zVJR z(Sk;|Iv`0An^Zioy?VgP!lI|3Sey~&@&gJ>Ct>M+d_RD8m)Ty&&Dhm6HQyHgt)x(} zUG4?`jT1TAPG}?0w9h7!hD*U%JN`9_TfCT z`yr~4AO_ohSJpb)TZ^)+YQ7{Si!fGx(5WU24+2^ePwrSn7cX{>PELDGjX<~LIC`F>$z_oNnkY@T=3Anzlcz3Tyzl7 z(T~dkG$a#=$QS2Xry^OMzeXgsM?f>81vMlvi%r9nIk_4)t6(E`M9o32O-(dsNoa?jE3umq zxZX^to(lDp34du|P@1nGy$MDJYP@R3(ru@IWO5TJDUf>m>eUWTe%e}>F7IFK?7%K6 z<@P%sGRQKMahekvhjf75nLy}lHV&4bC>%OcsU$@)G1OXlCy(4C{idT{uwSr@>#&vOaxcn6 znbnYuBj-hN!>TV)@tX&jRZf}T-_S#}uQN$S;Lv_WO^Y_VmMnfFX1@qxBg=#a@Ia6X4ZfB8BhoErF z06EV)8^O980c`7IYLofogiw)0ZYzCD)g6p_SUEvftE4_Ne;E4Ru(X0RB0rmxyJmZG zg5VC);-3xcT54CSVPjJlKQ&M6%#Ap9sJ#+7^WjAYiQ4hf?>F@X6}~8;nql`lqO5_d zmN5`r1?{hu0IA7>4t89XNjb$ch{u;9?ZM17%wFDq(GfD1YVnCIv3G1axV z1$`*Z^N!Oe@KnsXl`hX;6-PWsw{`G|G-QO3E~?$iD-=!1)4)jRvfQBQ34aAxt2m1X zzv=n@-JYg#7PX|>=M0V@Jtw@5gmpCDUEg+daWr@$%Ch60ZrOrm zh;Z({eKtg9IW~7MJ|PL5b55KYtyu`(B=59^!)<2$U3Va6`1*!c`JuleeZuS5(ys^+ z!Gq>DuZbrINrP44Huc}0c@&08s_>7w%(WGBD3t^k3hIKuK||FA=<23o%svbo7|8Kq zXbPS4(1DoAvi#OS>pE-5Pq`qO6n%u!X*9yo;;A zbiVp|*+VGmvOtc*OWE$i1=w-gW^ye+&K z15BBYAeiy#02oUs)ic%H{Q71J?&eAx-WGo4<;CtN1f!XO@q~QeW9v=owhRsu!JqXU zlJ@K$JYU81jht{YLvwJiUD^3QR)2+d=ce%D$hY2T1L7H6T$58cIx9=F7q>Ri>mVUl z`4+-4<~C;`;%Cs4X7=pN0AZ74vNl-IPQtlWMb;|_Wa~UgHeCcovmUIC$Le3hc`A3D zAI-8urbI~gD_nq8huWfO0+7i2UU*8(+MqFOFUTkNKRkTPIc2=w@X#WK`64M^Np zX`C%2)9a&p@Ji?0;uexBpM3DtDR}e@433zh;E61GEmv29MVN_S_}>OexbTA>XI`*7 z%e)k*kzHe=rx|p?%y6{SoMg7Q*T#VXM>^*)yW6ASE6~6tA_KzA(ix>K6!hu_b{i&< zMCSwEgXNfGN-?h|nc!^%O28{Q?-dtwQ}{U>FoAJ4ymE>Ij4h%C>I20u`s1!gEL;hCV1cq&8XB(#K@eY>q$HDT!v z*EQyr;z(dCcmvHFmMs<1~9?XnAuoCM*vT(k_)a zf-WGV$MHN6kvW4UPI`a0hs^)O+zMIm&IIjNmT)X4a|;VlC%4v@Fe_m!ox2V!WvD)# zw;(paSJwM;Bem$lyq#I`lQwM9sVNVAybSLh=a~t!1&`Q}6HB>_g%ic72#*YZQpr_K zZe3f&8Cv-6Z`$`VOETv-H>$}s9H}*lvkE39yZXYxl?5ju76Zp`U9R-EWgM?nO)gwp zLw)w$-?U$L;5C#3xAZdqql4cdbLZp+FKzVt)uf_HlSm|wvikTYH`g_QX>xJdLn@aS za4-KtrQL4F^3Ed`R8~E$!=f#%4wmt#xz&{}Zdpg|Rx7-6)g)p)SLFn@Nlk-~%jecE zBQVteTv=OMXL)XZ9_L8&FC5~4$JtHjGZuvmxfTB2T)W&u(CL=PynZWR*k)4g!GAY+ zC6Zr3KKz#7Qc_~*x*&gV1FF~8^e z1Fn-#+JAoswAZ8de@;3BAAU>!$cOD`b^QJWe*2r~e&$W)_e*+rRZ07$@7R7PSAW;( ztdIGX`F)c&n9t|^eE(1T{r7u?%)`v@uSEA_>t1+1@BbOl2?l9qV?2NF8(=l5Iv{^YXiUVr{2c||7=(O*Y`v>qnO@*pWmPM z-;p;M +#include +#include +#include + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" + +namespace boostjsn = boost::json; + +static const std::string method_name = "copy_series"; +static const std::string graph_state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Copies a subselector to a new subselector"}; + clip.add_required("from_sel", "Source Selector"); + clip.add_required("to_sel", "Name of new selector"); + clip.add_optional("desc", "Description of new selector", ""); + + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.add_required_state(graph_state_name, + "Internal state for the graph"); + + if (clip.parse(argc, argv)) { + return 0; + } + + auto from_selector_obj = clip.get("from_sel"); + auto to_selector = clip.get("to_sel"); + auto desc = clip.get("desc"); + + std::string from_selector; + try { + if (from_selector_obj["expression_type"].as_string() != + std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + from_selector = + from_selector_obj["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + // std::map selectors; + auto the_graph = clip.get_state(graph_state_name); + std::string_view fs_view = from_selector; + std::string_view top_level = fs_view.substr(5); + + bool edge_sel = false; + if (testgraph::testgraph::is_edge_selector(top_level)) { + edge_sel = true; + } else if (!testgraph::testgraph::is_node_selector(top_level)) { + std::cerr + << "!! ERROR: Parent must be either \"edge\" or \"node\" (received " + << top_level << ") !!"; + exit(-1); + } + if (edge_sel && the_graph.has_edge_series(to_selector)) { + std::cerr << "!! ERROR: Selector name " << to_selector + << " already exists in edge table !!" << std::endl; + exit(-1); + } + if (!edge_sel && the_graph.has_node_series(to_selector)) { + std::cerr << "!! ERROR: Selector name " << to_selector + << " already exists in node table !!" << std::endl; + exit(-1); + } + + if (clip.has_state(sel_state_name)) { + auto selectors = + clip.get_state>(sel_state_name); + if (selectors.contains(to_selector)) { + std::cerr << "Warning: Using unmanifested selector." << std::endl; + selectors.erase(to_selector); + } + if (edge_sel) { + if (!the_graph.copy_edge_series(from_selector, to_selector, desc)) { + std::cerr << "!! ERROR: copy failed from " << from_selector << " to " + << to_selector << "!!" << std::endl; + exit(1); + }; + } else { + if (!the_graph.copy_node_series(from_selector, to_selector, desc)) { + std::cerr << "!! ERROR: copy failed from " << from_selector << " to " + << to_selector << "!!" << std::endl; + exit(1); + }; + } + } + + return 0; +} diff --git a/test/TestGraph/foo.cpp b/test/TestGraph/foo.cpp deleted file mode 100644 index e8063f6..0000000 --- a/test/TestGraph/foo.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include - -struct Foo { - int x; - void print() { std::cout << "non-const" << x << "\n"; } - void print() const { std::cout << "const" << x << "\n"; } -}; - -int main() { - Foo f{10}; - f.print(); - - const Foo &g = f; - g.print(); - Foo h = g; - h.x = 20; - g.print(); -} diff --git a/test/TestGraph/mvmap.hpp b/test/TestGraph/mvmap.hpp index 128d40a..fb4ed69 100644 --- a/test/TestGraph/mvmap.hpp +++ b/test/TestGraph/mvmap.hpp @@ -20,8 +20,9 @@ class locator { locator(index loc) : loc(loc) {}; -public: - template friend class mvmap; + public: + template + friend class mvmap; friend void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, locator l); friend locator tag_invoke(boost::json::value_to_tag /*unused*/, @@ -38,8 +39,10 @@ locator tag_invoke(boost::json::value_to_tag /*unused*/, const boost::json::value &v) { return boost::json::value_to(v); } -template class mvmap { - template using series = std::map; +template +class mvmap { + template + using series = std::map; using key_to_idx = std::map; using idx_to_key = std::map; using variants = std::variant; @@ -51,15 +54,18 @@ template class mvmap { std::map...>> data; std::map series_desc; -public: + public: // A series_proxy is a reference to a series in an mvmap. - template class series_proxy { + template + class series_proxy { std::string id; std::string_view desc; key_to_idx &kti_r; idx_to_key &itk_r; series &series_r; + using series_type = V; + // returns true if there is an index assigned to a given key bool has_idx(K k) { return kti_r.count(k) > 0; } // returns true if there is a key assigned to a given locator @@ -76,13 +82,16 @@ template class mvmap { return kti_r[k]; } - public: + public: series_proxy(std::string id, series &ser, mvmap &m) : id(std::move(id)), kti_r(m.kti), itk_r(m.itk), series_r(ser) {} series_proxy(std::string id, const std::string &desc, series &ser, mvmap &m) - : id(std::move(id)), desc(desc), kti_r(m.kti), itk_r(m.itk), + : id(std::move(id)), + desc(desc), + kti_r(m.kti), + itk_r(m.itk), series_r(ser) {} V &operator[](K k) { return series_r[get_idx(k)]; } @@ -123,20 +132,23 @@ template class mvmap { locator get_loc(K k) { return locator(get_idx(k)); } // F takes (K key, locator, V value) - template void for_all(F f) { + template + void for_all(F f) { for (auto el : series_r) { f(itk_r[el.first], locator(el.first), el.second); } }; - template void for_all(F f) const { + template + void for_all(F f) const { for (auto el : series_r) { f(itk_r[el.first], locator(el.first), el.second); } }; // F takes (K key, locator, V value) - template void remove_if(F f) { + template + void remove_if(F f) { auto indices_to_delete = std::vector{}; for (auto el : series_r) { if (f(itk_r[el.first], locator(el.first), el.second)) { @@ -167,14 +179,14 @@ template class mvmap { // this returns the key for a given locator in a series, or nullopt if the // locator is invalid. - std::optional> - get_key(const locator &l) const { + std::optional> get_key( + const locator &l) const { if (!has_idx(l.loc)) { return {}; } return itk_r[l.loc]; } - }; // end of series + }; // end of series ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// mvmap(const idx_to_key &itk, const key_to_idx &kti, @@ -189,9 +201,9 @@ template class mvmap { {"data", boost::json::value_from(m.data)}}; } - friend mvmap - tag_invoke(boost::json::value_to_tag> /*unused*/, - const boost::json::value &v) { + friend mvmap tag_invoke( + boost::json::value_to_tag> /*unused*/, + const boost::json::value &v) { const auto &obj = v.as_object(); using index = uint64_t; // template using series = std::map; @@ -244,6 +256,18 @@ template class mvmap { return series_proxy(name, desc, std::get>(data[name]), *this); } + // copies an existing column (series) to a new column and returns true. If the + // new column already exists, or if the existing column doesn't, return false. + bool copy_series(const std::string &from, const std::string &to, + const std::string &desc = "") { + if (has_series(to) || !has_series(from)) { + return false; + } + data[to] = data[from]; + series_desc[to] = desc; + return true; + } + template std::optional> get_series(const std::string &name) { if (!has_series(name)) { @@ -253,6 +277,14 @@ template class mvmap { return series_proxy(name, std::get>(data[name]), *this); } + std::optional> get_variant_series( + const std::string &name) { + if (!has_series(name)) { + return std::nullopt; + } + return series_proxy(name, data[name], *this); + } + void drop_series(const std::string &name) { if (!has_series(name)) { return; @@ -261,6 +293,10 @@ template class mvmap { series_desc.erase(name); } + mvmap::variants get_as_variant(const std::string &name, const locator &loc) { + return data[name][loc.loc]; + } + // returns a series_proxy for the given string. If the series doesn't exist, // create it. // template @@ -279,13 +315,15 @@ template class mvmap { // F is a function that takes a key and a locator. // Users will need to close over series_proxies that they want to use. - template void for_all(F f) { + template + void for_all(F f) { for (auto &idx : kti) { f(idx.first, locator(idx.second)); } } - template void remove_if(F f) { + template + void remove_if(F f) { std::vector indices_to_delete; for (auto &idx : kti) { if (f(idx.first, locator(idx.second))) { @@ -302,4 +340,4 @@ template class mvmap { } } }; -}; // namespace mvmap +}; // namespace mvmap diff --git a/test/TestGraph/testgraph.hpp b/test/TestGraph/testgraph.hpp index 05cdee7..a6abc61 100644 --- a/test/TestGraph/testgraph.hpp +++ b/test/TestGraph/testgraph.hpp @@ -1,7 +1,4 @@ #pragma once -#include "boost/json.hpp" -#include "mvmap.hpp" -#include "selector.hpp" #include #include #include @@ -9,28 +6,35 @@ #include #include +#include "boost/json.hpp" +#include "mvmap.hpp" +#include "selector.hpp" + namespace testgraph { // map of (src, dst) : weight using node_t = std::string; using edge_t = std::pair; -template using sparsevec = std::map; +template +using sparsevec = std::map; +// using variants = std::variant; class testgraph { - using edge_mvmap = mvmap::mvmap; using node_mvmap = mvmap::mvmap; - template using edge_series_proxy = edge_mvmap::series_proxy; - template using node_series_proxy = node_mvmap::series_proxy; + template + using edge_series_proxy = edge_mvmap::series_proxy; + template + using node_series_proxy = node_mvmap::series_proxy; node_mvmap node_table; edge_mvmap edge_table; -public: - static bool is_edge_selector(const std::string &name) { + public: + static bool is_edge_selector(const std::string_view name) { return name.starts_with("edge."); } - static bool is_node_selector(const std::string &name) { + static bool is_node_selector(const std::string_view name) { return name.starts_with("node."); } @@ -67,11 +71,18 @@ class testgraph { // this function requires that the "edge." prefix be removed from the name. template - std::optional> - add_edge_series(const std::string &name, const std::string &desc = "") { + std::optional> add_edge_series( + const std::string &name, const std::string &desc = "") { return edge_table.add_series(name, desc); } + template + std::optional> add_edge_series( + const std::string &name, const edge_series_proxy &from, + const std::string &desc = "") { + return edge_table.add_series(name, from, desc); + } + void drop_edge_series(const std::string &name) { edge_table.drop_series(name); } @@ -81,15 +92,32 @@ class testgraph { } // this function requires that the "node." prefix be removed from the name. template - std::optional> - add_node_series(const std::string &name, const std::string &desc = "") { + std::optional> add_node_series( + const std::string &name, const std::string &desc = "") { return node_table.add_series(name, desc); } + template + std::optional> add_node_series( + const std::string &name, const node_series_proxy &from, + const std::string &desc = "") { + return node_table.add_series(name, from, desc); + } template std::optional> get_edge_series(const std::string &name) { return edge_table.get_series(name); } + + bool copy_edge_series(const std::string &from, const std::string &to, + const std::string &desc) { + return edge_table.copy_series(from, to, desc); + } + + bool copy_node_series(const std::string &from, const std::string &to, + const std::string &desc) { + return node_table.copy_series(from, to, desc); + } + template std::optional> get_node_series(const std::string &name) { return node_table.get_series(name); @@ -121,8 +149,14 @@ class testgraph { [[nodiscard]] size_t nv() const { return node_table.size(); } [[nodiscard]] size_t ne() const { return edge_table.size(); } - template void for_all_edges(F f) { edge_table.for_all(f); } - template void for_all_nodes(F f) { node_table.for_all(f); } + template + void for_all_edges(F f) { + edge_table.for_all(f); + } + template + void for_all_nodes(F f) { + node_table.for_all(f); + } [[nodiscard]] std::vector edges() const { auto kv = edge_table.keys(); @@ -181,6 +215,6 @@ class testgraph { return out_neighbors(node).size(); } -}; // class testgraph +}; // class testgraph -} // namespace testgraph +} // namespace testgraph From fd378b0ebefcfb0d9c63f5069e51ae98c73327c0 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Thu, 5 Dec 2024 09:16:23 -0800 Subject: [PATCH 04/14] copy_series working --- .devcontainer/Dockerfile | 18 ---------- .devcontainer/devcontainer.json | 23 ------------- .devcontainer/reinstall-cmake.sh | 59 -------------------------------- test/TestGraph/copy_series.cpp | 57 +++++++++++++++++------------- test/TestGraph/mvmap.hpp | 7 ++-- test/TestGraph/testgraph.hpp | 36 ++++++++++++++----- 6 files changed, 65 insertions(+), 135 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .devcontainer/reinstall-cmake.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 60fc29d..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04 - -ARG REINSTALL_CMAKE_VERSION_FROM_SOURCE="3.22.2" - -# Optionally install the cmake for vcpkg -COPY ./reinstall-cmake.sh /tmp/ - -RUN if [ "${REINSTALL_CMAKE_VERSION_FROM_SOURCE}" != "none" ]; then \ - chmod +x /tmp/reinstall-cmake.sh && /tmp/reinstall-cmake.sh ${REINSTALL_CMAKE_VERSION_FROM_SOURCE}; \ - fi \ - && rm -f /tmp/reinstall-cmake.sh - -# [Optional] Uncomment this section to install additional vcpkg ports. -# RUN su vscode -c "${VCPKG_ROOT}/vcpkg install " - -# [Optional] Uncomment this section to install additional packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index b519e25..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,23 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/cpp -{ - "name": "C++", - "build": { - "dockerfile": "Dockerfile" - } - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "gcc -v", - - // Configure tool-specific properties. - // "customizations": {}, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} diff --git a/.devcontainer/reinstall-cmake.sh b/.devcontainer/reinstall-cmake.sh deleted file mode 100644 index 408b81d..0000000 --- a/.devcontainer/reinstall-cmake.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -#------------------------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- -# -set -e - -CMAKE_VERSION=${1:-"none"} - -if [ "${CMAKE_VERSION}" = "none" ]; then - echo "No CMake version specified, skipping CMake reinstallation" - exit 0 -fi - -# Cleanup temporary directory and associated files when exiting the script. -cleanup() { - EXIT_CODE=$? - set +e - if [[ -n "${TMP_DIR}" ]]; then - echo "Executing cleanup of tmp files" - rm -Rf "${TMP_DIR}" - fi - exit $EXIT_CODE -} -trap cleanup EXIT - - -echo "Installing CMake..." -apt-get -y purge --auto-remove cmake -mkdir -p /opt/cmake - -architecture=$(dpkg --print-architecture) -case "${architecture}" in - arm64) - ARCH=aarch64 ;; - amd64) - ARCH=x86_64 ;; - *) - echo "Unsupported architecture ${architecture}." - exit 1 - ;; -esac - -CMAKE_BINARY_NAME="cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" -CMAKE_CHECKSUM_NAME="cmake-${CMAKE_VERSION}-SHA-256.txt" -TMP_DIR=$(mktemp -d -t cmake-XXXXXXXXXX) - -echo "${TMP_DIR}" -cd "${TMP_DIR}" - -curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_BINARY_NAME}" -O -curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_CHECKSUM_NAME}" -O - -sha256sum -c --ignore-missing "${CMAKE_CHECKSUM_NAME}" -sh "${TMP_DIR}/${CMAKE_BINARY_NAME}" --prefix=/opt/cmake --skip-license - -ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake -ln -s /opt/cmake/bin/ctest /usr/local/bin/ctest diff --git a/test/TestGraph/copy_series.cpp b/test/TestGraph/copy_series.cpp index e507a79..b6549f7 100644 --- a/test/TestGraph/copy_series.cpp +++ b/test/TestGraph/copy_series.cpp @@ -20,8 +20,7 @@ static const std::string sel_state_name = "selectors"; int main(int argc, char **argv) { clippy::clippy clip{method_name, "Copies a subselector to a new subselector"}; clip.add_required("from_sel", "Source Selector"); - clip.add_required("to_sel", "Name of new selector"); - clip.add_optional("desc", "Description of new selector", ""); + clip.add_required("to_sel", "Target Selector"); clip.add_required_state>( sel_state_name, "Internal container for pending selectors"); @@ -33,37 +32,45 @@ int main(int argc, char **argv) { } auto from_selector_obj = clip.get("from_sel"); - auto to_selector = clip.get("to_sel"); - auto desc = clip.get("desc"); - - std::string from_selector; - try { - if (from_selector_obj["expression_type"].as_string() != - std::string("jsonlogic")) { - std::cerr << " NOT A THINGY " << std::endl; - exit(-1); - } - from_selector = - from_selector_obj["rule"].as_object()["var"].as_string().c_str(); - } catch (...) { - std::cerr << "!! ERROR !!" << std::endl; + auto to_selector_obj = clip.get("to_sel"); + + auto from_selector_opt = + testgraph::testgraph::selector_obj_to_string(from_selector_obj); + + if (!from_selector_opt.has_value()) { + std::cerr << "!! ERROR: invalid from_selector !!" << std::endl; exit(-1); } + auto to_selector_opt = + testgraph::testgraph::selector_obj_to_string(to_selector_obj); + if (!to_selector_opt.has_value()) { + std::cerr << "!! ERROR: invalid to_selector !!" << std::endl; + exit(-1); + } + + auto from_selector = from_selector_opt.value(); + auto to_selector = to_selector_opt.value(); // std::map selectors; auto the_graph = clip.get_state(graph_state_name); - std::string_view fs_view = from_selector; - std::string_view top_level = fs_view.substr(5); bool edge_sel = false; - if (testgraph::testgraph::is_edge_selector(top_level)) { + if (testgraph::testgraph::is_edge_selector(from_selector)) { edge_sel = true; - } else if (!testgraph::testgraph::is_node_selector(top_level)) { + } else if (!testgraph::testgraph::is_node_selector(from_selector)) { std::cerr << "!! ERROR: Parent must be either \"edge\" or \"node\" (received " - << top_level << ") !!"; + << from_selector << ") !!"; exit(-1); } + + bool to_sel_has_prefix = + testgraph::testgraph::is_edge_selector(to_selector) || + testgraph::testgraph::is_node_selector(to_selector); + if (to_sel_has_prefix) { + std::cerr << "Warning: stripping prefix from to_selector" << std::endl; + to_selector = to_selector.substr(5); + } if (edge_sel && the_graph.has_edge_series(to_selector)) { std::cerr << "!! ERROR: Selector name " << to_selector << " already exists in edge table !!" << std::endl; @@ -82,14 +89,15 @@ int main(int argc, char **argv) { std::cerr << "Warning: Using unmanifested selector." << std::endl; selectors.erase(to_selector); } + from_selector = from_selector.substr(5); if (edge_sel) { - if (!the_graph.copy_edge_series(from_selector, to_selector, desc)) { + if (!the_graph.copy_edge_series(from_selector, to_selector)) { std::cerr << "!! ERROR: copy failed from " << from_selector << " to " << to_selector << "!!" << std::endl; exit(1); }; } else { - if (!the_graph.copy_node_series(from_selector, to_selector, desc)) { + if (!the_graph.copy_node_series(from_selector, to_selector)) { std::cerr << "!! ERROR: copy failed from " << from_selector << " to " << to_selector << "!!" << std::endl; exit(1); @@ -97,5 +105,8 @@ int main(int argc, char **argv) { } } + clip.set_state(graph_state_name, the_graph); + clip.return_self(); + return 0; } diff --git a/test/TestGraph/mvmap.hpp b/test/TestGraph/mvmap.hpp index fb4ed69..5d1782c 100644 --- a/test/TestGraph/mvmap.hpp +++ b/test/TestGraph/mvmap.hpp @@ -258,13 +258,14 @@ class mvmap { // copies an existing column (series) to a new column and returns true. If the // new column already exists, or if the existing column doesn't, return false. - bool copy_series(const std::string &from, const std::string &to, - const std::string &desc = "") { + bool copy_series(const std::string &from, const std::string &to) { if (has_series(to) || !has_series(from)) { + std::cerr << "copy_series failed from " << from << " to " << to + << std::endl; return false; } + std::cerr << "copying series from " << from << " to " << to << std::endl; data[to] = data[from]; - series_desc[to] = desc; return true; } diff --git a/test/TestGraph/testgraph.hpp b/test/TestGraph/testgraph.hpp index a6abc61..0ace928 100644 --- a/test/TestGraph/testgraph.hpp +++ b/test/TestGraph/testgraph.hpp @@ -30,18 +30,35 @@ class testgraph { edge_mvmap edge_table; public: - static bool is_edge_selector(const std::string_view name) { + static inline bool is_edge_selector(const std::string_view name) { return name.starts_with("edge."); } - static bool is_node_selector(const std::string_view name) { + static inline bool is_node_selector(const std::string_view name) { return name.starts_with("node."); } - static bool is_valid_selector(const std::string &name) { + static inline bool is_valid_selector(const std::string_view name) { return is_edge_selector(name) || is_node_selector(name); } + static inline std::optional selector_obj_to_string( + boost::json::object from_selector_obj) { + try { + if (from_selector_obj["expression_type"].as_string() != + std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + return std::nullopt; + } + std::string from_selector = + from_selector_obj["rule"].as_object()["var"].as_string().c_str(); + return from_selector; + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + return std::nullopt; + } + } + friend void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, testgraph const &g) { v = {{"node_table", boost::json::value_from(g.node_table)}, @@ -108,14 +125,15 @@ class testgraph { return edge_table.get_series(name); } - bool copy_edge_series(const std::string &from, const std::string &to, - const std::string &desc) { - return edge_table.copy_series(from, to, desc); + bool copy_edge_series(const std::string &from, const std::string &to) { + return edge_table.copy_series(from, to); } - bool copy_node_series(const std::string &from, const std::string &to, - const std::string &desc) { - return node_table.copy_series(from, to, desc); + bool copy_node_series(const std::string &from, const std::string &to) { + std::cerr << "copy_node_series: from = " << from << ", to = " << to + << std::endl; + + return node_table.copy_series(from, to); } template From 33bab5501adff56152ed07f9b6cb3688b3c38b81 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Wed, 11 Dec 2024 11:17:50 -0800 Subject: [PATCH 05/14] more testgraph --- include/clippy/selector.hpp | 44 ++++++++++++++++++ test/TestGraph/copy_series.cpp | 25 +++------- test/TestGraph/extrema.cpp | 85 ++++++++++++++++++++++++++++++++++ test/TestGraph/mvmap.hpp | 58 ++++++++++++++++++++++- test/TestGraph/selector.hpp | 46 +++++++++--------- test/TestGraph/testgraph.hpp | 53 ++++++++++++--------- 6 files changed, 247 insertions(+), 64 deletions(-) create mode 100644 include/clippy/selector.hpp create mode 100644 test/TestGraph/extrema.cpp diff --git a/include/clippy/selector.hpp b/include/clippy/selector.hpp new file mode 100644 index 0000000..bd6d3de --- /dev/null +++ b/include/clippy/selector.hpp @@ -0,0 +1,44 @@ +#pragma once +#include +#include + +#include "boost/json.hpp" + +class selector { + std::deque components; + + public: + selector(std::string sel_str) { + std::deque comps; + std::string::size_type start = 0; + std::string::size_type end = sel_str.find('.'); + while (end != std::string::npos) { + comps.push_back(sel_str.substr(start, end - start)); + start = end + 1; + end = sel_str.find('.', start); + } + comps.push_back(sel_str.substr(start)); + components = comps; + } + + std::string to_str() const { + std::string result; + for (const auto &comp : components) { + result += comp + "."; + } + return result.substr(0, result.size() - 1); + } + + bool headeq(std::string comp) const { return components[0] == comp; } + std::string pop() { + auto head = components[0]; + components.pop_front(); + return head; + } + selector tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + const auto &obj = v.as_object(); + auto sel = boost::json::value_to(obj.at("selector")); + return {sel}; + } +}; \ No newline at end of file diff --git a/test/TestGraph/copy_series.cpp b/test/TestGraph/copy_series.cpp index b6549f7..d59635c 100644 --- a/test/TestGraph/copy_series.cpp +++ b/test/TestGraph/copy_series.cpp @@ -54,31 +54,19 @@ int main(int argc, char **argv) { // std::map selectors; auto the_graph = clip.get_state(graph_state_name); - bool edge_sel = false; + bool edge_sel = false; // if false, then node selector if (testgraph::testgraph::is_edge_selector(from_selector)) { edge_sel = true; } else if (!testgraph::testgraph::is_node_selector(from_selector)) { - std::cerr - << "!! ERROR: Parent must be either \"edge\" or \"node\" (received " - << from_selector << ") !!"; + std::cerr << "!! ERROR: from_selector must start with either \"edge\" or " + "\"node\" (received " + << from_selector << ") !!"; exit(-1); } - bool to_sel_has_prefix = - testgraph::testgraph::is_edge_selector(to_selector) || - testgraph::testgraph::is_node_selector(to_selector); - if (to_sel_has_prefix) { - std::cerr << "Warning: stripping prefix from to_selector" << std::endl; - to_selector = to_selector.substr(5); - } - if (edge_sel && the_graph.has_edge_series(to_selector)) { - std::cerr << "!! ERROR: Selector name " << to_selector - << " already exists in edge table !!" << std::endl; - exit(-1); - } - if (!edge_sel && the_graph.has_node_series(to_selector)) { + if (the_graph.has_series(to_selector)) { std::cerr << "!! ERROR: Selector name " << to_selector - << " already exists in node table !!" << std::endl; + << " already exists in graph !!" << std::endl; exit(-1); } @@ -90,6 +78,7 @@ int main(int argc, char **argv) { selectors.erase(to_selector); } from_selector = from_selector.substr(5); + to_selector = to_selector.substr(5); if (edge_sel) { if (!the_graph.copy_edge_series(from_selector, to_selector)) { std::cerr << "!! ERROR: copy failed from " << from_selector << " to " diff --git a/test/TestGraph/extrema.cpp b/test/TestGraph/extrema.cpp new file mode 100644 index 0000000..e47ca23 --- /dev/null +++ b/test/TestGraph/extrema.cpp @@ -0,0 +1,85 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include "clippy/clippy-eval.hpp" +#include "testgraph.hpp" + +static const std::string method_name = "extrema"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "returns the extrema of a series based on selector"}; + clip.add_required( + "selector", "Existing selector name to calculate extrema"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns>("min and max values of the series"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto sel_json = clip.get("selector"); + + std::string sel; + try { + if (sel_json["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + sel = sel_json["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + if (!sel.starts_with("node.")) { + std::cerr << "Selector must be a node subselector" << std::endl; + return 1; + } + auto the_graph = clip.get_state(state_name); + + auto selectors = + clip.get_state>(sel_state_name); + if (!selectors.contains(sel)) { + std::cerr << "Selector not found" << std::endl; + return 1; + } + auto subsel = sel.substr(5); + if (the_graph.has_node_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto deg_o = the_graph.add_node_series(subsel, "Degree"); + if (!deg_o) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + + auto deg = deg_o.value(); + + the_graph.for_all_edges([°](auto edge, mvmap::locator /*unused*/) { + deg[edge.first]++; + if (edge.first != edge.second) { + deg[edge.second]++; + } + }); + + clip.set_state(state_name, the_graph); + clip.set_state(sel_state_name, selectors); + clip.update_selectors(selectors); + + clip.return_self(); + return 0; +} diff --git a/test/TestGraph/mvmap.hpp b/test/TestGraph/mvmap.hpp index 5d1782c..e9faf5a 100644 --- a/test/TestGraph/mvmap.hpp +++ b/test/TestGraph/mvmap.hpp @@ -186,6 +186,59 @@ class mvmap { } return itk_r[l.loc]; } + + std::pair>, + std::optional>> + extrema() { + V min = std::numeric_limits::max(); + V max = std::numeric_limits::min(); + bool found_min = false; + bool found_max = false; + locator min_loc; + locator max_loc; + for_all([&min, &max, &found_min, &found_max, &min_loc, &max_loc]( + auto k, auto l, auto v) { + if (v < min) { + min = v; + min_loc = l; + found_min = true; + } + if (v > max) { + max = v; + max_loc = l; + found_max = true; + } + }); + std::optional> min_opt = + found_min ? std::make_pair(min, min_loc) : std::nullopt; + std::optional> max_opt = + found_max ? std::make_pair(max, max_loc) : std::nullopt; + return std::make_pair(min_opt, max_opt); + } + + std::map histogram(size_t n_bins = 0) { + std::map hist; + auto [min, max] = extrema(); + if (!min.has_value() || !max.has_value()) { + return hist; + } + V min_val = min.value().first; + V max_val = max.value().first; + if (min_val == max_val) { + hist[min_val] = series_r.size(); + return hist; + } + if (n_bins == 0) { + n_bins = series_r.size(); + } + V bin_width = (max_val - min_val) / n_bins; + for_all([&hist, min_val, bin_width](auto /*unused*/, auto /*unused*/, + auto v) { + hist[min_val + (v - min_val) / bin_width]++; + }); + return hist; + } + }; // end of series ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// @@ -256,8 +309,9 @@ class mvmap { return series_proxy(name, desc, std::get>(data[name]), *this); } - // copies an existing column (series) to a new column and returns true. If the - // new column already exists, or if the existing column doesn't, return false. + // copies an existing column (series) to a new (unmanifested) column and + // returns true. If the new column already exists, or if the existing column + // doesn't, return false. bool copy_series(const std::string &from, const std::string &to) { if (has_series(to) || !has_series(from)) { std::cerr << "copy_series failed from " << from << " to " << to diff --git a/test/TestGraph/selector.hpp b/test/TestGraph/selector.hpp index 2da089f..f1c747f 100644 --- a/test/TestGraph/selector.hpp +++ b/test/TestGraph/selector.hpp @@ -1,26 +1,26 @@ -#pragma once -#include -#include -#include +// #pragma once +// #include +// #include +// #include -namespace testgraph { -class selector { - std::string name; +// namespace testgraph { +// class selector { +// std::string name; -public: - selector(boost::json::object &jo) { - try { - if (jo["expression_type"].as_string() != std::string("jsonlogic")) { - std::cerr << " NOT A THINGY\n"; - exit(-1); - } - name = jo["rule"].as_object()["var"].as_string().c_str(); - } catch (...) { - std::cerr << "!! ERROR !!\n"; - exit(-1); - } - } - [[nodiscard]] std::string to_string() const { return name; } -}; +// public: +// selector(boost::json::object &jo) { +// try { +// if (jo["expression_type"].as_string() != std::string("jsonlogic")) { +// std::cerr << " NOT A THINGY\n"; +// exit(-1); +// } +// name = jo["rule"].as_object()["var"].as_string().c_str(); +// } catch (...) { +// std::cerr << "!! ERROR !!\n"; +// exit(-1); +// } +// } +// [[nodiscard]] std::string to_string() const { return name; } +// }; -} // namespace testgraph +// } // namespace testgraph diff --git a/test/TestGraph/testgraph.hpp b/test/TestGraph/testgraph.hpp index 0ace928..ccf1c88 100644 --- a/test/TestGraph/testgraph.hpp +++ b/test/TestGraph/testgraph.hpp @@ -7,6 +7,7 @@ #include #include "boost/json.hpp" +#include "clippy/selector.hpp" #include "mvmap.hpp" #include "selector.hpp" @@ -30,34 +31,34 @@ class testgraph { edge_mvmap edge_table; public: - static inline bool is_edge_selector(const std::string_view name) { - return name.starts_with("edge."); + static inline bool is_edge_selector(const selector &name) { + return name.headeq("edge"); } - static inline bool is_node_selector(const std::string_view name) { - return name.starts_with("node."); + static inline bool is_node_selector(const selector &name) { + return name.headeq("node"); } - static inline bool is_valid_selector(const std::string_view name) { + static inline bool is_valid_selector(const std::string &name) { return is_edge_selector(name) || is_node_selector(name); } - static inline std::optional selector_obj_to_string( - boost::json::object from_selector_obj) { - try { - if (from_selector_obj["expression_type"].as_string() != - std::string("jsonlogic")) { - std::cerr << " NOT A THINGY " << std::endl; - return std::nullopt; - } - std::string from_selector = - from_selector_obj["rule"].as_object()["var"].as_string().c_str(); - return from_selector; - } catch (...) { - std::cerr << "!! ERROR !!" << std::endl; - return std::nullopt; - } - } + // static inline std::optional selector_obj_to_string( + // boost::json::object from_selector_obj) { + // try { + // if (from_selector_obj["expression_type"].as_string() != + // std::string("jsonlogic")) { + // std::cerr << " NOT A THINGY " << std::endl; + // return std::nullopt; + // } + // std::string from_selector = + // from_selector_obj["rule"].as_object()["var"].as_string().c_str(); + // return from_selector; + // } catch (...) { + // std::cerr << "!! ERROR !!" << std::endl; + // return std::nullopt; + // } + // } friend void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, testgraph const &g) { @@ -198,6 +199,16 @@ class testgraph { return edge_table.contains({src, dst}); }; + [[nodiscard]] bool has_series(const std::string &&name) const { + if (is_node_selector(name)) { + return has_node_series(name.substr(5)); + } + if (is_edge_selector(name)) { + return has_edge_series(name.substr(5)); + } + return false; + } + [[nodiscard]] bool has_node_series(const std::string &name) const { return node_table.has_series(name); } From 0a3e1821a663aa68f72339e57035b94ffa2929bc Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Wed, 11 Dec 2024 15:33:36 -0800 Subject: [PATCH 06/14] selectors plus copy_series --- include/clippy/selector.hpp | 72 +++++++++++-------- test/TestGraph/copy_series.cpp | 42 +++++------ test/TestGraph/mvmap.hpp | 82 +++++++++------------ test/TestGraph/selector.hpp | 26 ------- test/TestGraph/testgraph.hpp | 127 +++++++++++---------------------- 5 files changed, 134 insertions(+), 215 deletions(-) delete mode 100644 test/TestGraph/selector.hpp diff --git a/include/clippy/selector.hpp b/include/clippy/selector.hpp index bd6d3de..0ee8e31 100644 --- a/include/clippy/selector.hpp +++ b/include/clippy/selector.hpp @@ -1,44 +1,58 @@ #pragma once #include +#include #include #include "boost/json.hpp" class selector { - std::deque components; + std::string sel_str; - public: - selector(std::string sel_str) { - std::deque comps; - std::string::size_type start = 0; - std::string::size_type end = sel_str.find('.'); - while (end != std::string::npos) { - comps.push_back(sel_str.substr(start, end - start)); - start = end + 1; - end = sel_str.find('.', start); + std::vector dots; + + static std::vector make_dots(const std::string &sel_str) { + std::vector dots; + for (size_t i = 0; i < sel_str.size(); ++i) { + if (sel_str[i] == '.') { + dots.push_back(i); + } } - comps.push_back(sel_str.substr(start)); - components = comps; + dots.push_back(sel_str.size()); + return dots; } - std::string to_str() const { - std::string result; - for (const auto &comp : components) { - result += comp + "."; - } - return result.substr(0, result.size() - 1); + public: + friend std::ostream &operator<<(std::ostream &os, const selector &sel); + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, const selector &sel); + selector(std::string sel) : sel_str(sel), dots(make_dots(sel_str)) {} + + bool operator<(const selector &other) const { + return sel_str < other.sel_str; } - bool headeq(std::string comp) const { return components[0] == comp; } - std::string pop() { - auto head = components[0]; - components.pop_front(); - return head; + bool headeq(const std::string &comp) const { + return std::string_view(sel_str).substr(0, dots[0]) == comp; } - selector tag_invoke(boost::json::value_to_tag /*unused*/, - const boost::json::value &v) { - const auto &obj = v.as_object(); - auto sel = boost::json::value_to(obj.at("selector")); - return {sel}; + std::optional tail() const { + if (dots.size() <= 1) { + return std::nullopt; + } + return selector(sel_str.substr(dots[1])); } -}; \ No newline at end of file +}; + +std::ostream &operator<<(std::ostream &os, const selector &sel) { + os << sel.sel_str; + return os; +} + +selector tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + return boost::json::value_to(v); +} + +void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, + const selector &sel) { + v = sel.sel_str; +} \ No newline at end of file diff --git a/test/TestGraph/copy_series.cpp b/test/TestGraph/copy_series.cpp index d59635c..2b3c6bc 100644 --- a/test/TestGraph/copy_series.cpp +++ b/test/TestGraph/copy_series.cpp @@ -9,6 +9,7 @@ #include #include "clippy/clippy.hpp" +#include "clippy/selector.hpp" #include "testgraph.hpp" namespace boostjsn = boost::json; @@ -19,10 +20,10 @@ static const std::string sel_state_name = "selectors"; int main(int argc, char **argv) { clippy::clippy clip{method_name, "Copies a subselector to a new subselector"}; - clip.add_required("from_sel", "Source Selector"); - clip.add_required("to_sel", "Target Selector"); + clip.add_required("from_sel", "Source Selector"); + clip.add_required("to_sel", "Target Selector"); - clip.add_required_state>( + clip.add_required_state>( sel_state_name, "Internal container for pending selectors"); clip.add_required_state(graph_state_name, "Internal state for the graph"); @@ -31,26 +32,9 @@ int main(int argc, char **argv) { return 0; } - auto from_selector_obj = clip.get("from_sel"); - auto to_selector_obj = clip.get("to_sel"); + auto from_selector = clip.get("from_sel"); + auto to_selector = clip.get("to_sel"); - auto from_selector_opt = - testgraph::testgraph::selector_obj_to_string(from_selector_obj); - - if (!from_selector_opt.has_value()) { - std::cerr << "!! ERROR: invalid from_selector !!" << std::endl; - exit(-1); - } - auto to_selector_opt = - testgraph::testgraph::selector_obj_to_string(to_selector_obj); - - if (!to_selector_opt.has_value()) { - std::cerr << "!! ERROR: invalid to_selector !!" << std::endl; - exit(-1); - } - - auto from_selector = from_selector_opt.value(); - auto to_selector = to_selector_opt.value(); // std::map selectors; auto the_graph = clip.get_state(graph_state_name); @@ -72,13 +56,21 @@ int main(int argc, char **argv) { if (clip.has_state(sel_state_name)) { auto selectors = - clip.get_state>(sel_state_name); + clip.get_state>(sel_state_name); if (selectors.contains(to_selector)) { std::cerr << "Warning: Using unmanifested selector." << std::endl; selectors.erase(to_selector); } - from_selector = from_selector.substr(5); - to_selector = to_selector.substr(5); + auto from_selector_tail = from_selector.tail(); + auto to_selector_tail = to_selector.tail(); + if (!from_selector_tail.has_value() || !to_selector_tail.has_value()) { + std::cerr + << "!! ERROR: from_selector and to_selector must have content !!" + << std::endl; + exit(1); + } + from_selector = from_selector_tail.value(); + to_selector = to_selector_tail.value(); if (edge_sel) { if (!the_graph.copy_edge_series(from_selector, to_selector)) { std::cerr << "!! ERROR: copy failed from " << from_selector << " to " diff --git a/test/TestGraph/mvmap.hpp b/test/TestGraph/mvmap.hpp index e9faf5a..5731f9a 100644 --- a/test/TestGraph/mvmap.hpp +++ b/test/TestGraph/mvmap.hpp @@ -12,6 +12,8 @@ #include #include +#include "clippy/selector.hpp" + namespace mvmap { using index = uint64_t; class locator { @@ -51,14 +53,14 @@ class mvmap { idx_to_key itk; key_to_idx kti; - std::map...>> data; - std::map series_desc; + std::map...>> data; + std::map series_desc; public: // A series_proxy is a reference to a series in an mvmap. template class series_proxy { - std::string id; + selector id; std::string_view desc; key_to_idx &kti_r; idx_to_key &itk_r; @@ -83,10 +85,10 @@ class mvmap { } public: - series_proxy(std::string id, series &ser, mvmap &m) + series_proxy(selector id, series &ser, mvmap &m) : id(std::move(id)), kti_r(m.kti), itk_r(m.itk), series_r(ser) {} - series_proxy(std::string id, const std::string &desc, series &ser, + series_proxy(selector id, const std::string &desc, series &ser, mvmap &m) : id(std::move(id)), desc(desc), @@ -243,7 +245,7 @@ class mvmap { ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// mvmap(const idx_to_key &itk, const key_to_idx &kti, - const std::map...>> &data) + const std::map...>> &data) : itk(itk), kti(kti), data(data) {} mvmap() = default; @@ -262,11 +264,11 @@ class mvmap { // template using series = std::map; using key_to_idx = std::map; using idx_to_key = std::map; - return {boost::json::value_to(obj.at("itk")), - boost::json::value_to(obj.at("kti")), - boost::json::value_to< - std::map...>>>( - obj.at("data"))}; + return { + boost::json::value_to(obj.at("itk")), + boost::json::value_to(obj.at("kti")), + boost::json::value_to...>>>( + obj.at("data"))}; } [[nodiscard]] size_t size() const { return kti.size(); } @@ -280,16 +282,16 @@ class mvmap { return true; } - [[nodiscard]] std::vector list_series() const { + [[nodiscard]] std::vector list_series() const { return std::views::keys(data); } - [[nodiscard]] bool has_series(const std::string &id) const { + [[nodiscard]] bool has_series(const selector &id) const { return data.contains(id); } template - [[nodiscard]] bool has_series(const std::string &id) const { + [[nodiscard]] bool has_series(const selector &id) const { return data.contains(id) && std::holds_alternative>(data[id]); } @@ -299,20 +301,20 @@ class mvmap { // adds a new column (series) to the mvmap and returns true. If already // exists, return false template - std::optional> add_series(const std::string &name, + std::optional> add_series(const selector &sel, const std::string &desc = "") { - if (has_series(name)) { + if (has_series(sel)) { return std::nullopt; } - data[name] = series{}; - series_desc[name] = desc; - return series_proxy(name, desc, std::get>(data[name]), *this); + data[sel] = series{}; + series_desc[sel] = desc; + return series_proxy(sel, desc, std::get>(data[sel]), *this); } // copies an existing column (series) to a new (unmanifested) column and // returns true. If the new column already exists, or if the existing column // doesn't, return false. - bool copy_series(const std::string &from, const std::string &to) { + bool copy_series(const selector &from, const selector &to) { if (has_series(to) || !has_series(from)) { std::cerr << "copy_series failed from " << from << " to " << to << std::endl; @@ -324,50 +326,34 @@ class mvmap { } template - std::optional> get_series(const std::string &name) { - if (!has_series(name)) { + std::optional> get_series(const selector &sel) { + if (!has_series(sel)) { // series doesn't exist or is of the wrong type. return std::nullopt; } - return series_proxy(name, std::get>(data[name]), *this); + return series_proxy(sel, std::get>(data[sel]), *this); } std::optional> get_variant_series( - const std::string &name) { - if (!has_series(name)) { + const selector &sel) { + if (!has_series(sel)) { return std::nullopt; } - return series_proxy(name, data[name], *this); + return series_proxy(sel, data[sel], *this); } - void drop_series(const std::string &name) { - if (!has_series(name)) { + void drop_series(const selector &sel) { + if (!has_series(sel)) { return; } - data.erase(name); - series_desc.erase(name); + data.erase(sel); + series_desc.erase(sel); } - mvmap::variants get_as_variant(const std::string &name, const locator &loc) { - return data[name][loc.loc]; + mvmap::variants get_as_variant(const selector &sel, const locator &loc) { + return data[sel][loc.loc]; } - // returns a series_proxy for the given string. If the series doesn't exist, - // create it. - // template - // series_proxy get_or_create_series(const std::string &id, - // const std::string &desc = "") { - // if (data.count(id) == 0) { - // add_series(id, desc); - // } else { - // if (!std::holds_alternative>(data[id])) { - // throw std::runtime_error( - // "series id already exists with different type"); - // } - // } - // return series_proxy(id, desc, std::get>(data[id]), *this); - // } - // F is a function that takes a key and a locator. // Users will need to close over series_proxies that they want to use. template diff --git a/test/TestGraph/selector.hpp b/test/TestGraph/selector.hpp deleted file mode 100644 index f1c747f..0000000 --- a/test/TestGraph/selector.hpp +++ /dev/null @@ -1,26 +0,0 @@ -// #pragma once -// #include -// #include -// #include - -// namespace testgraph { -// class selector { -// std::string name; - -// public: -// selector(boost::json::object &jo) { -// try { -// if (jo["expression_type"].as_string() != std::string("jsonlogic")) { -// std::cerr << " NOT A THINGY\n"; -// exit(-1); -// } -// name = jo["rule"].as_object()["var"].as_string().c_str(); -// } catch (...) { -// std::cerr << "!! ERROR !!\n"; -// exit(-1); -// } -// } -// [[nodiscard]] std::string to_string() const { return name; } -// }; - -// } // namespace testgraph diff --git a/test/TestGraph/testgraph.hpp b/test/TestGraph/testgraph.hpp index ccf1c88..5f122f2 100644 --- a/test/TestGraph/testgraph.hpp +++ b/test/TestGraph/testgraph.hpp @@ -9,7 +9,6 @@ #include "boost/json.hpp" #include "clippy/selector.hpp" #include "mvmap.hpp" -#include "selector.hpp" namespace testgraph { // map of (src, dst) : weight @@ -31,35 +30,18 @@ class testgraph { edge_mvmap edge_table; public: - static inline bool is_edge_selector(const selector &name) { - return name.headeq("edge"); + static inline bool is_edge_selector(const selector &sel) { + return sel.headeq("edge"); } - static inline bool is_node_selector(const selector &name) { - return name.headeq("node"); + static inline bool is_node_selector(const selector &sel) { + return sel.headeq("node"); } - static inline bool is_valid_selector(const std::string &name) { - return is_edge_selector(name) || is_node_selector(name); + static inline bool is_valid_selector(const selector &sel) { + return is_edge_selector(sel) || is_node_selector(sel); } - // static inline std::optional selector_obj_to_string( - // boost::json::object from_selector_obj) { - // try { - // if (from_selector_obj["expression_type"].as_string() != - // std::string("jsonlogic")) { - // std::cerr << " NOT A THINGY " << std::endl; - // return std::nullopt; - // } - // std::string from_selector = - // from_selector_obj["rule"].as_object()["var"].as_string().c_str(); - // return from_selector; - // } catch (...) { - // std::cerr << "!! ERROR !!" << std::endl; - // return std::nullopt; - // } - // } - friend void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, testgraph const &g) { v = {{"node_table", boost::json::value_from(g.node_table)}, @@ -77,60 +59,48 @@ class testgraph { testgraph(node_mvmap nt, edge_mvmap et) : node_table(std::move(nt)), edge_table(std::move(et)) {}; - // template - // std::optional> get_edge_series(const std::string - // &name) { - // return edge_table.get_series(name); - // } - // edge_series_proxy get_or_create_edge_col(const std::string &name, - // const std::string &desc = "") { - // return edge_table.get_or_create_series(name, desc); - // } - // this function requires that the "edge." prefix be removed from the name. template std::optional> add_edge_series( - const std::string &name, const std::string &desc = "") { - return edge_table.add_series(name, desc); + const selector &sel, const std::string &desc = "") { + return edge_table.add_series(sel, desc); } template std::optional> add_edge_series( - const std::string &name, const edge_series_proxy &from, + const selector &sel, const edge_series_proxy &from, const std::string &desc = "") { - return edge_table.add_series(name, from, desc); + return edge_table.add_series(sel, from, desc); } - void drop_edge_series(const std::string &name) { - edge_table.drop_series(name); - } + void drop_edge_series(const selector &sel) { edge_table.drop_series(sel); } + + // this function requires that the "node." prefix be removed from the name. + void drop_node_series(const selector &sel) { node_table.drop_series(sel); } - void drop_node_series(const std::string &name) { - node_table.drop_series(name); - } // this function requires that the "node." prefix be removed from the name. template std::optional> add_node_series( - const std::string &name, const std::string &desc = "") { - return node_table.add_series(name, desc); + const selector &sel, const std::string &desc = "") { + return node_table.add_series(sel, desc); } template std::optional> add_node_series( - const std::string &name, const node_series_proxy &from, + const selector &sel, const node_series_proxy &from, const std::string &desc = "") { - return node_table.add_series(name, from, desc); + return node_table.add_series(sel, from, desc); } template - std::optional> get_edge_series(const std::string &name) { - return edge_table.get_series(name); + std::optional> get_edge_series(const selector &sel) { + return edge_table.get_series(sel); } - bool copy_edge_series(const std::string &from, const std::string &to) { + bool copy_edge_series(const selector &from, const selector &to) { return edge_table.copy_series(from, to); } - bool copy_node_series(const std::string &from, const std::string &to) { + bool copy_node_series(const selector &from, const selector &to) { std::cerr << "copy_node_series: from = " << from << ", to = " << to << std::endl; @@ -138,32 +108,9 @@ class testgraph { } template - std::optional> get_node_series(const std::string &name) { - return node_table.get_series(name); - } - - // template - // void get_or_create_edge_col(const std::string &name, - // std::map data) { - // auto proxy = get_or_create_edge_col(name); - // for (const auto &[k, v] : data) { - // proxy[k] = v; - // } - // } - // template - // node_series_proxy get_or_create_node_col(const std::string &name, - // const std::string &desc = "") { - // return node_table.get_or_create_series(name, desc); - // } - - // template - // void get_or_create_node_col(const std::string &name, - // std::map data) { - // auto proxy = get_or_create_node_col(name); - // for (const auto &[k, v] : data) { - // proxy[k] = v; - // } - // } + std::optional> get_node_series(const selector &sel) { + return node_table.get_series(sel); + } [[nodiscard]] size_t nv() const { return node_table.size(); } [[nodiscard]] size_t ne() const { return edge_table.size(); } @@ -199,22 +146,28 @@ class testgraph { return edge_table.contains({src, dst}); }; - [[nodiscard]] bool has_series(const std::string &&name) const { - if (is_node_selector(name)) { - return has_node_series(name.substr(5)); + // strips the head off the selector and passes the tail to the appropriate + // method. + [[nodiscard]] bool has_series(const selector &sel) const { + auto tail = sel.tail(); + + if (is_node_selector(sel) && tail.has_value()) { + return has_node_series(tail.value()); } - if (is_edge_selector(name)) { - return has_edge_series(name.substr(5)); + if (is_edge_selector(sel) && tail.has_value()) { + return has_edge_series(tail.value()); } return false; } - [[nodiscard]] bool has_node_series(const std::string &name) const { - return node_table.has_series(name); + // assumes sel has already been tail'ed. + [[nodiscard]] bool has_node_series(const selector &sel) const { + return node_table.has_series(sel); } - [[nodiscard]] bool has_edge_series(const std::string &name) const { - return edge_table.has_series(name); + // assumes sel has already been tail'ed. + [[nodiscard]] bool has_edge_series(const selector &sel) const { + return edge_table.has_series(sel); } [[nodiscard]] std::vector out_neighbors(const node_t &node) const { From 193cb98c82afeca62905f4cd94f0cbd120c1d003 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Fri, 13 Dec 2024 08:26:40 -0800 Subject: [PATCH 07/14] extrema --- include/clippy/clippy.hpp | 101 ++++++++++++++++-------------- include/clippy/selector.hpp | 24 +++++-- test/TestGraph/CMakeLists.txt | 1 + test/TestGraph/copy_series.cpp | 1 - test/TestGraph/extrema.cpp | 107 ++++++++++++++++++++------------ test/TestGraph/mvmap.hpp | 50 +++++++++------ test/TestGraph/testselector.cpp | 20 ++++++ 7 files changed, 191 insertions(+), 113 deletions(-) create mode 100644 test/TestGraph/testselector.cpp diff --git a/include/clippy/clippy.hpp b/include/clippy/clippy.hpp index 22ba8a0..a7acf39 100644 --- a/include/clippy/clippy.hpp +++ b/include/clippy/clippy.hpp @@ -5,6 +5,7 @@ #pragma once +#include #include #include #include @@ -13,7 +14,6 @@ #include #include #include -#include #include "clippy-object.hpp" @@ -36,23 +36,23 @@ static constexpr bool LOG_JSON = false; namespace clippy { namespace { -template struct is_container { +template +struct is_container { enum { value = false, }; }; -template struct is_container> { +template +struct is_container> { enum { value = true, }; }; boost::json::value asContainer(boost::json::value val, bool requiresContainer) { - if (!requiresContainer) - return val; - if (val.is_array()) - return val; + if (!requiresContainer) return val; + if (val.is_array()) return val; boost::json::array res; @@ -76,10 +76,10 @@ struct BcastInput { } }; #endif -} // namespace +} // namespace class clippy { -public: + public: clippy(const std::string &name, const std::string &desc) { get_value(m_json_config, "method_name") = name; get_value(m_json_config, "desc") = desc; @@ -98,7 +98,8 @@ class clippy { ~clippy() { const bool requiresResponse = - !(m_json_return.is_null() && m_json_state.empty() && m_json_overwrite_args.empty() && m_json_selectors.is_null()); + !(m_json_return.is_null() && m_json_state.empty() && + m_json_overwrite_args.empty() && m_json_selectors.is_null()); if (requiresResponse) { int rank = 0; @@ -121,14 +122,14 @@ class clippy { } } - template void log(std::ofstream &logfile, const M &msg) { - if (LOG_JSON) - logfile << msg << std::flush; + template + void log(std::ofstream &logfile, const M &msg) { + if (LOG_JSON) logfile << msg << std::flush; } - template void log(const M &msg) { - if (!LOG_JSON) - return; + template + void log(const M &msg) { + if (!LOG_JSON) return; std::ofstream logfile{clippyLogFile, std::ofstream::app}; log(logfile, msg); @@ -159,11 +160,13 @@ class clippy { boost::json::value_from(default_val); } - void update_selectors(const std::map& map_selectors) { + void update_selectors( + const std::map &map_selectors) { m_json_selectors = boost::json::value_from(map_selectors); } - template void returns(const std::string &desc) { + template + void returns(const std::string &desc) { get_value(m_json_config, returns_key, "desc") = desc; } @@ -172,11 +175,10 @@ class clippy { m_returns_self = true; } - void return_self() { - m_returns_self = true; - } + void return_self() { m_returns_self = true; } - template void to_return(const T &value) { + template + void to_return(const T &value) { // if (detail::get_type_name() != // m_json_config[returns_key]["type"].get()) { // throw std::runtime_error("clippy::to_return(value): Invalid type."); @@ -184,7 +186,6 @@ class clippy { m_json_return = boost::json::value_from(value); } - void to_return(::clippy::object value) { m_json_return = std::move(value).json(); } @@ -194,6 +195,7 @@ class clippy { } bool parse(int argc, char **argv) { + std::cerr << "in parse" << std::endl; const char *JSON_FLAG = "--clippy-help"; const char *DRYRUN_FLAG = "--clippy-validate"; if (argc == 2 && std::string(argv[1]) == JSON_FLAG) { @@ -202,8 +204,9 @@ class clippy { logfile << "<-hlp- " << m_json_config << std::endl; } - + std::cerr << "pre-cout" << std::endl; std::cout << m_json_config; + std::cerr << "post-cout" << std::endl; return true; } std::string buf; @@ -239,7 +242,7 @@ class clippy { logfile << "<-hlp- " << m_json_config << std::endl; } - if(world.rank0()) { + if (world.rank0()) { std::cout << m_json_config; } return true; @@ -270,14 +273,17 @@ class clippy { } #endif /* WITH_YGM */ - template T get(const std::string &name) { + template + T get(const std::string &name) { static constexpr bool requires_container = is_container::value; - if (has_argument(name)) { // if the argument exists + if (has_argument(name)) { // if the argument exists + auto foo = get_value(m_json_input, name); + std::cerr << "foo = " << foo << std::endl; return boost::json::value_to( asContainer(get_value(m_json_input, name), requires_container)); - } else { // it's an optional - // std::cout << "optional argument found: " + name << std::endl; + } else { // it's an optional + // std::cerr << "optional argument found: " + name << std::endl; return boost::json::value_to( asContainer(get_value(m_json_config, "args", name, "default_val"), requires_container)); @@ -288,11 +294,13 @@ class clippy { return has_value(m_json_input, state_key, name); } - template T get_state(const std::string &name) const { + template + T get_state(const std::string &name) const { return boost::json::value_to(get_value(m_json_input, state_key, name)); } - template void set_state(const std::string &name, T val) { + template + void set_state(const std::string &name, T val) { // if no state exists (= empty), then copy it from m_json_input if it exists // there; // otherwise just start with an empty state. @@ -319,23 +327,22 @@ class clippy { return false; } -private: + private: void write_response(std::ostream &os) const { // construct the response object boost::json::object json_response; // incl. the response if it has been set - if(m_returns_self) { + if (m_returns_self) { json_response["returns_self"] = true; } else if (!m_json_return.is_null()) json_response[returns_key] = m_json_return; // only communicate the state if it has been explicitly set. // no state -> no state update - if (!m_json_state.empty()) - json_response[state_key] = m_json_state; + if (!m_json_state.empty()) json_response[state_key] = m_json_state; - if(!m_json_selectors.is_null()) + if (!m_json_selectors.is_null()) json_response[selectors_key] = m_json_selectors; // only communicate the pass by reference arguments if explicitly set @@ -353,7 +360,8 @@ class clippy { // TODO: Warn/Check for unknown args } - template void add_optional_validator(const std::string &name) { + template + void add_optional_validator(const std::string &name) { if (m_input_validators.count(name) > 0) { std::stringstream ss; ss << "CLIPPy ERROR: Cannot have duplicate argument names: " << name @@ -363,7 +371,7 @@ class clippy { m_input_validators[name] = [name](const boost::json::value &j) { if (!j.get_object().contains(name)) { return; - } // Optional, only eval if present + } // Optional, only eval if present try { static constexpr bool requires_container = is_container::value; @@ -378,7 +386,8 @@ class clippy { }; } - template void add_required_validator(const std::string &name) { + template + void add_required_validator(const std::string &name) { if (m_input_validators.count(name) > 0) { throw std::runtime_error("Clippy:: Cannot have duplicate argument names"); } @@ -480,7 +489,7 @@ class clippy { boost::json::value m_json_selectors; boost::json::object m_json_state; boost::json::object m_json_overwrite_args; - bool m_returns_self=false; + bool m_returns_self = false; boost::json::object *m_json_input_state = nullptr; size_t m_next_position = 0; @@ -488,7 +497,7 @@ class clippy { std::map> m_input_validators; -public: + public: static constexpr const char *const state_key = "_state"; static constexpr const char *const selectors_key = "_selectors"; static constexpr const char *const returns_key = "returns"; @@ -496,7 +505,7 @@ class clippy { static constexpr const char *const class_desc_key = "class_desc"; }; -} // namespace clippy +} // namespace clippy namespace boost::json { void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, @@ -512,9 +521,9 @@ void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, } } -std::vector> -tag_invoke(boost::json::value_to_tag>>, - const boost::json::value &jv) { +std::vector> tag_invoke( + boost::json::value_to_tag>>, + const boost::json::value &jv) { std::vector> value; auto &outer_array = jv.get_array(); @@ -555,4 +564,4 @@ std::vector> tag_invoke( return value; } -} // namespace boost::json +} // namespace boost::json diff --git a/include/clippy/selector.hpp b/include/clippy/selector.hpp index 0ee8e31..21643be 100644 --- a/include/clippy/selector.hpp +++ b/include/clippy/selector.hpp @@ -25,8 +25,15 @@ class selector { friend std::ostream &operator<<(std::ostream &os, const selector &sel); friend void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, const selector &sel); - selector(std::string sel) : sel_str(sel), dots(make_dots(sel_str)) {} - + explicit selector(const std::string &sel) + : sel_str(sel), dots(make_dots(sel_str)) {} + explicit selector(const char *sel) : selector(std::string(sel)) {} + selector(boost::json::object o) { + std::cerr << "object constructor: o = " << o << std::endl; + auto v = o.at("rule").as_object()["var"]; + std::cerr << "object constructor: v = " << v << std::endl; + sel_str = v.as_string().c_str(); + } bool operator<(const selector &other) const { return sel_str < other.sel_str; } @@ -38,7 +45,7 @@ class selector { if (dots.size() <= 1) { return std::nullopt; } - return selector(sel_str.substr(dots[1])); + return selector(sel_str.substr(dots[1] + 1)); } }; @@ -49,10 +56,17 @@ std::ostream &operator<<(std::ostream &os, const selector &sel) { selector tag_invoke(boost::json::value_to_tag /*unused*/, const boost::json::value &v) { - return boost::json::value_to(v); + std::cerr << "tag_invoke 1 v = " << v << std::endl; + // std::cerr << "tag_invoke 2 v = " << v.as_string() << std::endl; + return selector(v.as_object()); } void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, const selector &sel) { - v = sel.sel_str; + std::cerr << "This should not be called." << std::endl; + // std::map o {}; + // o["expression_type"] = "jsonlogic"; + // o["rule"] = {{"var", sel.sel_str}}; + // v = {"expression_type": "jsonlogic", "rule": {"var": + // "node.degree"}}}sel.sel_str; } \ No newline at end of file diff --git a/test/TestGraph/CMakeLists.txt b/test/TestGraph/CMakeLists.txt index 6957a5b..744316b 100644 --- a/test/TestGraph/CMakeLists.txt +++ b/test/TestGraph/CMakeLists.txt @@ -9,6 +9,7 @@ add_test(TestGraph add_series) add_test(TestGraph connected_components) add_test(TestGraph drop_series) add_test(TestGraph copy_series) +add_test(TestGraph extrema) add_custom_command( TARGET TestGraph_nv POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy diff --git a/test/TestGraph/copy_series.cpp b/test/TestGraph/copy_series.cpp index 2b3c6bc..8c0f766 100644 --- a/test/TestGraph/copy_series.cpp +++ b/test/TestGraph/copy_series.cpp @@ -35,7 +35,6 @@ int main(int argc, char **argv) { auto from_selector = clip.get("from_sel"); auto to_selector = clip.get("to_sel"); - // std::map selectors; auto the_graph = clip.get_state(graph_state_name); bool edge_sel = false; // if false, then node selector diff --git a/test/TestGraph/extrema.cpp b/test/TestGraph/extrema.cpp index e47ca23..233420b 100644 --- a/test/TestGraph/extrema.cpp +++ b/test/TestGraph/extrema.cpp @@ -9,6 +9,7 @@ #include #include "clippy/clippy-eval.hpp" +#include "clippy/selector.hpp" #include "testgraph.hpp" static const std::string method_name = "extrema"; @@ -18,68 +19,92 @@ static const std::string sel_state_name = "selectors"; int main(int argc, char **argv) { clippy::clippy clip{method_name, "returns the extrema of a series based on selector"}; - clip.add_required( - "selector", "Existing selector name to calculate extrema"); + clip.add_required("selector", + "Existing selector name to calculate extrema"); clip.add_required_state(state_name, "Internal container"); - clip.returns>("min and max values of the series"); + std::cerr << "past add_required" << std::endl; // no object-state requirements in constructor + std::cerr << "argc = " << argc << ", argv[0] = " << argv[0] << std::endl; if (clip.parse(argc, argv)) { return 0; } - auto sel_json = clip.get("selector"); + std::cerr << "past clip.parse" << std::endl; + auto sel_str = clip.get("selector"); + std::cerr << "past clip.get; sel_str = " << sel_str << std::endl; + selector sel{sel_str}; + std::cerr << "past clip.get" << std::endl; + bool is_edge_sel = testgraph::testgraph::is_edge_selector(sel); + bool is_node_sel = testgraph::testgraph::is_node_selector(sel); - std::string sel; - try { - if (sel_json["expression_type"].as_string() != std::string("jsonlogic")) { - std::cerr << " NOT A THINGY " << std::endl; - exit(-1); - } - sel = sel_json["rule"].as_object()["var"].as_string().c_str(); - } catch (...) { - std::cerr << "!! ERROR !!" << std::endl; - exit(-1); + std::cerr << "past is_sel" << std::endl; + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; } - if (!sel.starts_with("node.")) { - std::cerr << "Selector must be a node subselector" << std::endl; + auto tail_opt = sel.tail(); + if (!tail_opt) { + std::cerr << "Selector must have a tail" << std::endl; return 1; } + auto tail_sel = tail_opt.value(); + auto the_graph = clip.get_state(state_name); - auto selectors = - clip.get_state>(sel_state_name); - if (!selectors.contains(sel)) { - std::cerr << "Selector not found" << std::endl; - return 1; - } - auto subsel = sel.substr(5); - if (the_graph.has_node_series(subsel)) { - std::cerr << "Selector already populated" << std::endl; - return 1; - } + std::cerr << "past the_graph" << std::endl; + if (is_edge_sel) { + clip.returns, + std::pair>>( + "min and max keys and values of the series"); + auto series = the_graph.get_edge_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); - auto deg_o = the_graph.add_node_series(subsel, "Degree"); - if (!deg_o) { - std::cerr << "Unable to manifest node series" << std::endl; - return 1; + testgraph::edge_t min_key = + min_tup ? std::get<1>(min_tup.value()) : std::make_pair("", ""); + testgraph::edge_t max_key = + max_tup ? std::get<1>(max_tup.value()) : std::make_pair("", ""); + + auto extrema = std::make_pair(min_key, max_key); + clip.to_return(extrema); } - auto deg = deg_o.value(); + if (is_node_sel) { + // clip.returns, + // std::pair>>( + // "min and max keys and values of the series"); + + // clip.returns>( + // "min and max keys of the series"); + + clip.returns("min of the series"); - the_graph.for_all_edges([°](auto edge, mvmap::locator /*unused*/) { - deg[edge.first]++; - if (edge.first != edge.second) { - deg[edge.second]++; + auto series = the_graph.get_node_series(tail_sel); + if (!series) { + std::cerr << "Node series not found" << std::endl; + return 1; } - }); + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); - clip.set_state(state_name, the_graph); - clip.set_state(sel_state_name, selectors); - clip.update_selectors(selectors); + testgraph::node_t min_key = min_tup ? std::get<1>(min_tup.value()) : ""; + testgraph::node_t max_key = max_tup ? std::get<1>(max_tup.value()) : ""; + + auto extrema = std::make_pair(min_key, max_key); + std::cerr << "extrema: " << extrema.first << ", " << extrema.second + << std::endl; + auto tempex = std::make_pair(min_key, max_key); + clip.to_return(min_key); + } - clip.return_self(); + clip.set_state(state_name, the_graph); return 0; } diff --git a/test/TestGraph/mvmap.hpp b/test/TestGraph/mvmap.hpp index 5731f9a..1a4f3fb 100644 --- a/test/TestGraph/mvmap.hpp +++ b/test/TestGraph/mvmap.hpp @@ -69,13 +69,13 @@ class mvmap { using series_type = V; // returns true if there is an index assigned to a given key - bool has_idx(K k) { return kti_r.count(k) > 0; } + bool has_idx_at_key(K k) const { return kti_r.contains(k); } // returns true if there is a key assigned to a given locator - bool has_key(locator l) { return itk_r.count(l) > 0; } + bool has_key_at_index(locator l) const { return itk_r.contains(l.loc); } // returns or creates the index for a key. index get_idx(K k) { - if (!has_idx(k)) { + if (!has_idx_at_key(k)) { index i{kti_r.size()}; kti_r[k] = i; itk_r[i] = k; @@ -104,27 +104,27 @@ class mvmap { const V &operator[](locator l) const { return series_r[l.loc]; } std::optional> at(locator l) { - if (!has_key(l)) { + if (!has_key_at_index(l)) { return {}; } return series_r[l.loc]; }; std::optional> at(locator l) const { - if (!has_key(l)) { + if (!has_key_at_index(l)) { return {}; } return series_r[l.loc]; }; std::optional> at(K k) { - if (!has_idx(k)) { + if (!has_idx_at_key(k)) { return {}; } return series_r[get_idx(k)]; }; std::optional> at(K k) const { - if (!has_idx(k)) { + if (!has_idx_at_key(k)) { return {}; } return series_r[get_idx(k)]; @@ -172,7 +172,7 @@ class mvmap { // if the key doesn't exist, do nothing. void erase(const K &k) { - if (!has_idx(k)) { + if (!has_idx_at_key(k)) { return; } auto i = kti_r[k]; @@ -183,14 +183,14 @@ class mvmap { // locator is invalid. std::optional> get_key( const locator &l) const { - if (!has_idx(l.loc)) { - return {}; + if (!has_key_at_index(l.loc)) { + return std::nullopt; } return itk_r[l.loc]; } - std::pair>, - std::optional>> + std::pair>, + std::optional>> extrema() { V min = std::numeric_limits::max(); V max = std::numeric_limits::min(); @@ -198,23 +198,34 @@ class mvmap { bool found_max = false; locator min_loc; locator max_loc; - for_all([&min, &max, &found_min, &found_max, &min_loc, &max_loc]( - auto k, auto l, auto v) { + K min_key; + K max_key; + for_all([&min, &max, &found_min, &found_max, &min_loc, &max_loc, &min_key, + &max_key](auto k, auto l, auto v) { if (v < min) { min = v; min_loc = l; + min_key = k; found_min = true; } if (v > max) { max = v; max_loc = l; + max_key = k; found_max = true; } }); - std::optional> min_opt = - found_min ? std::make_pair(min, min_loc) : std::nullopt; - std::optional> max_opt = - found_max ? std::make_pair(max, max_loc) : std::nullopt; + std::optional> min_opt, max_opt; + if (found_min) { + min_opt = std::make_tuple(min, min_key, min_loc); + } else { + min_opt = std::nullopt; + } + if (found_max) { + max_opt = std::make_tuple(max, max_key, max_loc); + } else { + max_opt = std::nullopt; + } return std::make_pair(min_opt, max_opt); } @@ -292,9 +303,8 @@ class mvmap { template [[nodiscard]] bool has_series(const selector &id) const { - return data.contains(id) && std::holds_alternative>(data[id]); + return data.contains(id) && std::holds_alternative>(data.at(id)); } - bool contains(const K &k) { return kti.contains(k); } auto keys() const { return std::views::keys(kti); } diff --git a/test/TestGraph/testselector.cpp b/test/TestGraph/testselector.cpp new file mode 100644 index 0000000..9b87720 --- /dev/null +++ b/test/TestGraph/testselector.cpp @@ -0,0 +1,20 @@ +#include "../../include/clippy/selector.hpp" +#include + +int main() { + selector s = selector("foo.bar.baz"); + + selector zzz = "foo.zoo.boo"; + + selector s2{"x.y.z"}; + std::cout << "s = " << s << "\n"; + assert(s.headeq("foo")); + assert(!s.headeq("bar")); + + auto val = boost::json::value_from(s); + auto str = boost::json::serialize(val); + + auto t = boost::json::value_to(val); + assert(t.headeq("foo")); + std::cout << str << "\n"; +} From 8106b0323ede10b5b607ceee2ce34eb55284efc6 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Fri, 13 Dec 2024 16:09:48 -0800 Subject: [PATCH 08/14] extrema and selectors working --- .gitignore | 1 + include/clippy/clippy.hpp | 4 -- include/clippy/selector.hpp | 14 +++---- test/TestGraph/connected_components.cpp | 25 +++++++------ test/TestGraph/degree.cpp | 8 ++-- test/TestGraph/extrema.cpp | 32 ++++++++-------- test/TestGraph/mvmap.hpp | 43 +++++++++++----------- test/TestGraph/testgraph.cpp | 4 +- test/TestGraph/testgraph.hpp | 49 ++++++++++++------------- 9 files changed, 88 insertions(+), 92 deletions(-) diff --git a/.gitignore b/.gitignore index d5f38ab..f7d66d5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ CMakeDoxygenDefaults.cmake DartConfiguration.tcl __pycache__ /.vscode +/.devcontainer diff --git a/include/clippy/clippy.hpp b/include/clippy/clippy.hpp index a7acf39..60322d3 100644 --- a/include/clippy/clippy.hpp +++ b/include/clippy/clippy.hpp @@ -195,7 +195,6 @@ class clippy { } bool parse(int argc, char **argv) { - std::cerr << "in parse" << std::endl; const char *JSON_FLAG = "--clippy-help"; const char *DRYRUN_FLAG = "--clippy-validate"; if (argc == 2 && std::string(argv[1]) == JSON_FLAG) { @@ -204,9 +203,7 @@ class clippy { logfile << "<-hlp- " << m_json_config << std::endl; } - std::cerr << "pre-cout" << std::endl; std::cout << m_json_config; - std::cerr << "post-cout" << std::endl; return true; } std::string buf; @@ -279,7 +276,6 @@ class clippy { if (has_argument(name)) { // if the argument exists auto foo = get_value(m_json_input, name); - std::cerr << "foo = " << foo << std::endl; return boost::json::value_to( asContainer(get_value(m_json_input, name), requires_container)); } else { // it's an optional diff --git a/include/clippy/selector.hpp b/include/clippy/selector.hpp index 21643be..af198ab 100644 --- a/include/clippy/selector.hpp +++ b/include/clippy/selector.hpp @@ -29,23 +29,23 @@ class selector { : sel_str(sel), dots(make_dots(sel_str)) {} explicit selector(const char *sel) : selector(std::string(sel)) {} selector(boost::json::object o) { - std::cerr << "object constructor: o = " << o << std::endl; auto v = o.at("rule").as_object()["var"]; - std::cerr << "object constructor: v = " << v << std::endl; sel_str = v.as_string().c_str(); + dots = make_dots(sel_str); } bool operator<(const selector &other) const { return sel_str < other.sel_str; } - + operator std::string() { return sel_str; } + operator std::string() const { return sel_str; } bool headeq(const std::string &comp) const { return std::string_view(sel_str).substr(0, dots[0]) == comp; } std::optional tail() const { - if (dots.size() <= 1) { + if (dots.size() <= 1) { // remember that end of string is a dot return std::nullopt; } - return selector(sel_str.substr(dots[1] + 1)); + return selector(sel_str.substr(dots[0] + 1)); } }; @@ -56,9 +56,7 @@ std::ostream &operator<<(std::ostream &os, const selector &sel) { selector tag_invoke(boost::json::value_to_tag /*unused*/, const boost::json::value &v) { - std::cerr << "tag_invoke 1 v = " << v << std::endl; - // std::cerr << "tag_invoke 2 v = " << v.as_string() << std::endl; - return selector(v.as_object()); + return v.as_object(); } void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, diff --git a/test/TestGraph/connected_components.cpp b/test/TestGraph/connected_components.cpp index 26c8e02..22421cc 100644 --- a/test/TestGraph/connected_components.cpp +++ b/test/TestGraph/connected_components.cpp @@ -5,13 +5,14 @@ // // SPDX-License-Identifier: MIT -#include "clippy/clippy-eval.hpp" -#include "testgraph.hpp" #include #include #include #include +#include "clippy/clippy-eval.hpp" +#include "testgraph.hpp" + static const std::string method_name = "connected_components"; static const std::string state_name = "INTERNAL"; static const std::string sel_state_name = "selectors"; @@ -65,20 +66,20 @@ int main(int argc, char **argv) { return 1; } - auto cc_o = the_graph.add_node_series(subsel, selectors.at(sel)); + auto cc_o = the_graph.add_node_series(subsel, selectors.at(sel)); if (!cc_o) { std::cerr << "Unable to manifest node series" << std::endl; return 1; } auto cc = cc_o.value(); - std::map ccmap; + std::map ccmap; - long int i = 0; + int64_t i = 0; for (auto &node : the_graph.nodes()) { ccmap[node] = i++; } - std::vector> adj(the_graph.nv()); + std::vector> adj(the_graph.nv()); the_graph.for_all_edges([&adj, &ccmap](auto edge, mvmap::locator /*unused*/) { long i = ccmap[edge.first]; long j = ccmap[edge.second]; @@ -87,18 +88,18 @@ int main(int argc, char **argv) { }); std::vector visited(the_graph.nv(), false); - std::vector components(the_graph.nv()); + std::vector components(the_graph.nv()); std::iota(components.begin(), components.end(), 0); - for (long int i = 0; i < the_graph.nv(); ++i) { + for (int64_t i = 0; i < the_graph.nv(); ++i) { if (!visited[i]) { - std::queue q; + std::queue q; q.push(i); while (!q.empty()) { - long int v = q.front(); + int64_t v = q.front(); q.pop(); visited[v] = true; - for (long int u : adj[v]) { + for (int64_t u : adj[v]) { if (!visited[u]) { q.push(u); components[u] = components[i]; @@ -110,7 +111,7 @@ int main(int argc, char **argv) { the_graph.for_all_nodes( [&components, &ccmap, &cc](auto node, mvmap::locator /*unused*/) { - long int i = ccmap[node]; + int64_t i = ccmap[node]; cc[node] = components[i]; }); diff --git a/test/TestGraph/degree.cpp b/test/TestGraph/degree.cpp index 2a844f4..3b8957e 100644 --- a/test/TestGraph/degree.cpp +++ b/test/TestGraph/degree.cpp @@ -4,11 +4,13 @@ // // SPDX-License-Identifier: MIT -#include "clippy/clippy-eval.hpp" -#include "testgraph.hpp" #include #include #include +#include + +#include "clippy/clippy-eval.hpp" +#include "testgraph.hpp" static const std::string method_name = "degree"; static const std::string state_name = "INTERNAL"; @@ -63,7 +65,7 @@ int main(int argc, char **argv) { return 1; } - auto deg_o = the_graph.add_node_series(subsel, "Degree"); + auto deg_o = the_graph.add_node_series(subsel, "Degree"); if (!deg_o) { std::cerr << "Unable to manifest node series" << std::endl; return 1; diff --git a/test/TestGraph/extrema.cpp b/test/TestGraph/extrema.cpp index 233420b..86d31c6 100644 --- a/test/TestGraph/extrema.cpp +++ b/test/TestGraph/extrema.cpp @@ -24,22 +24,16 @@ int main(int argc, char **argv) { clip.add_required_state(state_name, "Internal container"); - std::cerr << "past add_required" << std::endl; // no object-state requirements in constructor - std::cerr << "argc = " << argc << ", argv[0] = " << argv[0] << std::endl; if (clip.parse(argc, argv)) { return 0; } - - std::cerr << "past clip.parse" << std::endl; - auto sel_str = clip.get("selector"); - std::cerr << "past clip.get; sel_str = " << sel_str << std::endl; + auto sel_str = clip.get("selector"); selector sel{sel_str}; - std::cerr << "past clip.get" << std::endl; + bool is_edge_sel = testgraph::testgraph::is_edge_selector(sel); bool is_node_sel = testgraph::testgraph::is_node_selector(sel); - std::cerr << "past is_sel" << std::endl; if (!is_edge_sel && !is_node_sel) { std::cerr << "Selector must start with either \"edge\" or \"node\"" << std::endl; @@ -55,7 +49,6 @@ int main(int argc, char **argv) { auto the_graph = clip.get_state(state_name); - std::cerr << "past the_graph" << std::endl; if (is_edge_sel) { clip.returns, std::pair>>( @@ -73,7 +66,10 @@ int main(int argc, char **argv) { testgraph::edge_t max_key = max_tup ? std::get<1>(max_tup.value()) : std::make_pair("", ""); - auto extrema = std::make_pair(min_key, max_key); + double min_val = min_tup ? std::get<0>(min_tup.value()) : 0.0; + double max_val = max_tup ? std::get<0>(max_tup.value()) : 0.0; + auto extrema = std::make_pair(std::make_pair(min_key, min_val), + std::make_pair(max_key, max_val)); clip.to_return(extrema); } @@ -85,7 +81,9 @@ int main(int argc, char **argv) { // clip.returns>( // "min and max keys of the series"); - clip.returns("min of the series"); + clip.returns, + std::pair>>( + "min of the series"); auto series = the_graph.get_node_series(tail_sel); if (!series) { @@ -98,11 +96,13 @@ int main(int argc, char **argv) { testgraph::node_t min_key = min_tup ? std::get<1>(min_tup.value()) : ""; testgraph::node_t max_key = max_tup ? std::get<1>(max_tup.value()) : ""; - auto extrema = std::make_pair(min_key, max_key); - std::cerr << "extrema: " << extrema.first << ", " << extrema.second - << std::endl; - auto tempex = std::make_pair(min_key, max_key); - clip.to_return(min_key); + double min_val = min_tup ? std::get<0>(min_tup.value()) : 0.0; + double max_val = max_tup ? std::get<0>(max_tup.value()) : 0.0; + + auto extrema = std::make_pair(std::make_pair(min_key, min_val), + std::make_pair(max_key, max_val)); + clip.to_return, + std::pair>>(extrema); } clip.set_state(state_name, the_graph); diff --git a/test/TestGraph/mvmap.hpp b/test/TestGraph/mvmap.hpp index 1a4f3fb..2cbbb8b 100644 --- a/test/TestGraph/mvmap.hpp +++ b/test/TestGraph/mvmap.hpp @@ -11,8 +11,7 @@ #include #include #include - -#include "clippy/selector.hpp" +#include namespace mvmap { using index = uint64_t; @@ -53,14 +52,14 @@ class mvmap { idx_to_key itk; key_to_idx kti; - std::map...>> data; - std::map series_desc; + std::map...>> data; + std::map series_desc; public: // A series_proxy is a reference to a series in an mvmap. template class series_proxy { - selector id; + std::string id; std::string_view desc; key_to_idx &kti_r; idx_to_key &itk_r; @@ -85,10 +84,10 @@ class mvmap { } public: - series_proxy(selector id, series &ser, mvmap &m) + series_proxy(std::string id, series &ser, mvmap &m) : id(std::move(id)), kti_r(m.kti), itk_r(m.itk), series_r(ser) {} - series_proxy(selector id, const std::string &desc, series &ser, + series_proxy(std::string id, const std::string &desc, series &ser, mvmap &m) : id(std::move(id)), desc(desc), @@ -256,7 +255,7 @@ class mvmap { ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// mvmap(const idx_to_key &itk, const key_to_idx &kti, - const std::map...>> &data) + const std::map...>> &data) : itk(itk), kti(kti), data(data) {} mvmap() = default; @@ -275,11 +274,11 @@ class mvmap { // template using series = std::map; using key_to_idx = std::map; using idx_to_key = std::map; - return { - boost::json::value_to(obj.at("itk")), - boost::json::value_to(obj.at("kti")), - boost::json::value_to...>>>( - obj.at("data"))}; + return {boost::json::value_to(obj.at("itk")), + boost::json::value_to(obj.at("kti")), + boost::json::value_to< + std::map...>>>( + obj.at("data"))}; } [[nodiscard]] size_t size() const { return kti.size(); } @@ -293,16 +292,16 @@ class mvmap { return true; } - [[nodiscard]] std::vector list_series() const { + [[nodiscard]] std::vector list_series() const { return std::views::keys(data); } - [[nodiscard]] bool has_series(const selector &id) const { + [[nodiscard]] bool has_series(const std::string &id) const { return data.contains(id); } template - [[nodiscard]] bool has_series(const selector &id) const { + [[nodiscard]] bool has_series(const std::string &id) const { return data.contains(id) && std::holds_alternative>(data.at(id)); } bool contains(const K &k) { return kti.contains(k); } @@ -311,7 +310,7 @@ class mvmap { // adds a new column (series) to the mvmap and returns true. If already // exists, return false template - std::optional> add_series(const selector &sel, + std::optional> add_series(const std::string &sel, const std::string &desc = "") { if (has_series(sel)) { return std::nullopt; @@ -324,7 +323,7 @@ class mvmap { // copies an existing column (series) to a new (unmanifested) column and // returns true. If the new column already exists, or if the existing column // doesn't, return false. - bool copy_series(const selector &from, const selector &to) { + bool copy_series(const std::string &from, const std::string &to) { if (has_series(to) || !has_series(from)) { std::cerr << "copy_series failed from " << from << " to " << to << std::endl; @@ -336,7 +335,7 @@ class mvmap { } template - std::optional> get_series(const selector &sel) { + std::optional> get_series(const std::string &sel) { if (!has_series(sel)) { // series doesn't exist or is of the wrong type. return std::nullopt; @@ -345,14 +344,14 @@ class mvmap { } std::optional> get_variant_series( - const selector &sel) { + const std::string &sel) { if (!has_series(sel)) { return std::nullopt; } return series_proxy(sel, data[sel], *this); } - void drop_series(const selector &sel) { + void drop_series(const std::string &sel) { if (!has_series(sel)) { return; } @@ -360,7 +359,7 @@ class mvmap { series_desc.erase(sel); } - mvmap::variants get_as_variant(const selector &sel, const locator &loc) { + mvmap::variants get_as_variant(const std::string &sel, const locator &loc) { return data[sel][loc.loc]; } diff --git a/test/TestGraph/testgraph.cpp b/test/TestGraph/testgraph.cpp index e267c4d..3d2ebd8 100644 --- a/test/TestGraph/testgraph.cpp +++ b/test/TestGraph/testgraph.cpp @@ -1,7 +1,7 @@ -#include - #include "testgraph.hpp" +#include + int main() { auto g = testgraph::testgraph{}; diff --git a/test/TestGraph/testgraph.hpp b/test/TestGraph/testgraph.hpp index 5f122f2..762884a 100644 --- a/test/TestGraph/testgraph.hpp +++ b/test/TestGraph/testgraph.hpp @@ -7,7 +7,6 @@ #include #include "boost/json.hpp" -#include "clippy/selector.hpp" #include "mvmap.hpp" namespace testgraph { @@ -30,15 +29,15 @@ class testgraph { edge_mvmap edge_table; public: - static inline bool is_edge_selector(const selector &sel) { - return sel.headeq("edge"); + static inline bool is_edge_selector(const std::string &sel) { + return sel.starts_with("edge."); } - static inline bool is_node_selector(const selector &sel) { - return sel.headeq("node"); + static inline bool is_node_selector(const std::string &sel) { + return sel.starts_with("node."); } - static inline bool is_valid_selector(const selector &sel) { + static inline bool is_valid_selector(const std::string &sel) { return is_edge_selector(sel) || is_node_selector(sel); } @@ -62,45 +61,45 @@ class testgraph { // this function requires that the "edge." prefix be removed from the name. template std::optional> add_edge_series( - const selector &sel, const std::string &desc = "") { + const std::string &sel, const std::string &desc = "") { return edge_table.add_series(sel, desc); } template std::optional> add_edge_series( - const selector &sel, const edge_series_proxy &from, + const std::string &sel, const edge_series_proxy &from, const std::string &desc = "") { return edge_table.add_series(sel, from, desc); } - void drop_edge_series(const selector &sel) { edge_table.drop_series(sel); } + void drop_edge_series(const std::string &sel) { edge_table.drop_series(sel); } // this function requires that the "node." prefix be removed from the name. - void drop_node_series(const selector &sel) { node_table.drop_series(sel); } + void drop_node_series(const std::string &sel) { node_table.drop_series(sel); } // this function requires that the "node." prefix be removed from the name. template std::optional> add_node_series( - const selector &sel, const std::string &desc = "") { + const std::string &sel, const std::string &desc = "") { return node_table.add_series(sel, desc); } template std::optional> add_node_series( - const selector &sel, const node_series_proxy &from, + const std::string &sel, const node_series_proxy &from, const std::string &desc = "") { return node_table.add_series(sel, from, desc); } template - std::optional> get_edge_series(const selector &sel) { + std::optional> get_edge_series(const std::string &sel) { return edge_table.get_series(sel); } - bool copy_edge_series(const selector &from, const selector &to) { + bool copy_edge_series(const std::string &from, const std::string &to) { return edge_table.copy_series(from, to); } - bool copy_node_series(const selector &from, const selector &to) { + bool copy_node_series(const std::string &from, const std::string &to) { std::cerr << "copy_node_series: from = " << from << ", to = " << to << std::endl; @@ -108,7 +107,7 @@ class testgraph { } template - std::optional> get_node_series(const selector &sel) { + std::optional> get_node_series(const std::string &sel) { return node_table.get_series(sel); } @@ -146,27 +145,27 @@ class testgraph { return edge_table.contains({src, dst}); }; - // strips the head off the selector and passes the tail to the appropriate + // strips the head off the std::string and passes the tail to the appropriate // method. - [[nodiscard]] bool has_series(const selector &sel) const { - auto tail = sel.tail(); + [[nodiscard]] bool has_series(const std::string &sel) const { + auto tail = sel.substr(5); - if (is_node_selector(sel) && tail.has_value()) { - return has_node_series(tail.value()); + if (is_node_selector(sel)) { + return has_node_series(tail); } - if (is_edge_selector(sel) && tail.has_value()) { - return has_edge_series(tail.value()); + if (is_edge_selector(sel)) { + return has_edge_series(tail); } return false; } // assumes sel has already been tail'ed. - [[nodiscard]] bool has_node_series(const selector &sel) const { + [[nodiscard]] bool has_node_series(const std::string &sel) const { return node_table.has_series(sel); } // assumes sel has already been tail'ed. - [[nodiscard]] bool has_edge_series(const selector &sel) const { + [[nodiscard]] bool has_edge_series(const std::string &sel) const { return edge_table.has_series(sel); } From f611aec2dd0c6cce0a3dce96aeb0c8fabd173244 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Sat, 14 Dec 2024 16:30:20 -0800 Subject: [PATCH 09/14] fixed typing issues with extrema; added count. --- test/TestGraph/CMakeLists.txt | 1 + test/TestGraph/count.cpp | 124 +++++++++++++++++++++++++++++ test/TestGraph/extrema.cpp | 142 ++++++++++++++++++++++------------ test/TestGraph/mvmap.hpp | 56 ++++++++------ test/TestGraph/testgraph.hpp | 29 +++++-- 5 files changed, 277 insertions(+), 75 deletions(-) create mode 100644 test/TestGraph/count.cpp diff --git a/test/TestGraph/CMakeLists.txt b/test/TestGraph/CMakeLists.txt index 744316b..defbc49 100644 --- a/test/TestGraph/CMakeLists.txt +++ b/test/TestGraph/CMakeLists.txt @@ -10,6 +10,7 @@ add_test(TestGraph connected_components) add_test(TestGraph drop_series) add_test(TestGraph copy_series) add_test(TestGraph extrema) +add_test(TestGraph count) add_custom_command( TARGET TestGraph_nv POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy diff --git a/test/TestGraph/count.cpp b/test/TestGraph/count.cpp new file mode 100644 index 0000000..2391e4c --- /dev/null +++ b/test/TestGraph/count.cpp @@ -0,0 +1,124 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +#include "clippy/clippy-eval.hpp" +#include "clippy/selector.hpp" +#include "testgraph.hpp" + +static const std::string method_name = "count"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "returns a map containing the count of values in a " + "series based on selector"}; + clip.add_required("selector", + "Existing selector name to calculate extrema"); + clip.add_required_state(state_name, + "Internal container"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + auto the_graph = clip.get_state(state_name); + + bool is_edge_sel = testgraph::testgraph::is_edge_selector(sel); + bool is_node_sel = testgraph::testgraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + auto tailsel_opt = sel.tail(); + if (!tailsel_opt) { + std::cerr << "no tail" << std::endl; + return 1; + } + + auto tail_sel = tailsel_opt.value(); + if (is_edge_sel) { + if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else { + std::cerr << "UNKNOWN TYPE" << std::endl; + return 1; + } + } else if (is_node_sel) { + if (the_graph.has_series(sel)) { + sel = tailsel_opt.value(); + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else { + std::cerr << "UNKNOWN TYPE" << std::endl; + return 1; + } + } + clip.set_state(state_name, the_graph); + return 0; +} diff --git a/test/TestGraph/extrema.cpp b/test/TestGraph/extrema.cpp index 86d31c6..bab0eac 100644 --- a/test/TestGraph/extrema.cpp +++ b/test/TestGraph/extrema.cpp @@ -50,59 +50,105 @@ int main(int argc, char **argv) { auto the_graph = clip.get_state(state_name); if (is_edge_sel) { - clip.returns, - std::pair>>( + clip.returns>>( "min and max keys and values of the series"); - auto series = the_graph.get_edge_series(tail_sel); - if (!series) { - std::cerr << "Edge series not found" << std::endl; + if (the_graph.has_edge_series(tail_sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + clip.to_return(extrema); + } else if (the_graph.has_edge_series(tail_sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + clip.to_return(extrema); + } else { + std::cerr << "Edge series is an invalid type" << std::endl; return 1; } - auto series_val = series.value(); - auto [min_tup, max_tup] = series_val.extrema(); - - testgraph::edge_t min_key = - min_tup ? std::get<1>(min_tup.value()) : std::make_pair("", ""); - testgraph::edge_t max_key = - max_tup ? std::get<1>(max_tup.value()) : std::make_pair("", ""); - - double min_val = min_tup ? std::get<0>(min_tup.value()) : 0.0; - double max_val = max_tup ? std::get<0>(max_tup.value()) : 0.0; - auto extrema = std::make_pair(std::make_pair(min_key, min_val), - std::make_pair(max_key, max_val)); - clip.to_return(extrema); - } - - if (is_node_sel) { - // clip.returns, - // std::pair>>( - // "min and max keys and values of the series"); - - // clip.returns>( - // "min and max keys of the series"); - - clip.returns, - std::pair>>( - "min of the series"); - - auto series = the_graph.get_node_series(tail_sel); - if (!series) { - std::cerr << "Node series not found" << std::endl; + } else if (is_node_sel) { + if (the_graph.has_edge_series(tail_sel)) { + clip.returns>>( + "min and max keys and values of the series"); + + auto series = the_graph.get_node_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + + clip.to_return(extrema); + } else if (the_graph.has_node_series(tail_sel)) { + clip.returns< + std::map>>( + "min and max keys and values of the series"); + + auto series = the_graph.get_node_series(tail_sel); + if (!series) { + std::cerr << "Node series not found" << std::endl; + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + + clip.to_return(extrema); + } else { + std::cerr << "Node series is an invalid type" << std::endl; return 1; } - auto series_val = series.value(); - auto [min_tup, max_tup] = series_val.extrema(); - - testgraph::node_t min_key = min_tup ? std::get<1>(min_tup.value()) : ""; - testgraph::node_t max_key = max_tup ? std::get<1>(max_tup.value()) : ""; - - double min_val = min_tup ? std::get<0>(min_tup.value()) : 0.0; - double max_val = max_tup ? std::get<0>(max_tup.value()) : 0.0; - - auto extrema = std::make_pair(std::make_pair(min_key, min_val), - std::make_pair(max_key, max_val)); - clip.to_return, - std::pair>>(extrema); } clip.set_state(state_name, the_graph); diff --git a/test/TestGraph/mvmap.hpp b/test/TestGraph/mvmap.hpp index 2cbbb8b..4a76e66 100644 --- a/test/TestGraph/mvmap.hpp +++ b/test/TestGraph/mvmap.hpp @@ -95,6 +95,11 @@ class mvmap { itk_r(m.itk), series_r(ser) {} + bool is_string_v() const { return std::is_same_v; } + bool is_double_v() const { return std::is_same_v; } + bool is_int64_t_v() const { return std::is_same_v; } + bool is_bool_v() const { return std::is_same_v; } + V &operator[](K k) { return series_r[get_idx(k)]; } const V &operator[](K k) const { return series_r[get_idx(k)]; } @@ -228,27 +233,10 @@ class mvmap { return std::make_pair(min_opt, max_opt); } - std::map histogram(size_t n_bins = 0) { - std::map hist; - auto [min, max] = extrema(); - if (!min.has_value() || !max.has_value()) { - return hist; - } - V min_val = min.value().first; - V max_val = max.value().first; - if (min_val == max_val) { - hist[min_val] = series_r.size(); - return hist; - } - if (n_bins == 0) { - n_bins = series_r.size(); - } - V bin_width = (max_val - min_val) / n_bins; - for_all([&hist, min_val, bin_width](auto /*unused*/, auto /*unused*/, - auto v) { - hist[min_val + (v - min_val) / bin_width]++; - }); - return hist; + std::map count() { + std::map ct; + for_all([&ct](auto /*unused*/, auto /*unused*/, auto v) { ct[v]++; }); + return ct; } }; // end of series @@ -302,6 +290,14 @@ class mvmap { template [[nodiscard]] bool has_series(const std::string &id) const { + // std::cerr << "has_series: " << id << std::endl; + // std::cerr << "V = " << typeid(V).name() << std::endl; + // std::cerr << "data.contains(id): " << data.contains(id) << std::endl; + // if (data.contains(id)) { + // std::cerr << "std::holds_alternative>(data.at(id)): " + // << std::holds_alternative>(data.at(id)) << + // std::endl; + // } return data.contains(id) && std::holds_alternative>(data.at(id)); } bool contains(const K &k) { return kti.contains(k); } @@ -329,7 +325,7 @@ class mvmap { << std::endl; return false; } - std::cerr << "copying series from " << from << " to " << to << std::endl; + // std::cerr << "copying series from " << from << " to " << to << std::endl; data[to] = data[from]; return true; } @@ -343,6 +339,22 @@ class mvmap { return series_proxy(sel, std::get>(data[sel]), *this); } + bool series_is_string(const std::string &sel) const { + return has_series(sel); + } + + bool series_is_double(const std::string &sel) const { + return has_series(sel); + } + + bool series_is_int64_t(const std::string &sel) const { + return has_series(sel); + } + + bool series_is_bool(const std::string &sel) const { + return has_series(sel); + } + std::optional> get_variant_series( const std::string &sel) { if (!has_series(sel)) { diff --git a/test/TestGraph/testgraph.hpp b/test/TestGraph/testgraph.hpp index 762884a..fbdfc6b 100644 --- a/test/TestGraph/testgraph.hpp +++ b/test/TestGraph/testgraph.hpp @@ -19,8 +19,8 @@ using sparsevec = std::map; // using variants = std::variant; class testgraph { - using edge_mvmap = mvmap::mvmap; - using node_mvmap = mvmap::mvmap; + using edge_mvmap = mvmap::mvmap; + using node_mvmap = mvmap::mvmap; template using edge_series_proxy = edge_mvmap::series_proxy; template @@ -100,9 +100,6 @@ class testgraph { } bool copy_node_series(const std::string &from, const std::string &to) { - std::cerr << "copy_node_series: from = " << from << ", to = " << to - << std::endl; - return node_table.copy_series(from, to); } @@ -159,15 +156,37 @@ class testgraph { return false; } + template + [[nodiscard]] bool has_series(const std::string &sel) const { + auto tail = sel.substr(5); + + if (is_node_selector(sel)) { + return has_node_series(tail); + } + if (is_edge_selector(sel)) { + return has_edge_series(tail); + } + return false; + } + // assumes sel has already been tail'ed. [[nodiscard]] bool has_node_series(const std::string &sel) const { return node_table.has_series(sel); } + template + [[nodiscard]] bool has_node_series(const std::string &sel) const { + return node_table.has_series(sel); + } + // assumes sel has already been tail'ed. [[nodiscard]] bool has_edge_series(const std::string &sel) const { return edge_table.has_series(sel); } + template + [[nodiscard]] bool has_edge_series(const std::string &sel) const { + return edge_table.has_series(sel); + } [[nodiscard]] std::vector out_neighbors(const node_t &node) const { std::vector neighbors; From eab2ab8147eb962ef87f0e9499ab90521a78e376 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Fri, 20 Dec 2024 12:10:24 -0800 Subject: [PATCH 10/14] cleanup for utils to use selector. --- test/TestGraph/add_series.cpp | 31 ++++++++++++------------------- test/TestGraph/degree.cpp | 21 +++++---------------- test/TestGraph/drop_series.cpp | 28 ++++++++++------------------ 3 files changed, 27 insertions(+), 53 deletions(-) diff --git a/test/TestGraph/add_series.cpp b/test/TestGraph/add_series.cpp index bc5c462..fb18c73 100644 --- a/test/TestGraph/add_series.cpp +++ b/test/TestGraph/add_series.cpp @@ -3,12 +3,14 @@ // // SPDX-License-Identifier: MIT -#include "clippy/clippy.hpp" -#include "testgraph.hpp" #include #include #include +#include "clippy/clippy.hpp" +#include "clippy/selector.hpp" +#include "testgraph.hpp" + namespace boostjsn = boost::json; static const std::string method_name = "add_series"; @@ -17,7 +19,7 @@ static const std::string sel_state_name = "selectors"; int main(int argc, char **argv) { clippy::clippy clip{method_name, "Adds a subselector"}; - clip.add_required("parent_sel", "Parent Selector"); + clip.add_required("parent_sel", "Parent Selector"); clip.add_required("sub_sel", "Name of new selector"); clip.add_optional("desc", "Description of new selector", ""); @@ -30,32 +32,23 @@ int main(int argc, char **argv) { return 0; } - auto jo = clip.get("parent_sel"); + std::string parstr = clip.get("parent_sel"); + auto parsel = selector{parstr}; auto subsel = clip.get("sub_sel"); auto desc = clip.get("desc"); - std::string parentname; - try { - if (jo["expression_type"].as_string() != std::string("jsonlogic")) { - std::cerr << " NOT A THINGY " << std::endl; - exit(-1); - } - parentname = jo["rule"].as_object()["var"].as_string().c_str(); - } catch (...) { - std::cerr << "!! ERROR !!" << std::endl; - exit(-1); - } + std::string fullname = parstr + "." + subsel; // std::map selectors; auto the_graph = clip.get_state(graph_state_name); - auto fullname = parentname + "." + subsel; - if (parentname == "edge") { + + if (parsel.headeq("edge")) { if (the_graph.has_edge_series(subsel)) { std::cerr << "!! ERROR: Selector name already exists in edge table !!" << std::endl; exit(-1); } - } else if (parentname == "node") { + } else if (parsel.headeq("node")) { if (the_graph.has_node_series(subsel)) { std::cerr << "!! ERROR: Selector name already exists in node table !!" << std::endl; @@ -64,7 +57,7 @@ int main(int argc, char **argv) { } else { std::cerr << "((!! ERROR: Parent must be either \"edge\" or \"node\" (received " - << parentname << ") !!)"; + << parstr << ") !!)"; exit(-1); } diff --git a/test/TestGraph/degree.cpp b/test/TestGraph/degree.cpp index 3b8957e..657556d 100644 --- a/test/TestGraph/degree.cpp +++ b/test/TestGraph/degree.cpp @@ -10,6 +10,7 @@ #include #include "clippy/clippy-eval.hpp" +#include "clippy/selector.hpp" #include "testgraph.hpp" static const std::string method_name = "degree"; @@ -20,7 +21,7 @@ int main(int argc, char **argv) { clippy::clippy clip{ method_name, "Populates a column containing the degree of each node in a graph"}; - clip.add_required( + clip.add_required( "selector", "Existing selector name into which the degree will be written"); clip.add_required_state(state_name, @@ -33,21 +34,9 @@ int main(int argc, char **argv) { return 0; } - auto sel_json = clip.get("selector"); + selector sel = clip.get("selector"); - std::string sel; - try { - if (sel_json["expression_type"].as_string() != std::string("jsonlogic")) { - std::cerr << " NOT A THINGY " << std::endl; - exit(-1); - } - sel = sel_json["rule"].as_object()["var"].as_string().c_str(); - } catch (...) { - std::cerr << "!! ERROR !!" << std::endl; - exit(-1); - } - - if (!sel.starts_with("node.")) { + if (!sel.headeq("node")) { std::cerr << "Selector must be a node subselector" << std::endl; return 1; } @@ -59,7 +48,7 @@ int main(int argc, char **argv) { std::cerr << "Selector not found" << std::endl; return 1; } - auto subsel = sel.substr(5); + auto subsel = sel.tail().value(); if (the_graph.has_node_series(subsel)) { std::cerr << "Selector already populated" << std::endl; return 1; diff --git a/test/TestGraph/drop_series.cpp b/test/TestGraph/drop_series.cpp index b0af31b..3a6298d 100644 --- a/test/TestGraph/drop_series.cpp +++ b/test/TestGraph/drop_series.cpp @@ -4,12 +4,14 @@ // // SPDX-License-Identifier: MIT -#include "clippy/clippy.hpp" -#include "testgraph.hpp" #include #include #include +#include "clippy/clippy.hpp" +#include "clippy/selector.hpp" +#include "testgraph.hpp" + namespace boostjsn = boost::json; static const std::string method_name = "drop_series"; @@ -18,8 +20,8 @@ static const std::string sel_state_name = "selectors"; static const std::string sel_name = "selector"; int main(int argc, char **argv) { - clippy::clippy clip{method_name, "Drops a subselector"}; - clip.add_required(sel_name, "Selector to drop"); + clippy::clippy clip{method_name, "Drops a selector"}; + clip.add_required(sel_name, "Selector to drop"); clip.add_required_state>( sel_state_name, "Internal container for pending selectors"); @@ -32,17 +34,7 @@ int main(int argc, char **argv) { auto jo = clip.get(sel_name); - std::string sel; - try { - if (jo["expression_type"].as_string() != std::string("jsonlogic")) { - std::cerr << " NOT A THINGY " << std::endl; - exit(-1); - } - sel = jo["rule"].as_object()["var"].as_string().c_str(); - } catch (...) { - std::cerr << "!! ERROR !!" << std::endl; - exit(-1); - } + selector sel = clip.get(sel_name); auto sel_state = clip.get_state>(sel_state_name); @@ -51,12 +43,12 @@ int main(int argc, char **argv) { exit(-1); } auto the_graph = clip.get_state(graph_state_name); - auto subsel = sel.substr(5); - if (sel.starts_with("edge.")) { + auto subsel = sel.tail().value(); + if (sel.headeq("edge")) { if (the_graph.has_edge_series(subsel)) { the_graph.drop_edge_series(sel); } - } else if (sel.starts_with("node.")) { + } else if (sel.headeq("node")) { if (the_graph.has_node_series(subsel)) { the_graph.drop_node_series(subsel); } From f520f00557f9fec0ff19a4f88a410de4d2dbe7ce Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Mon, 23 Dec 2024 12:52:46 -0800 Subject: [PATCH 11/14] testdf take one --- test/CMakeLists.txt | 15 +-- test/TestDF/CMakeLists.txt | 0 test/TestDF/__init__.cpp | 32 ++++++ test/TestDF/soonCMakeLists | 10 ++ test/TestDF/testdf.hpp | 138 ++++++++++++++++++++++++++ test/TestGraph/testgraph.hpp | 10 +- test/{TestGraph => include}/mvmap.hpp | 13 ++- 7 files changed, 203 insertions(+), 15 deletions(-) create mode 100644 test/TestDF/CMakeLists.txt create mode 100644 test/TestDF/__init__.cpp create mode 100644 test/TestDF/soonCMakeLists create mode 100644 test/TestDF/testdf.hpp rename test/{TestGraph => include}/mvmap.hpp (96%) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 52a280c..b1ed928 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -11,16 +11,17 @@ function ( add_test class_name method_name ) set(target "${class_name}_${method_name}") add_executable(${target} ${source}) # target_include_directories(${target} PRIVATE ${Boost_INCLUDE_DIRS}) - include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} include/) set_target_properties(${target} PROPERTIES OUTPUT_NAME "${method_name}" ) - target_include_directories(${target} PRIVATE ${PROJECT_SOURCE_DIR}/include ${BOOST_INCLUDE_DIRS}) + target_include_directories(${target} PRIVATE ${PROJECT_SOURCE_DIR}/include ${BOOST_INCLUDE_DIRS} include/) target_link_libraries(${target} PRIVATE Boost::json) endfunction() -add_subdirectory(TestBag) -add_subdirectory(TestSet) -add_subdirectory(TestFunctions) -add_subdirectory(TestSelector) -add_subdirectory(TestGraph) +# add_subdirectory(TestBag) +# add_subdirectory(TestSet) +# add_subdirectory(TestFunctions) +# add_subdirectory(TestSelector) +# add_subdirectory(TestGraph) +add_subdirectory(TestDF) diff --git a/test/TestDF/CMakeLists.txt b/test/TestDF/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/TestDF/__init__.cpp b/test/TestDF/__init__.cpp new file mode 100644 index 0000000..8a4339d --- /dev/null +++ b/test/TestDF/__init__.cpp @@ -0,0 +1,32 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +#include "clippy/clippy.hpp" +#include "testdf.hpp" + +namespace boostjsn = boost::json; + +static const std::string method_name = "__init__"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes a TestDF"}; + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + // testgraph needs to be convertible to json + testdf the_df{}; + clip.set_state(state_name, the_df); + std::map selectors; + clip.set_state(sel_state_name, selectors); + + return 0; +} diff --git a/test/TestDF/soonCMakeLists b/test/TestDF/soonCMakeLists new file mode 100644 index 0000000..aa9605d --- /dev/null +++ b/test/TestDF/soonCMakeLists @@ -0,0 +1,10 @@ +add_test(TestDF __init__) +add_test(TestDF __str__) +add_test(TestDF add_row) +add_test(TestDF add_col) +add_test(TestDF drop_col) +add_test(TestDF remove_if) +add_test(TestDF copy_col) +add_test(TestDF create_idx) +add_test(TestDF drop_idx) +add_test(TestDF describe) diff --git a/test/TestDF/testdf.hpp b/test/TestDF/testdf.hpp new file mode 100644 index 0000000..2a22e47 --- /dev/null +++ b/test/TestDF/testdf.hpp @@ -0,0 +1,138 @@ +#pragma once +#include "mvmap.hpp" +class testdf { + using variants = std::variant; + using idx_variants = + std::variant; + using df_mvmap = mvmap::mvmap; + + df_mvmap data; + std::optional>> index; + + public: + testdf() = default; + testdf(const df_mvmap &data) : data(data) {}; + + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, const testdf &df) { + std::optional idx_name; + if (df.index.has_value()) { + auto idx_pair = df.index.value(); + idx_name = idx_pair.first; + } + + v = {{"index_name", boost::json::value_from(idx_name)}, + {"data", boost::json::value_from(df.data)}}; + } + + friend testdf tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + const auto &obj = v.as_object(); + df_mvmap data = boost::json::value_to(obj.at("data")); + auto idx_name_o = + boost::json::value_to>(obj.at("index_name")); + + testdf df{data}; + if (idx_name_o.has_value()) { + std::string idx_name = idx_name_o.value(); + if (data.series_is_bool(idx_name)) { + df.set_index(idx_name); + } else if (data.series_is_string(idx_name)) { + df.set_index(idx_name); + } else if (data.series_is_int64_t(idx_name)) { + df.set_index(idx_name); + } else if (data.series_is_double(idx_name)) { + df.set_index(idx_name); + } else { + std::cerr << "Unknown index type; ignoring" << std::endl; + } + } + + return df; + } + + bool is_index(const std::string &idx) { + if (!index.has_value()) { + return false; + } + return index.value().first == idx; + } + + template + bool set_index(const std::string &name) { + if (data.has_series(name)) { + return false; + } + auto ser_o = data.get_series(name); + if (!ser_o.has_value()) { + return false; + } + auto ser = ser_o.value(); + index.emplace(std::make_pair(name, ser)); + return true; + } + // candidate function not viable: no known conversion from 'pair::type, typename + // __unwrap_ref_decay &>::type>' (aka 'pair::series_proxy>') to 'nullopt_t' for 1st argument + + bool drop_index() { + if (!index.has_value()) { + return false; + } + index = std::nullopt; + return true; + } + + bool drop_index(const std::string &name) { + if (!is_index(name)) { + return false; + } + index = std::nullopt; + return true; + } + template + std::optional> add_col( + const std::string &name, const std::string &desc = "") { + return data.add_series(name, desc); + } + + void drop_col(const std::string &name) { + drop_index(name); + return data.drop_series(name); + } + + template + void remove_if(F f) { + return data.remove_if(f); + } + + bool copy_col(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { + return data.copy_series(from, to, desc); + } + + std::vector> columns() { + return data.list_series(); + } + + std::map dtypes() { + std::map d{}; + for (auto [ser, _] : data.list_series()) { + if (data.series_is_double(ser)) { + d[ser] = "double"; + } else if (data.series_is_int64_t(ser)) { + d[ser] = "int64_t"; + } else if (data.series_is_string(ser)) { + d[ser] = "string"; + } else if (data.series_is_bool(ser)) { + d[ser] = "bool"; + } else { + d[ser] = "UNKNOWN"; + } + } + return d; + } + void describe() { std::cout << "TestDF " << std::endl; } +}; diff --git a/test/TestGraph/testgraph.hpp b/test/TestGraph/testgraph.hpp index fbdfc6b..9598d7f 100644 --- a/test/TestGraph/testgraph.hpp +++ b/test/TestGraph/testgraph.hpp @@ -95,12 +95,14 @@ class testgraph { return edge_table.get_series(sel); } - bool copy_edge_series(const std::string &from, const std::string &to) { - return edge_table.copy_series(from, to); + bool copy_edge_series(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { + return edge_table.copy_series(from, to, desc); } - bool copy_node_series(const std::string &from, const std::string &to) { - return node_table.copy_series(from, to); + bool copy_node_series(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { + return node_table.copy_series(from, to, desc); } template diff --git a/test/TestGraph/mvmap.hpp b/test/include/mvmap.hpp similarity index 96% rename from test/TestGraph/mvmap.hpp rename to test/include/mvmap.hpp index 4a76e66..79dbbc7 100644 --- a/test/TestGraph/mvmap.hpp +++ b/test/include/mvmap.hpp @@ -18,7 +18,6 @@ using index = uint64_t; class locator { static const index INVALID_LOC = std::numeric_limits::max(); index loc; - locator(index loc) : loc(loc) {}; public: @@ -280,8 +279,12 @@ class mvmap { return true; } - [[nodiscard]] std::vector list_series() const { - return std::views::keys(data); + [[nodiscard]] std::vector> list_series() { + std::vector> ser_pairs; + for (auto el : series_desc) { + ser_pairs.push_back(el); + } + return ser_pairs; } [[nodiscard]] bool has_series(const std::string &id) const { @@ -319,7 +322,8 @@ class mvmap { // copies an existing column (series) to a new (unmanifested) column and // returns true. If the new column already exists, or if the existing column // doesn't, return false. - bool copy_series(const std::string &from, const std::string &to) { + bool copy_series(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { if (has_series(to) || !has_series(from)) { std::cerr << "copy_series failed from " << from << " to " << to << std::endl; @@ -327,6 +331,7 @@ class mvmap { } // std::cerr << "copying series from " << from << " to " << to << std::endl; data[to] = data[from]; + series_desc[to] = desc.has_value() ? desc.value() : series_desc[from]; return true; } From f844f99076a0ecfa48865cf190de454701f7295f Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Fri, 3 Jan 2025 15:05:20 -0800 Subject: [PATCH 12/14] testmvmap and some fixes, along with prints --- include/clippy/clippy.hpp | 4 +-- test/TestDF/testdf.cpp | 11 ++++++++ test/TestDF/testdf.hpp | 3 ++- test/include/mvmap.hpp | 54 ++++++++++++++++++++++++++++++++++++- test/include/testmvmap | Bin 0 -> 1746424 bytes test/include/testmvmap.cpp | 19 +++++++++++++ 6 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 test/TestDF/testdf.cpp create mode 100755 test/include/testmvmap create mode 100644 test/include/testmvmap.cpp diff --git a/include/clippy/clippy.hpp b/include/clippy/clippy.hpp index 60322d3..15989ca 100644 --- a/include/clippy/clippy.hpp +++ b/include/clippy/clippy.hpp @@ -59,8 +59,8 @@ boost::json::value asContainer(boost::json::value val, bool requiresContainer) { res.emplace_back(std::move(val)); return res; } - -std::string clippyLogFile{"clippy.log"}; +1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 std:: + string clippyLogFile{"clippy.log"}; #if WITH_YGM std::string userInputString; diff --git a/test/TestDF/testdf.cpp b/test/TestDF/testdf.cpp new file mode 100644 index 0000000..52c60e2 --- /dev/null +++ b/test/TestDF/testdf.cpp @@ -0,0 +1,11 @@ +#include "testdf.hpp" + +#include + +int main() { + testdf d{}; + d.add_col("boolean"); + d.add_col("int64"); + d.add_col("double"); + std::cout << d.dtypes << std::endl; +} \ No newline at end of file diff --git a/test/TestDF/testdf.hpp b/test/TestDF/testdf.hpp index 2a22e47..9bb9203 100644 --- a/test/TestDF/testdf.hpp +++ b/test/TestDF/testdf.hpp @@ -1,5 +1,6 @@ #pragma once -#include "mvmap.hpp" +#include "../include/mvmap.hpp" + class testdf { using variants = std::variant; using idx_variants = diff --git a/test/include/mvmap.hpp b/test/include/mvmap.hpp index 79dbbc7..c268e5e 100644 --- a/test/include/mvmap.hpp +++ b/test/include/mvmap.hpp @@ -47,6 +47,13 @@ class mvmap { using idx_to_key = std::map; using variants = std::variant; + template + static void print_series(const series &s) { + for (auto el : s) { + std::cout << el.first << " -> " << el.second << std::endl; + } + } + // A locator is an opaque handle to a key in a series. idx_to_key itk; @@ -238,6 +245,30 @@ class mvmap { return ct; } + void print() const { + std::cout << "id: " << id << ", "; + std::cout << "desc: " << desc << ", "; + std::string dtype = "unknown"; + if (is_string_v()) { + dtype = "string"; + } else if (is_double_v()) { + dtype = "double"; + } else if (is_int64_t_v()) { + dtype = "int64_t"; + } else if (is_bool_v()) { + dtype = "bool"; + } + std::cout << "dtype: " << dtype << ", "; + // std::cout << "kti_r.size(): " << kti_r.size() << std::endl; + // std::cout << "itk_r.size(): " << itk_r.size() << std::endl; + std::cout << series_r.size() << " entries" << std::endl; + // std::cout << "elements: " << std::endl; + for (auto el : series_r) { + std::cout << " " << itk_r[el.first] << " -> " << el.second + << std::endl; + } + } + }; // end of series ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// @@ -341,7 +372,8 @@ class mvmap { // series doesn't exist or is of the wrong type. return std::nullopt; } - return series_proxy(sel, std::get>(data[sel]), *this); + return series_proxy(sel, this->series_desc.at(sel), + std::get>(data[sel]), *this); } bool series_is_string(const std::string &sel) const { @@ -406,5 +438,25 @@ class mvmap { } } } + + void print() { + std::cout << "mvmap with " << data.size() << " series: " << std::endl; + for (auto &el : data) { + std::cout << "series " << el.first << ":" << std::endl; + std::visit( + [&el, this](auto &ser) { + using T = std::decay_tsecond)>; + auto sproxy = get_series>(el.first) + .value(); // this is a series_proxy + sproxy.print(); + }, + el.second); + + // std::cout << " second: " << el.second << std::endl; + // auto foo = get_variant_series(el.first).value(); + // foo.visit([](auto &ser) { print_series(ser); }); + // print_series(el.second); + } + } }; }; // namespace mvmap diff --git a/test/include/testmvmap b/test/include/testmvmap new file mode 100755 index 0000000000000000000000000000000000000000..c1a1f50b0cd5099cad8ea9241d38a7e412a3c844 GIT binary patch literal 1746424 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