1 /** @file mcf.cpp Definition of Multi-Commodity-Flow solver. */
4 #include "../core/math_func.hpp"
6 #include "../3rdparty/cpp-btree/btree_map.h"
9 #include "../safeguards.h"
11 typedef btree::btree_map
<NodeID
, Path
*> PathViaMap
;
14 * This is a wrapper around Tannotation* which also stores a cache of GetAnnotation() and GetNode()
15 * to remove the need dereference the Tannotation* pointer when sorting/inseting/erasing in MultiCommodityFlow::Dijkstra::AnnoSet
17 template<typename Tannotation
>
20 Tannotation
* anno_ptr
;
21 typename
Tannotation::AnnotationValueType cached_annotation
;
24 AnnoSetItem(Tannotation
*anno
) : anno_ptr(anno
), cached_annotation(anno
->GetAnnotation()), node_id(anno
->GetNode()) {}
28 * Distance-based annotation for use in the Dijkstra algorithm. This is close
29 * to the original meaning of "annotation" in this context. Paths are rated
30 * according to the sum of distances of their edges.
32 class DistanceAnnotation
: public Path
{
34 typedef uint AnnotationValueType
;
38 * @param n ID of node to be annotated.
39 * @param source If the node is the source of its path.
41 DistanceAnnotation(NodeID n
, bool source
= false) : Path(n
, source
) {}
43 bool IsBetter(const DistanceAnnotation
*base
, uint cap
, int free_cap
, uint dist
) const;
46 * Return the actual value of the annotation, in this case the distance.
49 inline uint
GetAnnotation() const { return this->distance
; }
52 * Update the cached annotation value
54 inline void UpdateAnnotation() { }
57 * Comparator for std containers.
60 bool operator()(const AnnoSetItem
<DistanceAnnotation
> &x
, const AnnoSetItem
<DistanceAnnotation
> &y
) const;
65 * Capacity-based annotation for use in the Dijkstra algorithm. This annotation
66 * rates paths according to the maximum capacity of their edges. The Dijkstra
67 * algorithm still gives meaningful results like this as the capacity of a path
68 * can only decrease or stay the same if you add more edges.
70 class CapacityAnnotation
: public Path
{
71 int cached_annotation
;
74 typedef int AnnotationValueType
;
78 * @param n ID of node to be annotated.
79 * @param source If the node is the source of its path.
81 CapacityAnnotation(NodeID n
, bool source
= false) : Path(n
, source
) {}
83 bool IsBetter(const CapacityAnnotation
*base
, uint cap
, int free_cap
, uint dist
) const;
86 * Return the actual value of the annotation, in this case the capacity.
89 inline int GetAnnotation() const { return this->cached_annotation
; }
92 * Update the cached annotation value
94 inline void UpdateAnnotation()
96 this->cached_annotation
= this->GetCapacityRatio();
100 * Comparator for std containers.
103 bool operator()(const AnnoSetItem
<CapacityAnnotation
> &x
, const AnnoSetItem
<CapacityAnnotation
> &y
) const;
108 * Iterator class for getting the edges in the order of their next_edge
111 class GraphEdgeIterator
{
113 LinkGraphJob
& job
; ///< Job being executed
114 EdgeIterator i
; ///< Iterator pointing to current edge.
115 EdgeIterator end
; ///< Iterator pointing beyond last edge.
120 * Construct a GraphEdgeIterator.
121 * @param job Job to iterate on.
123 GraphEdgeIterator(LinkGraphJob
&job
) : job(job
),
124 i(NULL
, NULL
, INVALID_NODE
), end(NULL
, NULL
, INVALID_NODE
)
128 * Setup the node to start iterating at.
129 * @param source Unused.
130 * @param node Node to start iterating at.
132 void SetNode(NodeID source
, NodeID node
)
134 this->i
= this->job
[node
].Begin();
135 this->end
= this->job
[node
].End();
139 * Retrieve the ID of the node the next edge points to.
140 * @return Next edge's target node ID or INVALID_NODE.
144 return this->i
!= this->end
? (this->i
++)->first
: INVALID_NODE
;
149 * Iterator class for getting edges from a FlowStatMap.
151 class FlowEdgeIterator
{
153 LinkGraphJob
& job
; ///< Link graph job we're working with.
155 /** Lookup table for getting NodeIDs from StationIDs. */
156 std::vector
<NodeID
> station_to_node
;
158 /** Current iterator in the shares map. */
159 FlowStat::SharesMap::const_iterator it
;
161 /** End of the shares map. */
162 FlowStat::SharesMap::const_iterator end
;
167 * @param job Link graph job to work with.
169 FlowEdgeIterator(LinkGraphJob
&job
) : job(job
)
171 for (NodeID i
= 0; i
< job
.Size(); ++i
) {
172 StationID st
= job
[i
].Station();
173 if (st
>= this->station_to_node
.size()) {
174 this->station_to_node
.resize(st
+ 1);
176 this->station_to_node
[st
] = i
;
181 * Setup the node to retrieve edges from.
182 * @param source Root of the current path tree.
183 * @param node Current node to be checked for outgoing flows.
185 void SetNode(NodeID source
, NodeID node
)
187 const FlowStatMap
&flows
= this->job
[node
].Flows();
188 FlowStatMap::const_iterator it
= flows
.find(this->job
[source
].Station());
189 if (it
!= flows
.end()) {
190 this->it
= it
->second
.GetShares()->begin();
191 this->end
= it
->second
.GetShares()->end();
194 this->it
= FlowStat::empty_sharesmap
.begin();
195 this->end
= FlowStat::empty_sharesmap
.end();
200 * Get the next node for which a flow exists.
201 * @return ID of next node with flow.
205 if (this->it
== this->end
) return INVALID_NODE
;
206 return this->station_to_node
[(this->it
++)->second
];
211 * Determines if an extension to the given Path with the given parameters is
212 * better than this path.
213 * @param base Other path.
214 * @param cap Capacity of the new edge to be added to base.
215 * @param dist Distance of the new edge.
216 * @return True if base + the new edge would be better than the path associated
217 * with this annotation.
219 bool DistanceAnnotation::IsBetter(const DistanceAnnotation
*base
, uint cap
,
220 int free_cap
, uint dist
) const
222 /* If any of the paths is disconnected, the other one is better. If both
223 * are disconnected, this path is better.*/
224 if (base
->distance
== UINT_MAX
) {
227 else if (this->distance
== UINT_MAX
) {
231 if (free_cap
> 0 && base
->free_capacity
> 0) {
232 /* If both paths have capacity left, compare their distances.
233 * If the other path has capacity left and this one hasn't, the
234 * other one's better (thus, return true). */
235 return this->free_capacity
> 0 ? (base
->distance
+ dist
< this->distance
) : true;
238 /* If the other path doesn't have capacity left, but this one has,
239 * the other one is worse (thus, return false).
240 * If both paths are out of capacity, do the regular distance
242 return this->free_capacity
> 0 ? false : (base
->distance
+ dist
< this->distance
);
247 * Determines if an extension to the given Path with the given parameters is
248 * better than this path.
249 * @param base Other path.
250 * @param cap Capacity of the new edge to be added to base.
251 * @param dist Distance of the new edge.
252 * @return True if base + the new edge would be better than the path associated
253 * with this annotation.
255 bool CapacityAnnotation::IsBetter(const CapacityAnnotation
*base
, uint cap
,
256 int free_cap
, uint dist
) const
258 int min_cap
= Path::GetCapacityRatio(min(base
->free_capacity
, free_cap
), min(base
->capacity
, cap
));
259 int this_cap
= this->GetAnnotation();
260 if (min_cap
== this_cap
) {
261 /* If the capacities are the same and the other path isn't disconnected
262 * choose the shorter path. */
263 return base
->distance
== UINT_MAX
? false : (base
->distance
+ dist
< this->distance
);
266 return min_cap
> this_cap
;
270 #ifdef CUSTOM_ALLOCATOR
272 * Storage for AnnoSetAllocator instances
274 struct AnnoSetAllocatorStore
{
275 std::vector
<void *> used_blocks
;
277 size_t next_position
;
280 AnnoSetAllocatorStore() : current_block(NULL
), next_position(0), last_freed(NULL
) {}
282 ~AnnoSetAllocatorStore()
284 for (std::vector
<void *>::iterator i
= used_blocks
.begin(); i
!= used_blocks
.end(); ++i
) {
291 * Custom allocator specifically for use with MultiCommodityFlow::Dijkstra::AnnoSet
292 * This allocates RB-set nodes in contiguous blocks, and frees all allocated nodes when destructed
293 * If a node is deallocated, it is returned in the next allocation, this is so that the same node
294 * can be re-used across a call to Path::Fork
296 template<class Ttype
>
297 struct AnnoSetAllocator
{
298 static const size_t block_size
= 1024;
300 AnnoSetAllocatorStore
&store
;
304 store
.current_block
= MallocT
<Ttype
>(block_size
);
305 store
.next_position
= 0;
306 store
.used_blocks
.push_back(store
.current_block
);
309 typedef Ttype value_type
;
311 template<typename Tother
>
313 typedef AnnoSetAllocator
<Tother
> other
;
316 AnnoSetAllocator(AnnoSetAllocatorStore
&store
) : store(store
) {}
318 template<typename Tother
>
319 AnnoSetAllocator(const AnnoSetAllocator
<Tother
> &other
) : store(other
.store
) {}
321 Ttype
* allocate(size_t n
)
323 if (store
.current_block
== NULL
) NewBlock();
326 if (store
.last_freed
!= NULL
) {
327 Ttype
* out
= static_cast<Ttype
*>(store
.last_freed
);
328 store
.last_freed
= NULL
;
332 if (store
.next_position
== block_size
) NewBlock();
334 Ttype
* next
= static_cast<Ttype
*>(store
.current_block
) + store
.next_position
;
335 store
.next_position
++;
339 void deallocate(Ttype
* p
, size_t n
)
341 store
.last_freed
= p
;
347 * Annotation wrapper class which also stores an iterator to the AnnoSet node which points to this annotation
348 * This is to enable erasing the AnnoSet node when calling Path::Fork without having to search the set
350 template<class Tannotation
>
351 struct AnnosWrapper
: public Tannotation
{
352 #ifdef CUSTOM_ALLOCATOR
353 typename
std::set
<AnnoSetItem
<Tannotation
>, typename
Tannotation::Comparator
, AnnoSetAllocator
<AnnoSetItem
<Tannotation
> > >::iterator self_iter
;
355 typename
std::set
<AnnoSetItem
<Tannotation
>, typename
Tannotation::Comparator
>::iterator self_iter
;
359 AnnosWrapper(NodeID n
, bool source
= false) : Tannotation(n
, source
) {}
363 * A slightly modified Dijkstra algorithm. Grades the paths not necessarily by
364 * distance, but by the value Tannotation computes. It uses the max_saturation
365 * setting to artificially decrease capacities.
366 * @tparam Tannotation Annotation to be used.
367 * @tparam Tedge_iterator Iterator to be used for getting outgoing edges.
368 * @param source_node Node where the algorithm starts.
369 * @param paths Container for the paths to be calculated.
371 template<class Tannotation
, class Tedge_iterator
>
372 void MultiCommodityFlow::Dijkstra(NodeID source_node
, PathVector
&paths
)
374 #ifdef CUSTOM_ALLOCATOR
375 typedef std::set
<AnnoSetItem
<Tannotation
>, typename
Tannotation::Comparator
, AnnoSetAllocator
<AnnoSetItem
<Tannotation
> > > AnnoSet
;
376 AnnoSetAllocatorStore annos_store
;
377 AnnoSet annos
= AnnoSet(typename
Tannotation::Comparator(), AnnoSetAllocator
<Tannotation
*>(annos_store
));
379 typedef std::set
<AnnoSetItem
<Tannotation
>, typename
Tannotation::Comparator
> AnnoSet
;
380 AnnoSet annos
= AnnoSet(typename
Tannotation::Comparator());
382 Tedge_iterator
iter(this->job
);
383 uint size
= this->job
.Size();
384 paths
.resize(size
, NULL
);
386 this->job
.path_allocator
.SetParameters(sizeof(AnnosWrapper
<Tannotation
>), (8192 - 32) / sizeof(AnnosWrapper
<Tannotation
>));
388 for (NodeID node
= 0; node
< size
; ++node
) {
389 AnnosWrapper
<Tannotation
> *anno
= new (this->job
.path_allocator
.Allocate()) AnnosWrapper
<Tannotation
>(node
, node
== source_node
);
390 anno
->UpdateAnnotation();
391 anno
->self_iter
= (node
== source_node
) ? annos
.insert(AnnoSetItem
<Tannotation
>(anno
)).first
: annos
.end(); // only insert the source node, the other nodes will be added as reached
394 while (!annos
.empty()) {
395 typename
AnnoSet::iterator i
= annos
.begin();
396 AnnosWrapper
<Tannotation
> *source
= static_cast<AnnosWrapper
<Tannotation
> *>(i
->anno_ptr
);
398 source
->self_iter
= annos
.end();
399 NodeID from
= source
->GetNode();
400 iter
.SetNode(source_node
, from
);
401 for (NodeID to
= iter
.Next(); to
!= INVALID_NODE
; to
= iter
.Next()) {
402 if (to
== from
) continue; // Not a real edge but a consumption sign.
403 Edge edge
= this->job
[from
][to
];
404 uint capacity
= edge
.Capacity();
405 if (this->max_saturation
!= UINT_MAX
) {
406 capacity
*= this->max_saturation
;
408 if (capacity
== 0) capacity
= 1;
410 /* punish in-between stops a little */
411 uint distance
= DistanceMaxPlusManhattan(this->job
[from
].XY(), this->job
[to
].XY()) + 1;
412 AnnosWrapper
<Tannotation
> *dest
= static_cast<AnnosWrapper
<Tannotation
> *>(paths
[to
]);
413 if (dest
->IsBetter(source
, capacity
, capacity
- edge
.Flow(), distance
)) {
414 if (dest
->self_iter
!= annos
.end()) annos
.erase(dest
->self_iter
);
415 dest
->Fork(source
, capacity
, capacity
- edge
.Flow(), distance
);
416 dest
->UpdateAnnotation();
417 dest
->self_iter
= annos
.insert(AnnoSetItem
<Tannotation
>(dest
)).first
;
424 * Clean up paths that lead nowhere and the root path.
425 * @param source_id ID of the root node.
426 * @param paths Paths to be cleaned up.
428 void MultiCommodityFlow::CleanupPaths(NodeID source_id
, PathVector
&paths
)
430 Path
*source
= paths
[source_id
];
431 paths
[source_id
] = NULL
;
432 for (PathVector::iterator i
= paths
.begin(); i
!= paths
.end(); ++i
) {
434 if (path
== NULL
) continue;
435 if (path
->GetParent() == source
) path
->Detach();
436 while (path
!= source
&& path
!= NULL
&& path
->GetFlow() == 0) {
437 Path
*parent
= path
->GetParent();
439 if (path
->GetNumChildren() == 0) {
440 paths
[path
->GetNode()] = NULL
;
441 this->job
.path_allocator
.Free(path
);
446 this->job
.path_allocator
.Free(source
);
451 * Push flow along a path and update the unsatisfied_demand of the associated
453 * @param edge Edge whose ends the path connects.
454 * @param path End of the path the flow should be pushed on.
455 * @param accuracy Accuracy of the calculation.
456 * @param max_saturation If < UINT_MAX only push flow up to the given
457 * saturation, otherwise the path can be "overloaded".
459 uint
MultiCommodityFlow::PushFlow(Edge
&edge
, Path
*path
, uint accuracy
,
462 assert(edge
.UnsatisfiedDemand() > 0);
463 uint flow
= Clamp(edge
.Demand() / accuracy
, 1, edge
.UnsatisfiedDemand());
464 flow
= path
->AddFlow(flow
, this->job
, max_saturation
);
465 edge
.SatisfyDemand(flow
);
470 * Find the flow along a cycle including cycle_begin in path.
471 * @param path Set of paths that form the cycle.
472 * @param cycle_begin Path to start at.
473 * @return Flow along the cycle.
475 uint
MCF1stPass::FindCycleFlow(const PathVector
&path
, const Path
*cycle_begin
)
477 uint flow
= UINT_MAX
;
478 const Path
*cycle_end
= cycle_begin
;
480 flow
= min(flow
, cycle_begin
->GetFlow());
481 cycle_begin
= path
[cycle_begin
->GetNode()];
482 } while (cycle_begin
!= cycle_end
);
487 * Eliminate a cycle of the given flow in the given set of paths.
488 * @param path Set of paths containing the cycle.
489 * @param cycle_begin Part of the cycle to start at.
490 * @param flow Flow along the cycle.
492 void MCF1stPass::EliminateCycle(PathVector
&path
, Path
*cycle_begin
, uint flow
)
494 Path
*cycle_end
= cycle_begin
;
496 NodeID prev
= cycle_begin
->GetNode();
497 cycle_begin
->ReduceFlow(flow
);
498 if (cycle_begin
->GetFlow() == 0) {
499 PathList
&node_paths
= this->job
[cycle_begin
->GetParent()->GetNode()].Paths();
500 for (PathList::reverse_iterator i
= node_paths
.rbegin(); i
!= node_paths
.rend(); ++i
) {
501 if (*i
== cycle_begin
) {
507 cycle_begin
= path
[prev
];
508 Edge edge
= this->job
[prev
][cycle_begin
->GetNode()];
509 edge
.RemoveFlow(flow
);
510 } while (cycle_begin
!= cycle_end
);
514 * Eliminate cycles for origin_id in the graph. Start searching at next_id and
515 * work recursively. Also "summarize" paths: Add up the flows along parallel
517 * @param path Paths checked in parent calls to this method.
518 * @param origin_id Origin of the paths to be checked.
519 * @param next_id Next node to be checked.
520 * @return If any cycles have been found and eliminated.
522 bool MCF1stPass::EliminateCycles(PathVector
&path
, NodeID origin_id
, NodeID next_id
)
524 Path
*at_next_pos
= path
[next_id
];
526 /* this node has already been searched */
527 if (at_next_pos
== Path::invalid_path
) return false;
529 if (at_next_pos
== NULL
) {
530 /* Summarize paths; add up the paths with the same source and next hop
531 * in one path each. */
532 PathList
&paths
= this->job
[next_id
].Paths();
533 PathViaMap next_hops
;
535 for (PathList::reverse_iterator i
= paths
.rbegin(); i
!= paths
.rend();) {
536 Path
*new_child
= *i
;
538 uint new_flow
= new_child
->GetFlow();
539 if (new_flow
== 0) break;
540 if (new_child
->GetOrigin() == origin_id
) {
541 PathViaMap::iterator via_it
= next_hops
.find(new_child
->GetNode());
542 if (via_it
== next_hops
.end()) {
543 next_hops
[new_child
->GetNode()] = new_child
;
546 Path
*child
= via_it
->second
;
547 child
->AddFlow(new_flow
);
548 new_child
->ReduceFlow(new_flow
);
560 if (holes
>= paths
.size() / 8) {
561 /* remove any holes */
562 paths
.erase(std::remove(paths
.begin(), paths
.end(), nullptr), paths
.end());
566 /* Search the next hops for nodes we have already visited */
567 for (PathViaMap::iterator via_it
= next_hops
.begin();
568 via_it
!= next_hops
.end(); ++via_it
) {
569 Path
*child
= via_it
->second
;
570 if (child
->GetFlow() > 0) {
571 /* Push one child into the path vector and search this child's
573 path
[next_id
] = child
;
574 found
= this->EliminateCycles(path
, origin_id
, child
->GetNode()) || found
;
577 /* All paths departing from this node have been searched. Mark as
578 * resolved if no cycles found. If cycles were found further cycles
579 * could be found in this branch, thus it has to be searched again next
582 path
[next_id
] = found
? NULL
: Path::invalid_path
;
586 /* This node has already been visited => we have a cycle.
587 * Backtrack to find the exact flow. */
588 uint flow
= this->FindCycleFlow(path
, at_next_pos
);
590 this->EliminateCycle(path
, at_next_pos
, flow
);
598 * Eliminate all cycles in the graph. Check paths starting at each node for
600 * @return If any cycles have been found and eliminated.
602 bool MCF1stPass::EliminateCycles()
604 bool cycles_found
= false;
605 uint size
= this->job
.Size();
606 PathVector
path(size
, NULL
);
607 for (NodeID node
= 0; node
< size
; ++node
) {
608 /* Starting at each node in the graph find all cycles involving this
610 std::fill(path
.begin(), path
.end(), (Path
*)NULL
);
611 cycles_found
|= this->EliminateCycles(path
, node
, node
);
617 * Run the first pass of the MCF calculation.
618 * @param job Link graph job to calculate.
620 MCF1stPass::MCF1stPass(LinkGraphJob
&job
) : MultiCommodityFlow(job
)
623 uint size
= job
.Size();
624 uint accuracy
= job
.Settings().accuracy
;
629 for (NodeID source
= 0; source
< size
; ++source
) {
630 /* First saturate the shortest paths. */
631 this->Dijkstra
<DistanceAnnotation
, GraphEdgeIterator
>(source
, paths
);
633 for (NodeID dest
= 0; dest
< size
; ++dest
) {
634 Edge edge
= job
[source
][dest
];
635 if (edge
.UnsatisfiedDemand() > 0) {
636 Path
*path
= paths
[dest
];
637 assert(path
!= NULL
);
638 /* Generally only allow paths that don't exceed the
639 * available capacity. But if no demand has been assigned
640 * yet, make an exception and allow any valid path *once*. */
641 if (path
->GetFreeCapacity() > 0 && this->PushFlow(edge
, path
,
642 accuracy
, this->max_saturation
) > 0) {
643 /* If a path has been found there is a chance we can
645 more_loops
= more_loops
|| (edge
.UnsatisfiedDemand() > 0);
647 else if (edge
.UnsatisfiedDemand() == edge
.Demand() &&
648 path
->GetFreeCapacity() > INT_MIN
) {
649 this->PushFlow(edge
, path
, accuracy
, UINT_MAX
);
653 this->CleanupPaths(source
, paths
);
655 } while ((more_loops
|| this->EliminateCycles()) && !job
.IsJobAborted());
659 * Run the second pass of the MCF calculation which assigns all remaining
660 * demands to existing paths.
661 * @param job Link graph job to calculate.
663 MCF2ndPass::MCF2ndPass(LinkGraphJob
&job
) : MultiCommodityFlow(job
)
665 this->max_saturation
= UINT_MAX
; // disable artificial cap on saturation
667 uint size
= job
.Size();
668 uint accuracy
= job
.Settings().accuracy
;
669 bool demand_left
= true;
670 while (demand_left
&& !job
.IsJobAborted()) {
672 for (NodeID source
= 0; source
< size
; ++source
) {
673 this->Dijkstra
<CapacityAnnotation
, FlowEdgeIterator
>(source
, paths
);
674 for (NodeID dest
= 0; dest
< size
; ++dest
) {
675 Edge edge
= this->job
[source
][dest
];
676 Path
*path
= paths
[dest
];
677 if (edge
.UnsatisfiedDemand() > 0 && path
->GetFreeCapacity() > INT_MIN
) {
678 this->PushFlow(edge
, path
, accuracy
, UINT_MAX
);
679 if (edge
.UnsatisfiedDemand() > 0) demand_left
= true;
682 this->CleanupPaths(source
, paths
);
688 * Relation that creates a weak order without duplicates.
689 * Avoid accidentally deleting different paths of the same capacity/distance in
690 * a set. When the annotation is the same node IDs are compared, so there are
692 * @tparam T Type to be compared on.
693 * @param x_anno First value.
694 * @param y_anno Second value.
695 * @param x Node id associated with the first value.
696 * @param y Node id associated with the second value.
698 template <typename T
>
699 bool Greater(T x_anno
, T y_anno
, NodeID x
, NodeID y
)
701 if (x_anno
> y_anno
) return true;
702 if (x_anno
< y_anno
) return false;
707 * Compare two capacity annotations.
708 * @param x First capacity annotation.
709 * @param y Second capacity annotation.
710 * @return If x is better than y.
712 bool CapacityAnnotation::Comparator::operator()(const AnnoSetItem
<CapacityAnnotation
> &x
,
713 const AnnoSetItem
<CapacityAnnotation
> &y
) const
715 return x
.anno_ptr
!= y
.anno_ptr
&& Greater
<int>(x
.cached_annotation
, y
.cached_annotation
,
716 x
.node_id
, y
.node_id
);
720 * Compare two distance annotations.
721 * @param x First distance annotation.
722 * @param y Second distance annotation.
723 * @return If x is better than y.
725 bool DistanceAnnotation::Comparator::operator()(const AnnoSetItem
<DistanceAnnotation
> &x
,
726 const AnnoSetItem
<DistanceAnnotation
> &y
) const
728 return x
.anno_ptr
!= y
.anno_ptr
&& !Greater
<uint
>(x
.cached_annotation
, y
.cached_annotation
,
729 x
.node_id
, y
.node_id
);