#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pandas as pd
from shapely.geometry import Point, LineString
from shapely import ops
import numpy as np
import time
from collections import Counter
from dhd.logs import log
from dhd.utils import reverse_linestring, distance
from dhd.exceptions import GeometryError
[docs]def append_superpositions(superpositions, idx1, idx2):
"""
Add indices *idx1* and *idx2* (associted to the new superposed sink) in the
appropriate set of indices ; in-place.
Parameters
----------
superpositions: list
List of sets. Each set arguments are the indices of the sinks located
at a same location.
idx1: int
Index of the first superposed sink.
idx2: int
Index of the second superposed sink.
"""
if len(superpositions) == 0:
superpositions.append(set([idx1, idx2]))
else:
new_superposition = True
for superposition in superpositions:
if idx1 in superposition or idx2 in superposition:
superposition.add(idx1)
superposition.add(idx2)
new_superposition = False
break
if new_superposition is True:
superpositions.append(set([idx1, idx2]))
[docs]def order_superpositions(superpositions):
"""
Put all superposition indices in one single list and remove one index from
each superposition set.
Parameters
----------
superpositions: list
List of sets. Each set arguments are the indices of the sinks located
at a same location.
Returns
-------
list
List of indices associated to the rows to remove from the dataframe
*sinks*.
"""
indices = list()
for superposition in superpositions:
l = list(superposition)
l.pop(0)
indices += l
return indices
[docs]def get_superpositions(sinks, epsilon):
"""
Find the sets of sinks superpositions.
Parameters
----------
sinks: DataFrame
Dataframe of the heating sinks.
epsilon:
Only sinks whose superposition area is larger than *epsilon* times the
area of the smallest sink area are considered in superposition.
Returns
-------
list
List of sets. Each set arguments are the indices of the sinks located
at a same location.
"""
superpositions = []
for idx1, sink1 in sinks.iterrows():
for idx2, sink2 in sinks.iterrows():
if idx2 < idx1:
p1 = sink1["geometry"]
p2 = sink2["geometry"]
if p1.intersection(p2).area > epsilon * min(p1.area, p2.area):
append_superpositions(superpositions, idx1, idx2)
return superpositions
[docs]def clean_superpositions(sinks, epsilon=0.01):
"""
Remove superposed sinks.
If multiple sink geometries are located at the same location (for example
because of a bad time evolution documentation) only one of them is retained.
Parameters
----------
sinks: DataFrame
Dataframe of the heating sinks.
epsilon: float, optional
Only sinks whose superposition area is larger than *epsilon* times the
area of the smallest sink area are considered in superposition. Default
is 0.01.
Returns
-------
DataFrame
Updated sinks dataframe.
"""
sinks_ = sinks.reset_index(inplace=False)
sinks_.columns = ["index"] + list(sinks_.columns[1:])
superpositions = get_superpositions(sinks_, epsilon)
indices = order_superpositions(superpositions)
sinks_.drop(index=indices, inplace=True)
if len(superpositions) > 0:
text = "The following set of sinks were superposed: "
for superposition in superpositions:
text += "{}, ".format(superposition)
text += "Only the first sink of each set is retained."
log.warning(text)
return sinks_.set_index("index", inplace=False)
[docs]def check_index_repetition(df):
"""
Check that there is no repetition amongst the indices of the given dataframe.
Parameters
----------
df: DataFrame
Returns
-------
bool
True if the assertion is passed.
"""
repetitions = {k: v for k, v in Counter(df.index).items() if v > 1}
text = (
"The following indices are repeated in the dataframe 'sinks' "
"(index, # of occurences): {}"
)
assert len(repetitions) == 0, text.format(repetitions)
return True
[docs]def check_data_structure(sinks, streets, sources, barriers, ignore_index):
"""
Ensure that the provided dataframes have the required structure.
Parameters
----------
streets: DataFrame
Dataframe of the street network.
sinks: DataFrame
Dataframe of the heating sinks.
sources: DataFrame
Dataframe of the heating source(s).
barriers: DataFrame
Dataframe of the natural barriers.
Returns
-------
bool
True if the assertion are passed.
"""
text1 = "The dataframe '{}' must contain a geometry column: 'geometry'."
text2 = "The dataframe '{}' must contain two index columns: 'idA' and 'idB'."
assert {"geometry"}.issubset(sinks.columns), text1.format("sinks")
assert {"geometry"}.issubset(streets.columns), text1.format("streets")
assert {"geometry"}.issubset(sources.columns), text1.format("sources")
if barriers is not None:
assert {"geometry"}.issubset(barriers.columns), text1.format("barriers")
assert {"idA", "idB"}.issubset(streets.columns), text2.format("streets")
if ignore_index is False:
check_index_repetition(sinks)
return True
[docs]def set_line_coordinates(vertices):
"""
Add coordinates columns ('xA','xB','yA','yB') to the dataframe *vertices* ;
in-place.
The end node coordinates of each geometry (edge) of *vertices* are
computed and stored in their associated column (first end -> 'A', second
end -> 'B').
Parameters
----------
vertices: DataFrame
Dataframe of the vertices.
"""
for idx, row in vertices.iterrows():
coords = list(row["geometry"].coords)
vertices.at[idx, "xA"] = coords[0][0]
vertices.at[idx, "yA"] = coords[0][1]
vertices.at[idx, "xB"] = coords[-1][0]
vertices.at[idx, "yB"] = coords[-1][1]
[docs]def connection_weight(line, func):
"""
Return the weight of the connection *line* according to weight function
*func*.
If *func* is None, the weight is set equal to the connection length.
Parameters
----------
line: LineString
Geometry of the connection.
func: function
Function of the connection length returning its weight.
Returns
-------
float
Weight of the connection.
"""
if func is None:
weight = line.length
else:
weight = func(line.length)
return weight
[docs]def set_default_weight_to_length(vertices):
"""
Add weight column to the dataframe *vertices* with element equal to the
length of the associated geometry ; in-place.
Parameters
----------
vertices: DataFrame
Dataframe of the vertices.
"""
for idx, vertex in vertices.iterrows():
vertices.loc[idx, "weight"] = vertex["geometry"].length
[docs]def init_vertices(streets):
"""
Initilization of the dataframe *vertices* from the input dataframe *streets*.
Parameters
----------
streets: DataFrame
Dataframe of the street network.
Returns
-------
DataFrame
Dataframe of the vertices.
"""
vertices = streets.copy()
# set coordinates
if not {"xA", "yA", "xB", "yB"}.issubset(vertices.columns):
set_line_coordinates(vertices)
# set weights
if not "weight" in vertices.columns:
set_default_weight_to_length(vertices)
# set edges indices
vertices["idE"] = [
"{}_{}".format(vertex["idA"], vertex["idB"])
for i, vertex in vertices.iterrows()
]
text = "Initilization of the dataframe 'vertices' with {} rows."
log.info(text.format(len(vertices)))
return vertices
[docs]def get_terminals_indices(sources, sinks, ignore_index):
"""
List the indices of the dataframe *terminals* from the dataframes *sources*
and *sinks*.
If *index* is True, use the indices of the dataframes, otherwise generate
new indices.
Parameters
----------
sources: DataFrame
Dataframe of the heating source(s).
sinks: DataFrame
Dataframe of the sinks.
ignore_index: bool
If False, use indices from the dataframes *sources* and *sinks*,
otherwise generate new indices.
Returns
-------
list
List of the dataframe *terminals* indices.
"""
indices1 = [str(i) + "S" for i in range(len(sources))]
if ignore_index == False:
indices2 = list(sinks.index)
else:
indices2 = [str(i) + "B" for i in range(len(sinks))]
indices = indices1 + indices2
return indices
[docs]def init_terminals(sources, sinks, ignore_index):
"""
Initialization of the dataframe *terminals* from the input dataframes *sinks*
and *sources*.
Parameters
----------
sources: DataFrame
Dataframe of the heating source(s).
sinks: DataFrame
Dataframe of the sinks.
ignore_index: bool
If False, use indices from the dataframes *sources* and *sinks*,
otherwise generate new indices.
Returns
-------
DataFrame
Dataframe of the terminals.
"""
# construct DataFrame structure
indices = get_terminals_indices(sources, sinks, ignore_index)
columns = ["_id", "_weight", "_geometry"]
terminals = pd.DataFrame(columns=columns, index=indices)
# fill in with empty lists
for column in columns:
terminals[column] = None
for idx in terminals.index:
terminals.loc[idx, column] = list()
# add columns
terminals["geometry"] = list(sources["geometry"].append(sinks["geometry"]))
terminals["kind"] = ["source"] * len(sources) + ["sink"] * len(sinks)
if "load" in sinks.columns:
list1 = [0] * len(sources)
list2 = list(sinks["load"])
terminals["load"] = list1 + list2
text = "Initilization of the dataframe 'terminals' with {} rows."
log.info(text.format(len(terminals)))
return terminals
[docs]def get_nearest_point(series1, series2):
"""
Get tuple coordinates of the orthogonal projections between the geometries
of the two given Series.
Parameters
----------
series1: Series
Series with a geometry key.
series2: Series
Series with a geometry key.
Returns
-------
tuple
Coordinates of the projection on *series1* geometry,
tuple
Coordinates of the projection on *series2* geometry.
"""
p1, p2 = ops.nearest_points(series1["geometry"], series2["geometry"])
p1, p2 = p1.coords[0], p2.coords[0]
return p1, p2
[docs]def is_crossing_a_barrier(p1, p2, barriers):
"""
Check if the two points *p1* and *p2* are on the same side of the list of
barriers *barriers*.
Parameters
----------
p1: tuple
Coordinates of the first point.
p2: tuple
Coordinates of the second point.
barriers: DataFrame
Dataframe of the natural barriers.
Returns
-------
bool
True if the line *p1*-*p2* crosses the *barriers* geometry, False
otherwise.
"""
l = LineString([p1, p2])
intersection = True in set([barrier.intersects(l) for barrier in barriers])
return intersection
[docs]def first_selection(vertices, terminal, R, barriers):
"""
Select the orthogonal projection on the edges of *vertices* within the
distance *R* from the geometry of *terminal*.
If not None the geometric constraint *barriers* prevent any connection
crossing it.
Parameters
----------
vertices: DataFrame
Dataframe of the vertices.
terminal: Series
Row of the *terminal* dataframe.
R: float
Maximal connection length.
barriers: DataFrame
Dataframe of the natural barriers.
Returns
-------
DataFrame
Dataframe of the selected connections with columns:
- 'idA': fist connection edge node index,
- 'idB': second connection edge node index,
- 'idE': connection edge index,
- 'xT', 'yT': coordinates of the connection point on the terminal,
- 'xC', 'yC': coordinates of the connection point on the vertex edge,
- 'd': length of the connection.
"""
# initialize the selection DataFrame
columns = ["idA", "idB", "idE", "xT", "yT", "xC", "yC", "d"]
selection = pd.DataFrame(columns=columns)
# loop over all vertices
for idx, vertex in vertices.iterrows():
# get projection points on the terminal and the vertex
p1, p2 = get_nearest_point(terminal, vertex)
# check natural barrier crossing
if barriers is not None:
intersection = is_crossing_a_barrier(p1, p2, barriers["geometry"])
if intersection:
# text = 'Impossible to connect terminal {} to vertex {} because of '\
# 'a natural barrier.'
# log.debug(text.format(terminal.name, vertex['idE']))
continue
# check distance within R
d = distance(p1, p2)
if d < R:
series = pd.Series(
[
vertex["idA"],
vertex["idB"],
vertex["idE"],
p1[0],
p1[1],
p2[0],
p2[1],
d,
],
index=columns,
)
selection = selection.append(series, ignore_index=True)
return selection
[docs]def remove_point(selection, idmin, r):
"""
Remove connections within distance *r* from the closest connection of index
*idmin* ; in-place.
Parameters
----------
selection: DataFrame
Dataframe of the connections selected by *dhd.connect.first_selection*.
idmin: int
Index of the connection having the smallest distance *d* in *selection*.
r: float
Minimal distance between two connections to the same terminal.
"""
# coordinates of the closest connection
p1 = (selection.at[idmin, "xC"], selection.at[idmin, "yC"])
# list of labels of connection to remove
labels = []
for i, point in selection.iterrows():
# coordinates of the tested connection
p2 = [point["xC"], point["yC"]]
d = distance(p1, p2)
if d < r:
labels.append(i)
# drop connection too close to p1
selection.drop(index=labels, inplace=True)
selection.reset_index(inplace=True, drop=True)
[docs]def second_selection(selection, r):
"""
Improve selection quality by removing connections within the distance *d*
from each other.
Parameters
----------
selection: DataFrame
Dataframe of the connections selected by *dhd.connect.first_selection*.
r: float
Minimal distance between two conections to the same terminal.
Returns
-------
DataFrame
Dataframe of the possible connections to the considered terminal. The
columns are identical to those of *selection*.
"""
# initialize DataFrame
connections = pd.DataFrame(columns=list(selection.keys()))
# remove connections from 'selection' and store them in 'connections' if
# distance criterion fulfilled until 'selection' is emplty
while True:
idmin = np.array(selection["d"]).argmin()
connections = connections.append(selection.loc[idmin], ignore_index=True)
remove_point(selection, idmin, r)
if len(selection) == 0:
break
return connections
[docs]def split_line(l, p, eps=0.1):
"""
Split the LineString *l* in two at a the Point *p* on the line.
Parameters
----------
l: LineString
Vertex edge to split at the connection point.
p: Point
Connection point on the vertex edge.
eps: float, optional
Distance under which two points are considered identical. Default is
0.1 m.
Returns
-------
LineString
Geometry of the first line,
LineString
Geometry of the second line.
"""
# check that 'p' belongs to 'l'.
if l.distance(p) > eps:
text = "The point {} must lie on the line {}."
raise GeometryError(text.format(p, l))
# get LineString coordinates
coords = list(l.coords)
n = len(coords)
# find first segment coordinates
coords1 = [coords[0]]
for i in range(n - 1):
p1, p2 = coords[i], coords[i + 1]
line = LineString([p1, p2])
# check that p is not on the line segment
if line.distance(p) > eps:
coords1.append(p2)
else:
coords1.append(p.coords[0])
break
# find second segment coordinates
coords2 = [p.coords[0]]
for j in range(i + 1, n):
coords2.append(coords[j])
# coordinates to LineString
line1 = LineString(coords1)
line2 = LineString(coords2)
return line1, line2
[docs]def sort_lines(line1, line2, coords):
"""
Sort *line1* and *line2* such that the first line has either its first points
equal to *coords* or its last point equal to *coords*.
Parameters
----------
line1: LineString
Geometry of the first line of the splitted edge.
line2: LineString
Geometry of the second line of the splitted edge.
coords: tuple
Coordinates of the edge end 'A'.
Returns
-------
LineString
Line with one end equal to the edge end 'A',
LineString
Line with one end equal to the edge end 'B'.
"""
coords1 = list(line1.coords)
coords2 = list(line2.coords)
if coords == coords1[0]:
lineA, lineB = line1, line2
elif coords == coords2[-1]:
lineA, lineB = reverse_linestring(line2), reverse_linestring(line1)
else:
raise GeometryError("The point {} correspond to none of the line ends.")
return lineA, lineB
[docs]def find_vertex(vertices, connection):
"""
Find which vertex of *vertices* corresponds to *connection*.
Parameters
----------
vertices: DataFrame
Dataframe of the vertices.
connection: Series
Row of the dataframe *connections* obtained with the function
*dhd.connect.second_selection*.
Returns
-------
Series
Series of the dataframe *vertices* corresponding to *connection*.
"""
vertex = vertices.loc[vertices["idE"] == connection["idE"]].iloc[0]
return vertex
[docs]def get_connection_coordinates(connection, vertex):
"""
Get the coordinates of the different points of the connection.
The vertex edges end are labeled 'A' and 'B', the terminal is labeld 'T' and
the connection point on the vertex edge is labeled 'C'.
Parameters
----------
connection: Series
Row of the dataframe *connections* obtained from the function
*dhd.connect.second_selection*. It contains all the information on the
terminal to be connected.
vertex: Series
Row of the dataframe *vertices*. It contains all the information on the
edge to be connected.
Returns
-------
dict
Dictionary of the four point tuple coordinates with keys: 'A', 'B', 'T'
and 'C'.
"""
coords = dict()
coords["T"] = (connection["xT"], connection["yT"])
coords["C"] = (connection["xC"], connection["yC"])
coords["A"] = (vertex["xA"], vertex["yA"])
coords["B"] = (vertex["xB"], vertex["yB"])
return coords
[docs]def get_connection_distances(coords):
"""
Measure the distances between the points 'A' and 'C' and the points 'B' and
'C' from the dictionary *coords*.
Parameters
----------
coords: dict
Tuple coordinates of the connection edge ends ('A' and 'B'), the terminal
('T') and the connection point on the vertex edge ('C').
Returns
-------
dict
Dictionary of the two distances ('A'-'C' and 'B'-'C') with keys 'A' and
'B'.
"""
d = dict()
d["A"] = distance(coords["A"], coords["C"])
d["B"] = distance(coords["B"], coords["C"])
return d
[docs]def get_connection_index(connection, terminal, d):
"""
Find if the connection 'C' lies on the edge 'A'-'B', on the edge end 'A' or
on the edge end 'B' and return the associted connection index.
Parameters
----------
connection: Series
Row of the dataframe *connections* obtained from the function
*dhd.connect.second_selection*.
terminal: Series
Row of the dataframe *terminals*.
d: dict
Dictionary of the distances between the connection and the edge ends.
Returns
-------
bool
True if 'C' lies on 'A'-'B', False otherwise.
str
Index of the connection 'C': index of 'A' if 'C'='A', index of 'B' if
'C'='B', concatenation of the *terminal* index and the *connection*
index otherwise.
"""
# if connection on the first edge end (A)
if d["A"] == 0:
idC = connection["idA"]
on_edge = False
# if connection on the second edge end (B)
elif d["B"] == 0:
idC = connection["idB"]
on_edge = False
# if connection inbetween the edge ends (A,B)
else:
idC = "{}_{}".format(terminal.name, connection.name)
on_edge = True
return on_edge, idC
[docs]def add_connection_to_terminal(terminal, coords, idC, func):
"""
Append the connection list of *terminal* with connection of coordinates
*coords* and index 'idC' ; in-place.
Parameters
----------
terminal: Series
Row of the *terminals* dataframe.
coords: dict
Tuple coordinates of the connection edge ends ('A' and 'B'), the terminal
('T') and the connection point on the vertex edge ('C').
idC: str
Index of the new connection.
func: function
Function of the connection length returning its weight.
"""
# get connection geometry
line = LineString([coords["T"], coords["C"]])
# append terminal lists
terminal["_geometry"].append(line)
terminal["_id"].append(idC)
terminal["_weight"].append(connection_weight(line, func))
[docs]def new_edge_weight(vertex, line):
"""
Compute the weight of the splitted edge of geometry *line* and original
vertex row *vertex*.
Parameters
----------
vertex: Series
Row of the dataframe *vertices*.
line: LineString
One of the two geometries of the splitted edge.
Returns
-------
float
Weight of the new vertex.
"""
return vertex["weight"] * line.length / vertex["geometry"].length
[docs]def update_edges(vertices, connection, vertex, coords, idC, func):
"""
Update the graph edges in *vertices* according to the new *connection* on
the considered *vertex* ; in-place.
This function is only called when the parent edge is splitted in two child
edges. The parent edge is removed and the child edges are appended.
Parameters
----------
vertices: DataFrame
Dataframe of the vertices.
connection: Series
Row of the dataframe *connections* obtained with the function
*dhd.connect.second_selection*.
vertex: Series
Row of the dataframe *vertices*.
coords: dict
Tuple coordinates of the connection edge ends ('A' and 'B'), the terminal
('T') and the connection point on the vertex edge ('C').
idC: str
Index of the new connection.
func: function
Function of the connection length returning its weight.
"""
# columns of 'vertices'
columns = ["idA", "idB", "idE", "geometry", "weight", "xA", "yA", "xB", "yB"]
# edge nodes indices
idA, idB = connection["idA"], connection["idB"]
# split edge (A,B) in two geometries
line1, line2 = split_line(vertex["geometry"], Point(coords["C"]))
lineA, lineB = sort_lines(line1, line2, coords["A"])
# remove edge (A,B)
vertices.drop(index=vertex.name, inplace=True)
vertices.reset_index(inplace=True, drop=True)
# new row for the edge (A, C)
seriesA = pd.Series(
[
idA,
idC,
"{}_{}".format(idA, idC),
lineA,
new_edge_weight(vertex, lineA),
coords["A"][0],
coords["A"][1],
coords["C"][0],
coords["C"][1],
],
index=columns,
)
vertices.loc[len(vertices)] = seriesA
# new row for the edge (C, B)
seriesB = pd.Series(
[
idC,
idB,
"{}_{}".format(idC, idB),
lineB,
new_edge_weight(vertex, lineB),
coords["C"][0],
coords["C"][1],
coords["B"][0],
coords["B"][1],
],
index=columns,
)
vertices.loc[len(vertices)] = seriesB
[docs]def update_graph(vertices, terminal, connections, func):
"""
Add the possible *connections* to the given *terminal* into the dataframe
*vertices* ; in-place.
The list associated to the given *terminal* in the dataframe *terminals* are
appended by each possible connection. Also the rows of the dataframe
*vertices* are modified to coincide with the new graph when a connection
occurs on a parent edge splitted in two child edges.
Parameters
----------
vertices: DataFrame
Dataframe of the vertices.
vertex: Series
Row of the dataframe *vertices*.
connections: DataFrame
Dataframe of the possible connections to the considered terminal,
obtained from the function *dhd.connect.second_selection*.
func: function
Function of the connection length returning its weight.
"""
# loop over all selected possible connections
for idx, connection in connections.iterrows():
# select the edge to be connected
vertex = find_vertex(vertices, connection)
# coordinates of the edge ends
coords = get_connection_coordinates(connection, vertex)
# distances A-C and B-C
d = get_connection_distances(coords)
# type of connection & connection index
on_edge, idC = get_connection_index(connection, terminal, d)
# append connection list
add_connection_to_terminal(terminal, coords, idC, func)
# update 'vertices'
if on_edge:
update_edges(vertices, connection, vertex, coords, idC, func)
[docs]def connect_terminal(vertices, terminal, barriers, R, r, func):
"""
Span the graph to find the possible connections to the given *terminal* and
then update the dataframes *vertices* and *terminals* accordingly ; in-place.
Parameters
----------
vertices: DataFrame
Dataframe of the vertices.
terminal: Series
Row of the dataframe *terminal*.
barriers: DataFrame
Dataframe of the natural barriers.
R: float
Maximal connection length.
r: float
Minimal distance between two connections to the same terminal.
func: function
Function of the connection length returning its weight.
"""
# select the possible connection around the given 'terminal'
selection = first_selection(vertices, terminal, R, barriers)
# if no possible connection
if len(selection) == 0:
if terminal["kind"] == "source":
text = (
"Impossible to connect the source {}. It is too far from "
"the network."
)
log.error(text.format(terminal.name))
else:
text = "Impossible to connect terminal {}."
log.info(text.format(terminal.name))
else:
# remove possible connections to close to each other
connections = second_selection(selection, r)
# update 'vertices' and 'terminals'
update_graph(vertices, terminal, connections, func)
[docs]def connect_terminals(
streets,
sinks,
sources,
barriers=None,
R=75,
r=25,
ignore_index=False,
connection_weight_function=None,
):
"""
Construct the dataframes *vertices* and *terminals* needed for the district
heating design evolutive algorithm in the module *dhd.evolve*.
The original street graph is enlarged with the connection nodes between the
terminals and the graph edges. All the possible connection edges are stored
in *terminals*.
Parameters
----------
streets: DataFrame
Dataframe of the street network with the following structure:
* INDEX: Integers.
* COLUMNS:
- 'idA': index of the first edge end (A),
- 'idB': index of the second edge end (B),
- 'geometry': shapely LineString of the edge between A and B (it
must go from A to B).
- 'weight': edge weight (optional, if not specified the edge
length is used instead).
- ...
sinks: DataFrame
Dataframe of the heating sinks with mandatory structure:
* INDEX: If *index* is True, set of indices disjoint from the set of
indices of *sources*. Otherwise the indices are generated.
* COLUMNS:
- 'geometry': shapely geometry of the sink,
- 'load': heating load of the sink (optional, if not specified
the module *dhd.load* cannot be used.)
- ...
sources: DataFrame
Dataframe of the heating source(s) of the district heating network with
the following structure:
* INDEX: If *index* is True, set of indices disjoint from the set of
indices of *sinks*. Otherwise the indices are generated.
* COLUMNS:
- 'geometry': shapely geometry of the source.
- ...
barriers: DataFrame, optional
Dataframe of the natural barriers (railways, rivers,...) which cannot be
crossed when connecting a sink to the heating network. Default is None.
The dataframe must have the following structure:
* INDEX: Integers.
* COLUMNS:
- 'geometry': shapely LineString of the barrier.
- ...
R: float, optional
Maximal connection length. Default is 50 m.
r: float, optional
Minimal distance between two connections to the same terminal. Default is
20 m.
ignore_index: bool, optional
If False, use indices from the dataframes *sources* and *sinks*,
otherwise generate new indices. Default is True. Note
that the indices of *sinks* and the indices of *sources* must be two
disjoint set.
connection_weight_function: function, optional
Function of the connection length returning its weight. If None the
weight is set equal to the length. Default is None.
Returns
-------
vertices: DataFrame
Dataframe of the street network plus the possible connection nodes to
the terminals with the following structure:
* INDEX: Integers
* COLUMNS:
- 'idA': first node index (from *streets*),
- 'idB': second node index (from *streets*),
- 'idE': edge index,
- 'xA', 'yA': first node coordinates,
- 'xB', 'yB': second node coordinates,
- 'geometry': edge geometry (shapely LineString),
- 'weight': edge weight,
- ... additional columns from *streets*.
terminals: DataFrame
Dataframe of the terminals to connect to the streets network and their
possible connections with the following structure:
* INDEX: If *index* is True, indices from *sources* and *sinks*,
generated indices : ['0S',...,'NS','0B',...,'MB'] for N sources
and M sinks.
* COLUMNS:
- '_id': list of the possible connections indices,
- '_weight': list of the possible connections weights,
- '_geometry': list of the possible connections geometries,
- 'geometry': geometry of the terminal,
- 'kind': terminal kind ('source' or 'sink'),
- 'load': terminal heating load (only if provided in *sinks*).
"""
tic = time.time()
check_data_structure(sinks, streets, sources, barriers, ignore_index)
sinks = clean_superpositions(sinks)
# initialize the dataframes 'vertices' and 'terminals'
vertices = init_vertices(streets)
terminals = init_terminals(sources, sinks, ignore_index)
# connect the terminals
for idx, terminal in terminals.iterrows():
connect_terminal(vertices, terminal, barriers, R, r, connection_weight_function)
tac = time.time()
text = "Initilization of the vertices and terminals dataframes in {} seconds."
log.info(text.format(round(tac - tic, 2)))
return vertices, terminals