mockgeo
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.
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
PYTHONHASHSEEDand 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. Passorigin=(lon, lat)to move it.
Installation
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
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):
{
"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:
{"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. EachTrackPointkeepstrue_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 ak x kzone grid (zones(city, k)).transit_feed(city, n_routes, headway_min)writesagency,stops,routes,trips,stop_times,calendar,shapesandfeed_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):
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.Rasterofferssample(x, y)(bilinear),stats(),to_ascii_grid()(Esri ASCII),to_xyz()andto_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):
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
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
$ 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
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.
License
MIT, see LICENSE.
Metadata
Release files for mockgeo 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mockgeo-0.1.0.tar.gz | 84.8 kB | With provenance | Details for mockgeo-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mockgeo-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for mockgeo-0.1.0-py3-none-any.whl |
Total release size: 155.6 kB
Release files / mockgeo-0.1.0.tar.gz
| Download URL | mockgeo-0.1.0.tar.gz |
|---|---|
| Size | 84.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cbb03bd2b66d8ba489486123298e78d8bedec6ed2a2edc5055c8fde22d510850
|
|
BLAKE2b-256 checksum How to use checksums |
cb539e40df309cfb86fd274bc15b0aa7cb99a7bea6aedf7976801cac46978fe7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / mockgeo-0.1.0-py3-none-any.whl
| Download URL | mockgeo-0.1.0-py3-none-any.whl |
|---|---|
| Size | 70.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f1f91a2d90d298087c6ee86516052e81f1f3efa37e458d8e8fe17457e4f3103b
|
|
BLAKE2b-256 checksum How to use checksums |
20992528f20645629dd8974a5b16ccd805e8800cdffdf629fcd31eb3420b4ccc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for mockgeo 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for mockgeo-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.