facloc 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. facloc-0.1.0/CHANGELOG.md +23 -0
  2. facloc-0.1.0/CITATION.cff +22 -0
  3. facloc-0.1.0/LICENSE +21 -0
  4. facloc-0.1.0/MANIFEST.in +9 -0
  5. facloc-0.1.0/PKG-INFO +294 -0
  6. facloc-0.1.0/README.md +255 -0
  7. facloc-0.1.0/examples/quickstart.py +61 -0
  8. facloc-0.1.0/pyproject.toml +119 -0
  9. facloc-0.1.0/setup.cfg +4 -0
  10. facloc-0.1.0/src/facloc/__init__.py +36 -0
  11. facloc-0.1.0/src/facloc/__main__.py +7 -0
  12. facloc-0.1.0/src/facloc/_version.py +1 -0
  13. facloc-0.1.0/src/facloc/brute.py +111 -0
  14. facloc-0.1.0/src/facloc/cli.py +221 -0
  15. facloc-0.1.0/src/facloc/distance.py +116 -0
  16. facloc-0.1.0/src/facloc/errors.py +17 -0
  17. facloc-0.1.0/src/facloc/exact.py +202 -0
  18. facloc-0.1.0/src/facloc/heuristics.py +494 -0
  19. facloc-0.1.0/src/facloc/io.py +162 -0
  20. facloc-0.1.0/src/facloc/lagrangian.py +82 -0
  21. facloc-0.1.0/src/facloc/models.py +486 -0
  22. facloc-0.1.0/src/facloc/problem.py +214 -0
  23. facloc-0.1.0/src/facloc/py.typed +0 -0
  24. facloc-0.1.0/src/facloc/solution.py +356 -0
  25. facloc-0.1.0/src/facloc.egg-info/PKG-INFO +294 -0
  26. facloc-0.1.0/src/facloc.egg-info/SOURCES.txt +35 -0
  27. facloc-0.1.0/src/facloc.egg-info/dependency_links.txt +1 -0
  28. facloc-0.1.0/src/facloc.egg-info/entry_points.txt +2 -0
  29. facloc-0.1.0/src/facloc.egg-info/requires.txt +11 -0
  30. facloc-0.1.0/src/facloc.egg-info/top_level.txt +1 -0
  31. facloc-0.1.0/tests/conftest.py +26 -0
  32. facloc-0.1.0/tests/test_cli_io.py +146 -0
  33. facloc-0.1.0/tests/test_distance.py +54 -0
  34. facloc-0.1.0/tests/test_exact.py +73 -0
  35. facloc-0.1.0/tests/test_heuristics_vs_brute.py +93 -0
  36. facloc-0.1.0/tests/test_models.py +181 -0
  37. facloc-0.1.0/tests/test_problem_solution.py +122 -0
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-09
8
+ ### Added
9
+ - `Problem` container: distance matrix (supplied, or computed by Euclidean, Manhattan or
10
+ haversine metric), demand weights, fixed existing facilities, capacities, ids and
11
+ coordinates; `inf` marks unreachable pairs.
12
+ - Models: `p_median`, `p_center` (plain or weighted), `mclp`, `lscp` and
13
+ `capacitated_p_median` (single source), each with `method="heuristic" | "exact" | "brute"`.
14
+ - Numpy heuristics: greedy ADD + Teitz-Bart vertex substitution (first-improvement and
15
+ steepest descent, seeded random restarts) shared by p-median, p-center and MCLP; greedy set
16
+ cover with redundancy removal and 2-for-1 replacement for LSCP; regret-based single-source
17
+ assignment with shift/swap moves for the capacitated p-median.
18
+ - Lagrangian lower bound for the p-median (`bound=True`, `pmedian_lower_bound`).
19
+ - Exact integer programmes through PuLP with HiGHS or CBC (`facloc[exact]` extra), supporting
20
+ the PuLP 2.x/3.x and PuLP 4 APIs.
21
+ - `Solution` with allocations, coverage, weighted mean and maximum distance, site loads,
22
+ optimality gap, JSON export and GeoJSON spider lines; `evaluate` for any given site set.
23
+ - `facloc` command-line interface (`solve`, `evaluate`) reading CSV points or a CSV matrix.
@@ -0,0 +1,22 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ authors:
4
+ - family-names: "Eminoğlu"
5
+ given-names: "Yusuf"
6
+ email: "geospacephilo@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "facloc"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/facloc_sdk"
13
+ url: "https://pypi.org/project/facloc/"
14
+ abstract: "Facility location-allocation models for public-service planning: p-median, p-center, maximal covering (MCLP), location set covering (LSCP) and capacitated p-median, with numpy heuristics (greedy, Teitz-Bart vertex substitution, Lagrangian bound), optional exact MILP solving through PuLP/HiGHS, a CLI and GeoJSON spider-line output."
15
+ keywords:
16
+ - facility location
17
+ - location-allocation
18
+ - p-median
19
+ - maximal covering
20
+ - set covering
21
+ - urban planning
22
+ - gis
facloc-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yusuf Eminoğlu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/facloc/py.typed
7
+ recursive-include src/facloc *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
facloc-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,294 @@
1
+ Metadata-Version: 2.4
2
+ Name: facloc
3
+ Version: 0.1.0
4
+ Summary: Facility location-allocation for public-service planning: p-median, p-center, maximal covering (MCLP), set covering (LSCP) and capacitated p-median, with numpy heuristics, optional exact MILP (PuLP/HiGHS) and GeoJSON spider lines.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/facloc_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/facloc_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/facloc_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/facloc_sdk/blob/main/CHANGELOG.md
11
+ Keywords: facility-location,location-allocation,p-median,p-center,maximal-covering,mclp,set-covering,lscp,teitz-bart,operations-research,public-services,urban-planning,accessibility,gis
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Environment :: Console
21
+ Classifier: Topic :: Scientific/Engineering :: GIS
22
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
23
+ Classifier: Intended Audience :: Science/Research
24
+ Classifier: Intended Audience :: Developers
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.21
30
+ Provides-Extra: exact
31
+ Requires-Dist: pulp>=2.9; extra == "exact"
32
+ Requires-Dist: highspy>=1.7; extra == "exact"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
35
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
36
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
37
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # facloc
41
+
42
+ [![PyPI version](https://img.shields.io/pypi/v/facloc.svg)](https://pypi.org/project/facloc/)
43
+ [![Python versions](https://img.shields.io/pypi/pyversions/facloc.svg)](https://pypi.org/project/facloc/)
44
+ [![CI](https://github.com/YusufEminoglu/facloc_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/facloc_sdk/actions/workflows/ci.yml)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/facloc_sdk/blob/main/LICENSE)
46
+
47
+ **Facility location-allocation for public-service planning.**
48
+
49
+ `facloc` answers the classic planning questions behind schools, health centres, fire
50
+ stations, libraries or parks: *where should the next p facilities go, and who would each
51
+ one serve?* It implements the five textbook discrete location models:
52
+
53
+ | Model | Question | Objective | Source |
54
+ |---|---|---|---|
55
+ | **p-median** | Where do `p` facilities minimise average travel? | min sum of weight x distance | Hakimi 1964; ReVelle & Swain 1970 |
56
+ | **p-center** | Where do `p` facilities minimise the worst-case distance? | min max distance (optionally weighted) | Hakimi 1964 |
57
+ | **MCLP** | Where do `p` facilities cover the most people within a standard `S`? | max covered weight | Church & ReVelle 1974 |
58
+ | **LSCP** | How few facilities put *everyone* within `S`? | min number of sites | Toregas et al. 1971 |
59
+ | **Capacitated p-median** | As p-median, but each facility has a capacity | min weighted distance, single-source | Mulvey & Beck 1984 |
60
+
61
+ Each model can be solved three ways:
62
+
63
+ - `method="heuristic"` (default) - numpy-only: greedy ADD + **Teitz-Bart vertex substitution**
64
+ (p-median), greedy + swap (p-center, MCLP), greedy set cover with redundancy removal
65
+ (LSCP), regret assignment inside vertex substitution (capacitated). An optional
66
+ **Lagrangian lower bound** tells you how far the p-median heuristic can be from optimal.
67
+ - `method="exact"` - integer programme through **PuLP** with **HiGHS** (or CBC), via the
68
+ optional `exact` extra.
69
+ - `method="brute"` - full enumeration for small instances; the reference used in the tests.
70
+
71
+ Existing facilities can be **fixed** open, demand points carry **weights** (population,
72
+ pupils, patients), the distance matrix is **supplied** (network distances, travel times) or
73
+ **computed** (Euclidean, Manhattan or haversine). Results report chosen sites, allocations,
74
+ coverage %, weighted mean distance, maximum distance, site loads and **GeoJSON spider lines**.
75
+
76
+ ```bash
77
+ pip install facloc # numpy only
78
+ pip install "facloc[exact]" # + PuLP and highspy for exact MILP solving
79
+ ```
80
+
81
+ Requires Python 3.9 or newer.
82
+
83
+ ---
84
+
85
+ ## Quick start (Python)
86
+
87
+ ```python
88
+ import numpy as np
89
+ import facloc
90
+
91
+ lonlat = np.array([[29.01, 41.02], [29.03, 41.01], [29.05, 41.03], [29.10, 41.05],
92
+ [29.12, 41.04], [29.11, 41.07]])
93
+ population = [1200, 800, 950, 2100, 1700, 600]
94
+
95
+ problem = facloc.Problem.from_points(
96
+ lonlat, metric="haversine", # lon/lat in, metres out
97
+ weights=population,
98
+ fixed=[0], # an existing facility at point 0
99
+ )
100
+
101
+ sol = facloc.p_median(problem, p=2, radius=3000, bound=True)
102
+ print(sol.summary())
103
+ sol.sites # open sites (column indices), existing included
104
+ sol.allocation # serving site for every demand point
105
+ sol.weighted_mean_distance, sol.max_distance, sol.coverage() # coverage within 3 km
106
+ geojson = sol.to_geojson() # spider lines + points
107
+
108
+ facloc.mclp(problem, p=2, radius=3000).coverage() # best possible coverage with 2 sites
109
+ facloc.lscp(problem, radius=3000).objective # new sites needed for full coverage
110
+ facloc.p_center(problem, p=2).objective # minimal worst-case distance
111
+ facloc.p_median(problem, p=2, method="exact") # needs facloc[exact]
112
+ ```
113
+
114
+ With your own (e.g. road-network) distance matrix:
115
+
116
+ ```python
117
+ problem = facloc.Problem(
118
+ distances, # (n_demand, n_sites); use np.inf for unreachable pairs
119
+ weights=pupils,
120
+ fixed=[3, 17], # existing schools
121
+ capacities=seats, # only used by capacitated_p_median
122
+ demand_ids=block_ids, site_ids=parcel_ids,
123
+ )
124
+ facloc.capacitated_p_median(problem, p=6)
125
+ ```
126
+
127
+ `examples/quickstart.py` is a complete offline walk-through.
128
+
129
+ ## Quick start (command line)
130
+
131
+ ```bash
132
+ facloc solve p-median -p 4 --demand blocks.csv --sites candidates.csv \
133
+ --metric haversine --radius 1500 --bound \
134
+ --json solution.json --geojson spider.geojson --allocations alloc.csv
135
+
136
+ facloc solve mclp -p 4 --radius 1500 --demand blocks.csv --sites candidates.csv
137
+ facloc solve lscp --radius 1500 --demand blocks.csv --sites candidates.csv
138
+ facloc solve p-center -p 4 --demand blocks.csv --method exact
139
+ facloc solve capacitated -p 4 --demand blocks.csv --sites candidates.csv
140
+ facloc solve p-median -p 3 --matrix od_matrix.csv --demand blocks.csv --existing S07
141
+ facloc evaluate --open S01,S07 --radius 1500 --demand blocks.csv --sites candidates.csv
142
+ ```
143
+
144
+ Input CSVs have a header row; the delimiter (`,` `;` or tab) is detected.
145
+
146
+ - **demand**: `id, x, y, weight` (weight optional, default 1).
147
+ - **sites**: `id, x, y, capacity, existing` (`capacity` and `existing` optional;
148
+ `existing` is truthy for `1/true/yes/y/x`). Without `--sites`, every demand point is a
149
+ candidate site.
150
+ - **matrix**: first row is a corner label followed by site ids; each further row a demand
151
+ id followed by distances. Empty, `inf` or `NA` cells mean unreachable.
152
+
153
+ Column names can be changed with `--id-col`, `--x-col`, `--y-col`, `--weight-col`,
154
+ `--capacity-col`, `--existing-col`. `facloc solve --help` lists every option.
155
+
156
+ ## API overview
157
+
158
+ | Object | Purpose |
159
+ |---|---|
160
+ | `Problem(distances, weights, fixed, capacities, demand_ids, site_ids, demand_xy, site_xy)` | instance container |
161
+ | `Problem.from_points(demand_xy, site_xy=None, metric=...)` | build from coordinates |
162
+ | `p_median(problem, p, method, radius, bound, restarts, seed)` | p-median |
163
+ | `p_center(problem, p, method, weighted)` | vertex p-center |
164
+ | `mclp(problem, p, radius, method)` | maximal covering |
165
+ | `lscp(problem, radius, method)` | location set covering |
166
+ | `capacitated_p_median(problem, p, method)` | capacitated p-median (single source) |
167
+ | `pmedian_lower_bound(problem, p)` | Lagrangian lower bound |
168
+ | `evaluate(problem, sites, radius)` | measure any given set of open sites |
169
+ | `Solution` | `.sites`, `.new_sites`, `.allocation`, `.objective`, `.optimal`, `.lower_bound`, `.gap`, `.weighted_mean_distance`, `.max_distance`, `.coverage(r)`, `.loads()`, `.to_dict()`, `.to_json()`, `.to_geojson()`, `.summary()` |
170
+ | `distance_matrix`, `euclidean_matrix`, `haversine_matrix`, `manhattan_matrix` | distance helpers |
171
+ | `solver_available()` | is the exact extra usable? |
172
+
173
+ Conventions:
174
+
175
+ - `p` is the **total** number of open facilities, existing ones included: `p=5` with two
176
+ fixed sites places three new ones.
177
+ - Uncapacitated models allocate each demand point to its **nearest** open site.
178
+ - Demand points with zero weight do not influence the optimisation but are still allocated.
179
+ - `objective` is the model's own objective: total weighted distance (p-median, capacitated),
180
+ maximum (weighted) distance (p-center), covered weight (MCLP), number of new sites (LSCP).
181
+
182
+ ## Method and sources
183
+
184
+ **Distances.** Euclidean and Manhattan in input units (use projected coordinates);
185
+ haversine on a sphere of radius 6 371 008.8 m (the IUGG mean radius R1, Moritz 2000),
186
+ returning metres. For real accessibility studies a network distance or travel-time matrix
187
+ is better; compute it elsewhere and pass it in.
188
+
189
+ **p-median heuristic.** Greedy ADD builds a first solution; Teitz & Bart (1968) vertex
190
+ substitution then cycles through the sites not in the solution and makes the best exchange
191
+ with an open (non-fixed) site whenever it lowers total weighted distance, until no single
192
+ swap improves it. Exchanges are evaluated in a vectorised way using each demand point's
193
+ nearest and second-nearest open site. The greedy solution is refined both by first-
194
+ improvement (the original rule) and by steepest descent, and `restarts` (default 10,
195
+ `seed=0` for reproducibility) random starts are added; the best local optimum is returned.
196
+ The same engine, with a different key, serves the p-center (max distance, ties broken by a
197
+ power mean of distances) and the MCLP (uncovered weight, ties broken by total weighted
198
+ distance). The p-center heuristic also starts from the smallest radius whose greedy set
199
+ cover needs at most `p` sites.
200
+
201
+ **Lagrangian bound.** Relaxing the assignment constraints gives the bound
202
+ `L(lambda) = sum_i lambda_i + min over p sites of sum_j min(0, w_i d_ij - lambda_i)`
203
+ (Cornuejols, Fisher & Nemhauser 1977), maximised by subgradient optimisation
204
+ (Held, Wolfe & Crowder 1974). `Solution.gap` is then a certificate: the heuristic is at
205
+ most that far from the optimum.
206
+
207
+ **LSCP heuristic.** Greedy set covering (Chvatal 1979) with two selection rules, followed by
208
+ removal of redundant sites and 2-for-1 replacements.
209
+
210
+ **Capacitated p-median.** Single-source allocation: each demand point is served by one
211
+ facility and loads may not exceed capacities. For a given set of open sites, demand is
212
+ assigned by maximum regret and improved by shift and swap moves; vertex substitution over
213
+ sites uses this evaluation (screening each candidate's exchanges by their uncapacitated
214
+ cost first), starting from the uncapacitated solution, the largest-capacity sites and
215
+ random starts.
216
+
217
+ **Exact models.** Standard integer programmes (ReVelle & Swain 1970 for the p-median,
218
+ Church & ReVelle 1974 for the MCLP, Toregas et al. 1971 for the LSCP, a min-max programme
219
+ for the p-center, Mulvey & Beck 1984 for the capacitated p-median) built with PuLP and
220
+ solved with HiGHS (preferred) or CBC. Both the PuLP 2.x/3.x and the PuLP 4 APIs are handled.
221
+
222
+ **Tests.** Worked examples with hand-computed optima, and every heuristic is checked against
223
+ the brute-force optimum on small random instances (and the exact MILPs against brute force
224
+ when the extra is installed).
225
+
226
+ ### References
227
+
228
+ - Chvatal, V. (1979). A greedy heuristic for the set-covering problem. *Mathematics of
229
+ Operations Research*, 4(3), 233-235. https://doi.org/10.1287/moor.4.3.233
230
+ - Church, R., & ReVelle, C. (1974). The maximal covering location problem. *Papers of the
231
+ Regional Science Association*, 32(1), 101-118. https://doi.org/10.1007/BF01942293
232
+ - Cornuejols, G., Fisher, M. L., & Nemhauser, G. L. (1977). Location of bank accounts to
233
+ optimize float: An analytic study of exact and approximate algorithms. *Management
234
+ Science*, 23(8), 789-810. https://doi.org/10.1287/mnsc.23.8.789
235
+ - Hakimi, S. L. (1964). Optimum locations of switching centers and the absolute centers and
236
+ medians of a graph. *Operations Research*, 12(3), 450-459.
237
+ https://doi.org/10.1287/opre.12.3.450
238
+ - Held, M., Wolfe, P., & Crowder, H. P. (1974). Validation of subgradient optimization.
239
+ *Mathematical Programming*, 6, 62-88. https://doi.org/10.1007/BF01580223
240
+ - Moritz, H. (2000). Geodetic Reference System 1980. *Journal of Geodesy*, 74(1), 128-133.
241
+ https://doi.org/10.1007/s001900050278
242
+ - Mulvey, J. M., & Beck, M. P. (1984). Solving capacitated clustering problems. *European
243
+ Journal of Operational Research*, 18(3), 339-348.
244
+ https://doi.org/10.1016/0377-2217(84)90155-3
245
+ - ReVelle, C. S., & Swain, R. W. (1970). Central facilities location. *Geographical
246
+ Analysis*, 2(1), 30-42. https://doi.org/10.1111/j.1538-4632.1970.tb00142.x
247
+ - Teitz, M. B., & Bart, P. (1968). Heuristic methods for estimating the generalized vertex
248
+ median of a weighted graph. *Operations Research*, 16(5), 955-961.
249
+ https://doi.org/10.1287/opre.16.5.955
250
+ - Toregas, C., Swain, R., ReVelle, C., & Bergman, L. (1971). The location of emergency
251
+ service facilities. *Operations Research*, 19(6), 1363-1373.
252
+ https://doi.org/10.1287/opre.19.6.1363
253
+
254
+ ## Limitations
255
+
256
+ - Heuristics return good local optima, not proven optima. Use `bound=True` (p-median) or
257
+ `method="exact"` when you need a certificate. More `restarts` trade time for quality.
258
+ - Only *vertex* (discrete candidate) problems: facilities sit on given candidate sites, not
259
+ anywhere in the plane or on network edges (absolute centres/medians are not covered).
260
+ - The capacitated model is single-source; split demand is not modelled. Its heuristic can
261
+ fail to find a feasible allocation on very tight instances (raising `InfeasibleError`);
262
+ the exact solver settles feasibility.
263
+ - Distance-matrix size grows with `n_demand x n_sites`; the uncapacitated heuristics are
264
+ comfortable up to a few thousand demand points and hundreds of candidates (about 15 s for
265
+ 3000 x 300, p = 10, on a laptop). The capacitated heuristic is much slower, since every
266
+ candidate exchange re-solves the assignment (about 20 s for 150 x 60, p = 8). Exact
267
+ MILPs grow faster still.
268
+ - Straight-line and haversine distances ignore the street network, barriers and travel mode.
269
+ - GeoJSON (RFC 7946) is defined for WGS 84 lon/lat; for projected coordinates assign the CRS
270
+ in your GIS.
271
+
272
+ ### Related software
273
+
274
+ For a fuller toolkit - more model variants, network integration with PySAL, GeoPandas
275
+ input - see **spopt** (Feng et al. 2022, *JOSS* 7(74), 3330,
276
+ https://doi.org/10.21105/joss.03330), part of PySAL. `facloc` is deliberately small: numpy
277
+ only by default, a solver only when you ask for exact answers, plus a CLI and spider-line
278
+ output for quick planning studies.
279
+
280
+ ## Academic citation
281
+
282
+ ```bibtex
283
+ @software{eminoglu_facloc,
284
+ author = {Eminoğlu, Yusuf},
285
+ title = {facloc},
286
+ year = {2026},
287
+ version = {0.1.0},
288
+ url = {https://github.com/YusufEminoglu/facloc_sdk}
289
+ }
290
+ ```
291
+
292
+ ## License
293
+
294
+ MIT - see [LICENSE](LICENSE).
facloc-0.1.0/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # facloc
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/facloc.svg)](https://pypi.org/project/facloc/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/facloc.svg)](https://pypi.org/project/facloc/)
5
+ [![CI](https://github.com/YusufEminoglu/facloc_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/facloc_sdk/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/facloc_sdk/blob/main/LICENSE)
7
+
8
+ **Facility location-allocation for public-service planning.**
9
+
10
+ `facloc` answers the classic planning questions behind schools, health centres, fire
11
+ stations, libraries or parks: *where should the next p facilities go, and who would each
12
+ one serve?* It implements the five textbook discrete location models:
13
+
14
+ | Model | Question | Objective | Source |
15
+ |---|---|---|---|
16
+ | **p-median** | Where do `p` facilities minimise average travel? | min sum of weight x distance | Hakimi 1964; ReVelle & Swain 1970 |
17
+ | **p-center** | Where do `p` facilities minimise the worst-case distance? | min max distance (optionally weighted) | Hakimi 1964 |
18
+ | **MCLP** | Where do `p` facilities cover the most people within a standard `S`? | max covered weight | Church & ReVelle 1974 |
19
+ | **LSCP** | How few facilities put *everyone* within `S`? | min number of sites | Toregas et al. 1971 |
20
+ | **Capacitated p-median** | As p-median, but each facility has a capacity | min weighted distance, single-source | Mulvey & Beck 1984 |
21
+
22
+ Each model can be solved three ways:
23
+
24
+ - `method="heuristic"` (default) - numpy-only: greedy ADD + **Teitz-Bart vertex substitution**
25
+ (p-median), greedy + swap (p-center, MCLP), greedy set cover with redundancy removal
26
+ (LSCP), regret assignment inside vertex substitution (capacitated). An optional
27
+ **Lagrangian lower bound** tells you how far the p-median heuristic can be from optimal.
28
+ - `method="exact"` - integer programme through **PuLP** with **HiGHS** (or CBC), via the
29
+ optional `exact` extra.
30
+ - `method="brute"` - full enumeration for small instances; the reference used in the tests.
31
+
32
+ Existing facilities can be **fixed** open, demand points carry **weights** (population,
33
+ pupils, patients), the distance matrix is **supplied** (network distances, travel times) or
34
+ **computed** (Euclidean, Manhattan or haversine). Results report chosen sites, allocations,
35
+ coverage %, weighted mean distance, maximum distance, site loads and **GeoJSON spider lines**.
36
+
37
+ ```bash
38
+ pip install facloc # numpy only
39
+ pip install "facloc[exact]" # + PuLP and highspy for exact MILP solving
40
+ ```
41
+
42
+ Requires Python 3.9 or newer.
43
+
44
+ ---
45
+
46
+ ## Quick start (Python)
47
+
48
+ ```python
49
+ import numpy as np
50
+ import facloc
51
+
52
+ lonlat = np.array([[29.01, 41.02], [29.03, 41.01], [29.05, 41.03], [29.10, 41.05],
53
+ [29.12, 41.04], [29.11, 41.07]])
54
+ population = [1200, 800, 950, 2100, 1700, 600]
55
+
56
+ problem = facloc.Problem.from_points(
57
+ lonlat, metric="haversine", # lon/lat in, metres out
58
+ weights=population,
59
+ fixed=[0], # an existing facility at point 0
60
+ )
61
+
62
+ sol = facloc.p_median(problem, p=2, radius=3000, bound=True)
63
+ print(sol.summary())
64
+ sol.sites # open sites (column indices), existing included
65
+ sol.allocation # serving site for every demand point
66
+ sol.weighted_mean_distance, sol.max_distance, sol.coverage() # coverage within 3 km
67
+ geojson = sol.to_geojson() # spider lines + points
68
+
69
+ facloc.mclp(problem, p=2, radius=3000).coverage() # best possible coverage with 2 sites
70
+ facloc.lscp(problem, radius=3000).objective # new sites needed for full coverage
71
+ facloc.p_center(problem, p=2).objective # minimal worst-case distance
72
+ facloc.p_median(problem, p=2, method="exact") # needs facloc[exact]
73
+ ```
74
+
75
+ With your own (e.g. road-network) distance matrix:
76
+
77
+ ```python
78
+ problem = facloc.Problem(
79
+ distances, # (n_demand, n_sites); use np.inf for unreachable pairs
80
+ weights=pupils,
81
+ fixed=[3, 17], # existing schools
82
+ capacities=seats, # only used by capacitated_p_median
83
+ demand_ids=block_ids, site_ids=parcel_ids,
84
+ )
85
+ facloc.capacitated_p_median(problem, p=6)
86
+ ```
87
+
88
+ `examples/quickstart.py` is a complete offline walk-through.
89
+
90
+ ## Quick start (command line)
91
+
92
+ ```bash
93
+ facloc solve p-median -p 4 --demand blocks.csv --sites candidates.csv \
94
+ --metric haversine --radius 1500 --bound \
95
+ --json solution.json --geojson spider.geojson --allocations alloc.csv
96
+
97
+ facloc solve mclp -p 4 --radius 1500 --demand blocks.csv --sites candidates.csv
98
+ facloc solve lscp --radius 1500 --demand blocks.csv --sites candidates.csv
99
+ facloc solve p-center -p 4 --demand blocks.csv --method exact
100
+ facloc solve capacitated -p 4 --demand blocks.csv --sites candidates.csv
101
+ facloc solve p-median -p 3 --matrix od_matrix.csv --demand blocks.csv --existing S07
102
+ facloc evaluate --open S01,S07 --radius 1500 --demand blocks.csv --sites candidates.csv
103
+ ```
104
+
105
+ Input CSVs have a header row; the delimiter (`,` `;` or tab) is detected.
106
+
107
+ - **demand**: `id, x, y, weight` (weight optional, default 1).
108
+ - **sites**: `id, x, y, capacity, existing` (`capacity` and `existing` optional;
109
+ `existing` is truthy for `1/true/yes/y/x`). Without `--sites`, every demand point is a
110
+ candidate site.
111
+ - **matrix**: first row is a corner label followed by site ids; each further row a demand
112
+ id followed by distances. Empty, `inf` or `NA` cells mean unreachable.
113
+
114
+ Column names can be changed with `--id-col`, `--x-col`, `--y-col`, `--weight-col`,
115
+ `--capacity-col`, `--existing-col`. `facloc solve --help` lists every option.
116
+
117
+ ## API overview
118
+
119
+ | Object | Purpose |
120
+ |---|---|
121
+ | `Problem(distances, weights, fixed, capacities, demand_ids, site_ids, demand_xy, site_xy)` | instance container |
122
+ | `Problem.from_points(demand_xy, site_xy=None, metric=...)` | build from coordinates |
123
+ | `p_median(problem, p, method, radius, bound, restarts, seed)` | p-median |
124
+ | `p_center(problem, p, method, weighted)` | vertex p-center |
125
+ | `mclp(problem, p, radius, method)` | maximal covering |
126
+ | `lscp(problem, radius, method)` | location set covering |
127
+ | `capacitated_p_median(problem, p, method)` | capacitated p-median (single source) |
128
+ | `pmedian_lower_bound(problem, p)` | Lagrangian lower bound |
129
+ | `evaluate(problem, sites, radius)` | measure any given set of open sites |
130
+ | `Solution` | `.sites`, `.new_sites`, `.allocation`, `.objective`, `.optimal`, `.lower_bound`, `.gap`, `.weighted_mean_distance`, `.max_distance`, `.coverage(r)`, `.loads()`, `.to_dict()`, `.to_json()`, `.to_geojson()`, `.summary()` |
131
+ | `distance_matrix`, `euclidean_matrix`, `haversine_matrix`, `manhattan_matrix` | distance helpers |
132
+ | `solver_available()` | is the exact extra usable? |
133
+
134
+ Conventions:
135
+
136
+ - `p` is the **total** number of open facilities, existing ones included: `p=5` with two
137
+ fixed sites places three new ones.
138
+ - Uncapacitated models allocate each demand point to its **nearest** open site.
139
+ - Demand points with zero weight do not influence the optimisation but are still allocated.
140
+ - `objective` is the model's own objective: total weighted distance (p-median, capacitated),
141
+ maximum (weighted) distance (p-center), covered weight (MCLP), number of new sites (LSCP).
142
+
143
+ ## Method and sources
144
+
145
+ **Distances.** Euclidean and Manhattan in input units (use projected coordinates);
146
+ haversine on a sphere of radius 6 371 008.8 m (the IUGG mean radius R1, Moritz 2000),
147
+ returning metres. For real accessibility studies a network distance or travel-time matrix
148
+ is better; compute it elsewhere and pass it in.
149
+
150
+ **p-median heuristic.** Greedy ADD builds a first solution; Teitz & Bart (1968) vertex
151
+ substitution then cycles through the sites not in the solution and makes the best exchange
152
+ with an open (non-fixed) site whenever it lowers total weighted distance, until no single
153
+ swap improves it. Exchanges are evaluated in a vectorised way using each demand point's
154
+ nearest and second-nearest open site. The greedy solution is refined both by first-
155
+ improvement (the original rule) and by steepest descent, and `restarts` (default 10,
156
+ `seed=0` for reproducibility) random starts are added; the best local optimum is returned.
157
+ The same engine, with a different key, serves the p-center (max distance, ties broken by a
158
+ power mean of distances) and the MCLP (uncovered weight, ties broken by total weighted
159
+ distance). The p-center heuristic also starts from the smallest radius whose greedy set
160
+ cover needs at most `p` sites.
161
+
162
+ **Lagrangian bound.** Relaxing the assignment constraints gives the bound
163
+ `L(lambda) = sum_i lambda_i + min over p sites of sum_j min(0, w_i d_ij - lambda_i)`
164
+ (Cornuejols, Fisher & Nemhauser 1977), maximised by subgradient optimisation
165
+ (Held, Wolfe & Crowder 1974). `Solution.gap` is then a certificate: the heuristic is at
166
+ most that far from the optimum.
167
+
168
+ **LSCP heuristic.** Greedy set covering (Chvatal 1979) with two selection rules, followed by
169
+ removal of redundant sites and 2-for-1 replacements.
170
+
171
+ **Capacitated p-median.** Single-source allocation: each demand point is served by one
172
+ facility and loads may not exceed capacities. For a given set of open sites, demand is
173
+ assigned by maximum regret and improved by shift and swap moves; vertex substitution over
174
+ sites uses this evaluation (screening each candidate's exchanges by their uncapacitated
175
+ cost first), starting from the uncapacitated solution, the largest-capacity sites and
176
+ random starts.
177
+
178
+ **Exact models.** Standard integer programmes (ReVelle & Swain 1970 for the p-median,
179
+ Church & ReVelle 1974 for the MCLP, Toregas et al. 1971 for the LSCP, a min-max programme
180
+ for the p-center, Mulvey & Beck 1984 for the capacitated p-median) built with PuLP and
181
+ solved with HiGHS (preferred) or CBC. Both the PuLP 2.x/3.x and the PuLP 4 APIs are handled.
182
+
183
+ **Tests.** Worked examples with hand-computed optima, and every heuristic is checked against
184
+ the brute-force optimum on small random instances (and the exact MILPs against brute force
185
+ when the extra is installed).
186
+
187
+ ### References
188
+
189
+ - Chvatal, V. (1979). A greedy heuristic for the set-covering problem. *Mathematics of
190
+ Operations Research*, 4(3), 233-235. https://doi.org/10.1287/moor.4.3.233
191
+ - Church, R., & ReVelle, C. (1974). The maximal covering location problem. *Papers of the
192
+ Regional Science Association*, 32(1), 101-118. https://doi.org/10.1007/BF01942293
193
+ - Cornuejols, G., Fisher, M. L., & Nemhauser, G. L. (1977). Location of bank accounts to
194
+ optimize float: An analytic study of exact and approximate algorithms. *Management
195
+ Science*, 23(8), 789-810. https://doi.org/10.1287/mnsc.23.8.789
196
+ - Hakimi, S. L. (1964). Optimum locations of switching centers and the absolute centers and
197
+ medians of a graph. *Operations Research*, 12(3), 450-459.
198
+ https://doi.org/10.1287/opre.12.3.450
199
+ - Held, M., Wolfe, P., & Crowder, H. P. (1974). Validation of subgradient optimization.
200
+ *Mathematical Programming*, 6, 62-88. https://doi.org/10.1007/BF01580223
201
+ - Moritz, H. (2000). Geodetic Reference System 1980. *Journal of Geodesy*, 74(1), 128-133.
202
+ https://doi.org/10.1007/s001900050278
203
+ - Mulvey, J. M., & Beck, M. P. (1984). Solving capacitated clustering problems. *European
204
+ Journal of Operational Research*, 18(3), 339-348.
205
+ https://doi.org/10.1016/0377-2217(84)90155-3
206
+ - ReVelle, C. S., & Swain, R. W. (1970). Central facilities location. *Geographical
207
+ Analysis*, 2(1), 30-42. https://doi.org/10.1111/j.1538-4632.1970.tb00142.x
208
+ - Teitz, M. B., & Bart, P. (1968). Heuristic methods for estimating the generalized vertex
209
+ median of a weighted graph. *Operations Research*, 16(5), 955-961.
210
+ https://doi.org/10.1287/opre.16.5.955
211
+ - Toregas, C., Swain, R., ReVelle, C., & Bergman, L. (1971). The location of emergency
212
+ service facilities. *Operations Research*, 19(6), 1363-1373.
213
+ https://doi.org/10.1287/opre.19.6.1363
214
+
215
+ ## Limitations
216
+
217
+ - Heuristics return good local optima, not proven optima. Use `bound=True` (p-median) or
218
+ `method="exact"` when you need a certificate. More `restarts` trade time for quality.
219
+ - Only *vertex* (discrete candidate) problems: facilities sit on given candidate sites, not
220
+ anywhere in the plane or on network edges (absolute centres/medians are not covered).
221
+ - The capacitated model is single-source; split demand is not modelled. Its heuristic can
222
+ fail to find a feasible allocation on very tight instances (raising `InfeasibleError`);
223
+ the exact solver settles feasibility.
224
+ - Distance-matrix size grows with `n_demand x n_sites`; the uncapacitated heuristics are
225
+ comfortable up to a few thousand demand points and hundreds of candidates (about 15 s for
226
+ 3000 x 300, p = 10, on a laptop). The capacitated heuristic is much slower, since every
227
+ candidate exchange re-solves the assignment (about 20 s for 150 x 60, p = 8). Exact
228
+ MILPs grow faster still.
229
+ - Straight-line and haversine distances ignore the street network, barriers and travel mode.
230
+ - GeoJSON (RFC 7946) is defined for WGS 84 lon/lat; for projected coordinates assign the CRS
231
+ in your GIS.
232
+
233
+ ### Related software
234
+
235
+ For a fuller toolkit - more model variants, network integration with PySAL, GeoPandas
236
+ input - see **spopt** (Feng et al. 2022, *JOSS* 7(74), 3330,
237
+ https://doi.org/10.21105/joss.03330), part of PySAL. `facloc` is deliberately small: numpy
238
+ only by default, a solver only when you ask for exact answers, plus a CLI and spider-line
239
+ output for quick planning studies.
240
+
241
+ ## Academic citation
242
+
243
+ ```bibtex
244
+ @software{eminoglu_facloc,
245
+ author = {Eminoğlu, Yusuf},
246
+ title = {facloc},
247
+ year = {2026},
248
+ version = {0.1.0},
249
+ url = {https://github.com/YusufEminoglu/facloc_sdk}
250
+ }
251
+ ```
252
+
253
+ ## License
254
+
255
+ MIT - see [LICENSE](LICENSE).