mskit-simu 0.6.1__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.
mskit/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """
2
+ MSKit — Mini Simulation Kit
3
+ ============================
4
+ Terrain-based simulations powered by real-world data:
5
+ * JAXA AW3D30 30m elevation (global, lazy-streamed from HuggingFace)
6
+ * OpenTraffic / OSRM — live road speeds, global
7
+ * UTD19 — 40 cities, 23 541 loop detectors (ETH Zurich)
8
+ * OpenCTV — free public traffic cameras (SG, London, NYC)
9
+ * Simu — built-in AI assistant (SmolLM2-360M, open-source)
10
+
11
+ Dataset : https://huggingface.co/datasets/MegaBites-AI/AW3D30-DEM-Tiles
12
+ GitHub : https://github.com/MegaBites-AI/MSKit
13
+ """
14
+
15
+ from .dem import DEMTile, DEMLoader
16
+ from .sims.random_walk import RandomWalk
17
+ from .sims.projectile import Projectile
18
+ from .sims.flow import WaterFlow
19
+ from .sims.agent import TerrainAgent
20
+ from .traffic import OpenTrafficLayer, UTD19Layer, TrafficRouter, OpenCTVLayer
21
+ from .simu import Simu
22
+
23
+ __version__ = "0.6.0"
24
+ __author__ = "MegaBites AI Team"
25
+ __dataset__ = "MegaBites-AI/AW3D30-DEM-Tiles"
26
+ __github__ = "https://github.com/MegaBites-AI/MSKit"
27
+
28
+ __all__ = [
29
+ "DEMTile", "DEMLoader",
30
+ "RandomWalk", "Projectile", "WaterFlow", "TerrainAgent",
31
+ "OpenTrafficLayer", "UTD19Layer", "TrafficRouter", "OpenCTVLayer",
32
+ "Simu",
33
+ ]
mskit/_cli.py ADDED
@@ -0,0 +1,8 @@
1
+ """CLI entry point for mskit-upload command."""
2
+ import sys
3
+ import os
4
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
5
+
6
+ def upload_main():
7
+ from scripts.upload_tiles import main
8
+ main()
mskit/cli.py ADDED
@@ -0,0 +1,354 @@
1
+ """
2
+ MSKit CLI — unified command-line interface.
3
+ Entry point: mskit
4
+
5
+ Commands:
6
+ mskit simu Interactive AI assistant
7
+ mskit run "desc..." One-shot simulation
8
+ mskit cameras --city NAME Traffic cameras near a city
9
+ mskit cameras --lat L --lon O Search cameras by coordinates
10
+ mskit traffic --city NAME Traffic speed/congestion
11
+ mskit demo Full feature demo
12
+ mskit info Version and data source info
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ import argparse
18
+ import textwrap
19
+ import datetime
20
+ from typing import Optional
21
+
22
+ # ── City table ───────────────────────────────────────────────────────────────
23
+ _CITIES = {
24
+ "tokyo": (35.6762, 139.6503),
25
+ "london": (51.5074, -0.1278),
26
+ "paris": (48.8566, 2.3522),
27
+ "new york": (40.7128, -74.0060),
28
+ "nyc": (40.7128, -74.0060),
29
+ "singapore": ( 1.3521, 103.8198),
30
+ "sydney": (-33.8688, 151.2093),
31
+ "berlin": (52.5200, 13.4050),
32
+ "zurich": (47.3769, 8.5417),
33
+ "hong kong": (22.3193, 114.1694),
34
+ "dubai": (25.2048, 55.2708),
35
+ "seoul": (37.5665, 127.0000),
36
+ "beijing": (39.9042, 116.4074),
37
+ "shanghai": (31.2304, 121.4737),
38
+ "mumbai": (19.0760, 72.8777),
39
+ "los angeles": (34.0522,-118.2437),
40
+ "san francisco": (37.7749,-122.4194),
41
+ "chicago": (41.8781, -87.6298),
42
+ "toronto": (43.6532, -79.3832),
43
+ "amsterdam": (52.3676, 4.9041),
44
+ "moscow": (55.7558, 37.6173),
45
+ "istanbul": (41.0082, 28.9784),
46
+ "bangkok": (13.7563, 100.5018),
47
+ "cairo": (30.0444, 31.2357),
48
+ "nairobi": (-1.2921, 36.8219),
49
+ "sao paulo": (-23.5505,-46.6333),
50
+ "buenos aires": (-34.6037,-58.3816),
51
+ "mount fuji": (35.3606, 138.7274),
52
+ "fuji": (35.3606, 138.7274),
53
+ "himalayas": (27.9881, 86.9250),
54
+ "everest": (27.9881, 86.9250),
55
+ "alps": (46.5000, 8.5000),
56
+ "grand canyon": (36.1069,-112.1129),
57
+ "yellowstone": (44.4280,-110.5885),
58
+ }
59
+
60
+ BANNER = r"""
61
+ __ __ ___ _ ___ _
62
+ | \/ |/ __|| |/ (_) |_
63
+ | |\/| |\__ \| ' <| | _|
64
+ |_| |_||___/|_|\_\_|\__|
65
+
66
+ Mini Simulation Kit v{version}
67
+ GitHub : https://github.com/MegaBites-AI/MSKit
68
+ Dataset : MegaBites-AI/AW3D30-DEM-Tiles (HuggingFace)
69
+ """
70
+
71
+ def _hr(w=56): return "─" * w
72
+ def _section(t): print(f"\n{'━'*56}\n {t}\n{'━'*56}")
73
+
74
+
75
+ def _resolve_location(city, lat, lon):
76
+ if city:
77
+ k = city.lower().strip()
78
+ if k in _CITIES:
79
+ la, lo = _CITIES[k]
80
+ return la, lo, city.title()
81
+ print(f"❌ Unknown city '{city}'.")
82
+ print(f" Known: {', '.join(sorted(_CITIES))}")
83
+ sys.exit(1)
84
+ if lat is not None and lon is not None:
85
+ return lat, lon, f"({lat:.4f}, {lon:.4f})"
86
+ print("❌ Provide --city NAME or --lat LAT --lon LON")
87
+ sys.exit(1)
88
+
89
+
90
+ def _add_loc(p):
91
+ g = p.add_mutually_exclusive_group()
92
+ g.add_argument("--city", default=None)
93
+ p.add_argument("--lat", type=float, default=None)
94
+ p.add_argument("--lon", type=float, default=None)
95
+
96
+
97
+ # ── Sub-command handlers ──────────────────────────────────────────────────────
98
+
99
+ def cmd_simu(args):
100
+ from mskit.simu.simu import _cli_main
101
+ _cli_main()
102
+
103
+
104
+ def cmd_run(args):
105
+ desc = " ".join(args.description).strip()
106
+ if not desc:
107
+ print('❌ Usage: mskit run "describe your simulation"')
108
+ sys.exit(1)
109
+ print(f'\n🤖 MSKit — Running: "{desc}"\n')
110
+ from mskit.simu import Simu
111
+ simu = Simu(verbose=True, auto_select="untrained", auto_simmode="custom")
112
+ try:
113
+ from mskit import DEMLoader, TrafficRouter
114
+ dem = DEMLoader()
115
+ simu._dem = dem
116
+ simu._traffic = TrafficRouter(dem)
117
+ except Exception:
118
+ pass
119
+ simu.chat(desc)
120
+
121
+
122
+ def cmd_cameras(args):
123
+ from mskit.traffic import OpenCTVLayer
124
+ lat, lon, name = _resolve_location(
125
+ getattr(args,"city",None),
126
+ getattr(args,"lat",None),
127
+ getattr(args,"lon",None),
128
+ )
129
+ radius = getattr(args,"radius",5.0) or 5.0
130
+ tfl_key = getattr(args,"tfl_key",None)
131
+
132
+ _section(f"📷 OpenCTV — Cameras near {name}")
133
+ print(f" Coords : {lat:.5f}, {lon:.5f}")
134
+ print(f" Radius : {radius} km")
135
+
136
+ layer = OpenCTVLayer(tfl_app_key=tfl_key)
137
+ src = layer.covered_at(lat, lon)
138
+ if src:
139
+ print(f" Source : {layer.SOURCES[src]}")
140
+ else:
141
+ print(" Source : Synthetic (area not covered by live feeds)")
142
+ print(" Covered : Singapore · London · New York City")
143
+
144
+ print("\n Fetching...", end="", flush=True)
145
+ report = layer.traffic_report(lat, lon, radius_km=radius)
146
+ print(f" {report.camera_count} cameras found.\n")
147
+
148
+ if not report.cameras:
149
+ print(" No cameras in this area.")
150
+ return
151
+
152
+ if report.nearest:
153
+ n = report.nearest
154
+ dist = n.distance_km(lat, lon)
155
+ print(f" 📍 Nearest : {n.name} ({dist:.2f} km)")
156
+ print(f" Source : {n.source}")
157
+ print(f" Coords : {n.lat:.5f}, {n.lon:.5f}")
158
+ if n.direction: print(f" Direction: {n.direction}")
159
+ if n.image_url: print(f" Image URL: {n.image_url}")
160
+ if n.timestamp: print(f" Updated : {n.timestamp}")
161
+
162
+ shown = report.cameras[:10]
163
+ if len(shown) > 1:
164
+ print(f"\n All cameras (showing {len(shown)} of {report.camera_count}):")
165
+ for i, c in enumerate(shown, 1):
166
+ dist = c.distance_km(lat, lon)
167
+ line = f" [{i:2d}] {c.name} ({dist:.2f} km)"
168
+ if c.image_url:
169
+ line += f"\n {c.image_url}"
170
+ print(line)
171
+ if report.camera_count > 10:
172
+ print(f"\n ... and {report.camera_count-10} more.")
173
+ print(f"\n Timestamp: {report.timestamp}")
174
+
175
+
176
+ def cmd_traffic(args):
177
+ lat, lon, name = _resolve_location(
178
+ getattr(args,"city",None),
179
+ getattr(args,"lat",None),
180
+ getattr(args,"lon",None),
181
+ )
182
+ _section(f"🚦 Traffic Report — {name}")
183
+ print(f" Coords : {lat:.5f}, {lon:.5f}")
184
+ print(f" Time : {datetime.datetime.utcnow():%Y-%m-%d %H:%M:%S} UTC\n")
185
+
186
+ try:
187
+ from mskit.traffic import TrafficRouter
188
+ from mskit import DEMLoader
189
+ info = TrafficRouter(DEMLoader()).traffic_at(lat, lon)
190
+ d = info.to_dict()
191
+ print(f" Speed : {d.get('speed_kmh',0):.1f} km/h")
192
+ print(f" Flow : {d.get('flow_veh_h',0):.0f} veh/h")
193
+ print(f" Occupancy : {d.get('occupancy_pct',0):.1f}%")
194
+ print(f" Congestion : {str(d.get('congestion_level','?')).upper()}")
195
+ print(f" Source : {d.get('source','?')}")
196
+ print(f" Confidence : {d.get('confidence',0)*100:.0f}%")
197
+ except Exception as e:
198
+ print(f" ⚠️ Traffic data: {e}")
199
+
200
+ try:
201
+ from mskit.traffic import OpenCTVLayer
202
+ report = OpenCTVLayer().traffic_report(lat, lon, radius_km=5)
203
+ print(f"\n 📷 Cameras : {report.camera_count} within 5 km [{report.source}]")
204
+ if report.nearest:
205
+ d = report.nearest.distance_km(lat,lon)
206
+ print(f" Nearest : {report.nearest.name} ({d:.2f} km)")
207
+ except Exception:
208
+ pass
209
+
210
+
211
+ def cmd_info(args):
212
+ import mskit
213
+ print(BANNER.format(version=mskit.__version__))
214
+ print(_hr())
215
+ print(" DATA SOURCES")
216
+ print(_hr())
217
+ rows = [
218
+ ("Elevation", "JAXA AW3D30 (30m global DEM)"),
219
+ ("", "MegaBites-AI/AW3D30-DEM-Tiles on HuggingFace"),
220
+ ("Traffic", "OpenTraffic / OSRM (global road network, priority 1)"),
221
+ ("", "UTD19 — 40 cities, 23,541 loop detectors (priority 2)"),
222
+ ("", "Synthetic terrain+time model (fallback)"),
223
+ ("Cameras", "OpenCTV aggregator:"),
224
+ ("", " • Singapore LTA — 87 cams, 20s refresh, no key"),
225
+ ("", " • TfL JamCam — 900+ London cams, free key"),
226
+ ("", " • NYC DOT — ~900 NYC cams, no key"),
227
+ ("", " • Synthetic fallback everywhere else"),
228
+ ("AI Brain", "HuggingFaceTB/SmolLM2-360M-Instruct"),
229
+ ("", "360M params · Apache 2.0 · CPU-capable"),
230
+ ]
231
+ for label, val in rows:
232
+ print(f" {label:<12} {val}")
233
+ print()
234
+ print(_hr())
235
+ print(" SIMULATIONS")
236
+ print(_hr())
237
+ for name, desc in [
238
+ ("random_walk", "Slope-biased terrain walk"),
239
+ ("projectile", "Ballistic trajectory over real terrain"),
240
+ ("water_flow", "D8 runoff routing"),
241
+ ("agent", "RL terrain navigator"),
242
+ ("traffic", "Road speed + congestion query"),
243
+ ("profile", "Elevation cross-section between two points"),
244
+ ]:
245
+ print(f" {name:<14} {desc}")
246
+ print(_hr())
247
+ print(f" Version : {mskit.__version__}")
248
+ print(f" GitHub : {mskit.__github__}")
249
+ print()
250
+
251
+
252
+ def cmd_demo(args):
253
+ import mskit
254
+ print(BANNER.format(version=mskit.__version__))
255
+ print(" Running full feature demo...\n")
256
+
257
+ _section("📷 OpenCTV — Singapore cameras")
258
+ try:
259
+ from mskit.traffic import OpenCTVLayer
260
+ r = OpenCTVLayer().traffic_report(1.3521, 103.8198, radius_km=3)
261
+ print(f" Found {r.camera_count} cameras near Singapore centre.")
262
+ if r.nearest:
263
+ print(f" Nearest : {r.nearest.name}")
264
+ print(f" Image : {r.nearest.image_url or '(no URL — synthetic)'}")
265
+ except Exception as e:
266
+ print(f" ⚠️ {e}")
267
+
268
+ _section("🚦 Traffic — Tokyo")
269
+ try:
270
+ from mskit.traffic import TrafficRouter
271
+ from mskit import DEMLoader
272
+ info = TrafficRouter(DEMLoader()).traffic_at(35.6762, 139.6503)
273
+ d = info.to_dict()
274
+ print(f" Speed : {d['speed_kmh']:.1f} km/h")
275
+ print(f" Congestion : {d['congestion_level']}")
276
+ print(f" Source : {d['source']}")
277
+ except Exception as e:
278
+ print(f" ⚠️ {e}")
279
+
280
+ _section("🤖 Simu — All 6 sims at London (untrained brain)")
281
+ try:
282
+ from mskit.simu import Simu
283
+ simu = Simu(verbose=True, auto_select="untrained", auto_simmode="everything")
284
+ result = simu.chat("London")
285
+ ok = sum(1 for v in result["output"].values() if "error" not in v)
286
+ print(f"\n {ok}/6 simulations completed.")
287
+ except Exception as e:
288
+ print(f" ⚠️ {e}")
289
+
290
+ print(f"\n{'━'*56}")
291
+ print(" Demo complete! Run `mskit simu` for the full AI experience.")
292
+ print('━'*56 + "\n")
293
+
294
+
295
+ # ── Argument parser ───────────────────────────────────────────────────────────
296
+
297
+ def build_parser() -> argparse.ArgumentParser:
298
+ parser = argparse.ArgumentParser(
299
+ prog="mskit",
300
+ description="MSKit — Mini Simulation Kit",
301
+ formatter_class=argparse.RawDescriptionHelpFormatter,
302
+ epilog=textwrap.dedent("""
303
+ Examples:
304
+ mskit simu
305
+ mskit run "random walk in Tokyo 500 steps"
306
+ mskit cameras --city singapore
307
+ mskit cameras --lat 51.5 --lon -0.1 --radius 3
308
+ mskit traffic --city london
309
+ mskit traffic --lat 40.71 --lon -74.00
310
+ mskit demo
311
+ mskit info
312
+ """),
313
+ )
314
+ sub = parser.add_subparsers(dest="command", metavar="COMMAND")
315
+
316
+ p = sub.add_parser("simu", help="Start Simu AI assistant (interactive)")
317
+ p.set_defaults(func=cmd_simu)
318
+
319
+ p = sub.add_parser("run", help="Run a simulation from plain-English description")
320
+ p.add_argument("description", nargs=argparse.REMAINDER)
321
+ p.set_defaults(func=cmd_run)
322
+
323
+ p = sub.add_parser("cameras", help="Query OpenCTV traffic cameras")
324
+ _add_loc(p)
325
+ p.add_argument("--radius", type=float, default=5.0)
326
+ p.add_argument("--tfl-key", dest="tfl_key", default=None)
327
+ p.set_defaults(func=cmd_cameras)
328
+
329
+ p = sub.add_parser("traffic", help="Query traffic speed/congestion")
330
+ _add_loc(p)
331
+ p.set_defaults(func=cmd_traffic)
332
+
333
+ p = sub.add_parser("info", help="Version and data source info")
334
+ p.set_defaults(func=cmd_info)
335
+
336
+ p = sub.add_parser("demo", help="Run a quick feature demo")
337
+ p.set_defaults(func=cmd_demo)
338
+
339
+ return parser
340
+
341
+
342
+ def main():
343
+ parser = build_parser()
344
+ args = parser.parse_args()
345
+ if not args.command:
346
+ import mskit
347
+ print(BANNER.format(version=mskit.__version__))
348
+ parser.print_help()
349
+ sys.exit(0)
350
+ args.func(args)
351
+
352
+
353
+ if __name__ == "__main__":
354
+ main()
mskit/dem.py ADDED
@@ -0,0 +1,231 @@
1
+ """
2
+ DEMTile & DEMLoader — fetch AW3D30 elevation tiles on demand from HuggingFace.
3
+
4
+ Tile naming follows JAXA convention: N{lat:03d}E{lon:03d} etc.
5
+ Each tile covers 1°×1° at 30m resolution (3600×3600 pixels).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import math
12
+ import struct
13
+ import tempfile
14
+ import urllib.request
15
+ from pathlib import Path
16
+ from typing import Optional, Tuple
17
+
18
+ import numpy as np
19
+
20
+ HF_DATASET = "MegaBites-AI/AW3D30-DEM-Tiles"
21
+ HF_BASE = f"https://huggingface.co/datasets/{HF_DATASET}/resolve/main"
22
+ CACHE_DIR = Path(os.path.expanduser("~/.cache/mskit/dem"))
23
+
24
+
25
+ def _tile_name(lat: float, lon: float) -> str:
26
+ """Return JAXA-style tile name for a lat/lon coordinate."""
27
+ lat_i = int(math.floor(lat))
28
+ lon_i = int(math.floor(lon))
29
+ lat_ch = "N" if lat_i >= 0 else "S"
30
+ lon_ch = "E" if lon_i >= 0 else "W"
31
+ return f"{lat_ch}{abs(lat_i):03d}{lon_ch}{abs(lon_i):03d}"
32
+
33
+
34
+ class DEMTile:
35
+ """
36
+ A single 1°×1° elevation tile fetched from the HF dataset.
37
+
38
+ Parameters
39
+ ----------
40
+ lat : float — latitude of any point inside the tile (or tile SW corner)
41
+ lon : float — longitude of any point inside the tile
42
+ token : str, optional — HuggingFace token for private repos
43
+
44
+ Examples
45
+ --------
46
+ >>> tile = DEMTile(35.6, 139.7) # Tokyo
47
+ >>> elev = tile.elevation_at(35.68, 139.69)
48
+ >>> print(f"{elev:.1f} m")
49
+ """
50
+
51
+ ROWS = 3600
52
+ COLS = 3600
53
+
54
+ def __init__(self, lat: float, lon: float, token: Optional[str] = None):
55
+ self.lat = math.floor(lat)
56
+ self.lon = math.floor(lon)
57
+ self.name = _tile_name(lat, lon)
58
+ self._token = token or os.environ.get("HF_TOKEN")
59
+ self._data: Optional[np.ndarray] = None
60
+
61
+ # ------------------------------------------------------------------
62
+ # Data loading
63
+ # ------------------------------------------------------------------
64
+
65
+ def _cache_path(self) -> Path:
66
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
67
+ return CACHE_DIR / f"{self.name}.npy"
68
+
69
+ def _remote_url(self) -> str:
70
+ """Build URL for the tile's .npy file on HuggingFace."""
71
+ # tiles are stored as: data/{lat_band}/{tile_name}.npy
72
+ lat_band = f"N{abs(self.lat):03d}" if self.lat >= 0 else f"S{abs(self.lat):03d}"
73
+ return f"{HF_BASE}/data/{lat_band}/{self.name}.npy"
74
+
75
+ def load(self) -> "DEMTile":
76
+ """Download (or load from cache) the elevation grid."""
77
+ cache = self._cache_path()
78
+ if cache.exists():
79
+ self._data = np.load(str(cache))
80
+ return self
81
+
82
+ url = self._remote_url()
83
+ headers = {}
84
+ if self._token:
85
+ headers["Authorization"] = f"Bearer {self._token}"
86
+
87
+ req = urllib.request.Request(url, headers=headers)
88
+ try:
89
+ with urllib.request.urlopen(req) as resp:
90
+ data = resp.read()
91
+ except urllib.error.HTTPError as e:
92
+ raise FileNotFoundError(
93
+ f"Tile {self.name} not found in dataset ({e.code}). "
94
+ "It may not have been uploaded yet."
95
+ ) from e
96
+
97
+ # Save to cache
98
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".npy") as tmp:
99
+ tmp.write(data)
100
+ tmp_path = tmp.name
101
+
102
+ arr = np.load(tmp_path)
103
+ np.save(str(cache), arr)
104
+ os.unlink(tmp_path)
105
+ self._data = arr
106
+ return self
107
+
108
+ @property
109
+ def data(self) -> np.ndarray:
110
+ if self._data is None:
111
+ self.load()
112
+ return self._data
113
+
114
+ # ------------------------------------------------------------------
115
+ # Spatial queries
116
+ # ------------------------------------------------------------------
117
+
118
+ def elevation_at(self, lat: float, lon: float) -> float:
119
+ """Return elevation (metres) at an exact lat/lon coordinate."""
120
+ row, col = self._latlon_to_rc(lat, lon)
121
+ return float(self.data[row, col])
122
+
123
+ def patch(
124
+ self,
125
+ lat: float,
126
+ lon: float,
127
+ radius_km: float = 5.0,
128
+ ) -> np.ndarray:
129
+ """
130
+ Return a square elevation patch centred on (lat, lon).
131
+
132
+ radius_km controls the half-width of the extracted square.
133
+ """
134
+ # 30 m per pixel → pixels per km ≈ 33.33
135
+ px = int(radius_km * 33.33)
136
+ r, c = self._latlon_to_rc(lat, lon)
137
+ r0, r1 = max(0, r - px), min(self.ROWS, r + px)
138
+ c0, c1 = max(0, c - px), min(self.COLS, c + px)
139
+ return self.data[r0:r1, c0:c1]
140
+
141
+ def slope(self) -> np.ndarray:
142
+ """Return slope grid (degrees) computed via central differences."""
143
+ dy, dx = np.gradient(self.data.astype(float), 30.0, 30.0)
144
+ return np.degrees(np.arctan(np.sqrt(dx**2 + dy**2)))
145
+
146
+ # ------------------------------------------------------------------
147
+ # Internal helpers
148
+ # ------------------------------------------------------------------
149
+
150
+ def _latlon_to_rc(self, lat: float, lon: float) -> Tuple[int, int]:
151
+ frac_lat = lat - self.lat # 0..1
152
+ frac_lon = lon - self.lon # 0..1
153
+ # Row 0 = north edge
154
+ row = int((1.0 - frac_lat) * (self.ROWS - 1))
155
+ col = int(frac_lon * (self.COLS - 1))
156
+ row = max(0, min(self.ROWS - 1, row))
157
+ col = max(0, min(self.COLS - 1, col))
158
+ return row, col
159
+
160
+ def __repr__(self) -> str:
161
+ loaded = "loaded" if self._data is not None else "not loaded"
162
+ return f"<DEMTile {self.name} ({loaded})>"
163
+
164
+
165
+ class DEMLoader:
166
+ """
167
+ Multi-tile loader with an LRU-style cache.
168
+
169
+ Automatically fetches and stitches adjacent tiles so simulations
170
+ can cross tile boundaries seamlessly.
171
+
172
+ Parameters
173
+ ----------
174
+ max_tiles : int — max tiles to keep in memory simultaneously
175
+ token : str, optional — HuggingFace token
176
+
177
+ Examples
178
+ --------
179
+ >>> loader = DEMLoader()
180
+ >>> elev = loader.elevation_at(35.68, 139.69)
181
+ """
182
+
183
+ def __init__(self, max_tiles: int = 9, token: Optional[str] = None):
184
+ self._max = max_tiles
185
+ self._token = token or os.environ.get("HF_TOKEN")
186
+ self._cache: dict[str, DEMTile] = {}
187
+
188
+ def _get_tile(self, lat: float, lon: float) -> DEMTile:
189
+ name = _tile_name(lat, lon)
190
+ if name not in self._cache:
191
+ if len(self._cache) >= self._max:
192
+ # evict oldest
193
+ oldest = next(iter(self._cache))
194
+ del self._cache[oldest]
195
+ tile = DEMTile(lat, lon, token=self._token)
196
+ tile.load()
197
+ self._cache[name] = tile
198
+ return self._cache[name]
199
+
200
+ def elevation_at(self, lat: float, lon: float) -> float:
201
+ """Return elevation (m) at any lat/lon, fetching tiles as needed."""
202
+ return self._get_tile(lat, lon).elevation_at(lat, lon)
203
+
204
+ def patch(self, lat: float, lon: float, radius_km: float = 5.0) -> np.ndarray:
205
+ """Return elevation patch centred on (lat, lon)."""
206
+ return self._get_tile(lat, lon).patch(lat, lon, radius_km)
207
+
208
+ def elevation_profile(
209
+ self,
210
+ lat1: float, lon1: float,
211
+ lat2: float, lon2: float,
212
+ steps: int = 200,
213
+ ) -> Tuple[np.ndarray, np.ndarray]:
214
+ """
215
+ Return (distances_km, elevations_m) along a straight-line transect.
216
+ """
217
+ lats = np.linspace(lat1, lat2, steps)
218
+ lons = np.linspace(lon1, lon2, steps)
219
+ elevs = np.array([self.elevation_at(la, lo) for la, lo in zip(lats, lons)])
220
+
221
+ # Haversine distances
222
+ R = 6371.0
223
+ dists = np.zeros(steps)
224
+ for i in range(1, steps):
225
+ dlat = math.radians(lats[i] - lats[i-1])
226
+ dlon = math.radians(lons[i] - lons[i-1])
227
+ a = math.sin(dlat/2)**2 + math.cos(math.radians(lats[i-1])) * \
228
+ math.cos(math.radians(lats[i])) * math.sin(dlon/2)**2
229
+ dists[i] = dists[i-1] + 2 * R * math.asin(math.sqrt(a))
230
+
231
+ return dists, elevs
mskit/sims/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # mskit.sims — simulation modules