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

# When any k >= p_facilities the k-nearest constraint is non-binding:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condense this 4-line comment down to 1 line please.

# only p facilities are ever selected, so a client with k >= p can always
# reach every selected facility. The model degenerates to a standard
# p-median; warn and set k = n_facilities to make the equivalence explicit.
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