Chris@16
|
1 // Copyright (C) 2005-2006 The Trustees of Indiana University.
|
Chris@16
|
2
|
Chris@16
|
3 // Use, modification and distribution is subject to the Boost Software
|
Chris@16
|
4 // License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
|
Chris@16
|
5 // http://www.boost.org/LICENSE_1_0.txt)
|
Chris@16
|
6
|
Chris@16
|
7 // Authors: Douglas Gregor
|
Chris@16
|
8 // Andrew Lumsdaine
|
Chris@16
|
9
|
Chris@16
|
10 //
|
Chris@16
|
11 // Implements redistribution of vertices for a distributed adjacency
|
Chris@16
|
12 // list. This file should not be included by users. It will be
|
Chris@16
|
13 // included by the distributed adjacency list header.
|
Chris@16
|
14 //
|
Chris@16
|
15
|
Chris@16
|
16 #ifndef BOOST_GRAPH_USE_MPI
|
Chris@16
|
17 #error "Parallel BGL files should not be included unless <boost/graph/use_mpi.hpp> has been included"
|
Chris@16
|
18 #endif
|
Chris@16
|
19
|
Chris@16
|
20 #include <boost/pending/container_traits.hpp>
|
Chris@16
|
21
|
Chris@16
|
22 namespace boost { namespace detail { namespace parallel {
|
Chris@16
|
23
|
Chris@16
|
24 /* This structure contains a (vertex or edge) descriptor that is being
|
Chris@16
|
25 moved from one processor to another. It contains the properties for
|
Chris@16
|
26 that descriptor (if any).
|
Chris@16
|
27 */
|
Chris@16
|
28 template<typename Descriptor, typename DescriptorProperty>
|
Chris@16
|
29 struct redistributed_descriptor : maybe_store_property<DescriptorProperty>
|
Chris@16
|
30 {
|
Chris@16
|
31 typedef maybe_store_property<DescriptorProperty> inherited;
|
Chris@16
|
32
|
Chris@16
|
33 redistributed_descriptor() { }
|
Chris@16
|
34
|
Chris@16
|
35 redistributed_descriptor(const Descriptor& v, const DescriptorProperty& p)
|
Chris@16
|
36 : inherited(p), descriptor(v) { }
|
Chris@16
|
37
|
Chris@16
|
38 Descriptor descriptor;
|
Chris@16
|
39
|
Chris@16
|
40 private:
|
Chris@16
|
41 friend class boost::serialization::access;
|
Chris@16
|
42
|
Chris@16
|
43 template<typename Archiver>
|
Chris@16
|
44 void serialize(Archiver& ar, unsigned int /*version*/)
|
Chris@16
|
45 {
|
Chris@16
|
46 ar & boost::serialization::base_object<inherited>(*this)
|
Chris@16
|
47 & unsafe_serialize(descriptor);
|
Chris@16
|
48 }
|
Chris@16
|
49 };
|
Chris@16
|
50
|
Chris@16
|
51 /* Predicate that returns true if the target has migrated. */
|
Chris@16
|
52 template<typename VertexProcessorMap, typename Graph>
|
Chris@16
|
53 struct target_migrated_t
|
Chris@16
|
54 {
|
Chris@16
|
55 typedef typename graph_traits<Graph>::vertex_descriptor Vertex;
|
Chris@16
|
56 typedef typename graph_traits<Graph>::edge_descriptor Edge;
|
Chris@16
|
57
|
Chris@16
|
58 target_migrated_t(VertexProcessorMap vertex_to_processor, const Graph& g)
|
Chris@16
|
59 : vertex_to_processor(vertex_to_processor), g(g) { }
|
Chris@16
|
60
|
Chris@16
|
61 bool operator()(Edge e) const
|
Chris@16
|
62 {
|
Chris@16
|
63 typedef global_descriptor<Vertex> DVertex;
|
Chris@16
|
64 processor_id_type owner = get(edge_target_processor_id, g, e);
|
Chris@16
|
65 return get(vertex_to_processor, DVertex(owner, target(e, g))) != owner;
|
Chris@16
|
66 }
|
Chris@16
|
67
|
Chris@16
|
68 private:
|
Chris@16
|
69 VertexProcessorMap vertex_to_processor;
|
Chris@16
|
70 const Graph& g;
|
Chris@16
|
71 };
|
Chris@16
|
72
|
Chris@16
|
73 template<typename VertexProcessorMap, typename Graph>
|
Chris@16
|
74 inline target_migrated_t<VertexProcessorMap, Graph>
|
Chris@16
|
75 target_migrated(VertexProcessorMap vertex_to_processor, const Graph& g)
|
Chris@16
|
76 { return target_migrated_t<VertexProcessorMap, Graph>(vertex_to_processor, g); }
|
Chris@16
|
77
|
Chris@16
|
78 /* Predicate that returns true if the source of an in-edge has migrated. */
|
Chris@16
|
79 template<typename VertexProcessorMap, typename Graph>
|
Chris@16
|
80 struct source_migrated_t
|
Chris@16
|
81 {
|
Chris@16
|
82 typedef typename graph_traits<Graph>::vertex_descriptor Vertex;
|
Chris@16
|
83 typedef typename graph_traits<Graph>::edge_descriptor Edge;
|
Chris@16
|
84
|
Chris@16
|
85 source_migrated_t(VertexProcessorMap vertex_to_processor, const Graph& g)
|
Chris@16
|
86 : vertex_to_processor(vertex_to_processor), g(g) { }
|
Chris@16
|
87
|
Chris@16
|
88 bool operator()(stored_in_edge<Edge> e) const
|
Chris@16
|
89 {
|
Chris@16
|
90 return get(vertex_to_processor, DVertex(e.source_processor, source(e.e, g)))
|
Chris@16
|
91 != e.source_processor;
|
Chris@16
|
92 }
|
Chris@16
|
93
|
Chris@16
|
94 private:
|
Chris@16
|
95 VertexProcessorMap vertex_to_processor;
|
Chris@16
|
96 const Graph& g;
|
Chris@16
|
97 };
|
Chris@16
|
98
|
Chris@16
|
99 template<typename VertexProcessorMap, typename Graph>
|
Chris@16
|
100 inline source_migrated_t<VertexProcessorMap, Graph>
|
Chris@16
|
101 source_migrated(VertexProcessorMap vertex_to_processor, const Graph& g)
|
Chris@16
|
102 { return source_migrated_t<VertexProcessorMap, Graph>(vertex_to_processor, g); }
|
Chris@16
|
103
|
Chris@16
|
104 /* Predicate that returns true if the target has migrated. */
|
Chris@16
|
105 template<typename VertexProcessorMap, typename Graph>
|
Chris@16
|
106 struct source_or_target_migrated_t
|
Chris@16
|
107 {
|
Chris@16
|
108 typedef typename graph_traits<Graph>::edge_descriptor Edge;
|
Chris@16
|
109
|
Chris@16
|
110 source_or_target_migrated_t(VertexProcessorMap vertex_to_processor,
|
Chris@16
|
111 const Graph& g)
|
Chris@16
|
112 : vertex_to_processor(vertex_to_processor), g(g) { }
|
Chris@16
|
113
|
Chris@16
|
114 bool operator()(Edge e) const
|
Chris@16
|
115 {
|
Chris@16
|
116 return get(vertex_to_processor, source(e, g)) != source(e, g).owner
|
Chris@16
|
117 || get(vertex_to_processor, target(e, g)) != target(e, g).owner;
|
Chris@16
|
118 }
|
Chris@16
|
119
|
Chris@16
|
120 private:
|
Chris@16
|
121 VertexProcessorMap vertex_to_processor;
|
Chris@16
|
122 const Graph& g;
|
Chris@16
|
123 };
|
Chris@16
|
124
|
Chris@16
|
125 template<typename VertexProcessorMap, typename Graph>
|
Chris@16
|
126 inline source_or_target_migrated_t<VertexProcessorMap, Graph>
|
Chris@16
|
127 source_or_target_migrated(VertexProcessorMap vertex_to_processor,
|
Chris@16
|
128 const Graph& g)
|
Chris@16
|
129 {
|
Chris@16
|
130 typedef source_or_target_migrated_t<VertexProcessorMap, Graph> result_type;
|
Chris@16
|
131 return result_type(vertex_to_processor, g);
|
Chris@16
|
132 }
|
Chris@16
|
133
|
Chris@16
|
134 } } // end of namespace detail::parallel
|
Chris@16
|
135
|
Chris@16
|
136 template<PBGL_DISTRIB_ADJLIST_TEMPLATE_PARMS>
|
Chris@16
|
137 template<typename VertexProcessorMap>
|
Chris@16
|
138 void
|
Chris@16
|
139 PBGL_DISTRIB_ADJLIST_TYPE
|
Chris@16
|
140 ::request_in_neighbors(vertex_descriptor v,
|
Chris@16
|
141 VertexProcessorMap vertex_to_processor,
|
Chris@16
|
142 bidirectionalS)
|
Chris@16
|
143 {
|
Chris@16
|
144 BGL_FORALL_INEDGES_T(v, e, *this, graph_type)
|
Chris@16
|
145 request(vertex_to_processor, source(e, *this));
|
Chris@16
|
146 }
|
Chris@16
|
147
|
Chris@16
|
148 template<PBGL_DISTRIB_ADJLIST_TEMPLATE_PARMS>
|
Chris@16
|
149 template<typename VertexProcessorMap>
|
Chris@16
|
150 void
|
Chris@16
|
151 PBGL_DISTRIB_ADJLIST_TYPE
|
Chris@16
|
152 ::remove_migrated_in_edges(vertex_descriptor v,
|
Chris@16
|
153 VertexProcessorMap vertex_to_processor,
|
Chris@16
|
154 bidirectionalS)
|
Chris@16
|
155 {
|
Chris@16
|
156 graph_detail::erase_if(get(vertex_in_edges, base())[v.local],
|
Chris@16
|
157 source_migrated(vertex_to_processor, base()));
|
Chris@16
|
158 }
|
Chris@16
|
159
|
Chris@16
|
160 template<PBGL_DISTRIB_ADJLIST_TEMPLATE_PARMS>
|
Chris@16
|
161 template<typename VertexProcessorMap>
|
Chris@16
|
162 void
|
Chris@16
|
163 PBGL_DISTRIB_ADJLIST_TYPE
|
Chris@16
|
164 ::redistribute(VertexProcessorMap vertex_to_processor)
|
Chris@16
|
165 {
|
Chris@16
|
166 using boost::parallel::inplace_all_to_all;
|
Chris@16
|
167
|
Chris@16
|
168 // When we have stable descriptors, we only move those descriptors
|
Chris@16
|
169 // that actually need to be moved. Otherwise, we essentially have to
|
Chris@16
|
170 // regenerate the entire graph.
|
Chris@16
|
171 const bool has_stable_descriptors =
|
Chris@16
|
172 is_same<typename config_type::vertex_list_selector, listS>::value
|
Chris@16
|
173 || is_same<typename config_type::vertex_list_selector, setS>::value
|
Chris@16
|
174 || is_same<typename config_type::vertex_list_selector, multisetS>::value;
|
Chris@16
|
175
|
Chris@16
|
176 typedef detail::parallel::redistributed_descriptor<vertex_descriptor,
|
Chris@16
|
177 vertex_property_type>
|
Chris@16
|
178 redistributed_vertex;
|
Chris@16
|
179 typedef detail::parallel::redistributed_descriptor<edge_descriptor,
|
Chris@16
|
180 edge_property_type>
|
Chris@16
|
181 redistributed_edge;
|
Chris@16
|
182
|
Chris@16
|
183 vertex_iterator vi, vi_end;
|
Chris@16
|
184 edge_iterator ei, ei_end;
|
Chris@16
|
185
|
Chris@16
|
186 process_group_type pg = process_group();
|
Chris@16
|
187
|
Chris@16
|
188 // Initial synchronization makes sure that we have all of our ducks
|
Chris@16
|
189 // in a row. We don't want any outstanding add/remove messages
|
Chris@16
|
190 // coming in mid-redistribution!
|
Chris@16
|
191 synchronize(process_group_);
|
Chris@16
|
192
|
Chris@16
|
193 // We cannot cope with eviction of ghost cells
|
Chris@16
|
194 vertex_to_processor.set_max_ghost_cells(0);
|
Chris@16
|
195
|
Chris@16
|
196 process_id_type p = num_processes(pg);
|
Chris@16
|
197
|
Chris@16
|
198 // Send vertices and edges to the processor where they will
|
Chris@16
|
199 // actually reside. This requires O(|V| + |E|) communication
|
Chris@16
|
200 std::vector<std::vector<redistributed_vertex> > redistributed_vertices(p);
|
Chris@16
|
201 std::vector<std::vector<redistributed_edge> > redistributed_edges(p);
|
Chris@16
|
202
|
Chris@16
|
203 // Build the sets of relocated vertices for each process and then do
|
Chris@16
|
204 // an all-to-all transfer.
|
Chris@16
|
205 for (boost::tie(vi, vi_end) = vertices(*this); vi != vi_end; ++vi) {
|
Chris@16
|
206 if (!has_stable_descriptors
|
Chris@16
|
207 || get(vertex_to_processor, *vi) != vi->owner) {
|
Chris@16
|
208 redistributed_vertices[get(vertex_to_processor, *vi)]
|
Chris@16
|
209 .push_back(redistributed_vertex(*vi, get(vertex_all_t(), base(),
|
Chris@16
|
210 vi->local)));
|
Chris@16
|
211 }
|
Chris@16
|
212
|
Chris@16
|
213 // When our descriptors are stable, we need to determine which
|
Chris@16
|
214 // adjacent descriptors are stable to determine which edges will
|
Chris@16
|
215 // be removed.
|
Chris@16
|
216 if (has_stable_descriptors) {
|
Chris@16
|
217 BGL_FORALL_OUTEDGES_T(*vi, e, *this, graph_type)
|
Chris@16
|
218 request(vertex_to_processor, target(e, *this));
|
Chris@16
|
219 request_in_neighbors(*vi, vertex_to_processor, directed_selector());
|
Chris@16
|
220 }
|
Chris@16
|
221 }
|
Chris@16
|
222
|
Chris@16
|
223 inplace_all_to_all(pg, redistributed_vertices);
|
Chris@16
|
224
|
Chris@16
|
225 // If we have stable descriptors, we need to know where our neighbor
|
Chris@16
|
226 // vertices are moving.
|
Chris@16
|
227 if (has_stable_descriptors)
|
Chris@16
|
228 synchronize(vertex_to_processor);
|
Chris@16
|
229
|
Chris@16
|
230 // Build the sets of relocated edges for each process and then do
|
Chris@16
|
231 // an all-to-all transfer.
|
Chris@16
|
232 for (boost::tie(ei, ei_end) = edges(*this); ei != ei_end; ++ei) {
|
Chris@16
|
233 vertex_descriptor src = source(*ei, *this);
|
Chris@16
|
234 vertex_descriptor tgt = target(*ei, *this);
|
Chris@16
|
235 if (!has_stable_descriptors
|
Chris@16
|
236 || get(vertex_to_processor, src) != src.owner
|
Chris@16
|
237 || get(vertex_to_processor, tgt) != tgt.owner)
|
Chris@16
|
238 redistributed_edges[get(vertex_to_processor, source(*ei, *this))]
|
Chris@16
|
239 .push_back(redistributed_edge(*ei, split_edge_property(get(edge_all_t(), base(),
|
Chris@16
|
240 ei->local))));
|
Chris@16
|
241 }
|
Chris@16
|
242 inplace_all_to_all(pg, redistributed_edges);
|
Chris@16
|
243
|
Chris@16
|
244 // A mapping from old vertex descriptors to new vertex
|
Chris@16
|
245 // descriptors. This is an STL map partly because I'm too lazy to
|
Chris@16
|
246 // build a real property map (which is hard in the general case) but
|
Chris@16
|
247 // also because it won't try to look in the graph itself, because
|
Chris@16
|
248 // the keys are all vertex descriptors that have been invalidated.
|
Chris@16
|
249 std::map<vertex_descriptor, vertex_descriptor> old_to_new_vertex_map;
|
Chris@16
|
250
|
Chris@16
|
251 if (has_stable_descriptors) {
|
Chris@16
|
252 // Clear out all vertices and edges that will have moved. There
|
Chris@16
|
253 // are several stages to this.
|
Chris@16
|
254
|
Chris@16
|
255 // First, eliminate all outgoing edges from the (local) vertices
|
Chris@16
|
256 // that have been moved or whose targets have been moved.
|
Chris@16
|
257 BGL_FORALL_VERTICES_T(v, *this, graph_type) {
|
Chris@16
|
258 if (get(vertex_to_processor, v) != v.owner) {
|
Chris@16
|
259 clear_out_edges(v.local, base());
|
Chris@16
|
260 clear_in_edges_local(v, directed_selector());
|
Chris@16
|
261 } else {
|
Chris@16
|
262 remove_out_edge_if(v.local,
|
Chris@16
|
263 target_migrated(vertex_to_processor, base()),
|
Chris@16
|
264 base());
|
Chris@16
|
265 remove_migrated_in_edges(v, vertex_to_processor, directed_selector());
|
Chris@16
|
266 }
|
Chris@16
|
267 }
|
Chris@16
|
268
|
Chris@16
|
269 // Next, eliminate locally-stored edges that have migrated (for
|
Chris@16
|
270 // undirected graphs).
|
Chris@16
|
271 graph_detail::erase_if(local_edges_,
|
Chris@16
|
272 source_or_target_migrated(vertex_to_processor, *this));
|
Chris@16
|
273
|
Chris@16
|
274 // Eliminate vertices that have migrated
|
Chris@16
|
275 for (boost::tie(vi, vi_end) = vertices(*this); vi != vi_end; /* in loop */) {
|
Chris@16
|
276 if (get(vertex_to_processor, *vi) != vi->owner)
|
Chris@16
|
277 remove_vertex((*vi++).local, base());
|
Chris@16
|
278 else {
|
Chris@16
|
279 // Add the identity relation for vertices that have not migrated
|
Chris@16
|
280 old_to_new_vertex_map[*vi] = *vi;
|
Chris@16
|
281 ++vi;
|
Chris@16
|
282 }
|
Chris@16
|
283 }
|
Chris@16
|
284 } else {
|
Chris@16
|
285 // Clear out the local graph: the entire graph is in transit
|
Chris@16
|
286 clear();
|
Chris@16
|
287 }
|
Chris@16
|
288
|
Chris@16
|
289 // Add the new vertices to the graph. When we do so, update the old
|
Chris@16
|
290 // -> new vertex mapping both locally and for the owner of the "old"
|
Chris@16
|
291 // vertex.
|
Chris@16
|
292 {
|
Chris@16
|
293 typedef std::pair<vertex_descriptor, vertex_descriptor> mapping_pair;
|
Chris@16
|
294 std::vector<std::vector<mapping_pair> > mappings(p);
|
Chris@16
|
295
|
Chris@16
|
296 for (process_id_type src = 0; src < p; ++src) {
|
Chris@16
|
297 for (typename std::vector<redistributed_vertex>::iterator vi =
|
Chris@16
|
298 redistributed_vertices[src].begin();
|
Chris@16
|
299 vi != redistributed_vertices[src].end(); ++vi) {
|
Chris@16
|
300 vertex_descriptor new_vertex =
|
Chris@16
|
301 add_vertex(vi->get_property(), *this);
|
Chris@16
|
302 old_to_new_vertex_map[vi->descriptor] = new_vertex;
|
Chris@16
|
303 mappings[vi->descriptor.owner].push_back(mapping_pair(vi->descriptor,
|
Chris@16
|
304 new_vertex));
|
Chris@16
|
305 }
|
Chris@16
|
306
|
Chris@16
|
307 redistributed_vertices[src].clear();
|
Chris@16
|
308 }
|
Chris@16
|
309
|
Chris@16
|
310 inplace_all_to_all(pg, mappings);
|
Chris@16
|
311
|
Chris@16
|
312 // Add the mappings we were sent into the old->new map.
|
Chris@16
|
313 for (process_id_type src = 0; src < p; ++src)
|
Chris@16
|
314 old_to_new_vertex_map.insert(mappings[src].begin(), mappings[src].end());
|
Chris@16
|
315 }
|
Chris@16
|
316
|
Chris@16
|
317 // Get old->new vertex mappings for all of the vertices we need to
|
Chris@16
|
318 // know about.
|
Chris@16
|
319
|
Chris@16
|
320 // TBD: An optimization here might involve sending the
|
Chris@16
|
321 // request-response pairs without an explicit request step (for
|
Chris@16
|
322 // bidirectional and undirected graphs). However, it may not matter
|
Chris@16
|
323 // all that much given the cost of redistribution.
|
Chris@16
|
324 {
|
Chris@16
|
325 std::vector<std::vector<vertex_descriptor> > vertex_map_requests(p);
|
Chris@16
|
326 std::vector<std::vector<vertex_descriptor> > vertex_map_responses(p);
|
Chris@16
|
327
|
Chris@16
|
328 // We need to know about all of the vertices incident on edges
|
Chris@16
|
329 // that have been relocated to this processor. Tell each processor
|
Chris@16
|
330 // what each other processor needs to know.
|
Chris@16
|
331 for (process_id_type src = 0; src < p; ++src)
|
Chris@16
|
332 for (typename std::vector<redistributed_edge>::iterator ei =
|
Chris@16
|
333 redistributed_edges[src].begin();
|
Chris@16
|
334 ei != redistributed_edges[src].end(); ++ei) {
|
Chris@16
|
335 vertex_descriptor need_vertex = target(ei->descriptor, *this);
|
Chris@16
|
336 if (old_to_new_vertex_map.find(need_vertex)
|
Chris@16
|
337 == old_to_new_vertex_map.end())
|
Chris@16
|
338 {
|
Chris@16
|
339 old_to_new_vertex_map[need_vertex] = need_vertex;
|
Chris@16
|
340 vertex_map_requests[need_vertex.owner].push_back(need_vertex);
|
Chris@16
|
341 }
|
Chris@16
|
342 }
|
Chris@16
|
343 inplace_all_to_all(pg,
|
Chris@16
|
344 vertex_map_requests,
|
Chris@16
|
345 vertex_map_responses);
|
Chris@16
|
346
|
Chris@16
|
347 // Process the requests made for vertices we own. Then perform yet
|
Chris@16
|
348 // another all-to-all swap. This one matches the requests we've
|
Chris@16
|
349 // made to the responses we were given.
|
Chris@16
|
350 for (process_id_type src = 0; src < p; ++src)
|
Chris@16
|
351 for (typename std::vector<vertex_descriptor>::iterator vi =
|
Chris@16
|
352 vertex_map_responses[src].begin();
|
Chris@16
|
353 vi != vertex_map_responses[src].end(); ++vi)
|
Chris@16
|
354 *vi = old_to_new_vertex_map[*vi];
|
Chris@16
|
355 inplace_all_to_all(pg, vertex_map_responses);
|
Chris@16
|
356
|
Chris@16
|
357 // Matching the requests to the responses, update the old->new
|
Chris@16
|
358 // vertex map for all of the vertices we will need to know.
|
Chris@16
|
359 for (process_id_type src = 0; src < p; ++src) {
|
Chris@16
|
360 typedef typename std::vector<vertex_descriptor>::size_type size_type;
|
Chris@16
|
361 for (size_type i = 0; i < vertex_map_requests[src].size(); ++i) {
|
Chris@16
|
362 old_to_new_vertex_map[vertex_map_requests[src][i]] =
|
Chris@16
|
363 vertex_map_responses[src][i];
|
Chris@16
|
364 }
|
Chris@16
|
365 }
|
Chris@16
|
366 }
|
Chris@16
|
367
|
Chris@16
|
368 // Add edges to the graph by mapping the source and target.
|
Chris@16
|
369 for (process_id_type src = 0; src < p; ++src) {
|
Chris@16
|
370 for (typename std::vector<redistributed_edge>::iterator ei =
|
Chris@16
|
371 redistributed_edges[src].begin();
|
Chris@16
|
372 ei != redistributed_edges[src].end(); ++ei) {
|
Chris@16
|
373 add_edge(old_to_new_vertex_map[source(ei->descriptor, *this)],
|
Chris@16
|
374 old_to_new_vertex_map[target(ei->descriptor, *this)],
|
Chris@16
|
375 ei->get_property(),
|
Chris@16
|
376 *this);
|
Chris@16
|
377 }
|
Chris@16
|
378
|
Chris@16
|
379 redistributed_edges[src].clear();
|
Chris@16
|
380 }
|
Chris@16
|
381
|
Chris@16
|
382 // Be sure that edge-addition messages are received now, completing
|
Chris@16
|
383 // the graph.
|
Chris@16
|
384 synchronize(process_group_);
|
Chris@16
|
385
|
Chris@16
|
386 this->distribution().clear();
|
Chris@16
|
387
|
Chris@16
|
388 detail::parallel::maybe_initialize_vertex_indices(vertices(base()),
|
Chris@16
|
389 get(vertex_index, base()));
|
Chris@16
|
390 }
|
Chris@16
|
391
|
Chris@16
|
392 } // end namespace boost
|