From 35f7a3e6e3be0f2b9aaac09085ecf79d660ed8ce Mon Sep 17 00:00:00 2001 From: Martijn Govers Date: Thu, 9 Nov 2023 15:30:23 +0100 Subject: [PATCH] working powerflow calculation on pydantic loaded cim data Signed-off-by: Martijn Govers --- src/pgm_service/app.py | 2 - src/pgm_service/pgm_powerflow/aux_models.py | 4 +- src/pgm_service/pgm_powerflow/models.py | 10 +++- src/pgm_service/pgm_powerflow/router.py | 52 +++++++++++------ src/pgm_service/power_grid/aux_models.py | 33 ----------- .../power_grid/power_grid_model.py | 58 +++++++++++++++++++ src/pgm_service/power_grid/router.py | 35 ----------- 7 files changed, 102 insertions(+), 92 deletions(-) delete mode 100644 src/pgm_service/power_grid/aux_models.py create mode 100644 src/pgm_service/power_grid/power_grid_model.py delete mode 100644 src/pgm_service/power_grid/router.py diff --git a/src/pgm_service/app.py b/src/pgm_service/app.py index 28d2e55..040882f 100644 --- a/src/pgm_service/app.py +++ b/src/pgm_service/app.py @@ -3,13 +3,11 @@ from power_grid_model import initialize_array # TODO(mgovers) remove -from pgm_service.power_grid.router import router as pg_router from pgm_service.pgm_powerflow.router import router as pf_router app = FastAPI(title="API") -app.include_router(pg_router, prefix="/api") app.include_router(pf_router, prefix="/api") diff --git a/src/pgm_service/pgm_powerflow/aux_models.py b/src/pgm_service/pgm_powerflow/aux_models.py index 9ad9c73..fc410e9 100644 --- a/src/pgm_service/pgm_powerflow/aux_models.py +++ b/src/pgm_service/pgm_powerflow/aux_models.py @@ -1,8 +1,8 @@ from enum import Enum from typing import Optional -from pydantic import BaseModel, ConfigDict, Field from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field from pgm_service.pgm_powerflow.models import PGM_Powerflow @@ -31,4 +31,4 @@ class JobComplete(JobBase): details: Optional[str] = None created: datetime = Field(default_factory=datetime.now) finished: Optional[datetime] = None - result: PGM_Powerflow = None \ No newline at end of file + result: PGM_Powerflow = None diff --git a/src/pgm_service/pgm_powerflow/models.py b/src/pgm_service/pgm_powerflow/models.py index 71f2bfd..631993f 100644 --- a/src/pgm_service/pgm_powerflow/models.py +++ b/src/pgm_service/pgm_powerflow/models.py @@ -13,9 +13,7 @@ class StrEnum(str, Enum): pass -# Basic input data -class PGM_Powerflow(BaseModel): - model: Union[str, Grid] +class PGM_PowerflowCalculationArgs(BaseModel): symmetric: bool = True error_tolerance: float = 1e-8 max_iterations: int = 20 @@ -24,3 +22,9 @@ class PGM_Powerflow(BaseModel): # threading: int = -1 output_component_types: Optional[List[str]] = None # continue_on_batch_error: bool = False + + +# Basic input data +class PGM_Powerflow(BaseModel): + model: Grid + calculation_args: PGM_PowerflowCalculationArgs diff --git a/src/pgm_service/pgm_powerflow/router.py b/src/pgm_service/pgm_powerflow/router.py index fdbebe2..d799def 100644 --- a/src/pgm_service/pgm_powerflow/router.py +++ b/src/pgm_service/pgm_powerflow/router.py @@ -1,39 +1,57 @@ -from fastapi import APIRouter +from datetime import datetime +from typing import Dict +from uuid import uuid4 +from fastapi import APIRouter, BackgroundTasks -from power_grid_model import PowerGridModel - -from pgm_service.pgm_powerflow.aux_models import JobComplete +from pgm_service.pgm_powerflow.aux_models import JobComplete, Status from pgm_service.pgm_powerflow.models import PGM_Powerflow -from pgm_service.power_grid.models import Grid +from pgm_service.power_grid.power_grid_model import calculate_powerflow router = APIRouter(prefix="/pgm_powerflow", tags=["Powerflow"]) +JOBS: Dict[str, JobComplete] = {} + @router.get("/") async def get_all_powerflow_calculation() -> list[str]: - """Returns list of existing powerflow_calculation IDs""" # XXX should this return url/uris? - raise NotImplementedError() # TODO this should look up ids from DB and return them + """Returns list of existing powerflow_calculation IDs""" + return list(JOBS.keys()) + + +async def _calculate(job: JobComplete): + job.status = Status.RUNNING + + try: + grid = job.input.model + pf_kwargs = job.input.calculation_args.model_dump() + + await calculate_powerflow(grid=grid, pf_kwargs=pf_kwargs) + + job.finished = datetime.now() + job.status = Status.SUCCESS + except Exception as e: + job.status = Status.FAILED + job.details = e @router.post("/") async def new_powerflow_calculation( resource: PGM_Powerflow, + background_tasks: BackgroundTasks, ) -> JobComplete: # TODO should be wrapped in jonb - # raise NotImplementedError() # TODO This should create a new job entry in DB - assert isinstance(resource.model, Grid) - model = PowerGridModel(input_data={}, system_frequency=resource.model.system_frequency) - pf_args = resource.model_dump() - del pf_args["model"] - calculation_result = model.calculate_power_flow(**pf_args) - print(calculation_result) - job = JobComplete(id="test", input=resource) + _id = str(uuid4()) + JOBS[_id] = JobComplete(id=_id, input=resource) + job = JOBS[_id] + + background_tasks.add_task(_calculate, job=job) + return job @router.get("/{id}") async def get_powerflow_calculation(id: str) -> JobComplete: - raise NotImplementedError() # TODO fetch Job with ID from the DB + return JOBS[id] # @router.put("/{id}") @@ -44,4 +62,4 @@ async def get_powerflow_calculation(id: str) -> JobComplete: @router.delete("/{id}") async def delete_powerflow_calculation(id: str) -> JobComplete: - raise NotImplementedError() # TODO this should fetch the hob if possible then delete and return it + return JOBS.pop(id) diff --git a/src/pgm_service/power_grid/aux_models.py b/src/pgm_service/power_grid/aux_models.py deleted file mode 100644 index 92fe4f7..0000000 --- a/src/pgm_service/power_grid/aux_models.py +++ /dev/null @@ -1,33 +0,0 @@ -from enum import Enum -from typing import Optional -from datetime import datetime - -from pydantic import BaseModel, ConfigDict, Field - -from pgm_service.power_grid.models import Grid - -base_config = ConfigDict(use_enum_values=True) # TODO Check if json_encoders = {datetime: lambda dt: dt.strftime("%Y-%m-%dT%H:%M:%SZ")} is needed as it will be deprecated in the future - - -# XXX StrEnum is available from enum in python 3.11 -class StrEnum(str, Enum): - pass - -class Status(StrEnum): - CREATED = "created" - RUNNING = "running" - SUCCESS = "success" - FAILED = "failed" - -class JobBase(BaseModel): - model_config = base_config - id: str # TODO add default generator - input: Grid - - -class JobComplete(JobBase): - status: Status = Status.CREATED - details: Optional[str] = None - created: datetime = Field(default_factory=datetime.now) - finished: Optional[datetime] = None - result: Grid = None diff --git a/src/pgm_service/power_grid/power_grid_model.py b/src/pgm_service/power_grid/power_grid_model.py new file mode 100644 index 0000000..ae14660 --- /dev/null +++ b/src/pgm_service/power_grid/power_grid_model.py @@ -0,0 +1,58 @@ +import glob +import os +from pathlib import Path +from typing import Dict, Any +import requests +import cimpy +from power_grid_model import PowerGridModel + +from pgm_service.power_grid.models import Grid +from pgm_service.power_grid.cgmes_pgm_converter import System as CGMESToPGMConverter + + +def download_data(url): + def download_grid_data(name, url): + with open(name, 'wb') as out_file: + content = requests.get(url, stream=True).content + out_file.write(content) + + url = 'https://raw.githubusercontent.com/dpsim-simulator/cim-grid-data/master/BasicGrids/NEPLAN/Slack_Load_Line_Sample/' + filename = 'Rootnet_FULL_NE_19J18h' + + download_grid_data(filename+'_EQ.xml', url + filename + '_EQ.xml') + download_grid_data(filename+'_TP.xml', url + filename + '_TP.xml') + download_grid_data(filename+'_SV.xml', url + filename + '_SV.xml') + + files = glob.glob(filename+'_*.xml') + + print('CGMES files downloaded:') + print(files) + + this_file_folder = Path(__file__).parents[3] + p = str(this_file_folder) + xml_path = Path(p) + xml_files = [os.path.join(xml_path, filename+'_EQ.xml'), + os.path.join(xml_path, filename+'_TP.xml'), + os.path.join(xml_path, filename+'_SV.xml')] + + print(xml_files) + return xml_files + + +def create_model(grid: Grid): # TODO make async + xml_files = download_data(url=grid.input_data) + cgmes_data = cimpy.cim_import(xml_files, "cgmes_v2_4_15") + converter = CGMESToPGMConverter() + converter.load_cim_data(cgmes_data) + pgm_data = converter.create_pgm_input() + return PowerGridModel(input_data=pgm_data, system_frequency=grid.system_frequency) + + +def produce_output(grid: Grid, calculation_result: Any): + print(calculation_result) + + +async def calculate_powerflow(grid: Grid, pf_kwargs: Dict[str, Any]): + model = create_model(grid=grid) + calculation_result = model.calculate_power_flow(**pf_kwargs) + produce_output(grid, calculation_result) diff --git a/src/pgm_service/power_grid/router.py b/src/pgm_service/power_grid/router.py deleted file mode 100644 index d0b3098..0000000 --- a/src/pgm_service/power_grid/router.py +++ /dev/null @@ -1,35 +0,0 @@ -from fastapi import APIRouter -from pydantic import BaseModel - -from pgm_service.power_grid.aux_models import JobComplete -from pgm_service.power_grid.models import Grid - - -router = APIRouter(prefix="/power_grid", tags=["Grid"]) - - -@router.get("/") -async def get_all_grid_model() -> list[str]: - """Returns list of existing grid_model IDs""" # XXX should this return url/uris? - raise NotImplementedError() # TODO this should look up ids from DB and return them - - -@router.post("/") -async def new_grid_model( - resource: Grid, -) -> JobComplete: # TODO should be wrapped in jonb - # raise NotImplementedError() # TODO This should create a new job entry in DB - resource = Grid(**resource.model_dump()) - job = JobComplete(id="test",input=resource) - return job - - -@router.get("/{id}") -async def get_grid_model(id: str) -> JobComplete: - raise NotImplementedError() # TODO fetch Job with ID from the DB - - - -@router.delete("/{id}") -async def delete_grid_model(id: str) -> JobComplete: - raise NotImplementedError() # TODO this should fetch the hob if possible then delete and return it \ No newline at end of file