Multiple Flows And Capacity#

When several OD pairs are allocated, NetworkFlows aggregates their flows on shared edges.

Capacity-constrained allocation limits assignments to available edge capacity and records residual demand in unassigned_od.

Shared edges#

The two OD pairs below both use edge BC on their least-cost path.

>>> import pandas as pd
>>> from transport_flow_model.model import Network, NetworkFlows, OD, ODFlows
>>> network = Network(
...     pd.DataFrame(
...         {
...             "edge_from": ["A", "B", "A", "C", "B"],
...             "edge_to": ["B", "C", "C", "D", "D"],
...             "edge_id": ["AB", "BC", "AC", "CD", "BD"],
...             "cost": [1, 2, 5, 1, 10],
...             "capacity": [100, 100, 100, 100, 100],
...         }
...     )
... )
>>> od = OD(
...     pd.DataFrame(
...         {
...             "origin_id": ["A", "B"],
...             "destination_id": ["C", "D"],
...             "flow": [10, 6],
...         }
...     )
... )
>>> result = network.allocate(od, directed=True)
>>> result.od_flows.to_dataframe().to_dict("records")
[{'origin_id': 'A', 'destination_id': 'C', 'flow': 10, 'edge_path': ['AB', 'BC'], 'cost': 3}, {'origin_id': 'B', 'destination_id': 'D', 'flow': 6, 'edge_path': ['BC', 'CD'], 'cost': 3}]
>>> result.network_flows.to_dataframe().set_index("edge_id")["flow"].to_dict()
{'AB': 10, 'BC': 16, 'AC': 0, 'CD': 6, 'BD': 0}

Fair bottleneck sharing#

With capacity constraints enabled, flows that require the same bottleneck edge share its available capacity proportionally. Here both OD pairs request 10 units through CD, but CD only has capacity 10. Each pair receives 5 and leaves 5 unassigned.

>>> bottleneck_network = Network(
...     pd.DataFrame(
...         {
...             "edge_from": ["A", "B", "C"],
...             "edge_to": ["C", "C", "D"],
...             "edge_id": ["AC", "BC", "CD"],
...             "cost": [1, 1, 1],
...             "capacity": [100, 100, 10],
...         }
...     )
... )
>>> bottleneck_od = OD(
...     pd.DataFrame(
...         {
...             "origin_id": ["A", "B"],
...             "destination_id": ["D", "D"],
...             "flow": [10, 10],
...         }
...     )
... )
>>> constrained = bottleneck_network.allocate(
...     bottleneck_od, capacity_constrained=True, directed=True
... )
>>> constrained.od_flows.to_dataframe().sort_values(
...     ["origin_id", "destination_id"]
... ).to_dict("records")
[{'origin_id': 'A', 'destination_id': 'D', 'flow': 5, 'edge_path': ['AC', 'CD'], 'cost': 2}, {'origin_id': 'B', 'destination_id': 'D', 'flow': 5, 'edge_path': ['BC', 'CD'], 'cost': 2}]
>>> constrained.unassigned_od.to_dataframe().sort_values(
...     ["origin_id", "destination_id"]
... ).to_dict("records")
[{'origin_id': 'A', 'destination_id': 'D', 'flow': 5}, {'origin_id': 'B', 'destination_id': 'D', 'flow': 5}]
>>> constrained.network_flows.to_dataframe().set_index("edge_id")["flow"].to_dict()
{'AC': 5, 'BC': 5, 'CD': 10}

Existing edge loads#

If the network already has a flow column, capacity-constrained allocation treats that flow as occupied capacity. Returned edge totals include both the existing load and newly assigned OD flows.

>>> loaded_network = Network(
...     pd.DataFrame(
...         {
...             "edge_from": ["A"],
...             "edge_to": ["B"],
...             "edge_id": ["AB"],
...             "cost": [1],
...             "capacity": [10],
...             "flow": [8],
...         }
...     )
... )
>>> extra_od = OD(
...     pd.DataFrame(
...         {
...             "origin_id": ["A"],
...             "destination_id": ["B"],
...             "flow": [2],
...         }
...     )
... )
>>> loaded = loaded_network.allocate(
...     extra_od, capacity_constrained=True, directed=True
... )
>>> loaded.network_flows.to_dataframe().set_index("edge_id")["flow"].to_dict()
{'AB': 10}

The same additive behavior is available directly through NetworkFlows.from_network_and_od_flows.

>>> od_flows = ODFlows(
...     pd.DataFrame(
...         {
...             "origin_id": ["A"],
...             "destination_id": ["B"],
...             "flow": [2],
...             "edge_path": [["AB"]],
...             "cost": [1],
...         }
...     )
... )
>>> NetworkFlows.from_network_and_od_flows(
...     loaded_network, od_flows
... ).to_dataframe().set_index("edge_id")["flow"].to_dict()
{'AB': 10}