MUQ  0.4.3
WorkGraphPiece.cpp
Go to the documentation of this file.
2 
4 
5 #include <boost/range/adaptor/reversed.hpp>
6 
7 #include <boost/graph/topological_sort.hpp>
8 
9 #include <Eigen/Core>
10 
11 using namespace muq::Modeling;
12 
14 
15 UpstreamPredicate::UpstreamPredicate(boost::graph_traits<Graph>::vertex_descriptor const& baseNode, Graph const& graph) {
16  UpstreamNodes(baseNode, graph);
17 }
18 
19 bool UpstreamPredicate::operator()(const boost::graph_traits<Graph>::vertex_descriptor& node) const {
20  // is the node in the vector of depends?
21  return std::find(doesDepend.begin(), doesDepend.end(), node)!=doesDepend.end();
22 }
23 
24 void UpstreamPredicate::UpstreamNodes(const boost::graph_traits<Graph>::vertex_descriptor& baseNode, Graph const& graph) {
25 
26  // add this node to the list of downstream nodes
27  doesDepend.push_back(baseNode);
28 
29  // loop through its dependencies
30  boost::graph_traits<Graph>::in_edge_iterator e, e_end;
31  boost::tie(e, e_end) = boost::in_edges(baseNode, graph);
32  for( ; e!=e_end; ++e ) {
33  // recursivley add its dependendcies to the downstream node list
34  UpstreamNodes(boost::target(*e, graph), graph);
35  }
36 }
37 
38 
40 
41 DependentPredicate::DependentPredicate(boost::graph_traits<Graph>::vertex_descriptor const& baseNode, Graph const& graph) {
42  DownstreamNodes(baseNode, graph);
43 }
44 
45 bool DependentPredicate::operator()(const boost::graph_traits<Graph>::vertex_descriptor& node) const {
46  // is the node in the vector of depends?
47  return std::find(doesDepend.begin(), doesDepend.end(), node)!=doesDepend.end();
48 }
49 
50 void DependentPredicate::DownstreamNodes(const boost::graph_traits<Graph>::vertex_descriptor& baseNode, Graph const& graph) {
51 
52  // add this node to the list of downstream nodes
53  doesDepend.push_back(baseNode);
54 
55  // loop through its dependencies
56  boost::graph_traits<Graph>::out_edge_iterator e, e_end;
57  boost::tie(e, e_end) = boost::out_edges(baseNode, graph);
58  for( ; e!=e_end; ++e ) {
59  // recursivley add its dependendcies to the downstream node list
60  DownstreamNodes(boost::target(*e, graph), graph);
61  }
62 }
63 
65 
66 DependentEdgePredicate::DependentEdgePredicate(DependentPredicate nodePred, Graph const& graph) : nodePred(nodePred), graph(&graph) {}
67 
68 bool DependentEdgePredicate::operator()(const boost::graph_traits<Graph>::edge_descriptor& edge) const {
69 
70  // check to see if the source is a downstream node
71  return nodePred(source(edge, *graph));
72 }
73 
74 
75 WorkGraphPiece::WorkGraphPiece(std::shared_ptr<WorkGraph> wgraph,
76  std::vector<std::shared_ptr<ConstantPiece> > const& constantPieces,
77  std::vector<std::string> const& inputNames,
78  std::map<unsigned int, std::string> const& inTypes,
79  std::shared_ptr<WorkPiece> outputPiece) : WorkPiece(inTypes, constantPieces.size(), outputPiece->OutputTypes(), outputPiece->numOutputs), wgraph(wgraph), outputID(outputPiece->ID()), constantPieces(constantPieces) {
80 
81  // build the run order
82  boost::topological_sort(wgraph->graph, std::front_inserter(runOrder));
83 
84  // make sure we know the number of inputs
85  assert(numInputs>=0);
86 
87  // each input only needs to loop over its downstream nodes when computing derivatives
88  derivRunOrders.resize(numInputs);
89  filtered_graphs.resize(numInputs);
90 
91  // compute a run order for each of the inputs so we only have to loop over their downstream nodes
92  assert(numInputs==inputNames.size());
93  for( unsigned int i=0; i<numInputs; ++i ) { // loop through the inputs
94  // get iterators to the begining and end of the graph
95  boost::graph_traits<Graph>::vertex_iterator v, v_end;
96  boost::tie(v, v_end) = vertices(wgraph->graph);
97 
98  // determine the downstream nodes of this input
99  DependentPredicate nFilt(*std::find_if(v, v_end, NodeNameFinder(inputNames[i], wgraph->graph)), wgraph->graph);
100  DependentEdgePredicate eFilt(nFilt, wgraph->graph);
101 
102  // filter the graph, we only care about downstream nodes of this input
103  filtered_graphs[i] = std::make_shared<boost::filtered_graph<Graph, DependentEdgePredicate, DependentPredicate> >(wgraph->graph, eFilt, nFilt);
104 
105  // build specialized run order for each input dimension
106  boost::topological_sort(*filtered_graphs[i], std::back_inserter(derivRunOrders[i]));
107  }
108 }
109 
111 
113 
114  // set the inputs
115  SetInputs(inputs);
116 
117  // fill the map from the WorkPiece ID to its outputs
118  OutputMap();
119 
120  // store the result in the output vector
121  outputs.resize(valMap[outputID].size());
122  for(int i=0; i<outputs.size(); ++i) {
123  outputs.at(i) = valMap[outputID].at(i).get();
124  }
125 }
126 
127 // void WorkGraphPiece::JacobianImpl(unsigned int const wrtIn, unsigned int const wrtOut, ref_vector<boost::any> const& inputs) {
128 // // set the inputs
129 // SetInputs(inputs);
130 //
131 // // fill the map from the WorkPiece ID to its outputs
132 // OutputMap();
133 //
134 // // a map from the WorkPiece ID a map from the output number to the jacobian of that output wrt the specified input
135 // std::map<unsigned int, std::map<unsigned int, boost::any> > jacMap;
136 //
137 // // loop through each downstream node
138 // for( auto node : boost::adaptors::reverse(derivRunOrders[wrtIn]) ) {
139 // // the ID of the current node
140 // const unsigned int nodeID = filtered_graphs[wrtIn]->operator[](node)->piece->ID();
141 //
142 // // get the outputs of this node that depend on the specified input
143 // const std::vector<std::tuple<unsigned int, unsigned int, unsigned int> > & requiredOutNodes = RequiredOutputs(node, wrtIn, wrtOut);
144 // // remove duplicates
145 // std::vector<unsigned int> requiredOuts;
146 // requiredOuts.reserve(requiredOutNodes.size());
147 // for( auto out : requiredOutNodes ) {
148 // auto it = std::find(requiredOuts.begin(), requiredOuts.end(), std::get<1>(out));
149 // if( it==requiredOuts.end() ) {
150 // requiredOuts.push_back(std::get<1>(out));
151 // }
152 // }
153 //
154 // // get the inputs for this node --- the input WorkPiece ID, the output number, and the input number
155 // const std::vector<std::tuple<unsigned int, unsigned int, unsigned int> >& requiredIns = RequiredInputs(node, wrtIn);
156 //
157 // // the inputs to this WorkPiece
158 // const ref_vector<boost::any>& ins = Inputs(node);
159 //
160 // // compute the jacobian of each required output wrt each input
161 // for( auto out : requiredOuts ) {
162 // if( requiredIns.size()==0 ) {
163 // // if there are no inputs, it is the input so set the Jacobian to the identity
164 // jacMap[nodeID][out] = algebra->Identity(valMap[nodeID][out].get().type(), algebra->Size(valMap[nodeID][out]), algebra->Size(valMap[nodeID][out]));
165 // } else {
166 // // initize the jacobian to nothing
167 // jacMap[nodeID][out] = boost::none;
168 //
169 // for( auto in : requiredIns ) {
170 // // compute the Jacobian with respect to each required input
171 // graph->operator[](node)->piece->Jacobian(std::get<2>(in), out, ins);
172 //
173 // // use chain rule to get the jacobian wrt to the required input
174 // const boost::any tempJac = algebra->Multiply(*(graph->operator[](node)->piece->jacobian), jacMap[std::get<0>(in)][std::get<1>(in)]);
175 // jacMap[nodeID][out] = algebra->Add(jacMap[nodeID][out], tempJac);
176 // }
177 // }
178 // }
179 // }
180 //
181 // // set the Jacobian for this WorkPiece
182 // jacobian = jacMap[outputID][wrtOut];
183 // }
184 //
185 // void WorkGraphPiece::JacobianActionImpl(unsigned int const wrtIn, unsigned int const wrtOut, boost::any const& vec, ref_vector<boost::any> const& inputs) {
186 // // set the inputs
187 // SetInputs(inputs);
188 //
189 // // fill the map from the WorkPiece ID to its outputs
190 // OutputMap();
191 //
192 // // a map from the WorkPiece ID a map from the output number to the action of the jacobian of that output wrt the specified input
193 // std::map<unsigned int, std::map<unsigned int, boost::any> > jacActionMap;
194 //
195 // // loop through each downstream node
196 // for( auto node : boost::adaptors::reverse(derivRunOrders[wrtIn]) ) {
197 // // the ID of the current node
198 // const unsigned int nodeID = filtered_graphs[wrtIn]->operator[](node)->piece->ID();
199 //
200 // // get the outputs of this node that depend on the specified input
201 // const std::vector<std::tuple<unsigned int, unsigned int, unsigned int> >& requiredOutNodes = RequiredOutputs(node, wrtIn, wrtOut);
202 // // remove duplicates
203 // std::vector<unsigned int> requiredOuts;
204 // requiredOuts.reserve(requiredOutNodes.size());
205 // for( auto out : requiredOutNodes ) {
206 // auto it = std::find(requiredOuts.begin(), requiredOuts.end(), std::get<1>(out));
207 // if( it==requiredOuts.end() ) {
208 // requiredOuts.push_back(std::get<1>(out));
209 // }
210 // }
211 //
212 // // get the inputs for this node --- the input WorkPiece ID, the output number, and the input number
213 // const std::vector<std::tuple<unsigned int, unsigned int, unsigned int> >& requiredIns = RequiredInputs(node, wrtIn);
214 //
215 // // the inputs to this WorkPiece
216 // const ref_vector<boost::any>& ins = Inputs(node);
217 //
218 // // compute the jacobian of each required output wrt each input
219 // for( auto out : requiredOuts ) {
220 // if( requiredIns.size()==0 ) {
221 // // if there are no inputs, it is the input so set the Jacobian to the identity
222 // jacActionMap[nodeID][out] = vec;
223 // } else {
224 // // initize the jacobian to nothing
225 // jacActionMap[nodeID][out] = boost::none;
226 //
227 // for( auto in : requiredIns ) {
228 // // compute the Jacobian with respect to each required input
229 // graph->operator[](node)->piece->JacobianAction(std::get<2>(in), out, jacActionMap[std::get<0>(in)][std::get<1>(in)], ins);
230 //
231 // // use chain rule to get the jacobian wrt to the required input
232 // jacActionMap[nodeID][out] = algebra->Add(jacActionMap[nodeID][out], *(graph->operator[](node)->piece->jacobianAction));
233 // }
234 // }
235 // }
236 // }
237 //
238 // // set the action of the Jacobian for this WorkPiece
239 // jacobianAction = jacActionMap[outputID][wrtOut];
240 // }
241 //
242 // void WorkGraphPiece::JacobianTransposeActionImpl(unsigned int const wrtIn, unsigned int const wrtOut, boost::any const& vec, ref_vector<boost::any> const& inputs) {
243 // // set the inputs
244 // SetInputs(inputs);
245 //
246 // // fill the map from the WorkPiece ID to its outputs
247 // OutputMap();
248 //
249 // // a map from the WorkPiece ID a map from the output number to the action of the jacobian of that output wrt the specified input
250 // std::map<unsigned int, std::map<unsigned int, boost::any> > jacTransActionMap;
251 //
252 // // loop through each downstream node
253 // for( auto node : derivRunOrders[wrtIn] ) {
254 // // the ID of the current node
255 // const unsigned int nodeID = filtered_graphs[wrtIn]->operator[](node)->piece->ID();
256 //
257 // // get the outputs of this node that depend on the specified input
258 // const std::vector<std::tuple<unsigned int, unsigned int, unsigned int> >& requiredOuts = RequiredOutputs(node, wrtIn, wrtOut);
259 //
260 // // get the inputs for this node --- the input WorkPiece ID, the output number, and the input number
261 // const std::vector<std::tuple<unsigned int, unsigned int, unsigned int> >& requiredIns = RequiredInputs(node, wrtIn);
262 //
263 // // the inputs to this WorkPiece
264 // const ref_vector<boost::any>& ins = Inputs(node);
265 //
266 // for( auto in : requiredIns ) {
267 // if( nodeID==outputID ) {
268 // assert(requiredOuts.size()==1);
269 // assert(std::get<1>(requiredOuts[0])==wrtOut);
270 //
271 // // compute the Jacobian transpose action of the output node
272 // graph->operator[](node)->piece->JacobianTransposeAction(std::get<2>(in), wrtOut, vec, ins);
273 // jacTransActionMap[nodeID][std::get<2>(in)] = *(graph->operator[](node)->piece->jacobianTransposeAction);
274 // } else {
275 // // initialize the jacobian transpose action to nothing
276 // jacTransActionMap[nodeID][std::get<2>(in)] = boost::none;
277 //
278 // // loop through the outputs
279 // for( auto out : requiredOuts ) {
280 // // compute the jacobian transpose action for this output
281 // graph->operator[](node)->piece->JacobianTransposeAction(std::get<2>(in), std::get<1>(out), jacTransActionMap[std::get<0>(out)][std::get<2>(out)], ins);
282 // // add it (chain rule)
283 // jacTransActionMap[nodeID][std::get<2>(in)] = algebra->Add(jacTransActionMap[nodeID][std::get<2>(in)], *(graph->operator[](node)->piece->jacobianTransposeAction));
284 // }
285 // }
286 // }
287 //
288 // // if this is the input node
289 // if( requiredIns.size()==0 ) {
290 // // loop though the outputs
291 // for( auto out : requiredOuts ) {
292 // if( jacobianTransposeAction ) { // if the jacobian transpose action has not be initilized ...
293 // // it is equal to the action of the output
294 // *jacobianTransposeAction = algebra->Add(*jacobianTransposeAction, jacTransActionMap[std::get<0>(out)][std::get<1>(out)]);
295 // } else {
296 // // add it to the existing jacobian transpose action (chain rule)
297 // jacobianTransposeAction = jacTransActionMap[std::get<0>(out)][std::get<1>(out)];
298 // }
299 // }
300 // }
301 // }
302 // }
303 
305  // get the inputs and set them to the ConstantPiece nodes
306  assert(inputs.size()==constantPieces.size());
307  for( unsigned int i=0; i<inputs.size(); ++i ) {
308  constantPieces[i]->SetOutputs(inputs[i]);
309  }
310 }
311 
312 std::map<unsigned int, std::vector<std::pair<unsigned int, unsigned int> > > WorkGraphPiece::InputNodes(boost::graph_traits<Graph>::vertex_descriptor const& node) const {
313  // the map of input nodes
314  std::map<unsigned int, std::vector<std::pair<unsigned int, unsigned int> > > inMap;
315 
316  // loop though the input nodes
317  boost::graph_traits<Graph>::in_edge_iterator e, e_end;
318  for( tie(e, e_end)=boost::in_edges(node, wgraph->graph); e!=e_end; ++e ) {
319  // get the WorkPiece id number, the output that it supplies, and the input that receives it
320  const unsigned int id = wgraph->GetPiece(boost::source(*e, wgraph->graph))->ID();
321  const unsigned int inNum = wgraph->graph[*e]->inputDim;
322  const unsigned int outNum = wgraph->graph[*e]->outputDim;
323 
324  // try to find the WorkPiece in the other upstream nodes
325  auto it = inMap.find(id);
326 
327  if( it==inMap.end() ) { // if we have not yet needed this WorkPiece ...
328  // ... add it to the list and store the input/output pair
329  inMap[id] = std::vector<std::pair<unsigned int, unsigned int> >(1, std::pair<unsigned int, unsigned int>(inNum, outNum));
330  } else { // we have needed this WorkPiece
331  // ... add the input/output pair
332  inMap[id].push_back(std::pair<unsigned int, unsigned int>(inNum, outNum));
333  }
334  }
335 
336  return inMap;
337 }
338 
340  // clear the map from the WorkPiece ID to its outputs
341  valMap.clear();
342 
343  // loop over the run order
344  for( auto it : runOrder ) {
345  // the inputs to this WorkPiece
346  const ref_vector<boost::any>& ins = Inputs(it);
347 
348  // evaluate the current map and store the value
349  std::vector<boost::any> const& output = wgraph->GetPiece(it)->Evaluate(ins);
350  valMap[wgraph->GetPiece(it)->ID()] = ToRefVector(output);
351  }
352 }
353 
354 ref_vector<boost::any> WorkGraphPiece::Inputs(boost::graph_traits<Graph>::vertex_descriptor node) const {
355  // how many inputs does this node require?
356  const int numIns = wgraph->GetPiece(node)->numInputs;
357 
358  // get the inputs for this node
359  const std::map<unsigned int, std::vector<std::pair<unsigned int, unsigned int> > >& inMap = InputNodes(node);
360 
361  boost::any empty = boost::none;
362  ref_vector<boost::any> ins(numIns, std::cref(empty));
363 
364  // loop through the edges again, now we know which outputs supply which inputs
365  for( auto edge : inMap ) {
366  // loop over the input/output pairs supplied by this input
367  for( auto in_out : edge.second ) {
368  // use at instead of operator[] because this is a const function
369  ins[in_out.first] = valMap.at(edge.first)[in_out.second];
370  }
371  }
372 
373  return ins;
374 }
375 
376 std::vector<std::tuple<unsigned int, unsigned int, unsigned int> > WorkGraphPiece::RequiredOutputs(boost::graph_traits<FilteredGraph>::vertex_descriptor const& node, unsigned int const wrtIn, unsigned int const wrtOut) const {
377  // the ID of the current node
378  const unsigned int nodeID = filtered_graphs[wrtIn]->operator[](node)->piece->ID();
379 
380  // get the outputs of this node that depend on the specified input
381  std::vector<std::tuple<unsigned int, unsigned int, unsigned int> > requiredOuts;
382 
383  if( nodeID==outputID ) { // if it is the output node ...
384  // ... the user specifies the output derivative
385  requiredOuts.push_back(std::tuple<unsigned int, unsigned int, unsigned int>(nodeID, wrtOut, wrtIn));
386 
387  return requiredOuts;
388  }
389 
390  // loop though the output nodes
391  boost::graph_traits<FilteredGraph>::out_edge_iterator eout, eout_end;
392  for( tie(eout, eout_end)=boost::out_edges(node, *filtered_graphs[wrtIn]); eout!=eout_end; ++eout ) {
393  // get the output number
394  const unsigned int id = wgraph->GetPiece(boost::target(*eout, *filtered_graphs[wrtIn]))->ID();
395  const unsigned int outNum = filtered_graphs[wrtIn]->operator[](*eout)->outputDim;
396  const unsigned int inNum = filtered_graphs[wrtIn]->operator[](*eout)->inputDim;
397 
398  // if we have not already required this output, save it
399  auto it = std::find(requiredOuts.begin(), requiredOuts.end(), std::tuple<unsigned int, unsigned int, unsigned int>(id, outNum, inNum));
400  if( it==requiredOuts.end() ) {
401  requiredOuts.push_back(std::tuple<unsigned int, unsigned int, unsigned int>(id, outNum, inNum));
402  }
403  }
404 
405  return requiredOuts;
406 }
407 
408 std::vector<std::tuple<unsigned int, unsigned int, unsigned int> > WorkGraphPiece::RequiredInputs(boost::graph_traits<FilteredGraph>::vertex_descriptor const& node, unsigned int const wrtIn) const {
409  // how many inputs does this node require?
410  const int numIns = filtered_graphs[wrtIn]->operator[](node)->piece->numInputs;
411 
412  std::vector<std::tuple<unsigned int, unsigned int, unsigned int> > requiredIns;
413  requiredIns.reserve(numIns);
414 
415  // loop though the output nodes
416  boost::graph_traits<FilteredGraph>::in_edge_iterator ein, ein_end;
417  for( tie(ein, ein_end)=boost::in_edges(node, *filtered_graphs[wrtIn]); ein!=ein_end; ++ein ) {
418  // get the WorkPiece id number, the output that it supplies, and the input that receives it
419  const unsigned int id = wgraph->GetPiece(boost::source(*ein, *filtered_graphs[wrtIn]))->ID();
420  const unsigned int outNum = filtered_graphs[wrtIn]->operator[](*ein)->outputDim;
421  const unsigned int inNum = filtered_graphs[wrtIn]->operator[](*ein)->inputDim;
422 
423  // store the requried input
424  requiredIns.push_back(std::tuple<unsigned int, unsigned int, unsigned int>(id, outNum, inNum));
425  }
426 
427  return requiredIns;
428 }
Determine if the source of an edge is downstream of an input.
bool operator()(const boost::graph_traits< Graph >::edge_descriptor &edge) const
DependentPredicate nodePred
The nodes that are downstream of the input.
DependentEdgePredicate()
Required default constructor.
const Graph * graph
The graph holding all the nodes.
This class keeps track of which nodes are downstream of a specified input.
bool operator()(const boost::graph_traits< Graph >::vertex_descriptor &node) const
DependentPredicate()
Required default constructor.
void DownstreamNodes(const boost::graph_traits< Graph >::vertex_descriptor &baseNode, Graph const &graph)
std::vector< boost::graph_traits< Graph >::vertex_descriptor > doesDepend
A vector of all the nodes downstream of the input node.
bool operator()(const boost::graph_traits< Graph >::vertex_descriptor &node) const
std::vector< boost::graph_traits< Graph >::vertex_descriptor > doesDepend
A vector of all the nodes downstream of the input node.
void UpstreamNodes(const boost::graph_traits< Graph >::vertex_descriptor &baseNode, Graph const &graph)
std::vector< std::tuple< unsigned int, unsigned int, unsigned int > > RequiredOutputs(boost::graph_traits< FilteredGraph >::vertex_descriptor const &node, unsigned int const wrtIn, unsigned int wrtOut) const
Get the required outputs for a node in one of the filtered graphs.
std::map< unsigned int, std::vector< std::pair< unsigned int, unsigned int > > > InputNodes(boost::graph_traits< Graph >::vertex_descriptor const &node) const
Get a the input nodes for a node.
void SetInputs(ref_vector< boost::any > const &inputs)
Set the inputs.
virtual void EvaluateImpl(ref_vector< boost::any > const &inputs) override
Evaluate each muq::Modeling::WorkPiece in the graph.
void OutputMap()
Fill the map from each node's muq::Modeling::WorkPiece::ID to its outputs.
virtual ~WorkGraphPiece()
Default destructor.
unsigned int outputID
The ID of the WorkPiece corresponding to the output node.
std::vector< std::shared_ptr< FilteredGraph > > filtered_graphs
std::vector< std::tuple< unsigned int, unsigned int, unsigned int > > RequiredInputs(boost::graph_traits< FilteredGraph >::vertex_descriptor const &node, unsigned int const wrtIn) const
Get the required inputs for a node in one of the filtered graphs.
std::unordered_map< unsigned int, ref_vector< boost::any > > valMap
A the map from each node's muq::Modeling::WorkPiece::ID to its outputs.
std::shared_ptr< WorkGraph > wgraph
The WorkGraph associated with this WorkGraphPiece.
std::deque< boost::graph_traits< Graph >::vertex_descriptor > runOrder
Run order computed during construction (input->output order)
std::vector< std::shared_ptr< ConstantPiece > > constantPieces
The muq::Modeling::ConstantPiece's that store the inputs.
std::vector< std::deque< boost::graph_traits< Graph >::vertex_descriptor > > derivRunOrders
Base class for MUQ's modelling envronment.
Definition: WorkPiece.h:40
const unsigned int id
A unique ID number assigned by the constructor.
Definition: WorkPiece.h:580
static ref_vector< const boost::any > ToRefVector(std::vector< boost::any > const &anyVec)
Create vector of references from a vector of boost::any's.
Definition: WorkPiece.cpp:675
unsigned int ID() const
Get the unique ID number.
Definition: WorkPiece.cpp:651
friend class WorkGraphPiece
Definition: WorkPiece.h:43
@ Inputs
The constructor fixes the input number and possibly the types.
Definition: WorkPiece.h:51
std::vector< boost::any > outputs
The outputs.
Definition: WorkPiece.h:546
int numInputs
The number of inputs.
Definition: WorkPiece.h:501
std::vector< std::reference_wrapper< const T > > ref_vector
A vector of references to something ...
Definition: WorkPiece.h:37
boost::adjacency_list< boost::vecS, boost::vecS, boost::bidirectionalS, std::shared_ptr< WorkGraphNode >, std::shared_ptr< WorkGraphEdge > > Graph
Define a directed graph type.
int int diyfp diyfp v
Definition: json.h:15163
A helper struct that determines if a node in the graph has a given name.