Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions spopt/locate/p_median.py
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,19 @@ def from_geodataframe(
f"of total facilities, which is {len(fac_data)}."
)

# If any k >= p_facilities, the k-nearest constraint is non-binding and the model reduces to standard p-median (warn and set k = n_facilities).

if (k_array >= p_facilities).any():
warnings.warn(
"Some ``k`` values are >= ``p_facilities`` "
f"({p_facilities}); the k-nearest constraint is "
"non-binding and the model degenerates to a standard "
"p-median. Solving as a standard p-median instead.",
UserWarning,
stacklevel=2,
)
k_array = np.full(len(k_array), len(fac_data), dtype=int)

# demand and capacity
service_load = gdf_demand[weights_cols].to_numpy()
weights_sum = service_load.sum()
Expand Down
48 changes: 45 additions & 3 deletions spopt/tests/test_locate/test_knearest_p_median.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,14 @@ def test_error_high_capacity(self):
_gdf_demand = self.gdf_demand.copy()
_gdf_demand["demand"] = [10, 10]
k = numpy.array([1, 1])
with pytest.raises(
SpecificationError,
match="Problem is infeasible. The highest possible capacity",
# k=1 >= p_facilities=1, so a UserWarning fires before the infeasibility
# error is raised on solve().
with (
pytest.warns(UserWarning, match="degenerates to a standard p-median"),
pytest.raises(
SpecificationError,
match="Problem is infeasible. The highest possible capacity",
),
):
KNearestPMedian.from_geodataframe(
_gdf_demand,
Expand All @@ -169,3 +174,40 @@ def test_error_high_capacity(self):
facility_capacity_col="capacity",
k_array=k,
).solve(self.solver)

def test_warn_k_gte_p_falls_back_to_pmedian(self):
# When k >= p_facilities the k-nearest constraint is non-binding and
# the model should warn then solve as a standard p-median (issue #428).
k = numpy.array([2, 2]) # k == p_facilities == 2
with pytest.warns(UserWarning, match="degenerates to a standard p-median"):
model = KNearestPMedian.from_geodataframe(
self.gdf_demand,
self.gdf_fac,
"geometry",
"geometry",
"demand",
p_facilities=2,
facility_capacity_col="capacity",
k_array=k,
)
result = model.solve(self.solver)
assert isinstance(result, KNearestPMedian)
assert result.problem.status == pulp.LpStatusOptimal

def test_warn_k_gt_p_falls_back_to_pmedian(self):
# Same degeneracy check when k > p_facilities (strictly greater).
k = numpy.array([3, 3]) # k > p_facilities == 2, k <= n_facilities == 3
with pytest.warns(UserWarning, match="degenerates to a standard p-median"):
model = KNearestPMedian.from_geodataframe(
self.gdf_demand,
self.gdf_fac,
"geometry",
"geometry",
"demand",
p_facilities=2,
facility_capacity_col="capacity",
k_array=k,
)
result = model.solve(self.solver)
assert isinstance(result, KNearestPMedian)
assert result.problem.status == pulp.LpStatusOptimal
Loading