spatialsample 0.1.0__py3-none-any.whl

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.
spatialsample/cli.py ADDED
@@ -0,0 +1,184 @@
1
+ """Command-line interface: ``spatialsample <command> ...``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from typing import Any, List, Optional, Sequence
9
+
10
+ from . import __version__
11
+ from .accuracy import olofsson, sample_size
12
+ from .balance import compare_designs, spatial_balance
13
+ from .bas import bas
14
+ from .designs import poisson_disk, srs, stratified, systematic
15
+ from .grts import grts
16
+ from .io import read_matrix_csv, read_points, read_region, read_strata
17
+ from .lpm import lpm
18
+ from .sample import Sample
19
+
20
+
21
+ def _floats(s: str) -> List[float]:
22
+ return [float(v) for v in s.replace(";", ",").split(",") if v.strip()]
23
+
24
+
25
+ def _write(sample: Sample, out: Optional[str], lines: bool = False) -> None:
26
+ if out is None:
27
+ json.dump(sample.to_geojson(include_lines=lines), sys.stdout)
28
+ sys.stdout.write("\n")
29
+ return
30
+ if out.lower().endswith(".csv"):
31
+ sample.to_csv(out)
32
+ else:
33
+ sample.to_geojson(out, include_lines=lines)
34
+ nb = len(sample.base())
35
+ print(f"{sample.design}: {nb} base + {len(sample) - nb} over sites -> {out}", file=sys.stderr)
36
+
37
+
38
+ def _common(p: argparse.ArgumentParser, over: bool = False) -> None:
39
+ p.add_argument("area", help="polygon GeoJSON file (projected coordinates)")
40
+ p.add_argument("--n", type=int, required=True, help="base sample size")
41
+ if over:
42
+ p.add_argument("--over", type=int, default=0, help="oversample (replacement) sites")
43
+ p.add_argument("--seed", type=int, default=None, help="random seed")
44
+ p.add_argument("-o", "--output", default=None, help="output .geojson or .csv (default: stdout)")
45
+
46
+
47
+ def build_parser() -> argparse.ArgumentParser:
48
+ ap = argparse.ArgumentParser(
49
+ prog="spatialsample",
50
+ description="Design-based spatial sampling: GRTS, BAS, LPM, stratified, "
51
+ "systematic designs, spatial balance and map accuracy assessment.",
52
+ )
53
+ ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
54
+ sub = ap.add_subparsers(dest="command", required=True)
55
+
56
+ _common(sub.add_parser("srs", help="simple random sample"))
57
+ p = sub.add_parser("systematic", help="systematic square or hexagonal grid")
58
+ _common(p)
59
+ p.add_argument("--shape", choices=["square", "hex"], default="square")
60
+ p.add_argument("--random-rotation", action="store_true")
61
+ _common(sub.add_parser("grts", help="GRTS spatially balanced sample"), over=True)
62
+ _common(sub.add_parser("bas", help="balanced acceptance sample (Halton)"), over=True)
63
+ p = sub.add_parser("lpm", help="local pivotal method")
64
+ _common(p)
65
+ p.add_argument("--frame-size", type=int, default=None)
66
+ p = sub.add_parser("poisson", help="minimum-distance (Poisson-disk) sample")
67
+ _common(p)
68
+ p.add_argument("--min-dist", type=float, default=None)
69
+ p = sub.add_parser("stratified", help="stratified sample by a polygon attribute")
70
+ _common(p)
71
+ p.add_argument("--field", required=True, help="property holding the stratum label")
72
+ p.add_argument("--allocation", choices=["proportional", "equal"], default="proportional")
73
+ p.add_argument("--method", choices=["srs", "bas", "grts", "lpm"], default="srs")
74
+
75
+ p = sub.add_parser("balance", help="Voronoi spatial balance of a set of sites")
76
+ p.add_argument("sites", help="sites (GeoJSON points or CSV with x,y)")
77
+ p.add_argument("--frame", required=True, help="polygon GeoJSON of the sampled area")
78
+ p.add_argument("--frame-size", type=int, default=20000)
79
+ p.add_argument("--all-sites", action="store_true", help="include oversample sites")
80
+ p.add_argument("--json", action="store_true")
81
+
82
+ p = sub.add_parser("compare", help="compare the spatial balance of designs")
83
+ p.add_argument("area")
84
+ p.add_argument("--n", type=int, required=True)
85
+ p.add_argument("--reps", type=int, default=20)
86
+ p.add_argument("--seed", type=int, default=0)
87
+ p.add_argument("--json", action="store_true")
88
+
89
+ p = sub.add_parser("olofsson", help="accuracy and area estimates from an error matrix")
90
+ p.add_argument("matrix", help="CSV of sample counts (rows map, columns reference)")
91
+ p.add_argument(
92
+ "--areas", required=True, help="mapped area/pixels per map class, comma separated"
93
+ )
94
+ p.add_argument(
95
+ "--pixel-area", type=float, default=1.0, help="multiply areas by this (e.g. 0.09 ha)"
96
+ )
97
+ p.add_argument("--labels", default=None, help="comma-separated class names")
98
+ p.add_argument("--conf", type=float, default=0.95)
99
+ p.add_argument("--json", action="store_true")
100
+
101
+ p = sub.add_parser("samplesize", help="sample size for a target SE of overall accuracy")
102
+ p.add_argument("--areas", required=True)
103
+ p.add_argument("--users", required=True, help="conjectured user's accuracies")
104
+ p.add_argument("--target-se", type=float, default=0.01)
105
+ return ap
106
+
107
+
108
+ def main(argv: Optional[Sequence[str]] = None) -> int:
109
+ args = build_parser().parse_args(argv)
110
+ cmd = args.command
111
+ try:
112
+ return _run(cmd, args)
113
+ except (ValueError, KeyError, OSError) as exc:
114
+ print(f"spatialsample {cmd}: error: {exc}", file=sys.stderr)
115
+ return 2
116
+
117
+
118
+ def _run(cmd: str, args: Any) -> int:
119
+ if cmd in ("srs", "systematic", "grts", "bas", "lpm", "poisson", "stratified"):
120
+ if cmd == "stratified":
121
+ strata = read_strata(args.area, args.field)
122
+ s = stratified(
123
+ strata, args.n, allocation=args.allocation, method=args.method, seed=args.seed
124
+ )
125
+ else:
126
+ region = read_region(args.area)
127
+ if cmd == "srs":
128
+ s = srs(region, args.n, seed=args.seed)
129
+ elif cmd == "systematic":
130
+ rot = "random" if args.random_rotation else 0.0
131
+ s = systematic(region, args.n, shape=args.shape, rotation=rot, seed=args.seed)
132
+ elif cmd == "grts":
133
+ s = grts(region, args.n, n_over=args.over, seed=args.seed)
134
+ elif cmd == "bas":
135
+ s = bas(region, args.n, n_over=args.over, seed=args.seed)
136
+ elif cmd == "lpm":
137
+ s = lpm(region, args.n, frame_size=args.frame_size, seed=args.seed)
138
+ else:
139
+ s = poisson_disk(region, args.n, min_dist=args.min_dist, seed=args.seed)
140
+ _write(s, args.output)
141
+ return 0
142
+ if cmd == "balance":
143
+ xy, props = read_points(args.sites)
144
+ if not args.all_sites and props and any("site_type" in p for p in props):
145
+ keep = [i for i, p in enumerate(props) if p.get("site_type", "base") == "base"]
146
+ xy = xy[keep]
147
+ res = spatial_balance(xy, domain=read_region(args.frame), frame_size=args.frame_size)
148
+ if args.json:
149
+ print(
150
+ json.dumps(
151
+ {"n": res.n, "voronoi_balance": res.value, "max_deviation": res.max_deviation}
152
+ )
153
+ )
154
+ else:
155
+ print(f"sites: {res.n}")
156
+ print(f"Voronoi spatial balance (lower is better): {res.value:.4f}")
157
+ print(f"largest |v_i - 1|: {res.max_deviation:.4f}")
158
+ return 0
159
+ if cmd == "compare":
160
+ cmp = compare_designs(read_region(args.area), args.n, reps=args.reps, seed=args.seed)
161
+ if args.json:
162
+ print(json.dumps(cmp, indent=2))
163
+ else:
164
+ print(f"{'design':<20}{'balance':>10}{'sd':>10}{'mean n':>10}")
165
+ for k, v in sorted(cmp.items(), key=lambda kv: kv[1]["mean"]):
166
+ print(f"{k:<20}{v['mean']:>10.4f}{v['sd']:>10.4f}{v['mean_n']:>10.1f}")
167
+ return 0
168
+ if cmd == "olofsson":
169
+ mat, labels = read_matrix_csv(args.matrix)
170
+ if args.labels:
171
+ labels = [s.strip() for s in args.labels.split(",")]
172
+ areas = [a * args.pixel_area for a in _floats(args.areas)]
173
+ acc = olofsson(mat, areas, labels=labels, conf=args.conf)
174
+ print(json.dumps(acc.as_dict(), indent=2) if args.json else acc.summary())
175
+ return 0
176
+ if cmd == "samplesize":
177
+ n = sample_size(_floats(args.areas), _floats(args.users), args.target_se)
178
+ print(n)
179
+ return 0
180
+ raise AssertionError(cmd) # pragma: no cover
181
+
182
+
183
+ if __name__ == "__main__": # pragma: no cover
184
+ sys.exit(main())