[{"data":1,"prerenderedAt":1400},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-network-graph-with-qgsgraphbuilder":3},{"id":4,"title":5,"body":6,"description":1389,"extension":1390,"meta":1391,"navigation":227,"path":1396,"seo":1397,"stem":1398,"__hash__":1399},"docs\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-network-graph-with-qgsgraphbuilder\u002Findex.md","Build a Network Graph with QgsGraphBuilder",{"type":7,"value":8,"toc":1376},"minimark",[9,13,17,26,151,156,190,194,295,301,305,308,334,447,456,460,563,655,691,698,748,765,769,772,937,953,1105,1114,1118,1194,1197,1201,1226,1230,1282,1286,1298,1302,1312,1318,1332,1338,1342,1372],[10,11,5],"h1",{"id":12},"build-a-network-graph-with-qgsgraphbuilder",[14,15,16],"p",{},"Every routing answer QGIS gives you is a property of the graph, not of the line layer. If a road is missing from the graph no algorithm will find it, and nothing about the build reports that it happened. Learning to inspect the graph you built — how many vertices, how many edges, how much of it is connected to how much else — is what separates routing that works from routing that produces a confident wrong answer.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002F","Network Analysis & Routing in PyQGIS",". It covers the builder's arguments in detail, the topology tolerance and how to choose it, reading the graph back, auditing connectivity, and what a large network costs.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,82,85,88,94,98,102,108,112,116,119,122,125,129,132,142,147],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 330","img","How a line layer becomes vertices and edges: each line endpoint becomes a vertex, endpoints within the tolerance merge into one, and each two-way segment becomes a pair of directed edges","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Endpoints become vertices; segments become edge pairs",[40,41,42],"desc",{},"Three line features meet near a junction. Their endpoints are separate vertices when the tolerance is zero, so nothing connects. Raising the tolerance merges the three nearby endpoints into a single vertex and the junction becomes traversable. Each two-way segment then contributes two directed edges to the graph.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","330","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The tolerance decides whether this is a junction",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","46","340","200","10","#fffdf7","#b91c1c","stroke-width:2.5",[51,70,74],{"x":71,"y":72,"style":73,"fill":67,"textAnchor":57},"194","72","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","tolerance 0 — three vertices",[76,77],"path",{"d":78,"fill":79,"stroke":80,"style":81},"M60 190 L184 148","none","#59645f","stroke-width:2.6",[76,83],{"d":84,"fill":79,"stroke":80,"style":81},"M192 138 L316 106",[76,86],{"d":87,"fill":79,"stroke":80,"style":81},"M186 158 L300 212",[89,90],"circle",{"cx":91,"cy":92,"r":93,"fill":67},"184","148","5",[89,95],{"cx":96,"cy":97,"r":93,"fill":67},"192","138",[89,99],{"cx":100,"cy":101,"r":93,"fill":67},"186","158",[51,103,107],{"x":71,"y":104,"style":105,"fill":106,"textAnchor":57},"228","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","18 cm apart — no route passes through",[44,109],{"x":110,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":111,"style":68},"396","#15803d",[51,113,115],{"x":114,"y":72,"style":73,"fill":111,"textAnchor":57},"566","tolerance 0.5 m — one vertex",[76,117],{"d":118,"fill":79,"stroke":80,"style":81},"M432 190 L560 148",[76,120],{"d":121,"fill":79,"stroke":80,"style":81},"M560 148 L688 106",[76,123],{"d":124,"fill":79,"stroke":80,"style":81},"M560 148 L672 212",[89,126],{"cx":127,"cy":92,"r":128,"fill":111},"560","7",[51,130,131],{"x":114,"y":104,"style":105,"fill":106,"textAnchor":57},"merged — all three directions traversable",[44,133],{"x":134,"y":135,"width":136,"height":137,"rx":138,"fill":139,"stroke":140,"style":141},"120","268","520","50","8","#fdf2e2","#b45309","stroke-width:2",[51,143,146],{"x":53,"y":144,"style":145,"fill":106,"textAnchor":57},"292","text-anchor:middle;font-size:11px;font-family:sans-serif","too small and nothing connects; too large and a bridge welds to the road beneath it",[51,148,150],{"x":53,"y":149,"style":105,"fill":80,"textAnchor":57},"311","match it to the digitising precision, not to your patience",[152,153,155],"h2",{"id":154},"prerequisites","Prerequisites",[157,158,159,172,179],"ul",{},[160,161,162,166,167,171],"li",{},[163,164,165],"strong",{},"QGIS 3.34 LTR"," or newer. The ",[168,169,170],"code",{},"qgis.analysis"," network classes have been stable for many releases.",[160,173,174,175,178],{},"A line layer ",[163,176,177],{},"in a projected CRS",". Tolerances and lengths in degrees are meaningless.",[160,180,181,182,185,186,189],{},"Lines split at junctions — ",[168,183,184],{},"native:splitwithlines"," against the layer itself, or ",[168,187,188],{},"native:explodelines",".",[152,191,193],{"id":192},"the-builders-arguments","The builder's arguments",[195,196,201],"pre",{"className":197,"code":198,"language":199,"meta":200,"style":200},"language-python shiki shiki-themes github-dark","from qgis.analysis import QgsGraphBuilder\n\nbuilder = QgsGraphBuilder(\n    roads.sourceCrs(),   # CRS the graph works in\n    True,                # otfEnabled — reproject features into that CRS\n    0.5,                 # topology tolerance, in CRS units\n    \"WGS84\",             # ellipsoid for geodesic length, when otf is on\n)\n","python","",[168,202,203,222,229,241,251,264,276,289],{"__ignoreMap":200},[204,205,208,212,216,219],"span",{"class":206,"line":207},"line",1,[204,209,211],{"class":210},"snl16","from",[204,213,215],{"class":214},"s95oV"," qgis.analysis ",[204,217,218],{"class":210},"import",[204,220,221],{"class":214}," QgsGraphBuilder\n",[204,223,225],{"class":206,"line":224},2,[204,226,228],{"emptyLinePlaceholder":227},true,"\n",[204,230,232,235,238],{"class":206,"line":231},3,[204,233,234],{"class":214},"builder ",[204,236,237],{"class":210},"=",[204,239,240],{"class":214}," QgsGraphBuilder(\n",[204,242,244,247],{"class":206,"line":243},4,[204,245,246],{"class":214},"    roads.sourceCrs(),   ",[204,248,250],{"class":249},"sjoCn","# CRS the graph works in\n",[204,252,254,258,261],{"class":206,"line":253},5,[204,255,257],{"class":256},"sDLfK","    True",[204,259,260],{"class":214},",                ",[204,262,263],{"class":249},"# otfEnabled — reproject features into that CRS\n",[204,265,267,270,273],{"class":206,"line":266},6,[204,268,269],{"class":256},"    0.5",[204,271,272],{"class":214},",                 ",[204,274,275],{"class":249},"# topology tolerance, in CRS units\n",[204,277,279,283,286],{"class":206,"line":278},7,[204,280,282],{"class":281},"sU2Wk","    \"WGS84\"",[204,284,285],{"class":214},",             ",[204,287,288],{"class":249},"# ellipsoid for geodesic length, when otf is on\n",[204,290,292],{"class":206,"line":291},8,[204,293,294],{"class":214},")\n",[14,296,297,300],{},[163,298,299],{},"Breakdown:"," The CRS is what the resulting vertex coordinates are expressed in, so it should be the CRS you want to ask questions in. The on-the-fly flag lets you feed a layer in a different projection and have the builder transform as it walks, which is convenient and slower than reprojecting the layer once beforehand. The tolerance is the argument that matters and it is covered below. The ellipsoid affects length computation and is irrelevant when you are already in a metric projected CRS, which you should be.",[152,302,304],{"id":303},"choosing-a-tolerance","Choosing a tolerance",[14,306,307],{},"The tolerance is a distance within which two endpoints are treated as the same vertex. Choose it from the data's digitising precision, not by trial and error:",[157,309,310,316,322,328],{},[160,311,312,315],{},[163,313,314],{},"Zero"," for a network you generated programmatically or that came from a topologically clean source — snapping was already exact, and a non-zero tolerance can only cause damage.",[160,317,318,321],{},[163,319,320],{},"A few centimetres"," for a professionally digitised network with snapping enabled during capture.",[160,323,324,327],{},[163,325,326],{},"Half a metre to a metre"," for a road centreline layer of ordinary quality.",[160,329,330,333],{},[163,331,332],{},"Never more than the narrowest real gap you want preserved."," On an urban layer with a footbridge two metres above a road, a two-metre tolerance connects them.",[195,335,337],{"className":197,"code":336,"language":199,"meta":200,"style":200},"from qgis.core import QgsProject\n\nroads = QgsProject.instance().mapLayersByName(\"roads\")[0]\n\nbuilder = QgsGraphBuilder(roads.sourceCrs(), True, 0.5)\ndirector.makeGraph(builder, [])\ngraph = builder.graph()\n\nprint(\"vertices:\", graph.vertexCount())\nprint(\"edges:\", graph.edgeCount())\n",[168,338,339,351,355,376,380,400,405,415,419,434],{"__ignoreMap":200},[204,340,341,343,346,348],{"class":206,"line":207},[204,342,211],{"class":210},[204,344,345],{"class":214}," qgis.core ",[204,347,218],{"class":210},[204,349,350],{"class":214}," QgsProject\n",[204,352,353],{"class":206,"line":224},[204,354,228],{"emptyLinePlaceholder":227},[204,356,357,360,362,365,368,371,373],{"class":206,"line":231},[204,358,359],{"class":214},"roads ",[204,361,237],{"class":210},[204,363,364],{"class":214}," QgsProject.instance().mapLayersByName(",[204,366,367],{"class":281},"\"roads\"",[204,369,370],{"class":214},")[",[204,372,46],{"class":256},[204,374,375],{"class":214},"]\n",[204,377,378],{"class":206,"line":243},[204,379,228],{"emptyLinePlaceholder":227},[204,381,382,384,386,389,392,395,398],{"class":206,"line":253},[204,383,234],{"class":214},[204,385,237],{"class":210},[204,387,388],{"class":214}," QgsGraphBuilder(roads.sourceCrs(), ",[204,390,391],{"class":256},"True",[204,393,394],{"class":214},", ",[204,396,397],{"class":256},"0.5",[204,399,294],{"class":214},[204,401,402],{"class":206,"line":266},[204,403,404],{"class":214},"director.makeGraph(builder, [])\n",[204,406,407,410,412],{"class":206,"line":278},[204,408,409],{"class":214},"graph ",[204,411,237],{"class":210},[204,413,414],{"class":214}," builder.graph()\n",[204,416,417],{"class":206,"line":291},[204,418,228],{"emptyLinePlaceholder":227},[204,420,422,425,428,431],{"class":206,"line":421},9,[204,423,424],{"class":256},"print",[204,426,427],{"class":214},"(",[204,429,430],{"class":281},"\"vertices:\"",[204,432,433],{"class":214},", graph.vertexCount())\n",[204,435,437,439,441,444],{"class":206,"line":436},10,[204,438,424],{"class":256},[204,440,427],{"class":214},[204,442,443],{"class":281},"\"edges:\"",[204,445,446],{"class":214},", graph.edgeCount())\n",[14,448,449,451,452,455],{},[163,450,299],{}," Passing an empty list to ",[168,453,454],{},"makeGraph"," builds the graph without tying any points to it, which is exactly what you want while tuning the tolerance. The two counts are the fastest diagnostic available: build the graph at tolerance 0 and again at your chosen value, and compare. A tolerance that merges nothing has produced identical counts and is doing no work; a tolerance that drops the vertex count by more than a few per cent is merging aggressively and deserves a look at what it joined.",[152,457,459],{"id":458},"reading-the-graph-back","Reading the graph back",[14,461,462],{},[29,463,466,469,472,475,487,490,496,501,506,510,514,519,523,529,534,538,542,545,548,551,554,559],{"viewBox":464,"role":32,"ariaLabel":465,"xmlns":34},"0 0 760 316","The structure of a built graph: a vertex holds a point and lists of incoming and outgoing edge ids, and an edge holds its from and to vertex ids plus a cost per strategy",[36,467,468],{},"What a vertex and an edge actually hold",[40,470,471],{},"A vertex stores its map coordinate and two lists of edge identifiers, one for edges arriving and one for edges leaving. An edge stores the identifiers of the vertices it runs between and a cost for each strategy that was added to the director, indexed in the order the strategies were added.",[44,473],{"x":46,"y":46,"width":47,"height":474,"fill":49},"316",[476,477,478],"defs",{},[479,480,484],"marker",{"id":481,"viewBox":482,"refX":138,"refY":93,"markerWidth":128,"markerHeight":128,"orient":483},"ngbStructArrow","0 0 10 10","auto-start-reverse",[76,485],{"d":486,"fill":106},"M0 0 L10 5 L0 10 z",[51,488,489],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Everything is an integer id",[44,491],{"x":61,"y":492,"width":493,"height":96,"rx":65,"fill":494,"stroke":495,"style":68},"48","304","#eef7f4","#0f766e",[51,497,500],{"x":498,"y":499,"style":73,"fill":495,"textAnchor":57},"176","74","graph.vertex(id)",[51,502,505],{"x":62,"y":503,"style":504,"fill":106},"106","font-size:10.5px;font-family:monospace",".point() → QgsPointXY",[51,507,509],{"x":62,"y":508,"style":504,"fill":106},"134",".incomingEdges() → [int]",[51,511,513],{"x":62,"y":512,"style":504,"fill":106},"162",".outgoingEdges() → [int]",[51,515,518],{"x":62,"y":516,"style":517,"fill":80},"196","font-size:10px;font-family:sans-serif","a dead end has an empty",[51,520,522],{"x":62,"y":521,"style":517,"fill":80},"214","outgoing list",[206,524],{"x1":525,"y1":526,"x2":527,"y2":526,"stroke":106,"style":528},"328","144","358","stroke-width:2;marker-end:url(#ngbStructArrow)",[44,530],{"x":531,"y":492,"width":531,"height":96,"rx":65,"fill":532,"stroke":533,"style":68},"368","#eff3ff","#2563eb",[51,535,537],{"x":536,"y":499,"style":73,"fill":533,"textAnchor":57},"552","graph.edge(id)",[51,539,541],{"x":540,"y":503,"style":504,"fill":106},"390",".fromVertex() → int",[51,543,544],{"x":540,"y":508,"style":504,"fill":106},".toVertex() → int",[51,546,547],{"x":540,"y":512,"style":504,"fill":106},".cost(criterion) → float",[51,549,550],{"x":540,"y":516,"style":517,"fill":80},"criterion 0 is the first strategy",[51,552,553],{"x":540,"y":521,"style":517,"fill":80},"added to the director",[44,555],{"x":556,"y":557,"width":558,"height":62,"rx":138,"fill":66,"stroke":67,"style":141},"80","262","600",[51,560,562],{"x":53,"y":561,"style":145,"fill":67,"textAnchor":57},"290","nothing here points back at a feature — the link to the source layer is gone",[195,564,566],{"className":197,"code":565,"language":199,"meta":200,"style":200},"vertex = graph.vertex(0)\nprint(vertex.point().asWkt(1))\nprint(\"out:\", vertex.outgoingEdges(), \"in:\", vertex.incomingEdges())\n\nedge = graph.edge(vertex.outgoingEdges()[0])\nprint(edge.fromVertex(), \"→\", edge.toVertex(), \"cost\", edge.cost(0))\n",[168,567,568,582,595,613,617,632],{"__ignoreMap":200},[204,569,570,573,575,578,580],{"class":206,"line":207},[204,571,572],{"class":214},"vertex ",[204,574,237],{"class":210},[204,576,577],{"class":214}," graph.vertex(",[204,579,46],{"class":256},[204,581,294],{"class":214},[204,583,584,586,589,592],{"class":206,"line":224},[204,585,424],{"class":256},[204,587,588],{"class":214},"(vertex.point().asWkt(",[204,590,591],{"class":256},"1",[204,593,594],{"class":214},"))\n",[204,596,597,599,601,604,607,610],{"class":206,"line":231},[204,598,424],{"class":256},[204,600,427],{"class":214},[204,602,603],{"class":281},"\"out:\"",[204,605,606],{"class":214},", vertex.outgoingEdges(), ",[204,608,609],{"class":281},"\"in:\"",[204,611,612],{"class":214},", vertex.incomingEdges())\n",[204,614,615],{"class":206,"line":243},[204,616,228],{"emptyLinePlaceholder":227},[204,618,619,622,624,627,629],{"class":206,"line":253},[204,620,621],{"class":214},"edge ",[204,623,237],{"class":210},[204,625,626],{"class":214}," graph.edge(vertex.outgoingEdges()[",[204,628,46],{"class":256},[204,630,631],{"class":214},"])\n",[204,633,634,636,639,642,645,648,651,653],{"class":206,"line":266},[204,635,424],{"class":256},[204,637,638],{"class":214},"(edge.fromVertex(), ",[204,640,641],{"class":281},"\"→\"",[204,643,644],{"class":214},", edge.toVertex(), ",[204,646,647],{"class":281},"\"cost\"",[204,649,650],{"class":214},", edge.cost(",[204,652,46],{"class":256},[204,654,594],{"class":214},[14,656,657,659,660,663,664,667,668,671,672,675,676,679,680,675,683,686,687,690],{},[163,658,299],{}," ",[168,661,662],{},"cost(0)"," is the first strategy added to the director, ",[168,665,666],{},"cost(1)"," the second, and so on — the index is positional, so reordering the ",[168,669,670],{},"addStrategy"," calls silently changes what every query means. ",[168,673,674],{},"fromVertex()"," and ",[168,677,678],{},"toVertex()"," are the modern accessor names; QGIS before 3.24 used ",[168,681,682],{},"inVertex()",[168,684,685],{},"outVertex()"," with the opposite-sounding semantics, which is a well-known source of reversed routes in older code. Where a script must run on both, ",[168,688,689],{},"getattr(edge, \"fromVertex\", edge.inVertex)()"," is the pragmatic bridge.",[14,692,693,694,697],{},"Finding a vertex from a coordinate uses ",[168,695,696],{},"findVertex",", which is an exact match rather than a nearest-neighbour search:",[195,699,701],{"className":197,"code":700,"language":199,"meta":200,"style":200},"from qgis.core import QgsPointXY\n\nindex = graph.findVertex(QgsPointXY(445120, 187330))\nprint(index)  # -1 if no vertex sits exactly there\n",[168,702,703,714,718,738],{"__ignoreMap":200},[204,704,705,707,709,711],{"class":206,"line":207},[204,706,211],{"class":210},[204,708,345],{"class":214},[204,710,218],{"class":210},[204,712,713],{"class":214}," QgsPointXY\n",[204,715,716],{"class":206,"line":224},[204,717,228],{"emptyLinePlaceholder":227},[204,719,720,723,725,728,731,733,736],{"class":206,"line":231},[204,721,722],{"class":214},"index ",[204,724,237],{"class":210},[204,726,727],{"class":214}," graph.findVertex(QgsPointXY(",[204,729,730],{"class":256},"445120",[204,732,394],{"class":214},[204,734,735],{"class":256},"187330",[204,737,594],{"class":214},[204,739,740,742,745],{"class":206,"line":243},[204,741,424],{"class":256},[204,743,744],{"class":214},"(index)  ",[204,746,747],{"class":249},"# -1 if no vertex sits exactly there\n",[14,749,750,752,753,756,757,759,760,764],{},[163,751,299],{}," This returns ",[168,754,755],{},"-1"," for anything that is not precisely a vertex position, which is why you never pass your own coordinates to it. Pass the points through ",[168,758,454],{},"'s tie list and look up the ",[761,762,763],"em",{},"returned"," positions instead — those are guaranteed to be on the graph.",[152,766,768],{"id":767},"auditing-connectivity-before-you-trust-it","Auditing connectivity before you trust it",[14,770,771],{},"A network in fragments routes perfectly within each fragment and reports failure between them, which reads as \"no route exists\" rather than \"your data is broken\".",[195,773,775],{"className":197,"code":774,"language":199,"meta":200,"style":200},"from qgis.analysis import QgsGraphAnalyzer\n\nseed = 0\ntree, costs = QgsGraphAnalyzer.dijkstra(graph, seed, 0)\n\nreached = sum(1 for edge_id in tree if edge_id != -1)\ntotal = graph.vertexCount()\nprint(f\"{reached}\u002F{total} vertices reachable from vertex 0 \"\n      f\"({100 * reached \u002F total:.1f}%)\")\n",[168,776,777,788,792,802,816,820,861,871,905],{"__ignoreMap":200},[204,778,779,781,783,785],{"class":206,"line":207},[204,780,211],{"class":210},[204,782,215],{"class":214},[204,784,218],{"class":210},[204,786,787],{"class":214}," QgsGraphAnalyzer\n",[204,789,790],{"class":206,"line":224},[204,791,228],{"emptyLinePlaceholder":227},[204,793,794,797,799],{"class":206,"line":231},[204,795,796],{"class":214},"seed ",[204,798,237],{"class":210},[204,800,801],{"class":256}," 0\n",[204,803,804,807,809,812,814],{"class":206,"line":243},[204,805,806],{"class":214},"tree, costs ",[204,808,237],{"class":210},[204,810,811],{"class":214}," QgsGraphAnalyzer.dijkstra(graph, seed, ",[204,813,46],{"class":256},[204,815,294],{"class":214},[204,817,818],{"class":206,"line":253},[204,819,228],{"emptyLinePlaceholder":227},[204,821,822,825,827,830,832,834,837,840,843,846,849,851,854,857,859],{"class":206,"line":266},[204,823,824],{"class":214},"reached ",[204,826,237],{"class":210},[204,828,829],{"class":256}," sum",[204,831,427],{"class":214},[204,833,591],{"class":256},[204,835,836],{"class":210}," for",[204,838,839],{"class":214}," edge_id ",[204,841,842],{"class":210},"in",[204,844,845],{"class":214}," tree ",[204,847,848],{"class":210},"if",[204,850,839],{"class":214},[204,852,853],{"class":210},"!=",[204,855,856],{"class":210}," -",[204,858,591],{"class":256},[204,860,294],{"class":214},[204,862,863,866,868],{"class":206,"line":278},[204,864,865],{"class":214},"total ",[204,867,237],{"class":210},[204,869,870],{"class":214}," graph.vertexCount()\n",[204,872,873,875,877,880,883,886,889,892,895,897,900,902],{"class":206,"line":291},[204,874,424],{"class":256},[204,876,427],{"class":214},[204,878,879],{"class":210},"f",[204,881,882],{"class":281},"\"",[204,884,885],{"class":256},"{",[204,887,888],{"class":214},"reached",[204,890,891],{"class":256},"}",[204,893,894],{"class":281},"\u002F",[204,896,885],{"class":256},[204,898,899],{"class":214},"total",[204,901,891],{"class":256},[204,903,904],{"class":281}," vertices reachable from vertex 0 \"\n",[204,906,907,910,913,916,919,922,924,927,930,932,935],{"class":206,"line":421},[204,908,909],{"class":210},"      f",[204,911,912],{"class":281},"\"(",[204,914,915],{"class":256},"{100",[204,917,918],{"class":210}," *",[204,920,921],{"class":214}," reached ",[204,923,894],{"class":210},[204,925,926],{"class":214}," total",[204,928,929],{"class":210},":.1f",[204,931,891],{"class":256},[204,933,934],{"class":281},"%)\"",[204,936,294],{"class":214},[14,938,939,941,942,945,946,949,950,952],{},[163,940,299],{}," Testing reachability on ",[168,943,944],{},"tree"," rather than on ",[168,947,948],{},"costs"," is the robust form — unreachable vertices carry a sentinel cost that is large but finite, and comparing against a magic threshold works until someone routes across a continent. The seed vertex is itself excluded from the count (its tree entry is ",[168,951,755],{},"), which is a one-vertex discrepancy not worth correcting. Anything under about 95% on a road network means real fragmentation; write the unreachable vertices out and look at where they are:",[195,954,956],{"className":197,"code":955,"language":199,"meta":200,"style":200},"from qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry\n\norphans = QgsVectorLayer(\"Point?crs=\" + roads.sourceCrs().authid(), \"orphans\", \"memory\")\nprovider = orphans.dataProvider()\n\nfeatures = []\nfor vertex_id, edge_id in enumerate(tree):\n    if edge_id == -1 and vertex_id != seed:\n        feature = QgsFeature()\n        feature.setGeometry(QgsGeometry.fromPointXY(graph.vertex(vertex_id).point()))\n        features.append(feature)\n\nprovider.addFeatures(features)\nQgsProject.instance().addMapLayer(orphans)\n",[168,957,958,969,973,1002,1012,1016,1026,1042,1067,1077,1082,1088,1093,1099],{"__ignoreMap":200},[204,959,960,962,964,966],{"class":206,"line":207},[204,961,211],{"class":210},[204,963,345],{"class":214},[204,965,218],{"class":210},[204,967,968],{"class":214}," QgsVectorLayer, QgsFeature, QgsGeometry\n",[204,970,971],{"class":206,"line":224},[204,972,228],{"emptyLinePlaceholder":227},[204,974,975,978,980,983,986,989,992,995,997,1000],{"class":206,"line":231},[204,976,977],{"class":214},"orphans ",[204,979,237],{"class":210},[204,981,982],{"class":214}," QgsVectorLayer(",[204,984,985],{"class":281},"\"Point?crs=\"",[204,987,988],{"class":210}," +",[204,990,991],{"class":214}," roads.sourceCrs().authid(), ",[204,993,994],{"class":281},"\"orphans\"",[204,996,394],{"class":214},[204,998,999],{"class":281},"\"memory\"",[204,1001,294],{"class":214},[204,1003,1004,1007,1009],{"class":206,"line":243},[204,1005,1006],{"class":214},"provider ",[204,1008,237],{"class":210},[204,1010,1011],{"class":214}," orphans.dataProvider()\n",[204,1013,1014],{"class":206,"line":253},[204,1015,228],{"emptyLinePlaceholder":227},[204,1017,1018,1021,1023],{"class":206,"line":266},[204,1019,1020],{"class":214},"features ",[204,1022,237],{"class":210},[204,1024,1025],{"class":214}," []\n",[204,1027,1028,1031,1034,1036,1039],{"class":206,"line":278},[204,1029,1030],{"class":210},"for",[204,1032,1033],{"class":214}," vertex_id, edge_id ",[204,1035,842],{"class":210},[204,1037,1038],{"class":256}," enumerate",[204,1040,1041],{"class":214},"(tree):\n",[204,1043,1044,1047,1049,1052,1054,1056,1059,1062,1064],{"class":206,"line":291},[204,1045,1046],{"class":210},"    if",[204,1048,839],{"class":214},[204,1050,1051],{"class":210},"==",[204,1053,856],{"class":210},[204,1055,591],{"class":256},[204,1057,1058],{"class":210}," and",[204,1060,1061],{"class":214}," vertex_id ",[204,1063,853],{"class":210},[204,1065,1066],{"class":214}," seed:\n",[204,1068,1069,1072,1074],{"class":206,"line":421},[204,1070,1071],{"class":214},"        feature ",[204,1073,237],{"class":210},[204,1075,1076],{"class":214}," QgsFeature()\n",[204,1078,1079],{"class":206,"line":436},[204,1080,1081],{"class":214},"        feature.setGeometry(QgsGeometry.fromPointXY(graph.vertex(vertex_id).point()))\n",[204,1083,1085],{"class":206,"line":1084},11,[204,1086,1087],{"class":214},"        features.append(feature)\n",[204,1089,1091],{"class":206,"line":1090},12,[204,1092,228],{"emptyLinePlaceholder":227},[204,1094,1096],{"class":206,"line":1095},13,[204,1097,1098],{"class":214},"provider.addFeatures(features)\n",[204,1100,1102],{"class":206,"line":1101},14,[204,1103,1104],{"class":214},"QgsProject.instance().addMapLayer(orphans)\n",[14,1106,1107,1109,1110,1113],{},[163,1108,299],{}," Batching the features into one ",[168,1111,1112],{},"addFeatures"," call rather than adding them one at a time is the difference between instant and slow on a large network. Looking at the result on the map is far more informative than any statistic: orphans clustered along one road mean a single break, orphans scattered evenly mean the tolerance is too tight everywhere, and orphans forming a neat separate town mean the extract simply does not include the road that joins them.",[152,1115,1117],{"id":1116},"what-a-large-network-costs","What a large network costs",[14,1119,1120],{},[29,1121,1124,1127,1130,1133,1136,1142,1145,1150,1155,1159,1162,1167,1170,1176,1180,1184,1189],{"viewBox":1122,"role":32,"ariaLabel":1123,"xmlns":34},"0 0 760 288","Graph build cost against network size, showing that build time and memory grow with the number of segments while query time stays small",[36,1125,1126],{},"The build dominates; the queries are cheap",[40,1128,1129],{},"As the number of line segments grows from ten thousand to a million, graph build time rises steeply and memory use rises with it, while the time to answer a single shortest-path query grows only slowly. This is why building once and querying many times is the right structure for anything beyond a single route.",[44,1131],{"x":46,"y":46,"width":47,"height":1132,"fill":49},"288",[51,1134,1135],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Build once. Query as often as you like.",[206,1137],{"x1":1138,"y1":1139,"x2":1140,"y2":1139,"stroke":106,"style":1141},"90","222","700","stroke-width:1.8",[206,1143],{"x1":1138,"y1":1144,"x2":1138,"y2":1139,"stroke":106,"style":1141},"52",[51,1146,1149],{"x":1147,"y":1148,"style":105,"fill":106,"textAnchor":57},"395","252","line segments in the network",[51,1151,1154],{"x":499,"y":1152,"style":1153,"fill":80},"60","text-anchor:end;font-size:10px;font-family:sans-serif","cost",[76,1156],{"d":1157,"fill":79,"stroke":140,"style":1158},"M110 214 L230 202 L350 176 L470 132 L590 82 L680 60","stroke-width:3",[76,1160],{"d":1161,"fill":79,"stroke":495,"style":1158},"M110 218 L230 216 L350 213 L470 209 L590 204 L680 200",[51,1163,1166],{"x":558,"y":1164,"style":1165,"fill":140},"76","font-size:10.5px;font-weight:bold;font-family:sans-serif","graph build",[51,1168,1169],{"x":558,"y":71,"style":1165,"fill":495},"one query",[51,1171,1175],{"x":1172,"y":1173,"style":1174,"fill":80,"textAnchor":57},"110","238","text-anchor:middle;font-size:9.5px;font-family:sans-serif","10k",[51,1177,1179],{"x":1178,"y":1173,"style":1174,"fill":80,"textAnchor":57},"350","100k",[51,1181,1183],{"x":1182,"y":1173,"style":1174,"fill":80,"textAnchor":57},"590","1M",[44,1185],{"x":134,"y":1186,"width":136,"height":1187,"rx":1188,"fill":139,"stroke":140,"style":1141},"264","22","6",[51,1190,1193],{"x":53,"y":1191,"style":1192,"fill":106,"textAnchor":57},"280","text-anchor:middle;font-size:10px;font-family:sans-serif","clip the network to the study area and the whole curve moves left",[14,1195,1196],{},"The graph lives entirely in memory and holds a vertex per merged endpoint and an edge per traversable direction, so a two-way network produces roughly two edges per segment. As a rough guide, a few hundred thousand segments builds in seconds and fits comfortably; a few million builds in minutes and will exhaust a modest machine. The fix is always the same and always available: clip the network to the area you are actually working in, with a buffer wider than the longest route you will ask for.",[152,1198,1200],{"id":1199},"qgis-version-compatibility","QGIS version compatibility",[14,1202,1203,1204,1206,1207,675,1210,1212,1213,675,1215,1217,1218,1221,1222,1225],{},"The ",[168,1205,170],{}," network classes have been stable since QGIS 3.0 with one notable change: ",[168,1208,1209],{},"QgsGraphEdge.fromVertex()",[168,1211,678],{}," were introduced in 3.24, deprecating ",[168,1214,682],{},[168,1216,685],{},". ",[168,1219,1220],{},"QgsVectorLayerDirector.DirectionBoth"," moved into a scoped ",[168,1223,1224],{},"Direction"," enum in newer releases while remaining accessible at the old location, so existing code keeps working.",[152,1227,1229],{"id":1228},"troubleshooting","Troubleshooting",[157,1231,1232,1241,1247,1253,1263,1276],{},[160,1233,1234,1240],{},[163,1235,1236,1239],{},[168,1237,1238],{},"graph.edgeCount()"," is zero."," No strategy was added to the director, or the layer has no line features.",[160,1242,1243,1246],{},[163,1244,1245],{},"Everything is disconnected."," Lines are not split at junctions, or the tolerance is zero on a hand-digitised layer.",[160,1248,1249,1252],{},[163,1250,1251],{},"The build never finishes."," The layer is enormous, or the tolerance is huge, which makes endpoint merging quadratic in the worst case.",[160,1254,1255,1260,1261,189],{},[163,1256,1257,1259],{},[168,1258,696],{}," always returns −1."," You passed your own coordinate rather than the tied point returned by ",[168,1262,454],{},[160,1264,1265,1268,1269,894,1272,1275],{},[163,1266,1267],{},"Routes run backwards."," Old ",[168,1270,1271],{},"inVertex",[168,1273,1274],{},"outVertex"," code, or a direction field whose forward and backward values are swapped.",[160,1277,1278,1281],{},[163,1279,1280],{},"Memory use explodes."," Two edges per segment plus per-vertex edge lists. Clip the network first.",[152,1283,1285],{"id":1284},"conclusion","Conclusion",[14,1287,1288,1289,394,1293,1297],{},"Build in a projected CRS, choose the tolerance from the data's precision rather than from frustration, compare vertex counts before and after to see what the tolerance actually did, and run a reachability audit before believing a single route. Once the graph is right, everything built on it — ",[21,1290,1292],{"href":1291},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fshortest-path-between-points-pyqgis\u002F","shortest paths",[21,1294,1296],{"href":1295},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fcalculate-service-areas-pyqgis\u002F","service areas",", matrices — is straightforward.",[152,1299,1301],{"id":1300},"frequently-asked-questions","Frequently Asked Questions",[14,1303,1304,1307,1308,1311],{},[163,1305,1306],{},"Can I save a built graph and reload it later?","\nNo — there is no serialisation for ",[168,1309,1310],{},"QgsGraph",". Rebuild it, and if the build is the bottleneck, keep the process alive rather than the graph, or clip the network smaller.",[14,1313,1314,1317],{},[163,1315,1316],{},"Does the builder respect a layer's selection or subset string?","\nIt reads the features the layer exposes, so a subset string does restrict it. A selection does not — the director iterates all visible features regardless.",[14,1319,1320,1323,1324,1327,1328,1331],{},[163,1321,1322],{},"How do I add my own per-edge cost?","\nThrough a strategy. ",[168,1325,1326],{},"QgsNetworkStrategy"," can be subclassed in Python, with ",[168,1329,1330],{},"cost()"," returning whatever you compute per feature, which is how penalties for surface type or gradient get modelled.",[14,1333,1334,1337],{},[163,1335,1336],{},"Is the graph directed even when everything is two-way?","\nYes. A two-way segment becomes two edges pointing opposite ways, which is why edge counts are about double what people expect.",[152,1339,1341],{"id":1340},"related","Related",[157,1343,1344,1349,1354,1360,1366],{},[160,1345,1346,1348],{},[21,1347,24],{"href":23}," — the guide this recipe belongs to",[160,1350,1351],{},[21,1352,1353],{"href":1291},"Find the Shortest Path Between Points in PyQGIS",[160,1355,1356],{},[21,1357,1359],{"href":1358},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fsnap-points-to-network-pyqgis\u002F","Snap Points to a Network in PyQGIS",[160,1361,1362],{},[21,1363,1365],{"href":1364},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuild-spatial-index-pyqgis\u002F","Build a Spatial Index in PyQGIS",[160,1367,1368],{},[21,1369,1371],{"href":1370},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fclip-vector-layer-pyqgis\u002F","Clip a Vector Layer in PyQGIS",[1373,1374,1375],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":200,"searchDepth":224,"depth":224,"links":1377},[1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388],{"id":154,"depth":224,"text":155},{"id":192,"depth":224,"text":193},{"id":303,"depth":224,"text":304},{"id":458,"depth":224,"text":459},{"id":767,"depth":224,"text":768},{"id":1116,"depth":224,"text":1117},{"id":1199,"depth":224,"text":1200},{"id":1228,"depth":224,"text":1229},{"id":1284,"depth":224,"text":1285},{"id":1300,"depth":224,"text":1301},{"id":1340,"depth":224,"text":1341},"Turn a line layer into a routable graph in PyQGIS — set the topology tolerance, inspect vertices and edges, audit connectivity, and keep the build affordable on a large network.","md",{"slug":1392,"type":1393,"breadcrumb":1394,"datePublished":1395,"dateModified":1395},"build-network-graph-with-qgsgraphbuilder","article","Build a Network Graph","2026-09-05","\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-network-graph-with-qgsgraphbuilder",{"title":5,"description":1389},"spatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-network-graph-with-qgsgraphbuilder\u002Findex","jFZis0bD8gaGynGtKYWtUFG-RmR8vBcjJzBc17PSpAA",1788563855517]