Geographical Support
Class |
Purpose |
|---|---|
|
A validated (latitude, longitude) point |
|
A shapely |
Both can be attached to TimeSeriesList and TimeSeriesTable objects.
[1]:
from datetime import datetime, timedelta, timezone
import numpy as np
import timedatamodel as tdm
base = datetime(2024, 1, 15, tzinfo=timezone.utc)
timestamps = [base + timedelta(hours=i) for i in range(24)]
rng = np.random.default_rng(42)
GeoLocation — point coordinates
GeoLocation is a frozen dataclass that validates latitude ∈ [−90, 90] and longitude ∈ [−180, 180].
[2]:
oslo = tdm.GeoLocation(latitude=59.91, longitude=10.75)
bergen = tdm.GeoLocation(latitude=60.39, longitude=5.32)
tromsoe = tdm.GeoLocation(latitude=69.65, longitude=18.96)
print(f"Oslo: {oslo}")
print(f"Bergen: {bergen}")
print(f"Tromsø: {tromsoe}")
Oslo: GeoLocation(latitude=59.91, longitude=10.75)
Bergen: GeoLocation(latitude=60.39, longitude=5.32)
Tromsø: GeoLocation(latitude=69.65, longitude=18.96)
[3]:
try:
bad = tdm.GeoLocation(latitude=200, longitude=0)
except ValueError as e:
print(f"Validation error: {e}")
Validation error: latitude must be between -90 and 90, got 200
Distance, bearing, and midpoint
GeoLocation provides Haversine-based spatial methods — no external dependency required.
[4]:
d_km = oslo.distance_to(bergen)
d_mi = oslo.distance_to(bergen, unit="mi")
print(f"Oslo → Bergen: {d_km:.1f} km ({d_mi:.1f} mi)")
d_tromsoe = oslo.distance_to(tromsoe)
print(f"Oslo → Tromsø: {d_tromsoe:.1f} km")
Oslo → Bergen: 305.1 km (189.6 mi)
Oslo → Tromsø: 1148.4 km
[5]:
bearing = oslo.bearing_to(bergen)
print(f"Initial bearing Oslo → Bergen: {bearing:.1f}°")
mid = oslo.midpoint(bergen)
print(f"Geographic midpoint: {mid}")
Initial bearing Oslo → Bergen: 282.4°
Geographic midpoint: GeoLocation(latitude=60.17777046667162, longitude=8.05483245837952)
Offset — displace a point
offset(distance_km, bearing_deg) produces a new GeoLocation displaced from the original.
[6]:
east_of_oslo = oslo.offset(100, 90) # 100 km due east
print(f"100 km east of Oslo: {east_of_oslo}")
print(f"Verify distance: {oslo.distance_to(east_of_oslo):.1f} km")
100 km east of Oslo: GeoLocation(latitude=59.89782201387188, longitude=12.54332661133784)
Verify distance: 100.0 km
Attaching location to a TimeSeriesList
Pass a GeoLocation via the location parameter. It shows up in the rich repr.
[7]:
ts_oslo = tdm.TimeSeriesList(
tdm.Frequency.PT1H,
timezone="Europe/Oslo",
timestamps=timestamps,
values=(5.0 + rng.normal(0, 2, 24)).tolist(),
name="temperature",
unit="°C",
description="Hourly temperature at Oslo Blindern",
data_type=tdm.DataType.OBSERVATION,
location=oslo,
)
ts_oslo
[7]:
| timestamp | temperature |
|---|---|
| 2024-01-15 00:00 | 5.60943 |
| 2024-01-15 01:00 | 2.92003 |
| 2024-01-15 02:00 | 6.5009 |
| … | … |
| 2024-01-15 21:00 | 3.63814 |
| 2024-01-15 22:00 | 7.44508 |
| 2024-01-15 23:00 | 4.69094 |
[8]:
print(f"Location: {ts_oslo.location}")
print(f"Lat: {ts_oslo.location.latitude}, Lon: {ts_oslo.location.longitude}")
Location: GeoLocation(latitude=59.91, longitude=10.75)
Lat: 59.91, Lon: 10.75
Per-column locations in a TimeSeriesTable
When columns represent different physical locations, attach one GeoLocation per column.
[9]:
table = tdm.TimeSeriesTable(
tdm.Frequency.PT1H,
timezone="Europe/Oslo",
timestamps=timestamps,
values=np.column_stack([
5.0 + rng.normal(0, 2, 24),
2.0 + rng.normal(0, 3, 24),
-8.0 + rng.normal(0, 4, 24),
]),
names=["Oslo", "Bergen", "Tromsø"],
units=["°C", "°C", "°C"],
locations=[oslo, bergen, tromsoe],
data_types=[tdm.DataType.OBSERVATION],
)
table
[9]:
| timestamp | Oslo | Bergen | Tromsø |
|---|---|---|---|
| 2024-01-15 00:00 | 4.14334 | 4.03674 | -11.6778 |
| 2024-01-15 01:00 | 4.29573 | 2.20274 | -6.01136 |
| 2024-01-15 02:00 | 6.06462 | 2.86736 | -7.4303 |
| … | … | … | … |
| 2024-01-15 21:00 | 5.43738 | 1.42609 | -8.31887 |
| 2024-01-15 22:00 | 6.74286 | -1.82706 | -14.7493 |
| 2024-01-15 23:00 | 5.44719 | -1.39986 | -13.7884 |
[10]:
for i, name in enumerate(table.column_names):
loc = table.locations[i] if len(table.locations) > 1 else table.locations[0]
print(f"{name:8s} {loc}")
Oslo GeoLocation(latitude=59.91, longitude=10.75)
Bergen GeoLocation(latitude=60.39, longitude=5.32)
Tromsø GeoLocation(latitude=69.65, longitude=18.96)
Computing distances between stations
[11]:
stations = {"Oslo": oslo, "Bergen": bergen, "Tromsø": tromsoe}
print("Pairwise distances (km):")
for a_name, a_loc in stations.items():
for b_name, b_loc in stations.items():
if a_name < b_name:
print(f" {a_name:8s} ↔ {b_name:8s} {a_loc.distance_to(b_loc):7.1f} km")
Pairwise distances (km):
Oslo ↔ Tromsø 1148.4 km
Bergen ↔ Oslo 305.1 km
Bergen ↔ Tromsø 1206.5 km
GeoArea — polygon regions
GeoArea wraps a shapely Polygon for representing bidding zones, countries, or catchment areas.shapely dependency:pip install timedatamodel[geo]
[12]:
try:
southern_norway = tdm.GeoArea.from_coordinates(
[
(57.5, 4.5),
(57.5, 12.5),
(63.0, 12.5),
(63.0, 4.5),
(57.5, 4.5),
],
name="Southern Norway",
)
print(f"Area name: {southern_norway.name}")
print(f"Bounds: {southern_norway.bounds}")
print(f"Centroid: {southern_norway.centroid}")
except ImportError:
southern_norway = None
print("shapely not installed — skip this cell or: pip install timedatamodel[geo]")
Area name: Southern Norway
Bounds: (4.5, 57.5, 12.5, 63.0)
Centroid: GeoLocation(latitude=60.25, longitude=8.5)
Bounding box — quick rectangular areas
GeoArea.bounding_box(center, radius_km) creates a rectangular area around a point.
[13]:
try:
oslo_region = tdm.GeoArea.bounding_box(oslo, radius_km=50, name="Oslo 50km")
print(f"Area: {oslo_region.name}")
print(f"Bounds: {oslo_region.bounds}")
print(f"Centroid: {oslo_region.centroid}")
except ImportError:
oslo_region = None
print("shapely not installed — skipping")
Area: Oslo 50km
Bounds: (9.853172269304775, 59.46033919704064, 11.646827730695227, 60.35966080295936)
Centroid: GeoLocation(latitude=59.91, longitude=10.75)
Spatial queries
GeoArea supports contains_point, contains_area, and overlaps.GeoLocation has a matching is_within(area) method.[14]:
try:
if southern_norway is not None:
for name, loc in stations.items():
inside = southern_norway.contains_point(loc)
also = loc.is_within(southern_norway)
print(f"{name:8s} in Southern Norway? {inside} (is_within: {also})")
except ImportError:
print("shapely not installed — skipping")
Oslo in Southern Norway? True (is_within: True)
Bergen in Southern Norway? True (is_within: True)
Tromsø in Southern Norway? False (is_within: False)
[15]:
try:
if southern_norway is not None and oslo_region is not None:
print(f"Southern Norway contains Oslo 50km region? {southern_norway.contains_area(oslo_region)}")
print(f"Oslo 50km region overlaps Southern Norway? {oslo_region.overlaps(southern_norway)}")
except ImportError:
print("shapely not installed — skipping")
Southern Norway contains Oslo 50km region? True
Oslo 50km region overlaps Southern Norway? True
Attaching a GeoArea to a TimeSeriesList
Both GeoLocation and GeoArea are accepted by the location parameter.
[16]:
try:
if southern_norway is not None:
ts_area = tdm.TimeSeriesList(
tdm.Frequency.PT1H,
timestamps=timestamps,
values=(120 + rng.normal(0, 15, 24)).tolist(),
name="wind_south",
unit="MW",
description="Aggregated wind production in southern Norway",
location=southern_norway,
)
ts_area
except ImportError:
print("shapely not installed — skipping")
Distance between areas and points
GeoArea.distance_to() accepts both GeoLocation and GeoArea.[17]:
try:
if southern_norway is not None:
print(f"Southern Norway → Oslo (contained): {southern_norway.distance_to(oslo):.1f} km")
print(f"Southern Norway → Tromsø (outside): {southern_norway.distance_to(tromsoe):.1f} km")
except ImportError:
print("shapely not installed — skipping")
Southern Norway → Oslo (contained): 0.0 km
Southern Norway → Tromsø (outside): 1151.7 km
Location survives serialization
GeoLocation round-trips through JSON.[18]:
json_str = ts_oslo.to_json()
ts_back = tdm.TimeSeriesList.from_json(json_str, location=oslo)
print(f"Name: {ts_back.name}")
print(f"Location: {ts_back.location}")
print(f"Match: {ts_back.location == oslo}")
Name: temperature
Location: GeoLocation(latitude=59.91, longitude=10.75)
Match: True
Summary
Feature |
API |
|---|---|
Point coordinates |
|
Distance |
|
Bearing |
|
Midpoint |
|
Offset |
|
Polygon regions |
|
Quick box |
|
Spatial queries |
|
Attach to series |
|
Next up: nb_10 demonstrates hierarchical time series — organising series into tree structures with bottom-up aggregation.