88 } |
92 } |
89 }; |
93 }; |
90 } |
94 } |
91 } |
95 } |
92 |
96 |
93 enum MappingType { |
97 ///Graph mapping types. |
|
98 |
|
99 ///\ingroup graph_isomorphism |
|
100 ///The \ref Vf2 "VF2" algorithm is capable of finding different kind of |
|
101 ///embeddings, this enum specifies its type. |
|
102 /// |
|
103 ///See \ref graph_isomorphism for a more detailed description. |
|
104 enum Vf2MappingType { |
|
105 /// Subgraph isomorphism |
94 SUBGRAPH = 0, |
106 SUBGRAPH = 0, |
|
107 /// Induced subgraph isomorphism |
95 INDUCED = 1, |
108 INDUCED = 1, |
|
109 /// Graph isomorphism |
|
110 |
|
111 /// If the two graph has the same number of nodes, than it is |
|
112 /// equivalent to \ref INDUCED, and if they also have the same |
|
113 /// number of edges, then it is also equivalent to \ref SUBGRAPH. |
|
114 /// |
|
115 /// However, using this setting is faster than the other two |
|
116 /// options. |
96 ISOMORPH = 2 |
117 ISOMORPH = 2 |
97 }; |
118 }; |
98 |
119 |
99 template<class G1, class G2, class I, class NEq = bits::vf2::AlwaysEq > |
120 ///%VF2 algorithm class. |
|
121 |
|
122 ///\ingroup graph_isomorphism This class provides an efficient |
|
123 ///implementation of the %VF2 algorithm \cite cordella2004sub |
|
124 ///for variants of the (Sub)graph Isomorphism problem. |
|
125 /// |
|
126 ///There is also a \ref vf2() "function-type interface" called \ref vf2() |
|
127 ///for the %VF2 algorithm, which is probably more convenient in most |
|
128 ///use-cases. |
|
129 /// |
|
130 ///\tparam G1 The type of the graph to be embedded. |
|
131 ///The default type is \ref ListDigraph. |
|
132 ///\tparam G2 The type of the graph g1 will be embedded into. |
|
133 ///The default type is \ref ListDigraph. |
|
134 ///\tparam M The type of the NodeMap storing the mapping. |
|
135 ///By default, it is G1::NodeMap<G2::Node> |
|
136 ///\tparam NEQ A bool-valued binary functor determinining whether a node is |
|
137 ///mappable to another. By default it is an always true operator. |
|
138 /// |
|
139 ///\sa vf2() |
|
140 #ifdef DOXYGEN |
|
141 template<class G1, class G2, class M, class NEQ > |
|
142 #else |
|
143 template<class G1=ListDigraph, |
|
144 class G2=ListDigraph, |
|
145 class M = typename G1::template NodeMap<G2::Node>, |
|
146 class NEQ = bits::vf2::AlwaysEq > |
|
147 #endif |
100 class Vf2 |
148 class Vf2 |
101 { |
149 { |
102 //Current depth in the DFS tree. |
150 //Current depth in the DFS tree. |
103 int _depth; |
151 int _depth; |
104 //Functor with bool operator()(G1::Node,G2::Node), which returns 1 |
152 //Functor with bool operator()(G1::Node,G2::Node), which returns 1 |
105 //if and only if the 2 nodes are equivalent. |
153 //if and only if the 2 nodes are equivalent. |
106 NEq _nEq; |
154 NEQ _nEq; |
107 |
155 |
108 typename G2::template NodeMap<int> _conn; |
156 typename G2::template NodeMap<int> _conn; |
109 //Current matching. We index it by the nodes of g1, and match[v] is |
157 //Current mapping. We index it by the nodes of g1, and match[v] is |
110 //a node of g2. |
158 //a node of g2. |
111 I &_match; |
159 M &_mapping; |
112 //order[i] is the node of g1, for which we find a pair if depth=i |
160 //order[i] is the node of g1, for which we find a pair if depth=i |
113 std::vector<typename G1::Node> order; |
161 std::vector<typename G1::Node> order; |
114 //currEdgeIts[i] is an edge iterator, witch is last used in the ith |
162 //currEdgeIts[i] is an edge iterator, witch is last used in the ith |
115 //depth to find a pair for order[i]. |
163 //depth to find a pair for order[i]. |
116 std::vector<typename G2::IncEdgeIt> currEdgeIts; |
164 std::vector<typename G2::IncEdgeIt> currEdgeIts; |
119 //The big graph. |
167 //The big graph. |
120 const G2 &_g2; |
168 const G2 &_g2; |
121 //lookup tables for cut the searchtree |
169 //lookup tables for cut the searchtree |
122 typename G1::template NodeMap<int> rNew1t,rInOut1t; |
170 typename G1::template NodeMap<int> rNew1t,rInOut1t; |
123 |
171 |
124 MappingType _mapping_type; |
172 Vf2MappingType _mapping_type; |
125 |
173 |
126 //cut the search tree |
174 //cut the search tree |
127 template<MappingType MT> |
175 template<Vf2MappingType MT> |
128 bool cut(const typename G1::Node n1,const typename G2::Node n2) const |
176 bool cut(const typename G1::Node n1,const typename G2::Node n2) const |
129 { |
177 { |
130 int rNew2=0,rInOut2=0; |
178 int rNew2=0,rInOut2=0; |
131 for(typename G2::IncEdgeIt e2(_g2,n2); e2!=INVALID; ++e2) |
179 for(typename G2::IncEdgeIt e2(_g2,n2); e2!=INVALID; ++e2) |
132 { |
180 { |
147 default: |
195 default: |
148 return false; |
196 return false; |
149 } |
197 } |
150 } |
198 } |
151 |
199 |
152 template<MappingType MT> |
200 template<Vf2MappingType MT> |
153 bool feas(const typename G1::Node n1,const typename G2::Node n2) |
201 bool feas(const typename G1::Node n1,const typename G2::Node n2) |
154 { |
202 { |
155 if(!_nEq(n1,n2)) |
203 if(!_nEq(n1,n2)) |
156 return 0; |
204 return 0; |
157 |
205 |
158 for(typename G1::IncEdgeIt e1(_g1,n1); e1!=INVALID; ++e1) |
206 for(typename G1::IncEdgeIt e1(_g1,n1); e1!=INVALID; ++e1) |
159 { |
207 { |
160 const typename G1::Node currNode=_g1.oppositeNode(n1,e1); |
208 const typename G1::Node currNode=_g1.oppositeNode(n1,e1); |
161 if(_match[currNode]!=INVALID) |
209 if(_mapping[currNode]!=INVALID) |
162 --_conn[_match[currNode]]; |
210 --_conn[_mapping[currNode]]; |
163 } |
211 } |
164 bool isIso=1; |
212 bool isIso=1; |
165 for(typename G2::IncEdgeIt e2(_g2,n2); e2!=INVALID; ++e2) |
213 for(typename G2::IncEdgeIt e2(_g2,n2); e2!=INVALID; ++e2) |
166 { |
214 { |
167 const typename G2::Node currNode=_g2.oppositeNode(n2,e2); |
215 const typename G2::Node currNode=_g2.oppositeNode(n2,e2); |
175 } |
223 } |
176 |
224 |
177 for(typename G1::IncEdgeIt e1(_g1,n1); e1!=INVALID; ++e1) |
225 for(typename G1::IncEdgeIt e1(_g1,n1); e1!=INVALID; ++e1) |
178 { |
226 { |
179 const typename G1::Node currNode=_g1.oppositeNode(n1,e1); |
227 const typename G1::Node currNode=_g1.oppositeNode(n1,e1); |
180 if(_match[currNode]!=INVALID&&_conn[_match[currNode]]!=-1) |
228 if(_mapping[currNode]!=INVALID&&_conn[_mapping[currNode]]!=-1) |
181 { |
229 { |
182 switch(MT) |
230 switch(MT) |
183 { |
231 { |
184 case INDUCED: |
232 case INDUCED: |
185 case ISOMORPH: |
233 case ISOMORPH: |
186 isIso=0; |
234 isIso=0; |
187 break; |
235 break; |
188 case SUBGRAPH: |
236 case SUBGRAPH: |
189 if(_conn[_match[currNode]]<-1) |
237 if(_conn[_mapping[currNode]]<-1) |
190 isIso=0; |
238 isIso=0; |
191 break; |
239 break; |
192 } |
240 } |
193 _conn[_match[currNode]]=-1; |
241 _conn[_mapping[currNode]]=-1; |
194 } |
242 } |
195 } |
243 } |
196 return isIso&&cut<MT>(n1,n2); |
244 return isIso&&cut<MT>(n1,n2); |
197 } |
245 } |
198 |
246 |
199 void addPair(const typename G1::Node n1,const typename G2::Node n2) |
247 void addPair(const typename G1::Node n1,const typename G2::Node n2) |
200 { |
248 { |
201 _conn[n2]=-1; |
249 _conn[n2]=-1; |
202 _match.set(n1,n2); |
250 _mapping.set(n1,n2); |
203 for(typename G2::IncEdgeIt e2(_g2,n2); e2!=INVALID; ++e2) |
251 for(typename G2::IncEdgeIt e2(_g2,n2); e2!=INVALID; ++e2) |
204 if(_conn[_g2.oppositeNode(n2,e2)]!=-1) |
252 if(_conn[_g2.oppositeNode(n2,e2)]!=-1) |
205 ++_conn[_g2.oppositeNode(n2,e2)]; |
253 ++_conn[_g2.oppositeNode(n2,e2)]; |
206 } |
254 } |
207 |
255 |
208 void subPair(const typename G1::Node n1,const typename G2::Node n2) |
256 void subPair(const typename G1::Node n1,const typename G2::Node n2) |
209 { |
257 { |
210 _conn[n2]=0; |
258 _conn[n2]=0; |
211 _match.set(n1,INVALID); |
259 _mapping.set(n1,INVALID); |
212 for(typename G2::IncEdgeIt e2(_g2,n2); e2!=INVALID; ++e2) |
260 for(typename G2::IncEdgeIt e2(_g2,n2); e2!=INVALID; ++e2) |
213 { |
261 { |
214 const typename G2::Node currNode=_g2.oppositeNode(n2,e2); |
262 const typename G2::Node currNode=_g2.oppositeNode(n2,e2); |
215 if(_conn[currNode]>0) |
263 if(_conn[currNode]>0) |
216 --_conn[currNode]; |
264 --_conn[currNode]; |
248 //the pair of order[_depth] |
294 //the pair of order[_depth] |
249 typename G2::Node currPNode; |
295 typename G2::Node currPNode; |
250 if(currEdgeIts[_depth]==INVALID) |
296 if(currEdgeIts[_depth]==INVALID) |
251 { |
297 { |
252 typename G1::IncEdgeIt fstMatchedE(_g1,order[_depth]); |
298 typename G1::IncEdgeIt fstMatchedE(_g1,order[_depth]); |
253 //if _match[order[_depth]]!=INVALID, we dont use |
299 //if _mapping[order[_depth]]!=INVALID, we dont use |
254 //fstMatchedE |
300 //fstMatchedE |
255 if(_match[order[_depth]]==INVALID) |
301 if(_mapping[order[_depth]]==INVALID) |
256 for(; fstMatchedE!=INVALID && |
302 for(; fstMatchedE!=INVALID && |
257 _match[_g1.oppositeNode(order[_depth], |
303 _mapping[_g1.oppositeNode(order[_depth], |
258 fstMatchedE)]==INVALID; |
304 fstMatchedE)]==INVALID; |
259 ++fstMatchedE) ; //find fstMatchedE |
305 ++fstMatchedE) ; //find fstMatchedE |
260 if(fstMatchedE==INVALID||_match[order[_depth]]!=INVALID) |
306 if(fstMatchedE==INVALID||_mapping[order[_depth]]!=INVALID) |
261 { |
307 { |
262 //We did not find an covered neighbour, this means |
308 //We did not find an covered neighbour, this means |
263 //the graph is not connected(or _depth==0). Every |
309 //the graph is not connected(or _depth==0). Every |
264 //uncovered(and there are some other properties due |
310 //uncovered(and there are some other properties due |
265 //to the spec. problem types) node of g2 is |
311 //to the spec. problem types) node of g2 is |
266 //candidate. We can read the iterator of the last |
312 //candidate. We can read the iterator of the last |
267 //tryed node from the match if it is not the first |
313 //tryed node from the match if it is not the first |
268 //try(match[order[_depth]]!=INVALID) |
314 //try(match[order[_depth]]!=INVALID) |
269 typename G2::NodeIt n2(_g2); |
315 typename G2::NodeIt n2(_g2); |
270 //if its not the first try |
316 //if its not the first try |
271 if(_match[order[_depth]]!=INVALID) |
317 if(_mapping[order[_depth]]!=INVALID) |
272 { |
318 { |
273 n2=++typename G2::NodeIt(_g2,_match[order[_depth]]); |
319 n2=++typename G2::NodeIt(_g2,_mapping[order[_depth]]); |
274 subPair(order[_depth],_match[order[_depth]]); |
320 subPair(order[_depth],_mapping[order[_depth]]); |
275 } |
321 } |
276 for(; n2!=INVALID; ++n2) |
322 for(; n2!=INVALID; ++n2) |
277 if(MT!=SUBGRAPH&&_conn[n2]==0) |
323 if(MT!=SUBGRAPH&&_conn[n2]==0) |
278 { |
324 { |
279 if(feas<MT>(order[_depth],n2)) |
325 if(feas<MT>(order[_depth],n2)) |
292 --_depth; |
338 --_depth; |
293 continue; |
339 continue; |
294 } |
340 } |
295 else |
341 else |
296 { |
342 { |
297 currPNode=_match[_g1.oppositeNode(order[_depth],fstMatchedE)]; |
343 currPNode=_mapping[_g1.oppositeNode(order[_depth], |
|
344 fstMatchedE)]; |
298 currEdgeIts[_depth]=typename G2::IncEdgeIt(_g2,currPNode); |
345 currEdgeIts[_depth]=typename G2::IncEdgeIt(_g2,currPNode); |
299 } |
346 } |
300 } |
347 } |
301 else |
348 else |
302 { |
349 { |
303 currPNode=_g2.oppositeNode(_match[order[_depth]], |
350 currPNode=_g2.oppositeNode(_mapping[order[_depth]], |
304 currEdgeIts[_depth]); |
351 currEdgeIts[_depth]); |
305 subPair(order[_depth],_match[order[_depth]]); |
352 subPair(order[_depth],_mapping[order[_depth]]); |
306 ++currEdgeIts[_depth]; |
353 ++currEdgeIts[_depth]; |
307 } |
354 } |
308 for(; currEdgeIts[_depth]!=INVALID; ++currEdgeIts[_depth]) |
355 for(; currEdgeIts[_depth]!=INVALID; ++currEdgeIts[_depth]) |
309 { |
356 { |
310 const typename G2::Node currNode = |
357 const typename G2::Node currNode = |
343 ++tmp[currNode]; |
390 ++tmp[currNode]; |
344 } |
391 } |
345 } |
392 } |
346 } |
393 } |
347 public: |
394 public: |
348 Vf2(const G1 &g1, const G2 &g2,I &i, const NEq &nEq = NEq() ) : |
395 ///Constructor |
349 _nEq(nEq), _conn(g2,0), _match(i), order(countNodes(g1)), |
396 |
|
397 ///Constructor |
|
398 |
|
399 ///\param g1 The graph to be embedded into \e g2. |
|
400 ///\param g2 The graph \e g1 will be embedded into. |
|
401 ///\param m \ref concepts::ReadWriteMap "read-write" NodeMap |
|
402 ///storing the found mapping. |
|
403 ///\param neq A bool-valued binary functor determinining whether a node is |
|
404 ///mappable to another. By default it is an always true operator. |
|
405 Vf2(const G1 &g1, const G2 &g2,M &m, const NEQ &neq = NEQ() ) : |
|
406 _nEq(neq), _conn(g2,0), _mapping(m), order(countNodes(g1)), |
350 currEdgeIts(countNodes(g1),INVALID), _g1(g1), _g2(g2), rNew1t(g1,0), |
407 currEdgeIts(countNodes(g1),INVALID), _g1(g1), _g2(g2), rNew1t(g1,0), |
351 rInOut1t(g1,0), _mapping_type(SUBGRAPH) |
408 rInOut1t(g1,0), _mapping_type(SUBGRAPH) |
352 { |
409 { |
353 _depth=0; |
410 _depth=0; |
354 setOrder(); |
411 setOrder(); |
355 setRNew1tRInOut1t(); |
412 setRNew1tRInOut1t(); |
356 } |
413 } |
357 |
414 |
358 MappingType mappingType() const { return _mapping_type; } |
415 ///Returns the current mapping type |
359 void mappingType(MappingType m_type) { _mapping_type = m_type; } |
416 |
360 |
417 ///Returns the current mapping type |
|
418 /// |
|
419 Vf2MappingType mappingType() const { return _mapping_type; } |
|
420 ///Sets mapping type |
|
421 |
|
422 ///Sets mapping type. |
|
423 /// |
|
424 ///The mapping type is set to \ref SUBGRAPH by default. |
|
425 /// |
|
426 ///\sa See \ref for the possible values. |
|
427 void mappingType(Vf2MappingType m_type) { _mapping_type = m_type; } |
|
428 |
|
429 ///Find a mapping |
|
430 |
|
431 ///It finds a mapping between from g1 into g2 according to the mapping |
|
432 ///type set by \ref mappingType(Vf2MappingType) "mappingType()". |
|
433 /// |
|
434 ///By subsequent calls, it returns all possible mappings one-by-one. |
|
435 /// |
|
436 ///\retval true if a mapping is found. |
|
437 ///\retval false if there is no (more) mapping. |
361 bool find() |
438 bool find() |
362 { |
439 { |
363 switch(_mapping_type) |
440 switch(_mapping_type) |
364 { |
441 { |
365 case SUBGRAPH: |
442 case SUBGRAPH: |
399 |
476 |
400 Vf2WizardBase(const G1 &g1,const G2 &g2) |
477 Vf2WizardBase(const G1 &g1,const G2 &g2) |
401 : _g1(g1), _g2(g2), _mapping_type(SUBGRAPH), _local_mapping(true) {} |
478 : _g1(g1), _g2(g2), _mapping_type(SUBGRAPH), _local_mapping(true) {} |
402 }; |
479 }; |
403 |
480 |
|
481 /// Auxiliary class for the function-type interface of %VF2 algorithm. |
|
482 |
|
483 /// This auxiliary class implements the named parameters of |
|
484 /// \ref vf2() "function-type interface" of \ref Vf2 algorithm. |
|
485 /// |
|
486 /// \warning This class should only be used through the function \ref vf2(). |
|
487 /// |
|
488 /// \tparam TR The traits class that defines various types used by the |
|
489 /// algorithm. |
404 template<class TR> |
490 template<class TR> |
405 class Vf2Wizard : public TR |
491 class Vf2Wizard : public TR |
406 { |
492 { |
407 typedef TR Base; |
493 typedef TR Base; |
408 typedef typename TR::Graph1 Graph1; |
494 typedef typename TR::Graph1 Graph1; |
455 ///\brief \ref named-templ-param "Named parameter" for setting |
541 ///\brief \ref named-templ-param "Named parameter" for setting |
456 ///the node equivalence relation. |
542 ///the node equivalence relation. |
457 /// |
543 /// |
458 ///\ref named-templ-param "Named parameter" function for setting |
544 ///\ref named-templ-param "Named parameter" function for setting |
459 ///the equivalence relation between the nodes. |
545 ///the equivalence relation between the nodes. |
|
546 /// |
|
547 ///\param node_eq A bool-valued binary functor determinining |
|
548 ///whether a node is mappable to another. By default it is an |
|
549 ///always true operator. |
460 template<class T> |
550 template<class T> |
461 Vf2Wizard< SetNodeEqBase<T> > nodeEq(const T &node_eq) |
551 Vf2Wizard< SetNodeEqBase<T> > nodeEq(const T &node_eq) |
462 { |
552 { |
463 return Vf2Wizard<SetNodeEqBase<T> >(SetNodeEqBase<T>(*this,node_eq)); |
553 return Vf2Wizard<SetNodeEqBase<T> >(SetNodeEqBase<T>(*this,node_eq)); |
464 } |
554 } |
466 ///\brief \ref named-templ-param "Named parameter" for setting |
556 ///\brief \ref named-templ-param "Named parameter" for setting |
467 ///the node labels. |
557 ///the node labels. |
468 /// |
558 /// |
469 ///\ref named-templ-param "Named parameter" function for setting |
559 ///\ref named-templ-param "Named parameter" function for setting |
470 ///the node labels defining equivalence relation between them. |
560 ///the node labels defining equivalence relation between them. |
|
561 /// |
|
562 ///\param m1 It is arbitrary \ref ReadMap "readable node map" of g1. |
|
563 ///\param m1 It is arbitrary \ref ReadMap "readable node map" of g2. |
|
564 /// |
|
565 ///The value type of these maps must be equal comparable. |
471 template<class M1, class M2> |
566 template<class M1, class M2> |
472 Vf2Wizard< SetNodeEqBase<bits::vf2::MapEq<M1,M2> > > |
567 Vf2Wizard< SetNodeEqBase<bits::vf2::MapEq<M1,M2> > > |
473 nodeLabels(const M1 &m1,const M2 &m2) |
568 nodeLabels(const M1 &m1,const M2 &m2) |
474 { |
569 { |
475 return nodeEq(bits::vf2::MapEq<M1,M2>(m1,m2)); |
570 return nodeEq(bits::vf2::MapEq<M1,M2>(m1,m2)); |
476 } |
571 } |
477 |
572 |
478 Vf2Wizard<Base> &mappingType(MappingType m_type) |
573 ///\brief \ref named-templ-param "Named parameter" for setting |
|
574 ///the mapping type. |
|
575 /// |
|
576 ///\ref named-templ-param "Named parameter" for setting |
|
577 ///the mapping type. |
|
578 /// |
|
579 ///The mapping type is set to \ref SUBGRAPH by default. |
|
580 /// |
|
581 ///\sa See \ref for the possible values. |
|
582 Vf2Wizard<Base> &mappingType(Vf2MappingType m_type) |
479 { |
583 { |
480 _mapping_type = m_type; |
584 _mapping_type = m_type; |
481 return *this; |
585 return *this; |
482 } |
586 } |
483 |
587 |
|
588 ///\brief \ref named-templ-param "Named parameter" for setting |
|
589 ///the mapping type to \ref INDUCED. |
|
590 /// |
|
591 ///\ref named-templ-param "Named parameter" for setting |
|
592 ///the mapping type to \ref INDUCED. |
484 Vf2Wizard<Base> &induced() |
593 Vf2Wizard<Base> &induced() |
485 { |
594 { |
486 _mapping_type = INDUCED; |
595 _mapping_type = INDUCED; |
487 return *this; |
596 return *this; |
488 } |
597 } |
489 |
598 |
|
599 ///\brief \ref named-templ-param "Named parameter" for setting |
|
600 ///the mapping type to \ref ISOMORPH. |
|
601 /// |
|
602 ///\ref named-templ-param "Named parameter" for setting |
|
603 ///the mapping type to \ref ISOMORPH. |
490 Vf2Wizard<Base> &iso() |
604 Vf2Wizard<Base> &iso() |
491 { |
605 { |
492 _mapping_type = ISOMORPH; |
606 _mapping_type = ISOMORPH; |
493 return *this; |
607 return *this; |
494 } |
608 } |
495 |
609 |
|
610 ///Runs VF2 algorithm. |
|
611 |
|
612 ///This method runs VF2 algorithm. |
|
613 /// |
|
614 ///\retval true if a mapping is found. |
|
615 ///\retval false if there is no (more) mapping. |
496 bool run() |
616 bool run() |
497 { |
617 { |
498 if(Base::_local_mapping) |
618 if(Base::_local_mapping) |
499 Base::createMapping(); |
619 Base::createMapping(); |
500 |
620 |
510 |
630 |
511 return ret; |
631 return ret; |
512 } |
632 } |
513 }; |
633 }; |
514 |
634 |
|
635 ///Function-type interface for VF2 algorithm. |
|
636 |
|
637 /// \ingroup graph_isomorphism |
|
638 ///Function-type interface for VF2 algorithm \cite cordella2004sub. |
|
639 /// |
|
640 ///This function has several \ref named-func-param "named parameters" |
|
641 ///declared as the members of class \ref Vf2Wizard. |
|
642 ///The following examples show how to use these parameters. |
|
643 ///\code |
|
644 /// // Find an embedding of graph g into graph h |
|
645 /// ListGraph::NodeMap<ListGraph::Node> m(g); |
|
646 /// vf2(g,h).mapping(m).run(); |
|
647 /// |
|
648 /// // Check whether graphs g and h are isomorphic |
|
649 /// bool is_iso = vf2(g,h).iso().run(); |
|
650 ///\endcode |
|
651 ///\warning Don't forget to put the \ref Vf2Wizard::run() "run()" |
|
652 ///to the end of the expression. |
|
653 ///\sa Vf2Wizard |
|
654 ///\sa Vf2 |
515 template<class G1, class G2> |
655 template<class G1, class G2> |
516 Vf2Wizard<Vf2WizardBase<G1,G2> > vf2(const G1 &g1, const G2 &g2) |
656 Vf2Wizard<Vf2WizardBase<G1,G2> > vf2(const G1 &g1, const G2 &g2) |
517 { |
657 { |
518 return Vf2Wizard<Vf2WizardBase<G1,G2> >(g1,g2); |
658 return Vf2Wizard<Vf2WizardBase<G1,G2> >(g1,g2); |
519 } |
659 } |