Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LSCP-B #259

Merged
merged 71 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
6d3672d
create add_backup_covering_constraint method
erinrolson Jun 22, 2022
1a06083
create LSCPB object
erinrolson Jun 22, 2022
70d35d5
post mtg changes
erinrolson Jun 22, 2022
5be9d0d
remove cov_minimun variable
erinrolson Jun 22, 2022
c21f3dd
testing
erinrolson Jun 22, 2022
4571d32
test
erinrolson Jun 22, 2022
b39c23d
remove dev comments and test class
erinrolson Jun 23, 2022
d7fd196
add LSCPB
erinrolson Jun 23, 2022
41d479b
remove add_backup_covering_constraint()
erinrolson Jun 23, 2022
b9f9008
add add_backup_covering_constraint, enhance LSCPB
erinrolson Jun 23, 2022
c852dcc
add comments and print messages to undrstnd error
erinrolson Jun 23, 2022
8f7a189
fix attribute error scoping and remove comments
erinrolson Jun 24, 2022
af29ab7
remove notes and update comments
erinrolson Jun 27, 2022
88ecf7f
update code notes
erinrolson Jun 27, 2022
630a114
add solver to from_geodataframe
erinrolson Jun 28, 2022
e958672
remove dev notes and update example
erinrolson Jun 28, 2022
6247f5d
run Black on coverage.py
erinrolson Jun 29, 2022
e04e228
remove inline comments from backup covering constraint method
erinrolson Jun 29, 2022
7be0aaa
remove inline comments from cost matrix class method
erinrolson Jun 29, 2022
d8f74aa
add lscp_obj_value attribute to LSCPB and alter LN 491 to set attribute
erinrolson Jun 29, 2022
f087f8f
lscpb.lscp_obj_value after object instantiation
erinrolson Jun 30, 2022
09a2f2c
copy test_locate.py file and rename test_lscpb.py
erinrolson Jul 5, 2022
f6cb3f4
remove all non-lscp tests from file
erinrolson Jul 5, 2022
5d04e2b
trans test_lscpb_from_cost_matrix to lscpb params
erinrolson Jul 5, 2022
214723a
update class methods for LSCPB
erinrolson Jul 5, 2022
d54a78f
update for LSCPB
erinrolson Jul 5, 2022
b1cf5d5
remove unnecessary class methods
erinrolson Jul 6, 2022
211a23c
create lscpb_cli2fac pickle file
erinrolson Jul 11, 2022
3aacbd8
rename new pickle file
erinrolson Jul 11, 2022
27d2da5
create lscpb_cli2fac pickle file
erinrolson Jul 11, 2022
62db449
create lscpb_geodataframe_fac2cli.pkl file
erinrolson Jul 11, 2022
0b219bb
new file created
erinrolson Jul 11, 2022
e1a9983
testing preselected fac_cli_array
erinrolson Jul 11, 2022
b3acae4
testing preselected fac2cli array
erinrolson Jul 11, 2022
42d2e00
add inline dev notes
erinrolson Jul 13, 2022
d482ea0
create copies of .csv's to modify data
erinrolson Jul 13, 2022
d179225
updated tests
erinrolson Jul 18, 2022
6483b68
remove print dev tests
erinrolson Jul 18, 2022
e7c1af7
create notebook and resolve errors to run
erinrolson Jul 18, 2022
4f2a9a6
add lscp-b to notebook
erinrolson Jul 18, 2022
08c2d59
create file just solving LSCPB for comparison
erinrolson Jul 18, 2022
f42e360
self.assertRaises(RuntimeError) to begin of funct
erinrolson Jul 19, 2022
773af09
run black
erinrolson Jul 19, 2022
cee8d51
remove nested assert runtime error
erinrolson Jul 20, 2022
c638a00
reduce line widths on plot
erinrolson Jul 21, 2022
8c59a6a
modify marrkers
erinrolson Jul 22, 2022
72512b4
remove comments and unnecessary variables
erinrolson Jul 24, 2022
3e2a6d5
started basic lscpb notebook
erinrolson Jul 24, 2022
8028510
add check for predef fac array when creating LSCP
erinrolson Jul 25, 2022
0c78f32
remmove dev notes
erinrolson Jul 25, 2022
fb514de
set service_radius to org val, created nw pkl file
erinrolson Jul 25, 2022
5e4da20
run black formatting
erinrolson Jul 25, 2022
4288bf1
formulate LSCP-B equation
erinrolson Jul 26, 2022
48d6a40
add formulation definition
erinrolson Jul 26, 2022
d9a5a86
add Daskin, Stern citation
erinrolson Jul 26, 2022
685d60f
Update tutorial description
erinrolson Jul 26, 2022
6ef826b
remove legacy code
erinrolson Jul 26, 2022
20a9c4a
update descriptions, create new plot function
erinrolson Jul 26, 2022
6ab3645
remove dev comments
erinrolson Jul 26, 2022
6426762
remove .coverage file
erinrolson Jul 27, 2022
d2a023b
change max service radius - gdf
erinrolson Jul 27, 2022
9439e7c
change service_dist from 5000 to 8000
erinrolson Jul 27, 2022
b94d39d
remove '$$' tags
erinrolson Jul 27, 2022
e92d9c0
rerun notebook
erinrolson Jul 27, 2022
6cd2f14
run lscpb notebook
erinrolson Jul 29, 2022
14e31c8
add facility constraint test
erinrolson Jul 29, 2022
1173e56
run black
erinrolson Jul 29, 2022
f57741e
add backup covering constraint
erinrolson Aug 4, 2022
5e0b0bc
add covering constraint
erinrolson Aug 4, 2022
1b61f87
remove backup covering constraint
erinrolson Aug 4, 2022
6888fcc
update tests
erinrolson Aug 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion spopt/locate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .base import BaseOutputMixin, CoveragePercentageMixin, MeanDistanceMixin
from .coverage import LSCP, MCLP
from .coverage import LSCP, MCLP, LSCPB
from .p_median import PMedian
from .p_center import PCenter
from .util import simulated_geo_points
359 changes: 358 additions & 1 deletion spopt/locate/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

import warnings


class LSCP(LocateSolver, BaseOutputMixin):
"""
LSCP class implements Location Set Covering optimization model and solve it.
Expand Down Expand Up @@ -314,6 +313,364 @@ def solve(self, solver: pulp.LpSolver, results: bool = True):
return self


class LSCPB(LocateSolver, BaseOutputMixin):
"""
LSCPB class implements Location Set Covering Problem - Backup optimization model and solves it.

Parameters
----------
name: str
Problem name
problem: pulp.LpProblem
Pulp instance of optimization model that contains constraints, variables and objective function.

Attributes
----------
name: str
Problem name
problem: pulp.LpProblem
Pulp instance of optimization model that contains constraints, variables and objective function.
fac2cli : np.array
2-d array MxN, where m is number of facilities and n is number of clients. Each row represents a facility and has an array containing clients index meaning that the facility-i cover the entire array.
cli2fac: np.array
2-d MxN, where m is number of clients and n is number of facilities. Each row represent a client and has an array containing facility index meaning that the client is covered by the facility ith.
aij: np.array
Cost matrix 2-d array
"""

def __init__(self, name: str, problem: pulp.LpProblem):
super().__init__(name, problem)

def __add_obj(self) -> None:
"""
Add objective function to model:
(Coverage Variable)
Maximize U1 + U2 + U3 + U4 + U5 + ... + Uj

Returns
-------
None
"""
cov_vars = getattr(self, "cli_vars")
self.problem += pulp.lpSum(cov_vars), "objective function"

def add_backup_covering_constraint(
gegen07 marked this conversation as resolved.
Show resolved Hide resolved
self,
model: pulp.LpProblem,
ni: np.array,
range_facility: range,
range_client: range,
) -> None:
"""
backup covering constraint:
- coverage_0 + facility_1 + facility_3 + facility_4 + facility_6 + facility_7 + facility_9 >= 1

Parameters
----------
obj: T_FacModel
bounded type of LocateSolver class
jGaboardi marked this conversation as resolved.
Show resolved Hide resolved
model: pulp.LpProblem
optimization model problem
ni: np.array
two-dimensional array that defines candidate sites between facility points within a distance to supply {i}
demand point
range_facility: range
range of facility points quantity
range_client: range
range of demand points quantity
Returns
jGaboardi marked this conversation as resolved.
Show resolved Hide resolved
-------
None

"""


if hasattr(self, "fac_vars"):
fac_vars = getattr(self, "fac_vars")
cli_vars = getattr(self, "cli_vars")
for i in range_client:
if sum(ni[i]) >= 2: # demand unit has backup coverage
model += (
jGaboardi marked this conversation as resolved.
Show resolved Hide resolved
pulp.lpSum( [ int( ni[i][j] ) * fac_vars[j] for j in range_facility ] ) >= 1 + 1*cli_vars[i]
)
else: #demand unit does not have backup coverage
model += (
pulp.lpSum( [ int( ni[i][j] ) * fac_vars[j] for j in range_facility ] ) >= 1 + 0*cli_vars[i]
)
else:
raise AttributeError(
erinrolson marked this conversation as resolved.
Show resolved Hide resolved
"before setting constraints must set facility variable"
)

@classmethod
def from_cost_matrix(
cls,
cost_matrix: np.array,
service_radius: float,
solver: pulp.LpSolver,
predefined_facilities_arr: np.array = None,
name: str = "LSCP-B",

):
"""
Create a LSCPB object based on a cost matrix.

Parameters
----------
cost_matrix: np.array
two-dimensional distance array between facility points and demand point
service_radius: float
maximum acceptable service distance by problem
name: str, default="LSCP-B"
name of the problem

Returns
-------
LSCPB object

Examples
--------
>>> from spopt.locate.coverage import LSCPB, LSCP
>>> from spopt.locate.util import simulated_geo_points
>>> import pulp
>>> import spaghetti

Create regular lattice

>>> lattice = spaghetti.regular_lattice((0, 0, 10, 10), 9, exterior=True)
>>> ntw = spaghetti.Network(in_data=lattice)
>>> street = spaghetti.element_as_gdf(ntw, arcs=True)
>>> street_buffered = geopandas.GeoDataFrame(
... geopandas.GeoSeries(street["geometry"].buffer(0.2).unary_union),
... crs=street.crs,
... columns=["geometry"])

Simulate points belong to lattice

>>> demand_points = simulated_geo_points(street_buffered, needed=100, seed=5)
>>> facility_points = simulated_geo_points(street_buffered, needed=5, seed=6)

Snap points to the network

>>> ntw.snapobservations(demand_points, "clients", attribute=True)
>>> clients_snapped = spaghetti.element_as_gdf(ntw, pp_name="clients", snapped=True)
>>> ntw.snapobservations(facility_points, "facilities", attribute=True)
>>> facilities_snapped = spaghetti.element_as_gdf(ntw, pp_name="facilities", snapped=True)

Calculate the cost matrix
>>> cost_matrix = ntw.allneighbordistances(
... sourcepattern=ntw.pointpatterns["clients"],
... destpattern=ntw.pointpatterns["facilities"])

Create LSCPB instance from cost matrix

>>> lscpb_from_cost_matrix = LSCPB.from_cost_matrix(cost_matrix, max_coverage=8, pulp.PULP_CBC_CMD(msg=False)
>>> lscpb_from_cost_matrix = lscpb_from_cost_matrix.solve(pulp.PULP_CBC_CMD(msg=False))

Get facility lookup demand coverage array

>>> lscpb_from_cost_matrix.fac2cli

"""
# create a lscp object
lscp = LSCP.from_cost_matrix(cost_matrix, service_radius)

#solve lscp
lscp.solve(solver)

jGaboardi marked this conversation as resolved.
Show resolved Hide resolved
r_fac = range(cost_matrix.shape[1])
r_cli = range(cost_matrix.shape[0])

model = pulp.LpProblem(name, pulp.LpMaximize)
lscpb = LSCPB(name, model)

FacilityModelBuilder.add_facility_integer_variable(lscpb, r_fac, "x[{i}]")
FacilityModelBuilder.add_client_integer_variable(lscpb, r_cli, "u[{i}]")

lscpb.aij = np.zeros(cost_matrix.shape)
lscpb.aij[cost_matrix <= service_radius] = 1

if predefined_facilities_arr is not None:
FacilityModelBuilder.add_predefined_facility_constraint(
lscpb, lscpb.problem, predefined_facilities_arr
)

lscpb.__add_obj()
FacilityModelBuilder.add_facility_constraint(lscpb, lscpb.problem, lscp.problem.objective.value())
LSCPB.add_backup_covering_constraint(
lscpb, lscpb.problem, lscpb.aij, r_fac, r_cli
)

return lscpb

@classmethod
def from_geodataframe(
cls,
gdf_demand: GeoDataFrame,
gdf_fac: GeoDataFrame,
demand_col: str,
facility_col: str,
service_radius: float,
solver: pulp.LpSolver,
predefined_facility_col: str = None,
distance_metric: str = "euclidean",
name: str = "LSCP-B",
):
"""
Create a LSCPB object based on geodataframes. Calculate the cost matrix between demand and facility,
and then use from_cost_matrix method.

Parameters
----------
gdf_demand: geopandas.GeoDataFrame
demand geodataframe with point geometry
gdf_fac: geopandas.GeoDataframe
facility geodataframe with point geometry
demand_col: str
demand geometry column name
facility_col: str
facility candidate sites geometry column name
service_radius: float
maximum acceptable service distance by problem
distance_metric: str, default="euclidean"
metrics supported by :method: `scipy.spatial.distance.cdist` used for the distance calculations
name: str, default="LSCP"
name of the problem

Returns
-------
LSCPB object

Examples
--------
>>> from spopt.locate.coverage import LSCB, LSCP
>>> from spopt.locate.util import simulated_geo_points
>>> import pulp
>>> import spaghetti

Create regular lattice

>>> lattice = spaghetti.regular_lattice((0, 0, 10, 10), 9, exterior=True)
>>> ntw = spaghetti.Network(in_data=lattice)
>>> street = spaghetti.element_as_gdf(ntw, arcs=True)
>>> street_buffered = geopandas.GeoDataFrame(
... geopandas.GeoSeries(street["geometry"].buffer(0.2).unary_union),
... crs=street.crs,
... columns=["geometry"])

Simulate points belong to lattice

>>> demand_points = simulated_geo_points(street_buffered, needed=100, seed=5)
>>> facility_points = simulated_geo_points(street_buffered, needed=5, seed=6)

Snap points to the network

>>> ntw.snapobservations(demand_points, "clients", attribute=True)
>>> clients_snapped = spaghetti.element_as_gdf(ntw, pp_name="clients", snapped=True)
>>> ntw.snapobservations(facility_points, "facilities", attribute=True)
>>> facilities_snapped = spaghetti.element_as_gdf(ntw, pp_name="facilities", snapped=True)

Create LSCPB instance from cost matrix

>>> lscpb_from_geodataframe = LSCPB.from_geodataframe(clients_snapped, facilities_snapped,
... "geometry", "geometry",
... max_coverage=8, pulp.PULP_CBC_CMD(msg=False), distance_metric="euclidean")
>>> lscpb_from_geodataframe = lscpb_from_geodataframe.solve(pulp.PULP_CBC_CMD(msg=False))

Get facility lookup demand coverage array

>>> lscpb_from_geodataframe.fac2cli

"""

predefined_facilities_arr = None
if predefined_facility_col is not None:
predefined_facilities_arr = gdf_fac[predefined_facility_col].to_numpy()

dem = gdf_demand[demand_col]
fac = gdf_fac[facility_col]

dem_type_geom = dem.geom_type.unique()
fac_type_geom = fac.geom_type.unique()

if len(dem_type_geom) > 1 or not "Point" in dem_type_geom:
warnings.warn(
"Demand geodataframe contains mixed type geometries or is not a point. Be sure deriving centroid from geometries doesn't affect the results.",
Warning,
)
dem = dem.centroid

if len(fac_type_geom) > 1 or not "Point" in fac_type_geom:
warnings.warn(
"Facility geodataframe contains mixed type geometries or is not a point. Be sure deriving centroid from geometries doesn't affect the results.",
Warning,
)
fac = fac.centroid

dem_data = np.array([dem.x.to_numpy(), dem.y.to_numpy()]).T
fac_data = np.array([fac.x.to_numpy(), fac.y.to_numpy()]).T

if gdf_demand.crs != gdf_fac.crs:
raise ValueError(
f"geodataframes crs are different: gdf_demand-{gdf_demand.crs}, gdf_fac-{gdf_fac.crs}"
)

distances = cdist(dem_data, fac_data, distance_metric)

return cls.from_cost_matrix(
distances, service_radius, solver, predefined_facilities_arr, name
)


def facility_client_array(self) -> None:
"""
Create an array 2d MxN, where m is number of facilities and n is number of clients. Each row represent a facility and has an array containing clients index meaning that the facility-i cover the entire array.

Returns
-------
None
"""

fac_vars = getattr(self, "fac_vars")
len_fac_vars = len(fac_vars)

self.fac2cli = []

for j in range(len_fac_vars):
array_cli = []
if fac_vars[j].value() > 0:
for i in range(self.aij.shape[0]):
if self.aij[i][j] > 0:
array_cli.append(i)

self.fac2cli.append(array_cli)

def solve(self, solver: pulp.LpSolver, results: bool = True):
"""
Solve the LSCPB model

Parameters
----------
solver: pulp.LpSolver
solver supported by pulp package

results: bool
if True it will create metainfo - which facilities cover which demand and vice-versa, and the uncovered demand - about the model results

Returns
-------
LSCPB object
"""
self.problem.solve(solver)
self.check_status()

if results:
self.facility_client_array()
self.client_facility_array()

return self


class MCLP(LocateSolver, BaseOutputMixin, CoveragePercentageMixin):
"""
MCLP class implements Maximal Coverage Location optimization model and solve it.
Expand Down