diff --git a/spopt/locate/p_median.py b/spopt/locate/p_median.py index 413ea451..c293943f 100644 --- a/spopt/locate/p_median.py +++ b/spopt/locate/p_median.py @@ -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() diff --git a/spopt/tests/test_locate/test_knearest_p_median.py b/spopt/tests/test_locate/test_knearest_p_median.py index dc8747e7..76dab363 100644 --- a/spopt/tests/test_locate/test_knearest_p_median.py +++ b/spopt/tests/test_locate/test_knearest_p_median.py @@ -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, @@ -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