diff --git a/src/Cinterface/spatialstructures_C.cpp b/src/Cinterface/spatialstructures_C.cpp index 25b4a005b..77d60e701 100644 --- a/src/Cinterface/spatialstructures_C.cpp +++ b/src/Cinterface/spatialstructures_C.cpp @@ -319,6 +319,38 @@ C_INTERFACE AddNodeAttributes( return OK; } +C_INTERFACE AddNodeAttributesFloat( + Graph* g, + const int* ids, + const char* attribute, + const float* scores, + int num_nodes +) +{ + // It is easy to convert raw pointers that are known to point to buffers/arrays + // to a std::vector. + + // ids is the base address, and ids + num_nodes is one-past the last address allocated for ids. + std::vector v_ids(ids, ids + num_nodes); + + // scores is the base address, and scores + num_nodes is one-past the last address allocated for scores. + std::vector v_scores(scores, scores + num_nodes); + + // If it turns out that v_ids and v_scores have different sizes, + // AddNodeAttributes will discover this. + try { + g->AddNodeAttributesFloat(v_ids, std::string(attribute), v_scores); + } + catch (std::logic_error) { + //return HF_STATUS::OUT_OF_RANGE; + assert(false); // This is purely due to programmer error. The top of this function should + // ONLY read num_nodes elements from either array, and this exception will + // only throw if the length of scores and ids is different + } + + return OK; +} + C_INTERFACE GetNodeAttributes(const HF::SpatialStructures::Graph* g, const char* attribute, char** out_scores, int* out_score_size) { // get all node attributes from the graph @@ -352,9 +384,7 @@ C_INTERFACE GetNodeAttributes(const HF::SpatialStructures::Graph* g, const char* C_INTERFACE GetNodeAttributesByID(const HF::SpatialStructures::Graph* g, const int* ids, const char* attribute, int num_nodes, char** out_scores, int* out_score_size) { - // If IDs are specified, create vector to use - // (std::vector, std::string) defintion of GetNodeAttributes - // ids is base address, ids + num_nodes is end address + // If IDs are specified, create vector for them vector v_ids(ids, ids + num_nodes); vector v_attrs = g->GetNodeAttributesByID(v_ids, std::string(attribute)); @@ -384,6 +414,56 @@ C_INTERFACE GetNodeAttributesByID(const HF::SpatialStructures::Graph* g, const i return OK; } +C_INTERFACE GetNodeAttributesFloat(const HF::SpatialStructures::Graph* g, const char* attribute, + float* out_scores, int* out_score_size) { + // get all node attributes from the graph + vector v_attrs = g->GetNodeAttributesFloat(std::string(attribute)); + + // Iterate through each returned value and copy it into + // the output array + for (int i = 0; i < v_attrs.size(); i++) { + // Copy the contents of v_attrs into the output array + std::memcpy(&out_scores[i], &v_attrs[i], sizeof(float)); + } + + // Update the *out_score_size value, which corresponds to v_attrs.size(). + // (it also corresponds to i, but this notation using v_attrs.size() is easier to understand) + *out_score_size = v_attrs.size(); + + // If v_attrs.size() == 0, do we want to throw an exception, + // which would mean that attribute does not exist as a attribute type? + return OK; +} + +C_INTERFACE GetNodeAttributesByIDFloat(const HF::SpatialStructures::Graph* g, const int* ids, const char* attribute, int num_nodes, + float* out_scores, int* out_score_size) { + + // If IDs are specified, create vector for them + vector v_ids(ids, ids + num_nodes); + vector v_attrs = g->GetNodeAttributesByIDFloat(v_ids, std::string(attribute)); + + // Iterate through each returned value and copy it into + // the output array + for (int i = 0; i < v_attrs.size(); i++) + { + // Copy the contents of v_attrs into the output array + std::memcpy(&out_scores[i], &v_attrs[i], sizeof(float)); + } + + // Update the *out_score_size value, which corresponds to v_attrs.size(). + // (it also corresponds to i, but this notation using v_attrs.size() is easier to understand) + *out_score_size = v_attrs.size(); + + // If v_attrs.size() == 0, do we want to throw an exception, + // which would mean that attribute does not exist as a attribute type? + return OK; +} + +C_INTERFACE IsFloatAttribute(const HF::SpatialStructures::Graph* g, const char* attribute) +{ + return g->IsFloatAttribute(std::string(attribute)); +} + C_INTERFACE DeleteScoreArray(char** scores_to_delete, int num_char_arrays) { // If it's null, then just ignore it if (scores_to_delete) { diff --git a/src/Cinterface/spatialstructures_C.h b/src/Cinterface/spatialstructures_C.h index eae17e14f..bd88b7e72 100644 --- a/src/Cinterface/spatialstructures_C.h +++ b/src/Cinterface/spatialstructures_C.h @@ -756,6 +756,8 @@ C_INTERFACE CalculateAndStoreEnergyExpenditure(HF::SpatialStructures::Graph* g); \see \ref graph_setup (how to create a graph) \see \ref graph_add_edge_from_nodes (how to add edges to a graph using nodes) \see \ref graph_add_edge_from_node_ids (how to add edges to a graph using node IDs) + \see \link GetNodeAttributes \endlink (how to get string node attributes) + \see \link GetNodeAttributesByID \endlink (how to get string node attributes by node ID) \see \ref graph_compress (how to compress a graph after adding/removing edges) \see \ref graph_get_csr_pointers (how to retrieve a CSR representation of a graph) \see \ref graph_teardown (how to destroy a graph) @@ -780,6 +782,56 @@ C_INTERFACE AddNodeAttributes( int num_nodes ); +/*! + \brief Add a new float node attribute in the graph for the nodes at ids. + + \param g Graph to add attributes to + \param ids IDs of nodes to add attributes to + \param attribute The name of the attribute to add the scores to. + + \param scores An ordered array of floats + that correspond to the score of the ID in ids at the same index. + + \param num_nodes Length of both the ids and scores arrays + + \returns \link HF_STATUS::OK \endlink on completion. + Note that this does not guarantee that some + or all of the node attributes have been added + + \details + For any id in ids, if said ID doesn't already exist in the graph, then it and its cost will + silently be ignored without error. + + \pre ids and scores arrays must be the same length + + \see \ref graph_setup (how to create a graph) + \see \ref graph_add_edge_from_nodes (how to add edges to a graph using nodes) + \see \ref graph_add_edge_from_node_ids (how to add edges to a graph using node IDs) + \see \link GetNodeAttributesFloat \endlink (how to get float node attributes) + \see \link GetNodeAttributesByIDFloat \endlink (how to get float node attributes by node ID) + \see \ref graph_compress (how to compress a graph after adding/removing edges) + \see \ref graph_get_csr_pointers (how to retrieve a CSR representation of a graph) + \see \ref graph_teardown (how to destroy a graph) + + Begin by reviewing the example at \ref graph_setup to create a graph.
+ + You may add edges to the graph using nodes (\ref graph_add_edge_from_nodes)
+ or alternative, you may provide node IDs (\ref graph_add_edge_from_node_IDs).
+ + Be sure to compress the graph (\ref graph_compress) every time you add/remove edges.
+ + \snippet tests\src\spatialstructures_C_cinterface.cpp snippet_spatialstructuresC_AddNodeAttributesFloat + + Finally, when you are finished with the graph,
+ it must be destroyed. (\ref graph_teardown) +*/ +C_INTERFACE AddNodeAttributesFloat( + HF::SpatialStructures::Graph* g, + const int* ids, + const char* attribute, + const float* scores, + int num_nodes +); /*! \brief Retrieve node attribute values from *g @@ -791,6 +843,8 @@ C_INTERFACE AddNodeAttributes( \param out_score_size Keeps track of the size of out_scores buffer, updated as required + \pre `attribute` is a string attribute. That is, at least one string value has been added to this attribute. + \returns \link HF_STATUS::OK \endlink on completion. \details Memory shall be allocated in *out_scores to hold the char arrays. @@ -802,8 +856,8 @@ C_INTERFACE AddNodeAttributes( \see \ref graph_setup (how to create a graph) \see \ref graph_compress (how to compress a graph after adding/removing edges) \see \ref graph_get_csr_pointers (how to retrieve a CSR representation of a graph) - \see \link AddNodeAttributes \endlink (how to add node attributes) - \see \link GetNodeAttributesByID \endlink (how to get node attributes by node ID) + \see \link AddNodeAttributes \endlink (how to add string node attributes) + \see \link GetNodeAttributesByID \endlink (how to get string node attributes by node ID) \see \ref graph_teardown (how to destroy a graph) Begin by reviewing the example at \ref graph_setup to create a graph.
@@ -835,6 +889,7 @@ C_INTERFACE GetNodeAttributes( \pre All node IDs in `ids` must exist in graph `g`. \pre If `ids` is not NULL, `num_nodes` must be equal to the length of `ids`. + \pre `attribute` is a string attribute. That is, at least one string value has been added to this attribute. \returns \link HF_STATUS::OK \endlink on completion. \details For the ID at `ids[i]`, `out_scores[i]` is the value of the attribute for the @@ -852,8 +907,8 @@ C_INTERFACE GetNodeAttributes( \see \ref graph_setup (how to create a graph) \see \ref graph_compress (how to compress a graph after adding/removing edges) \see \ref graph_get_csr_pointers (how to retrieve a CSR representation of a graph) - \see \link AddNodeAttributes \endlink (how to add node attributes) - \see \link GetNodeAttributes \endlink (how to get node attributes) + \see \link AddNodeAttributes \endlink (how to add string node attributes) + \see \link GetNodeAttributes \endlink (how to get string node attributes) \see \ref graph_teardown (how to destroy a graph) Begin by reviewing the example at \ref graph_setup to create a graph.
@@ -871,6 +926,106 @@ C_INTERFACE GetNodeAttributesByID( char** out_scores, int* out_score_size ); +/*! + \brief Retrieve float node attribute values from *g + + \param g The graph that will be used to retrieve + node attribute values from + + \param attribute The node attribute type to retrieve from *g + \param out_scores Pointer to array of float, allocated by the caller + \param out_score_size Keeps track of the size of out_scores buffer, + updated as required + + \pre `attribute` is a float attribute. That is, only float values have been added to this attribute. + + \returns \link HF_STATUS::OK \endlink on completion. + + \details The caller must deallocate the memory addressed by out_scores. + + \see \ref graph_setup (how to create a graph) + \see \ref graph_compress (how to compress a graph after adding/removing edges) + \see \ref graph_get_csr_pointers (how to retrieve a CSR representation of a graph) + \see \link AddNodeAttributesFloat \endlink (how to add float node attributes) + \see \link GetNodeAttributesByIDFloat \endlink (how to get float node attributes by node ID) + \see \ref graph_teardown (how to destroy a graph) + + Begin by reviewing the example at \ref graph_setup to create a graph.
+ + \snippet tests\src\spatialstructures_C_cinterface.cpp snippet_spatialstructuresC_GetNodeAttributesFloat + + Finally, when you are finished with the graph,
+ it must be destroyed. (\ref graph_teardown) +*/ +C_INTERFACE GetNodeAttributesFloat( + const HF::SpatialStructures::Graph* g, + const char* attribute, + float* out_scores, + int* out_score_size +); + +/*! + \brief Retrieve float node attribute values from *g + + \param g The graph that will be used to retrieve + node attribute values from + \param ids The list of node IDs to get attributes for. + If NULL, returns attributes for all nodes. + \param attribute The node attribute type to retrieve from *g + \param num_nodes The length of the ids array + \param out_scores Pointer to array of floats, allocated by the caller + \param out_score_size Keeps track of the size of out_scores buffer, + updated as required + + \pre All node IDs in `ids` must exist in graph `g`. + \pre If `ids` is not NULL, `num_nodes` must be equal to the length of `ids`. + \pre `attribute` is a float attribute. That is, only float values have been added to this attribute. + + \returns \link HF_STATUS::OK \endlink on completion. + + \details For the ID at `ids[i]`, `out_scores[i]` is the value of the attribute for the + node associated with that ID. + + If `ids` is NULL, `out_scores` is an array holding the value of the attribute for + all nodes, sorted in ascending order by ID. + + The caller must deallocate the memory addressed by out_scores. + + \see \ref graph_setup (how to create a graph) + \see \ref graph_compress (how to compress a graph after adding/removing edges) + \see \ref graph_get_csr_pointers (how to retrieve a CSR representation of a graph) + \see \link AddNodeAttributesFloat \endlink (how to add float node attributes) + \see \link GetNodeAttributesFloat \endlink (how to get float node attributes) + \see \ref graph_teardown (how to destroy a graph) + + Begin by reviewing the example at \ref graph_setup to create a graph.
+ + \snippet tests\src\spatialstructures_C_cinterface.cpp snippet_spatialstructuresC_GetNodeAttributesByIDFloat + + Finally, when you are finished with the graph,
+ it must be destroyed. (\ref graph_teardown) +*/ +C_INTERFACE GetNodeAttributesByIDFloat( + const HF::SpatialStructures::Graph* g, + const int* ids, + const char* attribute, + int num_nodes, + float* out_scores, + int* out_score_size +); + +/*! + \brief Check whether or not an attribute is stored with float values in a graph. + \param g The pointer of the graph to check + \param attribute The attribute to check + + \returns 1 if the attribute exists in the graph and contains only float values. + 0 otherwise. +*/ +C_INTERFACE IsFloatAttribute( + const HF::SpatialStructures::Graph* g, + const char* attribute +); /*! \brief Free the memory of every (char *) in scores_to_delete. diff --git a/src/Cpp/spatialstructures/src/graph.cpp b/src/Cpp/spatialstructures/src/graph.cpp index b40eae56e..66a21a7ff 100644 --- a/src/Cpp/spatialstructures/src/graph.cpp +++ b/src/Cpp/spatialstructures/src/graph.cpp @@ -181,7 +181,11 @@ namespace HF::SpatialStructures { // Get the costs for this attribute and convert every value to float. In the case that a // attribute score could not be converted due to not being a numeric value or not being set // in the first place, those values will be set to -1. - const auto& scores = ConvertStringsToFloat(this->GetNodeAttributes(node_attribute)); + vector scores; + if (this->IsFloatAttribute(node_attribute)) + scores = this->GetNodeAttributesFloat(node_attribute); + else + scores = ConvertStringsToFloat(this->GetNodeAttributes(node_attribute)); // Iterate through all nodes in the graph for (const auto& parent : ordered_nodes) { @@ -821,7 +825,12 @@ namespace HF::SpatialStructures { bool Graph::HasNodeAttribute(const std::string& key) const { - return this->node_attr_map.count(key) > 0; + return this->node_attr_map.count(key) > 0 || this->node_float_attr_map.count(key) > 0; + } + + bool Graph::IsFloatAttribute(const std::string& name) const + { + return this->node_float_attr_map.count(name) > 0; } void Graph::addEdge(const Node& parent, const Node& child, float score, const string & cost_type) @@ -1231,28 +1240,47 @@ namespace HF::SpatialStructures { return Subgraph{ parent_node, this->GetEdgesForNode(parent_id, false, cost_type) }; } - void Graph::AddNodeAttribute(int id, const std::string & attribute, const std::string & score) { + void Graph::AddNodeAttribute(int id, const std::string & name, const std::string & score) { // Check if this id belongs to any node in the graph if (id > this->MaxID()) return; - /* // requires #include , but not working? - std::string lower_cased = - std::transform(attribute.begin(), attribute.end(), - [](unsigned char c) { return std::tolower(c); } - ); - */ - std::string lower_cased = attribute; - // Retrieve an iterator to the [node attribute : NodeAttributeValueMap] - // that corresponds with attribute - auto node_attr_map_it = node_attr_map.find(lower_cased); + // that corresponds with name + auto node_attr_map_it = node_attr_map.find(name); if (node_attr_map_it == node_attr_map.end()) { + + // Check if name could be a float-type attribute + auto node_float_attr_map_it = node_float_attr_map.find(name); + // If the attribute type does not exist...create it. - node_attr_map[lower_cased] = NodeAttributeValueMap(); + // Regardless of whether or not we are converting a float-type attribute, + // we need to create a new map for the input string attribute. + node_attr_map[name] = NodeAttributeValueMap(); + + if (node_float_attr_map_it != node_float_attr_map.end()) + { + // name is in float map, so we assume user + // wants to convert the attribute to a string-type attribute. + NodeFloatAttributeValueMap& node_float_attr_value_map = node_float_attr_map_it->second; + + NodeAttributeValueMap& node_attr_value_map = node_attr_map.find(name)->second; + + for (auto it : node_float_attr_value_map) + { + node_attr_value_map[it.first] = std::to_string(it.second); + } + + // Remove float attribute from float map to avoid duplicates + node_float_attr_map.erase(node_float_attr_map_it->first); + + // This should only happen once the first time + // AddNodeAttribute is called with a float-type attribute. + // Subsequent calls should find this attribute in the string-type map. + } // Update this iterator so it can be used in the next code block - node_attr_map_it = node_attr_map.find(lower_cased); + node_attr_map_it = node_attr_map.find(name); } // We now have the NodeAttributeValueMap for the desired attribute. @@ -1268,45 +1296,62 @@ namespace HF::SpatialStructures { // If the node id provided does not exist in the value map...add it. node_attr_value_map[id] = score; - // Update this iterator so it can be used in the next code block - node_attr_value_map_it = node_attr_value_map.find(id); } + else + { + // Otherwise, the key-value pair exists, and we can set the value to score + node_attr_value_map_it->second = score; + } + } - // Will be used to assess whether it is floating point, or not - std::string found_attr_value = node_attr_value_map_it->second; - - // Let's determine the data type of score: - bool score_is_floating_pt = is_floating_type(score); - - // Let's determine the data type of found_attr_value: - bool attr_is_floating_pt = is_floating_type(found_attr_value); + void Graph::AddNodeAttributeFloat(int id, const std::string& name, const float score) + { + // Check if this id belongs to any node in the graph + if (id > this->MaxID()) return; - /* - Need to determine if found_attr_value is - - a string - - a floating point value + // Retrieve an iterator to the [node attribute : NodeFloatAttributeValueMap] + // that corresponds with attribute + auto node_float_attr_map_it = node_float_attr_map.find(name); - and if the data type for score matches that of found_attr_value - */ - if (attr_is_floating_pt) { - // if the current attribute value is floating point... - if (score_is_floating_pt) { - // Ok - data type matched. - node_attr_value_map_it->second = score; + if (node_float_attr_map_it == node_float_attr_map.end()) + { + // Check if attribute could be a string attribute + auto node_attr_map_it = node_attr_map.find(name); + if (node_attr_map_it != node_attr_map.end()) + { + // We assume the user wants to add strings representing floats + // to a string attribute, so convert to string and add. + // score is guaranteed to be convertable since it is a float. + AddNodeAttribute(id, name, std::to_string(score)); + return; } - else { - // error? + else + { + // If the attribute type does not exist...create it. + node_float_attr_map[name] = NodeFloatAttributeValueMap(); + + // Update this iterator so it can be used in the next code block + node_float_attr_map_it = node_float_attr_map.find(name); } } - else { - // if the current attribute value is not floating point... - if (score_is_floating_pt) { - // error? - } - else { - // Ok - data type matched - node_attr_value_map_it->second = score; - } + + // We now have the NodeFloatAttributeValueMap for the desired attribute. + // A NodeFloatAttributeValueMap stores buckets of [node id : node attribute value as float] + NodeFloatAttributeValueMap& node_float_attr_value_map = node_float_attr_map_it->second; + + // Need to see if id exists as a key within node_float_attr_value_map + // This will give us the position of a bucket containing: + // [node id : node attribute value as float] + auto node_float_attr_value_map_it = node_float_attr_value_map.find(id); + + if (node_float_attr_value_map_it == node_float_attr_value_map.end()) + { + // If the node id provided does not exist in the value map...add it. + node_float_attr_value_map[id] = score; + } + else + { + node_float_attr_value_map_it->second = score; } } @@ -1331,14 +1376,31 @@ namespace HF::SpatialStructures { } } - vector Graph::GetNodeAttributes(string attribute) const { + void Graph::AddNodeAttributesFloat(const std::vector& id, const std::string& name, const std::vector& scores) + { + // If size of id container and size of scores container are not in alignment, + // throw, since our precondition was violated + if (id.size() != scores.size()) + throw std::logic_error("Tried to pass id and string arrays that are different lengths"); + + auto scores_iterator = scores.begin(); + + for (int node_id : id) { + + // We can call AddNodeAttributeFloat for each node_id in id. + // If the attribute type name does not exist, + // it will be created with the first invocation of AddNodeAttributeFloat. + AddNodeAttributeFloat(node_id, name, *(scores_iterator++)); + } + } + vector Graph::GetNodeAttributes(string name) const { // Return an empty array if this attribute doesn't exist - if (node_attr_map.count(attribute) < 1) return vector(); + if (node_attr_map.count(name) < 1) return vector(); - // Get the attribute map for attribute now that we know it exists - const auto& attr_map = node_attr_map.at(attribute); - + // Get the attribute map for name now that we know it exists + const auto& attr_map = node_attr_map.at(name); + // Preallocate output array with empty strings. We'll only be modifying // the indexes that have scores assigned to them, and leaving the rest // as empty strings @@ -1361,19 +1423,19 @@ namespace HF::SpatialStructures { return out_attributes; } - vector Graph::GetNodeAttributesByID(vector& ids, string attribute) const { + vector Graph::GetNodeAttributesByID(vector& ids, string name) const { // Return an empty array if this attribute doesn't exist - if (node_attr_map.count(attribute) < 1) return vector(); + if (node_attr_map.count(name) < 1) return vector(); - // Get the attribute map for attribute now that we know it exists - const auto& attr_map = node_attr_map.at(attribute); + // Get the attribute map for name now that we know it exists + const auto& attr_map = node_attr_map.at(name); // Preallocate output array with empty strings. // Only allocate as many spots we need for the specified IDs. const int num_nodes = ids.size(); vector out_attributes(num_nodes, ""); - + // Keep index of return array, since it isn't necessarily the same as the ID int idx = 0; // Iterate through all specified IDs @@ -1391,6 +1453,67 @@ namespace HF::SpatialStructures { return out_attributes; } + vector Graph::GetNodeAttributesFloat(string name) const { + + // Return an empty array if this attribute doesn't exist + if (node_float_attr_map.count(name) < 1) return vector(); + + // Get the attribute map for name now that we know it exists + const auto& float_attr_map = node_float_attr_map.at(name); + + // Preallocate output array with empty strings. We'll only be modifying + // the indexes that have scores assigned to them, and leaving the rest + // as empty strings + const int num_nodes = ordered_nodes.size(); + vector out_attributes(num_nodes, 0.0); + + // Iterate through attribute map to assign scores for the nodes + // that have them + for (const auto& it : float_attr_map) + { + // Get the id and score of this element in the hashmap + const int id = it.first; + const float score = it.second; + + // Copy it to the index of the node's ID in our output array + out_attributes[id] = score; + } + + // Return all found attributes + return out_attributes; + } + + vector Graph::GetNodeAttributesByIDFloat(vector& ids, string name) const { + + // Return an empty array if this attribute doesn't exist + if (node_float_attr_map.count(name) < 1) return vector(); + + // Get the attribute map for name now that we know it exists + const auto& float_attr_map = node_float_attr_map.at(name); + + // Preallocate output array with empty strings. + // Only allocate as many spots we need for the specified IDs. + const int num_nodes = ids.size(); + vector out_attributes(num_nodes, 0.0); + + // Keep index of return array, since it isn't necessarily the same as the ID + int idx = 0; + // Iterate through all specified IDs + for (int i = 0; i < num_nodes; i++) + { + // For each ID, get associated node, and get the node's attribute + int id = ids[i]; + const auto& score = float_attr_map.at(id); + + out_attributes[idx] = score; + // Increment index to the next space in the output array + idx++; + } + // Return all found attributes + return out_attributes; + } + + void Graph::ClearNodeAttributes(std::string name) { /* // requires #include , but not working? std::string lower_cased = @@ -1408,6 +1531,10 @@ namespace HF::SpatialStructures { // it contains. node_attr_map.erase(name); } + if (node_float_attr_map.count(name) > 0) + { + node_float_attr_map.erase(name); + } } using namespace nlohmann; diff --git a/src/Cpp/spatialstructures/src/graph.h b/src/Cpp/spatialstructures/src/graph.h index 8244c5c17..0a5500cb6 100644 --- a/src/Cpp/spatialstructures/src/graph.h +++ b/src/Cpp/spatialstructures/src/graph.h @@ -485,7 +485,7 @@ namespace HF::SpatialStructures { */ class Graph { using NodeAttributeValueMap = robin_hood::unordered_map; - + using NodeFloatAttributeValueMap = robin_hood::unordered_map; private: int next_id = 0; ///< The id for the next unique node. std::vector ordered_nodes; ///< A list of nodes contained by the graph. @@ -497,6 +497,7 @@ namespace HF::SpatialStructures { bool needs_compression = true; ///< If true, the CSR is inaccurate and requires compression. robin_hood::unordered_map node_attr_map; ///< Node attribute type : Map of node id to node attribute + robin_hood::unordered_map node_float_attr_map; ///< Node attribute type : Map of node id to node attribute for float std::string active_cost_type; ///< The active edge matrix to use for the graph EdgeMatrix edge_matrix; ///< The underlying CSR containing edge information. @@ -1788,12 +1789,16 @@ namespace HF::SpatialStructures { Subgraph GetSubgraph(int parent_id, const std::string & cost_type = "") const; /// - /// Add an attribute to the node at id + /// Add a string attribute to the node at id. If the node at id already has a score for the + /// attribute at name, then existing score should be overwritten. + /// + /// If the attribute is a float attribute, it will be converted into a string attribute. + /// Any existing data for that attribute will be converted into strings. /// /// /// The ID of the node that will receive attribute /// - /// + /// /// The attribute that the node at ID will receive /// /// @@ -1805,11 +1810,40 @@ namespace HF::SpatialStructures { // TODO example \endcode */ - void AddNodeAttribute(int id, const std::string & attribute, const std::string & score); + void AddNodeAttribute(int id, const std::string & name, const std::string & score); /// - /// Add an attribute to the node at id. If the node at id already has a score for the - /// attribute at name, then existing score should be overwritten + /// Add a float attribute to the node at id. If the node at id already has a score for the + /// attribute at name, then existing score should be overwritten. + /// + /// If the attribute is a string attribute, score will be added + /// as a string value. The attribute will not be converted into a float + /// attribute. + /// + /// + /// The ID of the node that will receive attribute + /// + /// + /// The attribute that the node at ID will receive + /// + /// + /// The weight, or distance that extends from the node at id + /// + + /*! + \code + // TODO example + \endcode + */ + void AddNodeAttributeFloat(int id, const std::string& name, const float score); + + /// + /// Add a string attribute to the node at id. If the node at id already has a score for the + /// attribute at name, then existing score should be overwritten. + /// + /// If the attribute is a float attribute, it will be converted into a string attribute. + /// All existing data for that attribute will be converted into strings, and the new + /// data will be added as string values. /// /// /// The container of IDs from which nodes will be retrieved and given attributes @@ -1824,25 +1858,54 @@ namespace HF::SpatialStructures { /*! \pre The length of ids, and the length of scores must be equal \throws std::logic_error The length of scores and the length of ID do not match. - \code // TODO example \endcode */ void AddNodeAttributes(const std::vector & id, const std::string & name, const std::vector & scores); + /// + /// Add a float attribute to the node at id. If the node at id already has a score for the + /// attribute at name, then existing score should be overwritten. + /// + /// If the attribute is a string attribute, scores will be added + /// as string values. The attribute will not be converted into a float + /// attribute. + /// + /// + /// The container of IDs from which nodes will be retrieved and given attributes + /// + /// + /// The attribute that each node will receive + /// + /// + /// The container of score, ordered by the container of node IDs + /// + + /*! + \pre The length of ids, and the length of scores must be equal + \throws std::logic_error The length of scores and the length of ID do not match + + \code + // TODO example + \endcode + */ + void AddNodeAttributesFloat(const std::vector& id, const std::string& name, const std::vector& scores); + /// /// Get the score for the given attribute of every node in the graph. Nodes that do not have /// a score for this attribute should return an empty string for this array. /// - /// + /// /// The attribute from which a container of scores will be obtained /// /// - /// A container of score, each in the form of a std::string, obtained from attribute + /// A container of score, each in the form of a std::string, obtained from name /// /*! + \pre `name` is a string attribute. That is, at least one string value has been added to this attribute. + \code // be sure to #include "graph.h" @@ -1877,7 +1940,56 @@ namespace HF::SpatialStructures { std::vector cross_slopes = graph.GetNodeAttributes(attribute); // {"2.3", "6.1", "4.0"} \endcode */ - std::vector GetNodeAttributes(std::string attribute) const; + std::vector GetNodeAttributes(std::string name) const; + + /// + /// Get the score for the given attribute of every node in the graph. Nodes that do not have + /// a score for this attribute should return the default value 0.0 for this array. + /// + /// + /// The attribute from which a container of scores will be obtained. + /// + /// + /// A container of score, each in the form of a float, obtained from attribute + /// + /*! + \pre `name` is a float attribute. That is, only float values have been added to this attribute. + + \code + // be sure to #include "graph.h" + + // Create the nodes + HF::SpatialStructures::Node node_0(1.0f, 1.0f, 2.0f); + HF::SpatialStructures::Node node_1(2.0f, 3.0f, 4.0f, 5); + HF::SpatialStructures::Node node_2(11.0f, 22.0f, 140.0f); + + // Create a container (vector) of nodes + std::vector nodes = { node_0, node_1, node_2 }; + + // Create matrices for edges and distances, edges.size() == distances().size() + std::vector> edges = { { 1, 2 }, { 2 }, { 1 } }; + std::vector> distances = { { 1.0f, 2.5f }, { 54.0f }, { 39.0f } }; + + // Now you can create a Graph - note that nodes, edges, and distances are passed by reference + HF::SpatialStructures::Graph graph(edges, distances, nodes); + + // Get node IDs + int ID_0 = graph.getID(node_0); + int ID_1 = graph.getID(node_1); + int ID_2 = graph.getID(node_2); + + std::vector ids = {ID_0, ID_1, ID_2}; + + // Assign attributes to nodes + std::string attribute = "demo attribute"; + std::vector scores = {2.3, 6.1, 4.0}; + graph.AddNodeAttributesFloat(ids, attribute, scores); + + // Get attribute for all nodes + std::vector cross_slopes = graph.GetNodeAttributesFloat(attribute); // {2.3, 6.1, 4.0} + \endcode + */ + std::vector GetNodeAttributesFloat(std::string name) const; /// /// Get the score for the given attribute of the specified nodes. Nodes that do not have @@ -1886,13 +1998,15 @@ namespace HF::SpatialStructures { /// /// A list of node IDs to obtain scores for. /// - /// + /// /// The attribute from which a container of scores will be obtained /// /// - /// A container of score, each in the form of a std::string, obtained from attribute + /// A container of score, each in the form of a std::string, obtained from name /// /*! + \pre `name` is a string attribute. That is, at least one string value has been added to this attribute. + \code // be sure to #include "graph.h" @@ -1927,7 +2041,63 @@ namespace HF::SpatialStructures { std::vector cross_slope_02 = graph.GetNodeAttributesByID({ID_0, ID_2}, attribute); // {"1.8", "5.7"} \endcode */ - std::vector GetNodeAttributesByID(std::vector& ids, std::string attribute) const; + std::vector GetNodeAttributesByID(std::vector& ids, std::string name) const; + + /// + /// Get the score for the given attribute of the specified nodes. Nodes that do not have + /// a score for this attribute should return an empty string for this array. + /// + /// + /// A list of node IDs to obtain scores for. + /// + /// + /// The attribute from which a container of scores will be obtained. + /// + /// + /// A container of score, each in the form of a std::string, obtained from name + /// + /*! + \pre `name` is a float attribute. That is, only float values have been added to this attribute. + + \code + // be sure to #include "graph.h" + + // Create the nodes + HF::SpatialStructures::Node node_0(1.0f, 1.0f, 2.0f); + HF::SpatialStructures::Node node_1(2.0f, 3.0f, 4.0f, 5); + HF::SpatialStructures::Node node_2(11.0f, 22.0f, 140.0f); + + // Create a container (vector) of nodes + std::vector nodes = { node_0, node_1, node_2 }; + + // Create matrices for edges and distances, edges.size() == distances().size() + std::vector> edges = { { 1, 2 }, { 2 }, { 1 } }; + std::vector> distances = { { 1.0f, 2.5f }, { 54.0f }, { 39.0f } }; + + // Now you can create a Graph - note that nodes, edges, and distances are passed by reference + HF::SpatialStructures::Graph graph(edges, distances, nodes); + + // Get node IDs + int ID_0 = graph.getID(node_0); + int ID_1 = graph.getID(node_1); + int ID_2 = graph.getID(node_2); + std::vector ids = {0, 1, 2}; + + // Assign attributes to nodes + std::string attribute = "demo attribute"; + std::vector scores = {1.8, 9.6, 5.7}; + graph.AddNodeAttributesFloat(ids, attribute, scores); + + // Get attribute for specific nodes + std::vector cross_slope_1 = graph.GetNodeAttributesByIDFloat({ID_1}, attribute); // {9.6} + std::vector cross_slope_02 = graph.GetNodeAttributesByIDFloat({ID_0, ID_2}, attribute); // {1.8, 5.7} + \endcode + */ + std::vector GetNodeAttributesByIDFloat(std::vector& ids, std::string name) const; + + /*! \brief Check if this attribute exists in the graph and contains float values*/ + bool IsFloatAttribute(const std::string& name) const; + /// /// Clears the attribute at name and all of its contents from the internal hashmap /// diff --git a/src/Cpp/tests/src/SpatialStructures.cpp b/src/Cpp/tests/src/SpatialStructures.cpp index 0f287be12..02e05bf5d 100644 --- a/src/Cpp/tests/src/SpatialStructures.cpp +++ b/src/Cpp/tests/src/SpatialStructures.cpp @@ -1875,7 +1875,47 @@ namespace GraphExampleTests { auto attrs = g.GetNodeAttributes("cross slope"); ASSERT_TRUE(attrs.size() == g.size()); -} + + // Create a new float attribute that will eventually be turned into a string attribute + g.AddNodeAttributeFloat(0, "float_to_string_attribute", 5.1); + auto pre_string_add_float_attr = g.GetNodeAttributesFloat("float_to_string_attribute"); + ASSERT_TRUE(pre_string_add_float_attr.size() == g.size()); + + // Add a string as a value to a float attribute, + // which should convert the entire attribute to a string attribute + g.AddNodeAttribute(0, "float_to_string_attribute", "1.5"); + auto string_attrs = g.GetNodeAttributes("float_to_string_attribute"); + ASSERT_TRUE(string_attrs.size() == g.size()); + + // Check that the attribute is no longer a float attribute + auto post_string_add_float_attr = g.GetNodeAttributesFloat("float_to_string_attribute"); + ASSERT_TRUE(post_string_add_float_attr.size() == 0); + } + + TEST(_graph, AddNodeAttributeFloat) + { + Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + const string& attr = "test attribute"; + // Add floats for test attribute + g.AddNodeAttributeFloat(0, attr, 5.1); + g.AddNodeAttributeFloat(1, attr, 24.1); + g.AddNodeAttributeFloat(2, attr, 9); + g.AddNodeAttributeFloat(3, attr, 7.1); + + // Check that test attribute is a float attribute with all nodes... + auto float_attrs = g.GetNodeAttributesFloat(attr); + ASSERT_TRUE(float_attrs.size() == g.size()); + + // And not a string attribute + auto string_attrs = g.GetNodeAttributes(attr); + ASSERT_TRUE(string_attrs.size() == 0); + + } // This just tests that attributes can be added. See GetNodeAttributes // For a test of correctness @@ -1889,13 +1929,38 @@ namespace GraphExampleTests { std::vector ids{ 1, 3, 5, 7 }; std::string attr_type = "cross slope"; std::vector scores{ "1.4", "2.0", "2.8", "4.0" }; - + g.AddNodeAttributes(ids, attr_type, scores); auto attrs = g.GetNodeAttributes(attr_type); ASSERT_TRUE(attrs.size() == g.size()); } + TEST(_graph, AddNodeAttributesFloat) + { + Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + std::vector ids{ 1, 3, 5, 7 }; + std::string attr_type = "test attribute"; + std::vector scores{ 1.4, 2.0, 2.8, 4.0 }; + + g.AddNodeAttributesFloat(ids, attr_type, scores); + + // Check that all values were added to the float map + auto float_attrs = g.GetNodeAttributesFloat(attr_type); + ASSERT_TRUE(float_attrs.size() == g.size()); + + // And not a string map + auto string_attrs = g.GetNodeAttributes(attr_type); + ASSERT_TRUE(string_attrs.size() == 0); + + + } + // If this fails then the values of the returned attributes don't match the input TEST(_graph, GetNodeAttributes) { @@ -1954,6 +2019,112 @@ namespace GraphExampleTests { } } + TEST(_graph, GetNodeAttributesFloat) + { + Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + vector ids = { 0, 3, 4, 8 }; + std::string testattribute = "testattribute"; + g.AddNodeAttributeFloat(0, testattribute, 5.1); + g.AddNodeAttributeFloat(3, testattribute, 7.1); + g.AddNodeAttributeFloat(4, testattribute, 2.3); + g.AddNodeAttributeFloat(8, testattribute, 1.0); + + auto float_attrs = g.GetNodeAttributesFloat(testattribute); + vector expected_scores = { 5.1, 0.0, 0.0, 7.1, 2.3, 0, 0, 0, 1.0 }; + int expected_scores_size = expected_scores.size(); + ASSERT_EQ(float_attrs.size(), expected_scores_size); + + for (int i = 0; i < expected_scores_size; i++) + { + ASSERT_EQ(float_attrs[i], expected_scores[i]); + } + + } + + TEST(_graph, GetNodeAttributesByIDFloat) + { + Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + vector ids = { 0, 3, 4, 8 }; + std::string testattribute = "testattribute"; + g.AddNodeAttributeFloat(0, testattribute, 5.1); + g.AddNodeAttributeFloat(3, testattribute, 7.1); + g.AddNodeAttributeFloat(4, testattribute, 2.3); + g.AddNodeAttributeFloat(8, testattribute, 1.0); + + vector subset_ids = { 0, 4 }; + auto float_attrs = g.GetNodeAttributesByIDFloat(subset_ids, testattribute); + + vector expected_scores = { 5.1, 2.3 }; + int expected_scores_size = expected_scores.size(); + + ASSERT_EQ(float_attrs.size(), expected_scores_size); + + for (int i = 0; i < expected_scores_size; i++) + { + ASSERT_EQ(float_attrs[i], expected_scores[i]); + } + } + + TEST(_graph, AttributeValueMapsCheck) + { + Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + vector ids = { 0, 1, 2, 3, 4, 5, 6, 7, 8}; + vector float_test_attributes = { "float_test_attribute1", "float_test_attribute2", "float_test_attribute3", "float_test_attribute4", "float_test_attribute5"}; + vector string_test_attributes = { "string_test_attribute1", "string_test_attribute2", "string_test_attribute3", "string_test_attribute4", "string_test_attribute5"}; + ASSERT_EQ(float_test_attributes.size(), string_test_attributes.size()); + vector> float_scores = { { 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 }, + { 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0 } }; + vector> string_scores = { { "0.0", "1.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0"}, + { "9.0", "10.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0"} }; + for (int i = 0; i < float_test_attributes.size(); i++) + { + g.AddNodeAttributesFloat(ids, float_test_attributes[i], float_scores[i%2]); + g.AddNodeAttributes(ids, string_test_attributes[i], string_scores[i%2]); + } + + for (int i = 0; i < float_test_attributes.size(); i++) + { + vector float_attrs = g.GetNodeAttributesFloat(float_test_attributes[i]); + ASSERT_TRUE(float_attrs.size() == g.size()); + for (int j = 0; j < g.size(); j++) + { + ASSERT_EQ(float_attrs[j], float_scores[i%2][j]); + } + } + + for (int i = 0; i < string_test_attributes.size(); i++) + { + vector string_attrs = g.GetNodeAttributes(string_test_attributes[i]); + ASSERT_TRUE(string_attrs.size() == g.size()); + for (int j = 0; j < g.size(); j++) + { + ASSERT_EQ(string_attrs[j], string_scores[i%2][j]); + } + } + + for (int i = 0; i < float_test_attributes.size(); i++) + { + vector string_name_float_attrs = g.GetNodeAttributesFloat(string_test_attributes[i]); + vector float_name_string_attrs = g.GetNodeAttributes(float_test_attributes[i]); + ASSERT_TRUE(string_name_float_attrs.size() == 0); + ASSERT_TRUE(float_name_string_attrs.size() == 0); + } + } // Assert that clearing a score from the graph returns an empty array next time // it's called, as the function should gaurantee. TEST(_graph, ClearNodeAttributes) { @@ -2026,6 +2197,29 @@ namespace CInterfaceTests { // Assert that atleast that many attributes were added ASSERT_TRUE(g.GetNodeAttributes(attr_type).size() == g.size()); + // Assert that no float attribute was created + ASSERT_TRUE(g.GetNodeAttributesFloat(attr_type).size() == 0); + } + + TEST(_graphCInterface, AddNodeAttributesFloat) + { + // Create a graph + Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + // Add node attrbutes + std::vector ids{ 1, 3, 5, 7 }; + std::string attr_type = "testattribute"; + const float scores[4] = { 1.4, 2.0, 2.8, 4.0 }; + AddNodeAttributesFloat(&g, ids.data(), attr_type.c_str(), scores, ids.size()); + + // Assert that atleast that many attributes were added + ASSERT_TRUE(g.GetNodeAttributesFloat(attr_type).size() == g.size()); + // Assert that no string attribute was created + ASSERT_TRUE(g.GetNodeAttributes(attr_type).size() == 0); } // Verify that the contents of GetNodeAttributes matches the input to AddNodeAttributes @@ -2098,6 +2292,53 @@ namespace CInterfaceTests { } + TEST(_graphCInterface, GetNodeAttributesFloat) + { + Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + std::vector ids{ 1, 3, 5, 7 }; + std::string attr_type = "testattribute"; + const float scores[4] = { 1.4, 2.0, 2.8, 4.0 }; + + AddNodeAttributesFloat(&g, ids.data(), attr_type.c_str(), scores, ids.size()); + + float* scores_out = new float[g.size()]; + int scores_out_size = 0; + + GetNodeAttributesFloat(&g, attr_type.c_str(), scores_out, &scores_out_size); + ASSERT_EQ(scores_out_size, g.size()); + + for (int i = 0; i < scores_out_size; i++) + { + // Convert score at this index to a float. + float score = scores_out[i]; + + // If it's in our input array, ensure that the score at this value + // matches the one we passed + auto itr = std::find(ids.begin(), ids.end(), i); + if (itr != ids.end()) + { + // Get the index of the id in the scores array so we + // can compare use it to get our input score at that + // index as well. + int index = std::distance(ids.begin(), itr); + + + // Failures at either this or the assert below it could indicate + // problems in AddNodeAttributes as well + ASSERT_EQ(scores[index], score); + } + else + ASSERT_EQ(0.0, score); + + } + + delete[] scores_out; + } TEST(_graphCInterface, GetNodeAttributesByID) { // Create a graph and add edges Graph g; @@ -2140,6 +2381,38 @@ namespace CInterfaceTests { DeleteScoreArray(scores_out, scores_out_size); delete[] scores_out; } + + TEST(_graphCInterface, GetNodeAttributesByIDFloat) + { + // Create a graph and add edges + Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + // Create a vector of node IDs and their corresponding values for our attribute + std::vector ids = { 1, 2, 5, 7, 8 }; + std::string attr = "testattribute"; + const float scores[5] = { 1.0, 2.0, 3.0, 4.0, 5.0 }; + AddNodeAttributesFloat(&g, ids.data(), attr.c_str(), scores, ids.size()); + + std::vector subset_ids = { 1, 5, 7 }; + std::vector expected_scores_out = { 1.0, 3.0, 4.0 }; + int expected_scores_out_size = 3; + float* scores_out = new float[5]; + int scores_out_size = 0; + + GetNodeAttributesByIDFloat(&g, subset_ids.data(), attr.c_str(), subset_ids.size(), scores_out, &scores_out_size); + + ASSERT_EQ(scores_out_size, expected_scores_out_size); + + for (int i = 0; i < scores_out_size; i++) + { + ASSERT_EQ(scores_out[i], expected_scores_out[i]); + } + + } // Verify that deallocating the scores array doesn't corrupt the heap. // The other test cases cover things like Adding and getting node attributes. TEST(_graphCInterface, DeleteScoreArray) { diff --git a/src/Cpp/tests/src/spatialstructures_C_cinterface.cpp b/src/Cpp/tests/src/spatialstructures_C_cinterface.cpp index 480885194..11e4db194 100644 --- a/src/Cpp/tests/src/spatialstructures_C_cinterface.cpp +++ b/src/Cpp/tests/src/spatialstructures_C_cinterface.cpp @@ -856,6 +856,59 @@ namespace CInterfaceTests { } } + TEST(_spatialstructures_cinterface, AddNodeAttributesFloat) { + // Status code variable, value returned by C Interface functions + // See documentation for HF::Exceptions::HF_STATUS for error code definitions. + int status = 0; + + // Declare a pointer to Graph. + // This will point to memory on the free store; + // it will be allocated within CreateGraph. + // It is the caller's responsibility to call DestroyGraph on g. + HF::SpatialStructures::Graph* g = nullptr; + + // The first two parameters are unused, according to documentation. + status = CreateGraph(nullptr, -1, &g); + + //! [snippet_spatialstructuresC_AddNodeAttributesFloat] + // We do not need edge cost type now. + const char* cost_type = ""; + + // Add edges by node IDs. + AddEdgeFromNodeIDs(g, 0, 1, 1, cost_type); + AddEdgeFromNodeIDs(g, 0, 2, 1, cost_type); + AddEdgeFromNodeIDs(g, 1, 3, 1, cost_type); + AddEdgeFromNodeIDs(g, 1, 4, 1, cost_type); + AddEdgeFromNodeIDs(g, 2, 4, 1, cost_type); + AddEdgeFromNodeIDs(g, 3, 5, 1, cost_type); + AddEdgeFromNodeIDs(g, 3, 6, 1, cost_type); + AddEdgeFromNodeIDs(g, 4, 5, 1, cost_type); + AddEdgeFromNodeIDs(g, 5, 6, 1, cost_type); + AddEdgeFromNodeIDs(g, 5, 7, 1, cost_type); + AddEdgeFromNodeIDs(g, 5, 8, 1, cost_type); + AddEdgeFromNodeIDs(g, 4, 8, 1, cost_type); + AddEdgeFromNodeIDs(g, 6, 7, 1, cost_type); + AddEdgeFromNodeIDs(g, 7, 8, 1, cost_type); + + // Add some node attributes + std::vector ids{ 1, 3, 5, 7 }; + std::string attr_type = "cross slope"; + const float scores[4] = { 1.4, 2.0, 2.8, 4.0 }; + + AddNodeAttributesFloat(g, ids.data(), attr_type.c_str(), scores, ids.size()); + //! [snippet_spatialstructuresC_AddNodeAttributesFloat] + // Destroy graph + status = DestroyGraph(g); + + if (status != 1) { + // Error! + std::cerr << "Error at DestroyGraph, code: " << status << std::endl; + } + else { + std::cout << "DestroyGraph ran successfully on address " << g << ", code: " << status << std::endl; + } + } + TEST(_spatialstructures_cinterface, GetNodeAttributes) { //! [snippet_spatialstructuresC_GetNodeAttributes] // Create a graph and add edges @@ -905,6 +958,55 @@ namespace CInterfaceTests { //! [snippet_spatialstructuresC_GetNodeAttributes] } + TEST(_spatialstructures_cinterface, GetNodeAttributesFloat) { + //! [snippet_spatialstructuresC_GetNodeAttributesFloat] + // Create a graph and add edges + HF::SpatialStructures::Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + // Create a vector of node IDs and their corresponding values for our attribute + std::vector ids{ 1, 3, 5, 7 }; + std::string attr_type = "cross slope"; + const float scores[4] = { 1.4, 2.0, 2.8, 4.0 }; + + // Add node attributes to the graph + AddNodeAttributesFloat(&g, ids.data(), attr_type.c_str(), scores, ids.size()); + + // Allocate an array of char arrays to meet the preconditions of GetNodeAttributesFloat + float* scores_out = new float [g.size()]; + int scores_out_size = 0; + + // By the postconditions of GetNodeAttributesFloat, this should update scores_out, + // and scores_out_size with the variables we need + GetNodeAttributesFloat(&g, attr_type.c_str(), scores_out, &scores_out_size); + + // Assert that we can get the scores from this array + for (int i = 0; i < scores_out_size; i++) + { + // Convert score at this index to a float. + float score = scores_out[i]; + + // If it's in our input array, ensure that the score at this value + // matches the one we passed + auto itr = std::find(ids.begin(), ids.end(), i); + if (itr != ids.end()) + { + // Get the index of the id in the scores array so we + // can compare use it to get our input score at that + // index as well. + int index = std::distance(ids.begin(), itr); + } + + } + + // Resource cleanup + delete[] scores_out; + //! [snippet_spatialstructuresC_GetNodeAttributesFloat] + } + TEST(_spatialstructures_cinterface, GetNodeAttributesByID) { //! [snippet_spatialstructuresC_GetNodeAttributesByID] // Create a graph and add edges @@ -920,10 +1022,15 @@ namespace CInterfaceTests { AddNodeAttributes(&g, ids.data(), attr_type.c_str(), scores, ids.size()); - char** scores_out = new char* [ids.size()]; + // Define a list of the node IDs we want attributes for + std::vector subset_ids{ 1, 3 }; + + // Define the output array and an integer to store the actual output size + char** scores_out = new char* [subset_ids.size()]; int scores_out_size = 0; - GetNodeAttributesByID(&g, ids.data(), attr_type.c_str(), ids.size(), scores_out, &scores_out_size); + // Get node attrbutes for the IDs + GetNodeAttributesByID(&g, subset_ids.data(), attr_type.c_str(), subset_ids.size(), scores_out, &scores_out_size); // Assert that we can get the scores from this array for (int i = 0; i < scores_out_size; i++) @@ -943,12 +1050,62 @@ namespace CInterfaceTests { } } - + // Resource cleanup DeleteScoreArray(scores_out, g.size()); //! [snippet_spatialstructuresC_GetNodeAttributesByID] } + TEST(_spatialstructures_cinterface, GetNodeAttributesByIDFloat) { + //! [snippet_spatialstructuresC_GetNodeAttributesByIDFloat] + // Create a graph and add edges + HF::SpatialStructures::Graph g; + g.addEdge(0, 1, 1); g.addEdge(0, 2, 1); g.addEdge(1, 3, 1); g.addEdge(1, 4, 1); + g.addEdge(2, 4, 1); g.addEdge(3, 5, 1); g.addEdge(3, 6, 1); g.addEdge(4, 5, 1); + g.addEdge(5, 6, 1); g.addEdge(5, 7, 1); g.addEdge(5, 8, 1); g.addEdge(4, 8, 1); + g.addEdge(6, 7, 1); g.addEdge(7, 8, 1); + + std::vector ids{ 1, 3, 5, 7 }; + std::string attr_type = "test attribute"; + const float scores[4] = { 29.3, 10.7, 3.5, 18.6 }; + + AddNodeAttributesFloat(&g, ids.data(), attr_type.c_str(), scores, ids.size()); + + // Define a list of the node IDs we want attributes for + std::vector subset_ids{ 1, 3 }; + + // Define the output array and an integer to store the actual output size + float* scores_out = new float[subset_ids.size()]; + int scores_out_size = 0; + + // Get node attrbutes for the IDs + GetNodeAttributesByIDFloat(&g, subset_ids.data(), attr_type.c_str(), subset_ids.size(), scores_out, &scores_out_size); + + // Assert that we can get the scores from this array + for (int i = 0; i < scores_out_size; i++) + { + // Convert score at this index to a float. + float score = scores_out[i]; + + // If it's in our input array, ensure that the score at this value + // matches the one we passed + auto itr = std::find(ids.begin(), ids.end(), i); + if (itr != ids.end()) + { + // Get the index of the id in the scores array so we + // can compare use it to get our input score at that + // index as well. + int index = std::distance(ids.begin(), itr); + } + + } + + // Resource cleanup + delete[] scores_out; + //! [snippet_spatialstructuresC_GetNodeAttributesByIDFloat] + + } + TEST(_spatialstructures_cinterface, DeleteScoreArray) { //! [snippet_spatialstructuresC_DeleteScoreArray] diff --git a/src/Python/dhart/spatialstructures/graph.py b/src/Python/dhart/spatialstructures/graph.py index d540c94ac..530422fff 100644 --- a/src/Python/dhart/spatialstructures/graph.py +++ b/src/Python/dhart/spatialstructures/graph.py @@ -469,7 +469,7 @@ def NumNodes(self) -> int: return spatial_structures_native_functions.C_NumNodes(self.graph_ptr) def add_node_attributes( - self, attribute: str, ids: Union[int, List[int]], scores: Union[str, List[Any]], + self, attribute: str, ids: Union[int, List[int]], scores: Union[List[str], List[float]], ) -> None: """ Add attributes to one or more nodes @@ -481,6 +481,7 @@ def add_node_attributes( Preconditions: 1) IDs in ids must already belong to nodes in the graph 2) The length of scores and ids must match + 3) All values in scores are of the same type Raises: ValueError : the length of ids and scores did not match @@ -497,15 +498,20 @@ def add_node_attributes( >>> csr = g.CompressToCSR() >>> # Add node attributes to the simple graph - >>> attr = "Test" + >>> string_attr = "Strings" + >>> float_attr = "Floats" >>> ids = [0, 1, 2] - >>> scores = ["zero", "one", "two"] - >>> g.add_node_attributes(attr, ids, scores) + >>> string_scores = ["zero", "one", "two"] + >>> float_scores = [0.0, 1.0, 2.0] + >>> g.add_node_attributes(string_attr, ids, string_scores) + >>> g.add_node_attributes(float_attr, ids, float_scores) >>> # To ensure that they've been added properly we will call >>> # get_node_attributes. - >>> g.get_node_attributes(attr) + >>> g.get_node_attributes(string_attr) ['zero', 'one', 'two'] + >>> g.get_node_attributes(float_attr) + [0.0, 1.0, 2.0] """ # Just send it to C++ @@ -513,7 +519,9 @@ def add_node_attributes( self.graph_ptr, attribute, ids, scores ) - def get_node_attributes(self, attribute: str, ids : List[int] | None = None) -> List[str]: + def get_node_attributes( + self, attribute: str, ids : List[int] | None = None + ) -> Union[List[str], List[float]]: """ Get scores of every node for a specific attribute Args: @@ -521,7 +529,7 @@ def get_node_attributes(self, attribute: str, ids : List[int] | None = None) -> ids : Node IDs in the graph to get attributes for, optional Preconditions: - 1) Node IDs in `ids` must already belong to nodes in the graph + 1) Node IDs in ids must already belong to nodes in the graph Returns: A list of strings representing the score of the specified nodes - or @@ -544,21 +552,24 @@ def get_node_attributes(self, attribute: str, ids : List[int] | None = None) -> >>> csr = g.CompressToCSR() >>> # Add node attributes to the simple graph - >>> attr = "Test" + >>> string_attr = "Strings" + >>> float_attr = "Floats" >>> ids = [0, 1, 2] - >>> scores = ["zero", "one", "two"] - >>> g.add_node_attributes(attr, ids, scores) + >>> string_scores = ["zero", "one", "two"] + >>> float_scores = [0.0, 1.0, 2.0] + >>> g.add_node_attributes(string_attr, ids, string_scores) + >>> g.add_node_attributes(float_attr, ids, float_scores) >>> # Get attribute scores from the graph - >>> g.get_node_attributes(attr) + >>> g.get_node_attributes(string_attr) ['zero', 'one', 'two'] >>> # Get attribute scores for specific nodes - >>> g.get_node_attributes(attr, [0]) + >>> g.get_node_attributes(string_attr, [0]) ['zero'] - >>> g.get_node_attributes(attr, [2, 1]) - ['two', 'one'] + >>> g.get_node_attributes(float_attr, [2, 1]) + [2.0, 1.0] """ return spatial_structures_native_functions.c_get_node_attributes( diff --git a/src/Python/dhart/spatialstructures/spatial_structures_native_functions.py b/src/Python/dhart/spatialstructures/spatial_structures_native_functions.py index a7b16dd33..d60e1f0ff 100644 --- a/src/Python/dhart/spatialstructures/spatial_structures_native_functions.py +++ b/src/Python/dhart/spatialstructures/spatial_structures_native_functions.py @@ -1,11 +1,20 @@ from ctypes import * from dhart.Exceptions import * from typing import * +from numpy import ( + ndarray, + float32, + float64, + int32, + int64, + str_ +) from dhart.common_native_functions import ( getDLLHandle, ConvertPointsToArray, ConvertIntsToArray, + ConvertFloatsToArray, convert_strings_to_array, GetStringPtr ) @@ -398,24 +407,45 @@ def c_get_node_attributes( # Define variables to meet preconditions attr_ptr = GetStringPtr(attr) - out_score_type = c_char_p * num_nodes + + # Native function for determining if the data stored for attribute + # is float or string type + is_float = HFPython.IsFloatAttribute(graph_ptr, attr_ptr) + if is_float: + out_score_type = c_float * num_nodes + else: + out_score_type = c_char_p * num_nodes + + # Define other variables out_scores = out_score_type() out_scores_size = c_int(0) - # Call into the function in C++. This will update - # out_scores and out_scores_size + # Convert array to C array if non-null, otherwise leave alone. + # Null check is handled by C interface function if ids is not None: - # convert array to C array if non-null, otherwise leave alone. - # null check is handled by C interface function id_arr = ConvertIntsToArray(ids) num_ids = len(ids) - error_code = HFPython.GetNodeAttributesByID( - graph_ptr, id_arr, attr_ptr, num_ids, byref(out_scores), byref(out_scores_size) - ) + if is_float: + # float and ids => get float values by ID + error_code = HFPython.GetNodeAttributesByIDFloat( + graph_ptr, id_arr, attr_ptr, num_ids, byref(out_scores), byref(out_scores_size) + ) + else: + # not float and ids => get string values by ID + error_code = HFPython.GetNodeAttributesByID( + graph_ptr, id_arr, attr_ptr, num_ids, byref(out_scores), byref(out_scores_size) + ) else: - error_code = HFPython.GetNodeAttributes( - graph_ptr, attr_ptr, byref(out_scores), byref(out_scores_size) - ) + if is_float: + # float and not ids => get all float values + error_code = HFPython.GetNodeAttributesFloat( + graph_ptr, attr_ptr, byref(out_scores), byref(out_scores_size) + ) + else: + # not float and not ids => get all string values + error_code = HFPython.GetNodeAttributes( + graph_ptr, attr_ptr, byref(out_scores), byref(out_scores_size) + ) # This function shouldn't return anything other than OK @@ -426,22 +456,27 @@ def c_get_node_attributes( return [] # Read strings out of pointer and append them to our output - out_strings = [] + out_vals = [] for i in range(0, out_scores_size.value): # .value of a charp reads the string - score = out_scores[i].decode("utf-8") - out_strings.append(score) + if is_float: + score = out_scores[i] + else: + score = out_scores[i].decode("utf-8") + out_vals.append(score) # Deallocate the memory of the strings in C++ - HFPython.DeleteScoreArray(out_scores, out_scores_size) + # Only required if the values were strings + if not is_float: + HFPython.DeleteScoreArray(out_scores, out_scores_size) # Return output - return out_strings + return out_vals def c_add_node_attributes( - graph_ptr: c_void_p, attr: str, ids: List[int], scores: List[str] + graph_ptr: c_void_p, attr: str, ids: List[int], scores: Union[List[str], List[float]] ) -> None: """ Assign or update scores for nodes for a specific attribute @@ -454,21 +489,47 @@ def c_add_node_attributes( to the id in ids at the same index. """ - + # Possible input types for scores + # May need to add more valid types + float_types = (int, float, complex, float32, float64, int32, int64) + string_types = (str, str_) # Convert to CTypes id_arr = ConvertIntsToArray(ids) - score_arr = convert_strings_to_array(scores) attribute_ptr = GetStringPtr(attr) num_nodes = c_int(len(ids)) - - # Call native function - error_code = HFPython.AddNodeAttributes( - graph_ptr, id_arr, attribute_ptr, score_arr, num_nodes - ) + + # Will be used to check if the array is valid + # and can be stored as a float + is_float_iterable = False + # Determine whether this is a normal Python list with lists/floats + if isinstance(scores, List): + is_float_iterable = type(scores[0]) in float_types + # Or a numpy array with any correct dtype + elif isinstance(scores, ndarray): + if scores.dtype in float_types: + is_float_iterable = True + elif scores.dtype in string_types: + is_float_iterable = False + else: + return + + # Convert array based on type, and call native function + if is_float_iterable: + score_arr = ConvertFloatsToArray(scores) + # Call native function + error_code = HFPython.AddNodeAttributesFloat( + graph_ptr, id_arr, attribute_ptr, score_arr, num_nodes + ) + else: + score_arr = convert_strings_to_array(scores) + # Call native function + error_code = HFPython.AddNodeAttributes( + graph_ptr, id_arr, attribute_ptr, score_arr, num_nodes + ) + # Error code should only be OK assert error_code == HF_STATUS.OK - return