Skip to content

Use wkb element geography for coordinates #293

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c0ef9c3
Use WKBElement Geography for Coordinates
danielfeismann Mar 18, 2025
126c341
Merge branch 'main' into df(#287-WKBElement-Geography-for-Coordinates
danielfeismann Mar 18, 2025
f1bcf2c
poetry
danielfeismann Mar 18, 2025
b212581
rollback get lat and lon of coordinate
danielfeismann Mar 18, 2025
e36e8f3
make WeatherValue.time noon optional
danielfeismann Mar 18, 2025
44cb898
adapt lon and lat
danielfeismann Mar 19, 2025
ec6c396
adapt coordinate sql model and adapt tests
danielfeismann Mar 19, 2025
0f4f0e0
remove unnecessary things, fmt etc.
danielfeismann Mar 19, 2025
10b47d0
docker compose compatibility
danielfeismann Mar 19, 2025
956b564
skip tests if docker is required but not available
danielfeismann Mar 19, 2025
76a1268
adapt ci
danielfeismann Mar 19, 2025
1daedc9
ci
danielfeismann Mar 19, 2025
5b69a4b
start docker before
danielfeismann Mar 19, 2025
233e6ae
start docker
danielfeismann Mar 19, 2025
7fb9dcd
try docker desktop
danielfeismann Mar 19, 2025
7b19549
some more ci tests
danielfeismann Mar 19, 2025
1115862
even more ci try
danielfeismann Mar 19, 2025
e1eca76
even more ci tries
danielfeismann Mar 19, 2025
6c624fb
even more more ci tries
danielfeismann Mar 19, 2025
c51c1f8
next try
danielfeismann Mar 19, 2025
0655873
try another approach
danielfeismann Mar 19, 2025
da008e8
Merge branch 'main' into df(#287-WKBElement-Geography-for-Coordinates
danielfeismann Mar 19, 2025
7ee0955
fmt
danielfeismann Mar 19, 2025
ba22087
remove pytest marks
danielfeismann Mar 19, 2025
35cd753
Merge branch 'main' into df(#287-WKBElement-Geography-for-Coordinates
danielfeismann Mar 19, 2025
3e9bf5b
poetry
danielfeismann Mar 19, 2025
43297ba
test additional marks
danielfeismann Mar 19, 2025
5df6fe9
remove some marks
danielfeismann Mar 19, 2025
5b8c9a3
rollback fixture conftest
danielfeismann Mar 19, 2025
27e4e22
at mark and try check os again
danielfeismann Mar 19, 2025
4741015
linting
danielfeismann Mar 19, 2025
cb66fb5
change name for testing purpose
danielfeismann Mar 19, 2025
932c141
add mark also to this test...
danielfeismann Mar 19, 2025
474f32d
my tolerance level is quite high
danielfeismann Mar 19, 2025
5a0e5d6
fix container and database naming
danielfeismann Mar 21, 2025
2ad1c3f
rollback some changes, use Coordinate.from_hex
danielfeismann Mar 21, 2025
0e4f3ea
update coordinates model
danielfeismann Mar 21, 2025
710f642
poetry
danielfeismann Mar 21, 2025
b800f65
adapt test for coordinates
danielfeismann Mar 21, 2025
8dec2bc
fmt
danielfeismann Mar 21, 2025
9de5a8f
fix sql database creation
danielfeismann Mar 24, 2025
ddbae39
add pytest.mark
danielfeismann Mar 24, 2025
c4226dc
the other one of test_weighted_interpolation_coordinates
danielfeismann Mar 24, 2025
b25b1dc
pytest.mark necessary for both tests
danielfeismann Mar 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -88,6 +88,12 @@ jobs:
# run test suite
#----------------------------------------------
- name: Run tests
if: matrix.os != 'windows-latest'
run: |
source $VENV
pytest tests/
- name: Run tests (Windows - skip Docker tests)
if: matrix.os == 'windows-latest'
run: |
source $VENV
pytest tests/ -m "not docker_required"
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -19,7 +19,8 @@ All notable changes to this project will be documented in this file.
- Adapt to recent PSDM changes with regard to energy management systems [#164](https://github.com/ie3-institute/pypsdm/issue/164)
- Removed node from create method of energy management systems [#267](https://github.com/ie3-institute/pypsdm/issue/267)
- Changed default value for `cos_Phi` of create method of `ElectricVehicleChargingStations` to 1.0 [#272](https://github.com/ie3-institute/pypsdm/issue/272)
- Fix `create_grid` notebook to PSDM changes with respect to Em [#281](https://github.com/ie3-institute/pypsdm/issues/281)
- Fix `create_grid` notebook to PSDM changes with respect to Em [#281](https://github.com/ie3-institute/pypsdm/issues/281)
- Adapt Coordinate type to use WKBElement Geography [#287](https://github.com/ie3-institute/pypsdm/issues/287)

### Removed

6 changes: 5 additions & 1 deletion nbs/create_grid.ipynb
Original file line number Diff line number Diff line change
@@ -66,7 +66,11 @@
"source": [
"from pypsdm.models.input.create.participants import create_energy_management_systems\n",
"\n",
"em_uuids = [\"74d45b31-c35b-403b-96ac-5210754eb4de\",\"093e03ee-2d38-4e91-9f44-c71d92c07d89\",\"22af972d-19a8-415d-8c0a-50fa1238a7d9\"]\n",
"em_uuids = [\n",
" \"74d45b31-c35b-403b-96ac-5210754eb4de\",\n",
" \"093e03ee-2d38-4e91-9f44-c71d92c07d89\",\n",
" \"22af972d-19a8-415d-8c0a-50fa1238a7d9\",\n",
"]\n",
"\n",
"data_dict = {\n",
" \"uuid\": em_uuids,\n",
1,219 changes: 530 additions & 689 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ sqlmodel = "^0.0.24"
pyhocon = "^0.3.60"
psycopg2 = "^2.9.9"
numba = "^0.61.0"
geoalchemy2 = "^0.17.1"

[tool.poetry.group.dev.dependencies]
pytest = "^8.3.5"
56 changes: 40 additions & 16 deletions pypsdm/db/weather/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import binascii
from datetime import datetime
from typing import Optional
from typing import Any, ClassVar, Dict

from geoalchemy2 import Geometry, WKBElement
from shapely import Point
from shapely.wkb import loads
from shapely.geometry.base import BaseGeometry
from shapely.wkb import dumps, loads
from sqlalchemy import Column
from sqlmodel import Field, SQLModel


@@ -12,10 +15,8 @@ class WeatherValue(SQLModel, table=True):
Represents the ICON weather model.
"""

time: Optional[datetime] = Field(primary_key=True)
coordinate_id: Optional[int] = Field(
primary_key=True, default=None, foreign_key="coordinate.id"
)
time: datetime = Field(primary_key=True)
coordinate_id: int = Field(primary_key=True, foreign_key="coordinate.id")
aswdifd_s: float = Field()
aswdir_s: float = Field()
t2m: float = Field()
@@ -57,20 +58,37 @@ def name_mapping():


class Coordinate(SQLModel, table=True):
id: int = Field(primary_key=True)
coordinate: str = Field()
"""Represents a geographical coordinate."""

model_config: ClassVar[Dict[str, Any]] = {"arbitrary_types_allowed": True}

id: int = Field(default=None, primary_key=True)

# Use Geometry for storing WKB data (binary format)
coordinate: WKBElement = Field(
sa_column=Column(
Geometry(geometry_type="POINT", srid=4326, from_text="ST_GeomFromWKB")
)
)

def __init__(self, id: int, coordinate: Geometry):
self.id = id
self.coordinate = coordinate

def __eq__(self, other):
return self.id == other.id
return self.id == other.id if isinstance(other, Coordinate) else NotImplemented

def __hash__(self):
return hash(self.id)

@property
def point(self) -> Point:
wkb_str = self.coordinate
wkb_bytes = binascii.unhexlify(wkb_str)
return loads(wkb_bytes)
def point(self) -> BaseGeometry:
if isinstance(self.coordinate, WKBElement):
wkb_str = str(self.coordinate)
coordinate = bytes.fromhex(wkb_str)
else:
coordinate = self.coordinate
return loads(coordinate)

@property
def latitude(self) -> float:
@@ -89,6 +107,12 @@ def x(self) -> float:
return self.point.x

@staticmethod
def from_xy(id, x, y):
wkb = Point(x, y).wkb_hex
return Coordinate(id=id, coordinate=wkb)
def from_xy(id: int, x: float, y: float) -> "Coordinate":
point = Point(x, y)
wkb_data = dumps(point)
return Coordinate(id=id, coordinate=wkb_data)

@staticmethod
def from_hex(id: int, wkb_hex: str) -> "Coordinate":
bytes = binascii.unhexlify(wkb_hex)
return Coordinate(id=id, coordinate=bytes)
109 changes: 109 additions & 0 deletions tests/db/weather/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import subprocess
import time

import pytest
import sqlalchemy
from sqlalchemy import text
from sqlmodel import Session, SQLModel, create_engine


@pytest.fixture(scope="session")
@pytest.mark.docker_required
def docker_postgres():
"""Start a PostgreSQL container for testing."""
container_name = "test_weather"

# Check if the container already exists
check_cmd = [
"docker",
"ps",
"-a",
"--filter",
f"name={container_name}",
"--format",
"{{.Names}}",
]
result = subprocess.run(check_cmd, capture_output=True, text=True)

if container_name in result.stdout:
# Remove existing container
subprocess.run(["docker", "rm", "-f", container_name], check=True)

# Start a new container
run_cmd = [
"docker",
"run",
"--name",
container_name,
"-e",
"POSTGRES_USER=postgres",
"-e",
"POSTGRES_PASSWORD=postgres",
"-e",
"POSTGRES_DB=test_weather",
"-p",
"5433:5432",
"-d",
"postgis/postgis:17-3.4",
]
subprocess.run(run_cmd, check=True)

# Wait for the database to be ready
time.sleep(5)

yield "postgresql://postgres:postgres@localhost:5433/test_weather"

# Cleanup
subprocess.run(["docker", "rm", "-f", container_name], check=True)


@pytest.fixture(scope="session")
@pytest.mark.docker_required
def db_engine(docker_postgres):
"""Create a database engine connected to the Docker PostgreSQL instance."""
engine = create_engine(docker_postgres)

# Try connecting multiple times with increasing delays
max_retries = 5
retry_delay = 1 # seconds

for attempt in range(max_retries):
try:
# Enable PostGIS
with engine.connect() as conn:
conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis;"))
conn.commit()

SQLModel.metadata.create_all(engine)
break

except sqlalchemy.exc.OperationalError as e:
if attempt < max_retries - 1:
print(
f"Connection attempt {attempt + 1} failed. Retrying in {retry_delay}s..."
)
time.sleep(retry_delay)
retry_delay *= 2 # Exponential backoff
else:
raise Exception(
f"Failed to connect after {max_retries} attempts"
) from e

return engine


@pytest.fixture
@pytest.mark.docker_required
def db_session(db_engine):
"""Create a new database session for a test."""
with Session(db_engine) as session:
yield session
session.rollback() # Rollback after each test


@pytest.fixture(scope="function", autouse=True)
@pytest.mark.docker_required
def reset_db(db_engine):
"""Drop and recreate all tables before each test."""
SQLModel.metadata.drop_all(db_engine)
SQLModel.metadata.create_all(db_engine)
22 changes: 22 additions & 0 deletions tests/db/weather/docker_compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: '3'

services:
postgis:
image: postgis/postgis:17-3.4
container_name: test_weather
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=test_weather
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5

volumes:
pgdata:
35 changes: 19 additions & 16 deletions tests/db/weather/test_proxy.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,67 @@
import math

import pytest

from pypsdm.db.weather.models import Coordinate
from pypsdm.db.weather.utils import weighted_interpolation_coordinates


@pytest.mark.docker_required
def test_weighted_interpolation_coordinates():
nearest_coordinates = [
(
Coordinate(
Coordinate.from_hex(
id=13890,
coordinate="0101000020E610000000000000008026400000000000E04A40",
wkb_hex="0101000020E610000000000000008026400000000000E04A40",
),
15336.967985536437,
),
(
Coordinate(
Coordinate.from_hex(
id=13891,
coordinate="0101000020E610000000000000000027400000000000E04A40",
wkb_hex="0101000020E610000000000000000027400000000000E04A40",
),
15741.508288878984,
),
(
Coordinate(
Coordinate.from_hex(
id=14079,
coordinate="0101000020E610000000000000008026400000000000C04A40",
wkb_hex="0101000020E610000000000000008026400000000000C04A40",
),
16606.405396805483,
),
(
Coordinate(
Coordinate.from_hex(
id=14080,
coordinate="0101000020E610000000000000000027400000000000C04A40",
wkb_hex="0101000020E610000000000000000027400000000000C04A40",
),
16982.92952636213,
),
(
Coordinate(
Coordinate.from_hex(
id=13889,
coordinate="0101000020E610000000000000000026400000000000E04A40",
wkb_hex="0101000020E610000000000000000026400000000000E04A40",
),
27650.80452994915,
),
(
Coordinate(
Coordinate.from_hex(
id=13892,
coordinate="0101000020E610000000000000008027400000000000E04A40",
wkb_hex="0101000020E610000000000000008027400000000000E04A40",
),
28324.62399357631,
),
(
Coordinate(
Coordinate.from_hex(
id=14078,
coordinate="0101000020E610000000000000000026400000000000C04A40",
wkb_hex="0101000020E610000000000000000026400000000000C04A40",
),
28429.952194089583,
),
(
Coordinate(
Coordinate.from_hex(
id=14081,
coordinate="0101000020E610000000000000008027400000000000C04A40",
wkb_hex="0101000020E610000000000000008027400000000000C04A40",
),
29089.579082292785,
),
3 changes: 3 additions & 0 deletions tests/db/weather/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import pytest

from pypsdm.db.weather.models import Coordinate
from pypsdm.db.weather.utils import weighted_interpolation_coordinates


@pytest.mark.docker_required
def test_weighted_interpolation_coordinates():
target = (0, 0)

Loading