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.
- facloc-0.1.0/CHANGELOG.md +23 -0
- facloc-0.1.0/CITATION.cff +22 -0
- facloc-0.1.0/LICENSE +21 -0
- facloc-0.1.0/MANIFEST.in +9 -0
- facloc-0.1.0/PKG-INFO +294 -0
- facloc-0.1.0/README.md +255 -0
- facloc-0.1.0/examples/quickstart.py +61 -0
- facloc-0.1.0/pyproject.toml +119 -0
- facloc-0.1.0/setup.cfg +4 -0
- facloc-0.1.0/src/facloc/__init__.py +36 -0
- facloc-0.1.0/src/facloc/__main__.py +7 -0
- facloc-0.1.0/src/facloc/_version.py +1 -0
- facloc-0.1.0/src/facloc/brute.py +111 -0
- facloc-0.1.0/src/facloc/cli.py +221 -0
- facloc-0.1.0/src/facloc/distance.py +116 -0
- facloc-0.1.0/src/facloc/errors.py +17 -0
- facloc-0.1.0/src/facloc/exact.py +202 -0
- facloc-0.1.0/src/facloc/heuristics.py +494 -0
- facloc-0.1.0/src/facloc/io.py +162 -0
- facloc-0.1.0/src/facloc/lagrangian.py +82 -0
- facloc-0.1.0/src/facloc/models.py +486 -0
- facloc-0.1.0/src/facloc/problem.py +214 -0
- facloc-0.1.0/src/facloc/py.typed +0 -0
- facloc-0.1.0/src/facloc/solution.py +356 -0
- facloc-0.1.0/src/facloc.egg-info/PKG-INFO +294 -0
- facloc-0.1.0/src/facloc.egg-info/SOURCES.txt +35 -0
- facloc-0.1.0/src/facloc.egg-info/dependency_links.txt +1 -0
- facloc-0.1.0/src/facloc.egg-info/entry_points.txt +2 -0
- facloc-0.1.0/src/facloc.egg-info/requires.txt +11 -0
- facloc-0.1.0/src/facloc.egg-info/top_level.txt +1 -0
- facloc-0.1.0/tests/conftest.py +26 -0
- facloc-0.1.0/tests/test_cli_io.py +146 -0
- facloc-0.1.0/tests/test_distance.py +54 -0
- facloc-0.1.0/tests/test_exact.py +73 -0
- facloc-0.1.0/tests/test_heuristics_vs_brute.py +93 -0
- facloc-0.1.0/tests/test_models.py +181 -0
- 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.
|
facloc-0.1.0/MANIFEST.in
ADDED
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
|
+
[](https://pypi.org/project/facloc/)
|
|
43
|
+
[](https://pypi.org/project/facloc/)
|
|
44
|
+
[](https://github.com/YusufEminoglu/facloc_sdk/actions/workflows/ci.yml)
|
|
45
|
+
[](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
|
+
[](https://pypi.org/project/facloc/)
|
|
4
|
+
[](https://pypi.org/project/facloc/)
|
|
5
|
+
[](https://github.com/YusufEminoglu/facloc_sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](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).
|