Metadata-Version: 2.4
Name: mockgeo
Version: 0.1.0
Summary: Deterministic synthetic geospatial test data: cities, street networks, buildings, GPS tracks, GTFS feeds, DEMs and fake addresses for tests, demos and benchmarks.
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/YusufEminoglu/mockgeo_sdk
Project-URL: Repository, https://github.com/YusufEminoglu/mockgeo_sdk
Project-URL: Issues, https://github.com/YusufEminoglu/mockgeo_sdk/issues
Project-URL: Changelog, https://github.com/YusufEminoglu/mockgeo_sdk/blob/main/CHANGELOG.md
Keywords: gis,geospatial,synthetic-data,test-data,mock,fixtures,pytest,faker,geojson,gpx,gtfs,dem,point-process,street-network,procedural-generation
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pytest
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Software Development :: Testing :: Mocking
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: numpy
Requires-Dist: numpy>=1.20.0; extra == "numpy"
Provides-Extra: geo
Requires-Dist: shapely>=2.0.0; extra == "geo"
Requires-Dist: geopandas>=0.12.0; extra == "geo"
Provides-Extra: faker
Requires-Dist: Faker>=18.0.0; extra == "faker"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# mockgeo

[![PyPI version](https://img.shields.io/pypi/v/mockgeo.svg?color=10b981)](https://pypi.org/project/mockgeo/)
[![Python version support](https://img.shields.io/pypi/pyversions/mockgeo.svg?color=3b82f6)](https://pypi.org/project/mockgeo/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/YusufEminoglu/mockgeo_sdk/blob/main/LICENSE)
[![Code style: Ruff](https://img.shields.io/badge/code%20style-ruff-D7FF64.svg)](https://docs.astral.sh/ruff/)

**Deterministic synthetic geospatial test data for unit tests, demos, tutorials and benchmarks.**

`mockgeo` builds a believable small city from a seed: a noded street network, blocks,
parcels, building footprints with heights and uses, named POIs, parks and ponds. On top of
it you get simulated GPS tracks, trip and OD tables, a GTFS-like transit feed, a census
table, fractal DEMs and land-cover grids. Everything is topologically valid, reproducible
byte-for-byte, and written in pure Python with **no required dependencies**.

```bash
pip install mockgeo
mockgeo city --seed 1 --size 2km -o city.geojson
```

- **Zero dependencies.** The core uses only the standard library, so it installs fast in
  any CI job. NumPy, Shapely/GeoPandas and Faker are optional extras.
- **Deterministic.** The same seed and arguments give identical bytes, independent of
  `PYTHONHASHSEED` and of the order in which you call generators.
- **Valid by construction, and checked.** Rings are closed and counter-clockwise, no ring
  self-intersects, parcels lie inside blocks and buildings inside parcels, POIs inside
  buildings, GPS ground truth on the streets. `City.validate()` checks all of it again.
- **Batteries for testing.** A pytest plugin with ready fixtures (`mock_city`,
  `mock_points`, `mock_tracks`), a Faker provider, and validators for GeoJSON, GPX and
  GTFS that you can reuse in your own tests.
- **Obviously fake.** Cities are centred in the open North Atlantic (`-35.0, 42.0`) by
  default and names are recombined from small built-in word lists, so mock data cannot be
  mistaken for a real place, business or person. Pass `origin=(lon, lat)` to move it.

## Installation

```bash
pip install mockgeo                 # standard library only
pip install "mockgeo[numpy]"        # Raster.to_numpy(), interop.to_numpy()
pip install "mockgeo[geo]"          # Shapely / GeoPandas bridges
pip install "mockgeo[faker]"        # mockgeo.faker.GeoProvider
pip install "mockgeo[dev]"          # pytest, pytest-cov, ruff, mypy
```

Python 3.9 to 3.13 on Windows, macOS and Linux.

## Quickstart

```python
import mockgeo

city = mockgeo.generate_city(seed=1, size="2km", layout="organic", locale="en_GB")
print(city.name, city.summary())
assert city.validate() == []                 # topology re-checked

city.to_geojson("city.geojson")              # all layers, WGS84 lon/lat (RFC 7946)
buildings = city.features(["buildings"])     # list of GeoJSON-like dicts
local = city.features(["parcels"], crs="local")  # metres from the city centre

tracks = mockgeo.gps_tracks(city, n=5, seed=1, mode="bike", noise=4.0)
open("tracks.gpx", "w", encoding="utf-8").write(mockgeo.dumps_gpx(tracks))

feed = mockgeo.transit_feed(city, n_routes=4, seed=1)
assert mockgeo.validate_gtfs(feed) == []
feed.write("feed.zip")

dem = mockgeo.dem(257, cell_size=10, seed=1, hills=3, valley=True)
dem.to_ascii_grid("dem.asc")                 # opens in QGIS / GDAL / rasterio
```

## What you can generate

### Points

| Function | Pattern |
| --- | --- |
| `uniform_points(n, bbox=..., polygon=...)` | Complete spatial randomness in a box or any polygon |
| `thomas_points(n_clusters, mean_size, sigma, ...)` | Thomas cluster process (Gaussian offspring) |
| `matern_points(n_clusters, mean_size, radius, ...)` | Matern cluster process (uniform disc offspring) |
| `points_along_line(line, n= / spacing=, jitter=)` | Even, random or jittered positions along a polyline |
| `density_weighted_points(n, density, bbox)` | Intensity from a function `f(x, y)` or a 2D weight grid |

Clustered processes simulate parents in an enlarged window to avoid edge effects, and
accept `n=` to return exactly that many points. `points_to_features()` wraps any of them
as GeoJSON features.

### Synthetic city

`generate_city(seed, size, layout, locale, origin, block_size, irregularity, name)`

| Layout | Street pattern |
| --- | --- |
| `grid` | Manhattan grid with arterials every few blocks; `irregularity` jitters the nodes |
| `organic` | Voronoi diagram of a perturbed lattice, irregular junctions, perimeter road |
| `radial` | Concentric ring roads and spokes; spokes double outwards to keep blocks small |

Layers (`city.layers[...]`, or `city.features(layers)` for export copies):

| Layer | Geometry | Main attributes |
| --- | --- | --- |
| `streets` | LineString per segment | `name`, `highway` (primary / secondary / tertiary / residential), `maxspeed`, `lanes`, `width`, `way_id` |
| `intersections` | Point | `degree`, `traffic_signals` |
| `blocks` | Polygon | `land_use`, `zone` (downtown / inner / outer), `postcode`, `area_m2` |
| `parcels` | Polygon | `block_id`, `street`, `housenumber`, `address` |
| `buildings` | Polygon | `use`, `floors`, `height`, `roof`, `year_built`, `population`, `address` |
| `pois` | Point | `category`, `name`, `address`, `opening_hours`, `within` |
| `parks` | Polygon | `name`, `area_m2` |
| `water` | Polygon | `park_id`, `water` = pond |

How it stays valid: every street face is convex; blocks are faces shrunk by half the
right-of-way of each bounding street; parcels come from recursive splits of the block
across its longest edge (convex pieces of a convex polygon); buildings are set back and
scaled inside their parcel, with some L shapes that are validated before use. House
numbers run odd on the left and even on the right, increasing along each named street.

A building as exported (`city.features(["buildings"])[0]`, coordinates shortened):

```json
{
  "type": "Feature",
  "geometry": {"type": "Polygon", "coordinates": [[[-35.0118464, 41.9912307],
    [-35.0117906, 41.9912307], [-35.0117906, 41.991193], [-35.0116767, 41.991193],
    [-35.0116767, 41.9913104], [-35.0118464, 41.9913104], [-35.0118464, 41.9912307]]]},
  "properties": {
    "layer": "buildings", "id": "bldg-0", "parcel_id": "parcel-0", "block_id": "block-0",
    "use": "residential", "floors": 2, "height": 8.7, "roof": "gabled", "year_built": 1958,
    "area_m2": 163.7, "population": 8, "street": "Pine Avenue", "housenumber": 1,
    "postcode": "37996", "address": "1 Pine Avenue, Fairdale 37996"
  }
}
```

A POI and a street segment:

```json
{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-35.0073403, 41.9915604]},
 "properties": {"layer": "pois", "id": "poi-0", "category": "restaurant",
  "name": "The Highland Kitchen", "within": "bldg-20",
  "address": "4 Lake Terrace, Fairdale 37996", "opening_hours": "Mo-Fr 09:00-17:00; Sa 10:00-14:00"}}

{"type": "Feature", "geometry": {"type": "LineString",
  "coordinates": [[-35.0121015, 41.9910068], [-35.0105889, 41.9910068]]},
 "properties": {"layer": "streets", "id": "street-0", "name": "Pine Avenue", "highway": "primary",
  "maxspeed": 50, "lanes": 4, "width": 24.0, "length_m": 125.0, "way_id": 0,
  "from_node": 0, "to_node": 1}}
```

`city.network` is a `StreetNetwork` with `shortest_path(u, v, weight="length"|"time")`,
`nearest_node`, `nearest_street`, `distance_to_network` and `is_connected`.

### Mobility

- `gps_tracks(city, n, seed, mode="car"|"bike"|"walk", interval, noise, stops, gaps, outlier_rate)`
  simulates driving rather than interpolating: vehicles accelerate to the street's speed
  limit, slow for sharp turns, stop at red lights and in short congestion, and arrive at
  rest. The receiver adds AR(1)-correlated noise, can lose the signal for 15 to 90 s (the
  track is split into segments) and can emit multipath outliers. Each `TrackPoint` keeps
  `true_x`/`true_y`, which lie exactly on the network, so you can score map matching.
- `generate_trips(city, n)` gives person trips from homes (weighted by population) to
  activity places (weighted by floor area) with purpose, mode, times and network distance.
- `od_matrix(city, trips, k)` aggregates trips to a `k x k` zone grid (`zones(city, k)`).
- `transit_feed(city, n_routes, headway_min)` writes `agency`, `stops`, `routes`, `trips`,
  `stop_times`, `calendar`, `shapes` and `feed_info`; `validate_gtfs()` checks required
  columns, unique ids, references, increasing sequences and times. Zips are
  byte-reproducible.

Track rows (`track.rows()`, as written by `mockgeo tracks -o tracks.csv`):

```text
track_id,seq,time,lon,lat,speed_ms,segment,outlier
track-0,0,2026-01-05T08:46:08Z,-35.00757,42.0082671,0.0,0,0
track-0,1,2026-01-05T08:46:09Z,-35.0075445,42.0082774,1.8,0,0
track-0,2,2026-01-05T08:46:10Z,-35.0075311,42.0082745,3.6,0,0
```

### Rasters

- `dem(size, cell_size, seed, roughness, relief, base, hills, valley)`: diamond-square
  fractal terrain, optional Gaussian hills/basins and a meandering river valley.
- `fractal_noise(size, seed, octaves, persistence, scale)`: value-noise fBm in `[0, 1]`.
- `landcover(size, seed, elevation=None)`: water, wetland, grassland, cropland, forest,
  urban and bare rock (`LANDCOVER_CLASSES`) driven by elevation and moisture noise.
- `Raster` offers `sample(x, y)` (bilinear), `stats()`, `to_ascii_grid()` (Esri ASCII),
  `to_xyz()` and `to_numpy()`.

### Attributes and names

`NameGenerator(locale)` builds street, city, park, person and POI names, postcodes and
addresses in the local order for `en_US`, `en_GB`, `de_DE`, `fr_FR`, `es_ES`, `it_IT` and
`tr_TR`. For example, `tr_TR` gives `Bahar Eczanesi | Defne Sokak No:4, 37095 Yeşilköy`.
`census_blocks(city)` returns block polygons with population (consistent with the
buildings), households, three age bands, jobs and density.

## Output formats

| Format | Functions | CLI extension |
| --- | --- | --- |
| GeoJSON (RFC 7946) | `dumps_geojson`, `write_geojson`, `City.to_geojson` | `.geojson`, `.json` |
| GeoJSONSeq | `dumps_geojsonseq(features, rs=False)` (`rs=True` for RFC 8142) | `.geojsonl`, `.ndjson` |
| CSV | `dumps_csv` (x/y for points, WKT otherwise; plain rows too) | `.csv` |
| WKT | `to_wkt`, `dumps_wkt` | `.wkt` |
| GPX 1.1 | `dumps_gpx(tracks, waypoints=pois)` | `.gpx` |
| GTFS | `TransitFeed.write("feed.zip")` or a directory | `mockgeo gtfs` |
| Esri ASCII grid / XYZ | `Raster.to_ascii_grid`, `Raster.to_xyz` | `.asc`, `.xyz` |
| GeoPandas / Shapely | `interop.to_geodataframe`, `interop.city_to_geodataframes`, `interop.to_shapely` | |

`validate_geojson()` and `validate_gpx()` return a list of problems, so they double as
assertions in your own tests.

## pytest plugin

Installing mockgeo registers a pytest plugin (`pytest11` entry point):

```python
def test_my_spatial_join(mock_city, mock_points):
    buildings = mock_city.features(["buildings"])
    assert my_join(buildings, mock_points)

def test_my_map_matcher(mock_tracks, mock_city):
    for track in mock_tracks:
        matched = my_match(track, mock_city.network)
        ...

def test_bigger(mock_city_factory):
    city = mock_city_factory(seed=3, size="1500m", layout="radial")
```

| Fixture | Value |
| --- | --- |
| `mockgeo_seed` | 42, or `--mockgeo-seed`, or `mockgeo_seed = ...` in the ini file |
| `mock_city` | 600 m grid `City` |
| `mock_city_factory` | callable taking `generate_city` arguments (cached per arguments) |
| `mock_points` | 200 uniform lon/lat Point features over the city |
| `mock_tracks` | 3 simulated car tracks on `mock_city` |

Fixtures hand out deep copies of cached objects, so tests can mutate them safely. Disable
the plugin with `-p no:mockgeo`.

## Faker provider

```python
from faker import Faker
from mockgeo.faker import GeoProvider

fake = Faker()
fake.add_provider(GeoProvider)
Faker.seed(42)

fake.geo_point()               # {"type": "Point", "coordinates": [lon, lat]}
fake.geo_polygon(n_vertices=7, radius_m=80)   # simple, closed, CCW
fake.building_footprint()      # Feature with floors, height, use
fake.geojson_feature_collection(10, "LineString")
fake.wkt_polygon()             # "POLYGON ((...))"
fake.mock_address("de_DE")     # e.g. "Lindenweg 12, 48151 Neuburg"
fake.poi("fr_FR")              # Feature with category, name, address
```

## Command line

```text
$ mockgeo city --seed 1 --size 2km -o city.geojson --summary
Fairdale: 8286 features -> city.geojson
{
  "streets": 808,
  "intersections": 425,
  "blocks": 384,
  "parcels": 3175,
  "buildings": 2990,
  "pois": 462,
  "parks": 29,
  "water": 13,
  "street_length_km": 84.0,
  "population": 49097
}

$ mockgeo points --n 1000 --bbox 29.0,41.0,29.1,41.1 --process thomas -o pts.csv
1000 points -> pts.csv

$ mockgeo tracks --seed 1 --n 5 --mode bike -o bike.gpx
5 tracks, 694 fixes -> bike.gpx

$ mockgeo dem --size 513 --hills 3 --valley -o dem.asc
DEM 513x513, 0.5-304.5 m -> dem.asc

$ mockgeo gtfs --seed 1 --size 2km -o feed.zip
4 routes, 25 stops, 776 trips -> feed.zip

$ mockgeo trips --size 2km --n 500 --od --zones 3
origin,destination,trips,mean_distance_m
Z00,Z01,1,750.0
Z00,Z02,3,1597.2
...
```

Sub-commands: `city`, `points`, `tracks`, `dem`, `landcover`, `gtfs`, `trips`. Without
`-o` the result goes to stdout (UTF-8), the summary line to stderr. Run
`mockgeo <command> --help` for every option; `python -m mockgeo` works too.

## Determinism

Every generator draws from `random.Random` streams seeded with a SHA-512 hash of
`(seed, generator name)`. Seeds may be integers or strings. Output is identical across
runs and machines with the same Python version; coordinates are rounded on export
(7 decimals for lon/lat, 3 for metres) so last-bit floating point differences between
platforms do not show up. Pass `seed=None` for fresh randomness.

## Development

```bash
git clone https://github.com/YusufEminoglu/mockgeo_sdk.git
cd mockgeo_sdk
pip install -e ".[dev,numpy,geo,faker]"
python -m pytest --cov=mockgeo
python -m ruff check . && python -m ruff format --check src tests
python -m mypy src
```

## Citation

See [`CITATION.cff`](CITATION.cff).

## License

MIT, see [LICENSE](LICENSE).
