diff --git a/.gitignore b/.gitignore index 6ef35d7..45e6508 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ env/ __pycache__/ venv/ -.env \ No newline at end of file +.env +.venv \ No newline at end of file diff --git a/src/aeros_project/model.py b/src/aeros_project/model.py index d600c5d..6e9757d 100644 --- a/src/aeros_project/model.py +++ b/src/aeros_project/model.py @@ -1,12 +1,534 @@ -from sqlalchemy import JSON, UUID, Column, DateTime, Float, ForeignKey, Integer, String +from sqlalchemy import ( + JSON, + UUID, + TIMESTAMP, + Column, + DateTime, + Float, + ForeignKey, + Numeric, + Integer, + String, + Text +) from sqlalchemy.orm import relationship +from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.sql import func from src.database.core import Base from src.models import DefaultMixin +import uuid + class AerosProject(Base, DefaultMixin): __tablename__ = "rbd_ms_aeros_project" project_name = Column(String, nullable=False) aro_file_path = Column(String, nullable=False) + + + +# AF +class SimulationModel(Base): + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + # server_default=text("uuid_generate_v4()") + ) + name = Column( + String, + # unique=True, + index=True, + nullable=True + ) + payload = Column(JSON, nullable=True) + gui_payload = Column(JSON, nullable=True) + + # realtionship + schemas = relationship("SimulationModelSchema", back_populates="model") # 1-to-many + assets = relationship("SimulationModelAsset", back_populates="model") # 1-to-many + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + +class SimulationTask(Base): + id = Column(String, primary_key=True, index=True) + model_id = Column(Integer, nullable=False) + status = Column(String, default="pending") # pending, running, completed, failed + progress = Column(Integer, default=0) + params = Column(JSON, nullable=True) + result = Column(JSON, nullable=True) + error = Column(String, nullable=True) + created_at = Column(String, nullable=False) + updated_at = Column(String, nullable=False) + + +# AF +class SimulationMasterType(Base): + __tablename__ = "simulation_master_types" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + # server_default=text("uuid_generate_v4()") + ) + name = Column(Text, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + model_asset_distributions = relationship("SimulationModelAssetDistribution", back_populates="type") # 1-to-many + model_schemas = relationship("SimulationModelSchema", back_populates="type") # 1-to-many + model_asset_repair_durations = relationship("SimulationModelAssetRepairDuration", back_populates="type") # many-to-1 + result_events = relationship("SimulationResultEvent", back_populates="type") # 1-to-many + + +class SimulationModelSchema(Base): + __tablename__ = "simulation_model_schemas" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + # server_default=text("uuid_generate_v4()") + ) + + simulation_model_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model.id"), + nullable=True + ) + # self reference + simulation_model_schemas_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model_schemas.id"), + nullable=True + ) + type_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_master_types.id"), + nullable=True + ) + + schemaId = Column(Text, nullable=True) # not a foreign key + name = Column(Text, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + model = relationship("SimulationModel", back_populates="schemas") # many-to-1 + type = relationship("SimulationMasterType", back_populates="model_schemas") # many-to-1 + assets = relationship("SimulationModelAsset", back_populates="model_schema") # 1-to-many + schema_results = relationship("SimulationResultSchema", back_populates="schema_model") # 1-to-many + + # SELF RELATION + parent = relationship( + "SimulationModelSchema", + remote_side=[id], + back_populates="children" + ) + + children = relationship( + "SimulationModelSchema", + back_populates="parent", + # cascade="all, delete" + ) + + + +class SimulationModelAsset(Base): + __tablename__ = "simulation_model_assets" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + # server_default=text("uuid_generate_v4()") + ) + assetId = Column(Text, nullable=True) # not a foreign key + name = Column(Text, nullable=True) + simulation_model_schemas_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model_schemas.id"), + nullable=True + ) + simulation_model_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model.id"), + nullable=True + ) + + max_capacity = Column(Numeric, nullable=True) + design_capacity = Column(Numeric, nullable=True) + buffer_duration = Column(Numeric, nullable=True) + transition_delay = Column(Numeric, nullable=True) + initial_age = Column(Numeric, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + model = relationship("SimulationModel", back_populates="assets") # many-to-1 + asset_distribution = relationship( + "SimulationModelAssetDistribution", + back_populates="asset", + uselist=False # 1-to-1 + ) + asset_repair_duration = relationship( + "SimulationModelAssetRepairDuration", + back_populates="asset", + uselist=False # 1-to-1 + ) + asset_overhaul = relationship( + "SimulationModelAssetOverhaul", + back_populates="asset", + uselist=False # 1-to-1 + ) + asset_pm = relationship( + "SimulationModelAssetPm", + back_populates="asset", + uselist=False # 1-to-1 + ) + model_schema = relationship( + "SimulationModelSchema", + back_populates="assets" # many-to-1 + ) + asset_results = relationship( + "SimulationResultAsset", + back_populates="asset_model" # 1-to-many + ) + + +class SimulationModelAssetDistribution(Base): + __tablename__ = "simulation_model_assets_distributions" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + # server_default=text("uuid_generate_v4()") + ) + simulation_model_assets_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model_assets.id"), + nullable=True + ) + type_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_master_types.id"), + nullable=True + ) + shape = Column(Numeric, nullable=True) + scale = Column(Numeric, nullable=True) + mean = Column(Numeric, nullable=True) + std_dev = Column(Numeric, nullable=True) + eta = Column(Numeric, nullable=True) + beta = Column(Numeric, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + asset = relationship("SimulationModelAsset", back_populates="asset_distribution") # 1-to-1 + type = relationship("SimulationMasterType", back_populates="model_asset_distributions") # many-to-1 + + +class SimulationModelAssetRepairDuration(Base): + __tablename__ = "simulation_model_assets_repair_durations" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + # server_default=text("uuid_generate_v4()") + ) + simulation_model_assets_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model_assets.id"), + nullable=True + ) + type_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_master_types.id"), + nullable=True + ) + mean = Column(Numeric, nullable=True) + std_dev = Column(Numeric, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + asset = relationship("SimulationModelAsset", back_populates="asset_repair_duration") # 1-to-1 + type = relationship("SimulationMasterType", back_populates="model_asset_repair_durations") # many-to-1 + + +class SimulationModelAssetOverhaul(Base): + __tablename__ = "simulation_model_assets_overhauls" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + # server_default=text("uuid_generate_v4()") + ) + simulation_model_assets_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model_assets.id"), + nullable=True + ) + interval = Column(Numeric, nullable=True) + duration = Column(Numeric, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + asset = relationship("SimulationModelAsset", back_populates="asset_overhaul") # 1-to-1 + + +class SimulationModelAssetPm(Base): + __tablename__ = "simulation_model_assets_pms" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + # server_default=text("uuid_generate_v4()") + ) + simulation_model_assets_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model_assets.id"), + nullable=True + ) + interval = Column(Numeric, nullable=True) + duration = Column(Numeric, nullable=True) + restoration_factor = Column(Numeric, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + asset = relationship("SimulationModelAsset", back_populates="asset_pm") # 1-to-1 + + + + +class SimulationResult(Base): + __tablename__ = "simulation_results" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + ) + + production_availability = Column(Numeric, nullable=True) + simulation_duration = Column(Numeric, nullable=True) + simulation_replications = Column(Numeric, nullable=True) + system_availability = Column(Numeric, nullable=True) + system_downtime = Column(Numeric, nullable=True) + system_failures = Column(Numeric, nullable=True) + system_uptime = Column(Numeric, nullable=True) + total_demand = Column(Numeric, nullable=True) + total_productions = Column(Numeric, nullable=True) + + system_down_causes = Column(JSONB, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + schemas = relationship("SimulationResultSchema", back_populates="result") # 1-to-many + assets = relationship("SimulationResultAsset", back_populates="result") # 1-to-many + events = relationship("SimulationResultEvent", back_populates="result") # 1-to-many + # down_causes = relationship("SimulationResultDownCauses", back_populates="result") # 1-to-many + + +class SimulationResultAsset(Base): + __tablename__ = "simulation_result_assets" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + ) + + simulation_results_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_results.id"), + nullable=True + ) + + simulation_model_assets_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model_assets.id"), + nullable=True + ) + + assetId = Column(Text, nullable=True) # "node_blablabla" + name = Column(Text, nullable=True) + + availability = Column(Numeric, nullable=True) + contribution_factor = Column(Numeric, nullable=True) + downtime = Column(Numeric, nullable=True) + uptime = Column(Numeric, nullable=True) + failures = Column(Numeric, nullable=True) + total_downtime_concurrent_with_system = Column(Numeric, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + result = relationship("SimulationResult", back_populates="assets") # many-to-1 + events = relationship("SimulationResultEvent", back_populates="asset") # 1-to-many + asset_model = relationship("SimulationModelAsset", back_populates="asset_results") # many-to-1 + + + +class SimulationResultSchema(Base): + __tablename__ = "simulation_result_schemas" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + ) + + simulation_results_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_results.id"), + nullable=True + ) + + simulation_model_schemas_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_model_schemas.id"), + nullable=True + ) + + schemaId = Column(Text, nullable=True) + name = Column(Text, nullable=True) + + availability = Column(Numeric, nullable=True) + downtime = Column(Numeric, nullable=True) + uptime = Column(Numeric, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + result = relationship("SimulationResult", back_populates="schemas") # many-to-1 + events = relationship("SimulationResultEvent", back_populates="schema") # 1-to-many + schema_model = relationship("SimulationModelSchema", back_populates="schema_results") # many-to-1 + + +class SimulationResultEvent(Base): + __tablename__ = "simulation_result_events" + + id = Column( + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + ) + + simulation_result_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_results.id"), + nullable=True + ) + + simulation_result_assets_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_result_assets.id"), + nullable=True + ) + + simulation_result_schemas_id = Column( + UUID(as_uuid=True), + ForeignKey("simulation_result_schemas.id"), + nullable=True + ) + + type_id = Column( + UUID(as_uuid=True), + # asumsi ada tabel ini + ForeignKey("simulation_master_types.id"), + nullable=True + ) + + cause = Column(Text, nullable=True) + state = Column(Text, nullable=True) + + load_factor = Column(Numeric, nullable=True) + start_time = Column(Numeric, nullable=True) + end_time = Column(Numeric, nullable=True) + + created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) + updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) + deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + # relationships + result = relationship("SimulationResult", back_populates="events") + asset = relationship("SimulationResultAsset", back_populates="events") + schema = relationship("SimulationResultSchema", back_populates="events") + type = relationship("SimulationMasterType", back_populates="result_events") + + +# class SimulationResultDownCause(Base): +# __tablename__ = "simulation_result_down_causes" + +# id = Column( +# UUID(as_uuid=True), +# primary_key=True, +# index=True, +# default=uuid.uuid4, +# ) + +# simulation_results_id = Column( +# UUID(as_uuid=True), +# ForeignKey("simulation_results.id"), +# nullable=True +# ) + +# availability = Column(Numeric, nullable=True) +# downtime = Column(Numeric, nullable=True) +# uptime = Column(Numeric, nullable=True) +# failures = Column(Numeric, nullable=True) + +# created_at = Column(TIMESTAMP(timezone=True), server_default=func.now()) +# updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()) +# deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) + + + + + diff --git a/src/aeros_project/router.py b/src/aeros_project/router.py index cbd9d5b..e086c12 100644 --- a/src/aeros_project/router.py +++ b/src/aeros_project/router.py @@ -1,7 +1,19 @@ +import json import os from typing import List, Optional - -from fastapi import APIRouter, HTTPException, Response, status, File, UploadFile, Form +from uuid import UUID + +from fastapi import ( + APIRouter, + HTTPException, + Response, + status, + File, + UploadFile, + Form, + Request, + Body +) from fastapi.responses import StreamingResponse import httpx from sqlalchemy import select @@ -14,7 +26,7 @@ from src.database.service import CommonParameters from src.models import StandardResponse from src.aeros_utils import aeros_post from .schema import AerosProjectInput, AerosMetadata, OverhaulScheduleCreate, OverhaulScheduleUpdate -from .service import import_aro_project, fetch_aro_record, reset_project, create, update, delete +from .service import get_simulation_model_by_id_service, get_simulation_result_service, import_aro_project, fetch_aro_record, import_simulation_model_service, import_simulation_result_service, reset_project, create, update, delete router = APIRouter() @@ -157,3 +169,357 @@ async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id: data=None, message="Data deleted successfully", ) + + +# import & store simulation ramp model into db (use this: input .ramp/.json file) +@router.post("/import-simulation-model/file") +async def import_simulation_model_file( + db: DbSession, + file: UploadFile = File(...) +): + + try: + + allowed_extensions = ( + ".ramp", + ".json" + ) + + if not file.filename.endswith( + allowed_extensions + ): + raise HTTPException( + 400, + "Only .ramp and .json files are supported" + ) + + contents = await file.read() + + try: + data = json.loads( + contents.decode("utf-8") + ) + + except Exception: + raise HTTPException( + 400, + "Invalid JSON inside uploaded file" + ) + + # ===================================== + # VALIDATION + # ===================================== + validate_simulation_payload(data) + + # ===================================== + # IMPORT + # ===================================== + result = await import_simulation_model_service( + db=db, + name=None, + data=data + ) + + return StandardResponse( + data=result, + message="File imported successfully" + ) + + except HTTPException: + raise + + except Exception as e: + raise HTTPException( + 500, + f"Import failed: {str(e)}" + ) + +# helper: validate simulation model payload from raw json +def validate_simulation_model_payload(data: dict): + + if "rbd_root" not in data: + raise HTTPException( + 400, + "Invalid structure: 'rbd_root' missing" + ) + + if "assets" not in data: + raise HTTPException( + 400, + "Invalid structure: 'assets' missing" + ) + + if "schemas" not in data: + raise HTTPException( + 400, + "Invalid structure: 'schemas' missing" + ) + + +# import & store simulation ramp model into db (use this: input raw json) +@router.post("/import-simulation-model/json") +async def import_simulation_model_json( + db: DbSession, + payload: dict = Body(...) +): + + try: + + # ===================================== + # VALIDATION + # ===================================== + validate_simulation_model_payload(payload) + + # ===================================== + # IMPORT + # ===================================== + result = await import_simulation_model_service( + db=db, + name=None, + data=payload + ) + + return StandardResponse( + data=result, + message="JSON imported successfully" + ) + + except HTTPException: + raise + + except Exception as e: + raise HTTPException( + 500, + f"Import failed: {str(e)}" + ) + + +# get simulation model by id (USE THIS) +@router.get("/simulation-model-by-id/{simulation_model_id}") +async def get_simulation_model_by_id(db: DbSession, simulation_model_id: str): + + result = await ( + get_simulation_model_by_id_service(db, simulation_model_id) + ) + + if not result: + + raise HTTPException( + status_code=404, + detail="Simulation model not found" + ) + + + return StandardResponse( + data=result, + message="Data showed successfully", + ) + + +# helper: validate simulation result payload from raw json +def validate_simulation_result_payload(data: dict): + + required_fields = [ + "asset_results", + "schema_results", + "system_events", + ] + + for field in required_fields: + + if field not in data: + raise HTTPException( + status_code=400, + detail=f"Invalid structure: '{field}' missing" + ) + + +# import & store simulation result into db (use this: input .json file) +@router.post("/import-simulation-result/file") +async def import_simulation_result_file( + db: DbSession, + file: UploadFile = File(...) +): + + try: + + # ===================================== + # FILE VALIDATION + # ===================================== + if not file.filename.endswith(".json"): + + raise HTTPException( + status_code=400, + detail="Only .json files are supported" + ) + + contents = await file.read() + + # ===================================== + # PARSE JSON + # ===================================== + try: + + data = json.loads( + contents.decode("utf-8") + ) + + except Exception: + + raise HTTPException( + status_code=400, + detail="Invalid JSON inside uploaded file" + ) + + # ===================================== + # VALIDATION + # ===================================== + validate_simulation_result_payload( + data + ) + + # ===================================== + # IMPORT + # ===================================== + # result = + await import_simulation_result_service( + db, + data + ) + + # ===================================== + # RESPONSE + # ===================================== + # final_result = { + # "status": "success", + + # "simulation_result_id": result[ + # "result_id" + # ], + + # "asset_imported": result[ + # "asset_count" + # ], + + # "schema_imported": result[ + # "schema_count" + # ], + + # "system_event_imported": result[ + # "system_event_count" + # ], + # } + + return StandardResponse( + data=None, + message="Data stored successfully", + ) + + except HTTPException: + raise + + except Exception as e: + + raise HTTPException( + status_code=500, + detail={ + "message": str(e), + "type": type(e).__name__, + } + ) + + +# import & store simulation result into db (use this: input raw json) +@router.post("/import-simulation-result/json") +async def import_simulation_result_json( + db: DbSession, + data: dict +): + + try: + + # ===================================== + # VALIDATION + # ===================================== + validate_simulation_result_payload( + data + ) + + # ===================================== + # IMPORT + # ===================================== + # result = + await import_simulation_result_service( + db, + data + ) + + # ===================================== + # RESPONSE + # ===================================== + # final_result = { + # "status": "success", + + # "simulation_result_id": result[ + # "result_id" + # ], + + # "asset_imported": result[ + # "asset_count" + # ], + + # "schema_imported": result[ + # "schema_count" + # ], + + # "system_event_imported": result[ + # "system_event_count" + # ], + # } + + return StandardResponse( + data=None, + message="Data stored successfully", + ) + + + except HTTPException: + raise + + except Exception as e: + + raise HTTPException( + status_code=500, + detail={ + "message": str(e), + "type": type(e).__name__, + } + ) + + +# get simulation result by id +@router.get("/simulation-result-by-id/{simulation_result_id}") +async def get_simulation_result_by_id(db: DbSession, simulation_result_id: str): + + result = await ( + get_simulation_result_service( + db, + simulation_result_id + ) + ) + + if not result: + + raise HTTPException( + status_code=404, + detail="Simulation result not found" + ) + + # return result + return StandardResponse( + data=result, + message="Data showed successfully", + ) + + + diff --git a/src/aeros_project/service.py b/src/aeros_project/service.py index d347931..8d0df03 100644 --- a/src/aeros_project/service.py +++ b/src/aeros_project/service.py @@ -1,6 +1,8 @@ +from decimal import Decimal import os from typing import Optional import json +import uuid import httpx from fastapi import HTTPException, status from sqlalchemy import Delete, desc, Select, select, func @@ -17,7 +19,21 @@ from src.utils import sanitize_filename import clamd import io -from .model import AerosProject +from .model import ( + AerosProject, + SimulationMasterType, + SimulationModel, + SimulationModelAsset, + SimulationModelAssetDistribution, + SimulationModelAssetOverhaul, + SimulationModelAssetPm, + SimulationModelAssetRepairDuration, + SimulationModelSchema, + SimulationResult, + SimulationResultAsset, + SimulationResultEvent, + SimulationResultSchema +) from .schema import AerosProjectInput, OverhaulScheduleCreate, OverhaulScheduleUpdate import asyncio ALLOWED_EXTENSIONS = {".aro"} @@ -276,3 +292,1355 @@ async def delete(*, db_session: DbSession, overhaul_schedule_id: str): # Placeholder for delete logic print(f"delete overhaul job called for id {overhaul_schedule_id}") pass + + +# helper: get or create type +async def get_or_create_type(db: DbSession, name: str): + result = await db.execute( + select(SimulationMasterType).where(SimulationMasterType.name == name) + ) + obj = result.scalar_one_or_none() + + if obj: + return obj + + obj = SimulationMasterType(name=name) + db.add(obj) + await db.flush() # biar dapet id + + return obj + +# helper: convert to float +def to_float(value): + if isinstance(value, Decimal): + return float(value) + return value + + +# store simulation model +async def import_simulation_model_service( + db: DbSession, + name: str, + data: dict +): + + asset_count = 0 + schema_count = 0 + + # ===================================== + # CREATE ROOT MODEL + # ===================================== + simulation_model = SimulationModel( + name=name, + + # raw original json + # payload=data, + + # full gui state + gui_payload=data.get("gui_state") + ) + + db.add(simulation_model) + + await db.flush() + + # ===================================== + # MEMORY MAPS + # ===================================== + asset_map = {} + schema_map = {} + + # ===================================== + # IMPORT ASSETS + # ===================================== + for asset in data.get("assets", []): + + asset_uuid = uuid.uuid4() + + db_asset = SimulationModelAsset( + + id=asset_uuid, + + simulation_model_id=simulation_model.id, + + assetId=asset.get("id"), + + name=asset.get("name"), + + max_capacity=asset.get( + "max_capacity" + ), + + design_capacity=asset.get( + "design_capacity" + ), + + buffer_duration=asset.get( + "buffer_duration" + ), + + transition_delay=asset.get( + "transition_delay" + ), + + initial_age=asset.get( + "initial_age" + ), + ) + + db.add(db_asset) + + asset_map[ + db_asset.assetId + ] = db_asset + + # ================================= + # NORMALIZE DISTRIBUTION + # ================================= + model = asset.get("model", {}) + + dist = asset.get("distribution") + + if not dist: + dist = model.get( + "distribution" + ) + + if not dist and model: + dist = model + + # ================================= + # DISTRIBUTION + # ================================= + if dist and dist.get("type"): + + dist_type = ( + await get_or_create_type( + db, + dist.get("type") + ) + ) + + db.add( + SimulationModelAssetDistribution( + + id=uuid.uuid4(), + + simulation_model_assets_id=( + asset_uuid + ), + + type_id=dist_type.id, + + eta=dist.get("eta"), + + beta=dist.get("beta"), + + mean=dist.get("mean"), + + std_dev=dist.get( + "std_dev" + ), + + shape=dist.get("shape"), + + scale=dist.get("scale"), + ) + ) + + # ================================= + # REPAIR + # ================================= + repair = asset.get( + "repair_duration" + ) + + if repair and repair.get("type"): + + repair_type = ( + await get_or_create_type( + db, + repair.get("type") + ) + ) + + db.add( + SimulationModelAssetRepairDuration( + + id=uuid.uuid4(), + + simulation_model_assets_id=( + asset_uuid + ), + + type_id=repair_type.id, + + mean=repair.get("mean"), + + std_dev=repair.get( + "std_dev" + ), + ) + ) + + # ================================= + # OVERHAUL + # ================================= + overhaul = asset.get( + "overhaul" + ) + + if overhaul: + + db.add( + SimulationModelAssetOverhaul( + + id=uuid.uuid4(), + + simulation_model_assets_id=( + asset_uuid + ), + + interval=overhaul.get( + "interval" + ), + + duration=overhaul.get( + "duration" + ), + ) + ) + + # ================================= + # PM + # ================================= + pm = asset.get("pm") + + if pm: + + db.add( + SimulationModelAssetPm( + + id=uuid.uuid4(), + + simulation_model_assets_id=( + asset_uuid + ), + + interval=pm.get( + "interval" + ), + + duration=pm.get( + "duration" + ), + + restoration_factor=( + pm.get( + "restoration_factor" + ) + ), + ) + ) + + asset_count += 1 + + # ===================================== + # PREPARE SCHEMAS + # ===================================== + schemas = data.get( + "schemas", + {} + ) + + rbd_root = data.get( + "rbd_root" + ) + + # inject root + if rbd_root: + + root_id = rbd_root.get( + "id", + "rbd_root" + ) + + if root_id not in schemas: + schemas[root_id] = rbd_root + + # ===================================== + # CREATE SCHEMAS + # ===================================== + for schema_key, schema_value in schemas.items(): + + structure = schema_value.get( + "structure", + {} + ) + + type_name = structure.get( + "type" + ) + + name = schema_value.get( + "name" + ) + + type_obj = ( + await get_or_create_type( + db, + type_name + ) + ) + + db_schema = SimulationModelSchema( + + id=uuid.uuid4(), + + simulation_model_id=( + simulation_model.id + ), + + schemaId=schema_key, + + name=name, + + type_id=type_obj.id, + ) + + db.add(db_schema) + + schema_map[ + schema_key + ] = db_schema + + schema_count += 1 + + # ===================================== + # LINK TREE + # ===================================== + for schema_key, schema_value in schemas.items(): + + parent = schema_map.get( + schema_key + ) + + if not parent: + continue + + children = ( + schema_value + .get("structure", {}) + .get("children", []) + ) + + for child in children: + + child_type = child.get( + "type" + ) + + ref_id = child.get( + "ref_id" + ) + + # ============================= + # SCHEMA CHILD + # ============================= + if child_type == "Schema": + + child_schema = ( + schema_map.get(ref_id) + ) + + if child_schema: + + child_schema.simulation_model_schemas_id = ( + parent.id + ) + + # ============================= + # ASSET CHILD + # ============================= + elif child_type == "Asset": + + asset_obj = asset_map.get( + ref_id + ) + + if asset_obj: + + asset_obj.simulation_model_schemas_id = ( + parent.id + ) + + # ===================================== + # COMMIT + # ===================================== + await db.commit() + + return { + + "simulation_model_id": ( + simulation_model.id + ), + + "schema_count": schema_count, + + "asset_count": asset_count, + } + + +# ========================================================= +# GET SIMULATION MODEL (USE THIS) +# ========================================================= +async def get_simulation_model_by_id_service( + db: DbSession, + simulation_model_id: str +): + + # ====================================== + # LOAD MODEL + # ====================================== + stmt = ( + select(SimulationModel) + .where( + SimulationModel.id == simulation_model_id + ) + .options( + + # ================================== + # SCHEMAS + # ================================== + selectinload( + SimulationModel.schemas + ).selectinload( + SimulationModelSchema.type + ), + + selectinload( + SimulationModel.schemas + ).selectinload( + SimulationModelSchema.assets + ), + + # ================================== + # ASSETS + # ================================== + selectinload( + SimulationModel.assets + ) + .selectinload( + SimulationModelAsset.asset_distribution + ) + .selectinload( + SimulationModelAssetDistribution.type + ), + + selectinload( + SimulationModel.assets + ) + .selectinload( + SimulationModelAsset.asset_repair_duration + ) + .selectinload( + SimulationModelAssetRepairDuration.type + ), + + selectinload( + SimulationModel.assets + ) + .selectinload( + SimulationModelAsset.asset_overhaul + ), + + selectinload( + SimulationModel.assets + ) + .selectinload( + SimulationModelAsset.asset_pm + ), + ) + ) + + result = await db.execute(stmt) + + simulation_model = ( + result.scalar_one_or_none() + ) + + if not simulation_model: + return None + + # ====================================== + # HELPERS + # ====================================== + def clean_nulls(data): + + if isinstance(data, dict): + + return { + k: clean_nulls(v) + for k, v in data.items() + if v is not None + } + + if isinstance(data, list): + + return [ + clean_nulls(v) + for v in data + ] + + return data + + # ====================================== + # SCHEMA MAP + # ====================================== + schema_map = {} + + for schema in simulation_model.schemas: + + schema_map[str(schema.id)] = schema + + # ====================================== + # CHILDREN MAP + # ====================================== + children_map = {} + + for schema in simulation_model.schemas: + + parent_id = ( + str( + schema.simulation_model_schemas_id + ) + if schema.simulation_model_schemas_id + else None + ) + + children_map.setdefault( + parent_id, + [] + ).append(schema) + + # ====================================== + # SERIALIZE ASSET + # ====================================== + def serialize_asset(asset): + + distribution = None + + if asset.asset_distribution: + + distribution = { + + "type": ( + asset.asset_distribution.type.name + if asset.asset_distribution.type + else None + ), + + "shape": to_float( + asset.asset_distribution.shape + ), + + "scale": to_float( + asset.asset_distribution.scale + ), + + "mean": to_float( + asset.asset_distribution.mean + ), + + "std_dev": to_float( + asset.asset_distribution.std_dev + ), + + "eta": to_float( + asset.asset_distribution.eta + ), + + "beta": to_float( + asset.asset_distribution.beta + ), + } + + repair_duration = None + + if asset.asset_repair_duration: + + repair_duration = { + + "type": ( + asset.asset_repair_duration.type.name + if asset.asset_repair_duration.type + else None + ), + + "mean": to_float( + asset.asset_repair_duration.mean + ), + + "std_dev": to_float( + asset.asset_repair_duration.std_dev + ), + } + + overhaul = None + + if asset.asset_overhaul: + + overhaul = { + + "interval": to_float( + asset.asset_overhaul.interval + ), + + "duration": to_float( + asset.asset_overhaul.duration + ), + } + + pm = None + + if asset.asset_pm: + + pm = { + + "interval": to_float( + asset.asset_pm.interval + ), + + "duration": to_float( + asset.asset_pm.duration + ), + + "restoration_factor": to_float( + asset.asset_pm.restoration_factor + ), + } + + asset_json = { + + "id": asset.assetId, + + "name": asset.name, + + "max_capacity": to_float( + asset.max_capacity + ), + + "design_capacity": to_float( + asset.design_capacity + ), + + "buffer_duration": to_float( + asset.buffer_duration + ), + + "transition_delay": to_float( + asset.transition_delay + ), + + "initial_age": to_float( + asset.initial_age + ), + + # normalized + "distribution": distribution, + + "repair_duration": repair_duration, + + "overhaul": overhaul, + + "pm": pm, + } + + return clean_nulls(asset_json) + + # ====================================== + # BUILD SCHEMA JSON + # ====================================== + schemas_json = {} + + root_schema = None + + for schema in simulation_model.schemas: + + if ( + not schema.simulation_model_schemas_id + ): + root_schema = schema + + children_json = [] + + # ================================== + # CHILD SCHEMAS + # ================================== + child_schemas = children_map.get( + str(schema.id), + [] + ) + + for child_schema in child_schemas: + + children_json.append({ + + "type": "Schema", + + "ref_id": ( + child_schema.schemaId + ) + }) + + # ================================== + # CHILD ASSETS + # ================================== + for asset in schema.assets: + + children_json.append({ + + "type": "Asset", + + "ref_id": asset.assetId + }) + + schemas_json[ + schema.schemaId + ] = { + + "name": schema.name, + + "structure": { + + "type": ( + schema.type.name + if schema.type + else None + ), + + "children": children_json + } + } + + # ====================================== + # ROOT + # ====================================== + rbd_root = None + + if root_schema: + + rbd_root = schemas_json.pop( + root_schema.schemaId + ) + + # ====================================== + # ASSETS + # ====================================== + assets_json = [] + + for asset in simulation_model.assets: + + assets_json.append( + serialize_asset(asset) + ) + + # ====================================== + # FINAL JSON + # ====================================== + final_json = { + + "id": str( + simulation_model.id + ), + + "name": simulation_model.name, + + "rbd_root": rbd_root, + + "schemas": schemas_json, + + "assets": assets_json, + + # restore gui state + "gui_state": ( + simulation_model.gui_payload + ), + } + + return clean_nulls( + final_json + ) + + +# store simulation result into DB (JSON: input) +async def import_simulation_result_service(db: DbSession, data: dict): + + try: + # ========================================= + # CREATE MAIN RESULT + # ========================================= + result_obj = SimulationResult( + production_availability=data.get("production_availability"), + simulation_duration=data.get("simulation_duration"), + simulation_replications=data.get("simulation_replications"), + system_availability=data.get("system_availability"), + system_downtime=data.get("system_downtime"), + system_failures=data.get("system_failures"), + system_uptime=data.get("system_uptime"), + total_demand=data.get("total_demand"), + total_productions=data.get("total_productions"), + system_down_causes=data.get("system_down_causes"), + ) + + db.add(result_obj) + + await db.flush() + + # ========================================= + # PRELOAD MODEL ASSETS + # assetId -> model asset + # ========================================= + asset_model_result = await db.execute( + select(SimulationModelAsset) + ) + + asset_models = asset_model_result.scalars().all() + + asset_model_map = { + asset.assetId: asset + for asset in asset_models + } + + # ========================================= + # PRELOAD MODEL SCHEMAS + # schemaId -> model schema + # ========================================= + schema_model_result = await db.execute( + select(SimulationModelSchema) + ) + + schema_models = schema_model_result.scalars().all() + + schema_model_map = { + schema.schemaId: schema + for schema in schema_models + } + + # ========================================= + # STORE ASSET RESULTS + # ========================================= + asset_result_map = {} + + for asset in data.get("asset_results", []): + + model_asset = asset_model_map.get( + asset.get("id") + ) + + asset_result_obj = SimulationResultAsset( + simulation_results_id=result_obj.id, + + simulation_model_assets_id=( + model_asset.id + if model_asset + else None + ), + + assetId=asset.get("id"), + name=asset.get("name"), + + availability=asset.get("availability"), + contribution_factor=asset.get("contribution_factor"), + downtime=asset.get("downtime"), + uptime=asset.get("uptime"), + failures=asset.get("failures"), + + total_downtime_concurrent_with_system=( + asset.get( + "total_downtime_concurrent_with_system" + ) + ), + ) + + db.add(asset_result_obj) + + await db.flush() + + asset_result_map[ + asset.get("id") + ] = asset_result_obj + + # ===================================== + # EVENTS + # ===================================== + for event in asset.get("events", []): + + type_obj = None + + if event.get("event_type"): + type_obj = await get_or_create_type( + db, + event.get("event_type") + ) + + db.add( + SimulationResultEvent( + simulation_result_id=result_obj.id, + + simulation_result_assets_id=( + asset_result_obj.id + ), + + type_id=( + type_obj.id + if type_obj + else None + ), + + cause=event.get("cause"), + state=event.get("state"), + load_factor=event.get("load_factor"), + + start_time=event.get("start"), + end_time=event.get("end"), + ) + ) + + # ========================================= + # STORE SCHEMA RESULTS + # ========================================= + schema_result_map = {} + + for schema in data.get("schema_results", []): + + model_schema = schema_model_map.get( + schema.get("id") + ) + + schema_result_obj = SimulationResultSchema( + simulation_results_id=result_obj.id, + + simulation_model_schemas_id=( + model_schema.id + if model_schema + else None + ), + + schemaId=schema.get("id"), + name=schema.get("name"), + + availability=schema.get("availability"), + downtime=schema.get("downtime"), + uptime=schema.get("uptime"), + ) + + db.add(schema_result_obj) + + await db.flush() + + schema_result_map[ + schema.get("id") + ] = schema_result_obj + + # ===================================== + # EVENTS + # ===================================== + for event in schema.get("events", []): + + type_obj = None + + if event.get("event_type"): + type_obj = await get_or_create_type( + db, + event.get("event_type") + ) + + db.add( + SimulationResultEvent( + simulation_result_id=result_obj.id, + + simulation_result_schemas_id=( + schema_result_obj.id + ), + + type_id=( + type_obj.id + if type_obj + else None + ), + + cause=event.get("cause"), + state=event.get("state"), + load_factor=event.get("load_factor"), + + start_time=event.get("start"), + end_time=event.get("end"), + ) + ) + + # ========================================= + # STORE SYSTEM EVENTS + # ========================================= + for event in data.get("system_events", []): + + type_obj = None + + if event.get("event_type"): + type_obj = await get_or_create_type( + db, + event.get("event_type") + ) + + db.add( + SimulationResultEvent( + simulation_result_id=result_obj.id, + + type_id=( + type_obj.id + if type_obj + else None + ), + + cause=event.get("cause"), + state=event.get("state"), + load_factor=event.get("load_factor"), + + start_time=event.get("start"), + end_time=event.get("end"), + ) + ) + + # ========================================= + # SINGLE COMMIT + # ========================================= + await db.commit() + + return { + "result_id": str(result_obj.id), + "asset_count": len( + data.get("asset_results", []) + ), + "schema_count": len( + data.get("schema_results", []) + ), + "system_event_count": len( + data.get("system_events", []) + ), + } + + except Exception as e: + + # logger.exception( + # "Failed importing simulation result" + # ) + + await db.rollback() + + raise + + +# ========================================================= +# GET SIMULATION RESULT V1 (USE THIS) +# ========================================================= +async def get_simulation_result_service( + db: DbSession, + simulation_result_id: str +): + + # ===================================================== + # LOAD RESULT + # ===================================================== + stmt = ( + select(SimulationResult) + .where( + SimulationResult.id == simulation_result_id + ) + .options( + + # ========================================= + # ASSETS + # ========================================= + selectinload( + SimulationResult.assets + ), + + # ========================================= + # SCHEMAS + # ========================================= + selectinload( + SimulationResult.schemas + ), + + # ========================================= + # EVENTS + # ========================================= + selectinload( + SimulationResult.events + ).selectinload( + SimulationResultEvent.type + ), + ) + ) + + result = await db.execute(stmt) + + simulation_result = ( + result.scalar_one_or_none() + ) + + if not simulation_result: + return None + + # ===================================================== + # EVENT SERIALIZER + # ===================================================== + def serialize_event(event): + + return { + "event_type": ( + event.type.name + if event.type + else None + ), + + "cause": event.cause, + "state": event.state, + + "start": to_float( + event.start_time + ), + + "end": to_float( + event.end_time + ), + + "load_factor": to_float( + event.load_factor + ), + } + + # ===================================================== + # GROUP EVENTS + # ===================================================== + asset_event_map = {} + schema_event_map = {} + system_events = [] + + # stable ordering + events_sorted = sorted( + simulation_result.events, + key=lambda x: ( + x.start_time or 0 + ) + ) + + for event in events_sorted: + + event_json = serialize_event( + event + ) + + # ========================================= + # ASSET EVENT + # ========================================= + if event.simulation_result_assets_id: + + key = str( + event.simulation_result_assets_id + ) + + asset_event_map.setdefault( + key, + [] + ).append(event_json) + + # ========================================= + # SCHEMA EVENT + # ========================================= + elif event.simulation_result_schemas_id: + + key = str( + event.simulation_result_schemas_id + ) + + schema_event_map.setdefault( + key, + [] + ).append(event_json) + + # ========================================= + # SYSTEM EVENT + # ========================================= + else: + + system_events.append( + event_json + ) + + # ===================================================== + # ASSET SERIALIZER + # ===================================================== + def serialize_asset(asset): + + asset_json = { + + "id": asset.assetId, + + "name": asset.name, + + "availability": to_float( + asset.availability + ), + + "contribution_factor": to_float( + asset.contribution_factor + ), + + "downtime": to_float( + asset.downtime + ), + + "uptime": to_float( + asset.uptime + ), + + "failures": to_float( + asset.failures + ), + + "total_downtime_concurrent_with_system": ( + to_float( + asset.total_downtime_concurrent_with_system + ) + ), + + "events": asset_event_map.get( + str(asset.id), + [] + ), + } + + # remove nulls + return { + k: v + for k, v in asset_json.items() + if v is not None + } + + # ===================================================== + # SCHEMA SERIALIZER + # ===================================================== + def serialize_schema(schema): + + schema_json = { + + "id": schema.schemaId, + + "name": schema.name, + + "availability": to_float( + schema.availability + ), + + "downtime": to_float( + schema.downtime + ), + + "uptime": to_float( + schema.uptime + ), + + "events": schema_event_map.get( + str(schema.id), + [] + ), + } + + return { + k: v + for k, v in schema_json.items() + if v is not None + } + + # ===================================================== + # BUILD FINAL JSON + # ===================================================== + final_json = { + + "id": str( + simulation_result.id + ), + + "production_availability": to_float( + simulation_result.production_availability + ), + + "simulation_duration": to_float( + simulation_result.simulation_duration + ), + + "simulation_replications": to_float( + simulation_result.simulation_replications + ), + + "system_availability": to_float( + simulation_result.system_availability + ), + + "system_downtime": to_float( + simulation_result.system_downtime + ), + + "system_failures": to_float( + simulation_result.system_failures + ), + + "system_uptime": to_float( + simulation_result.system_uptime + ), + + "total_demand": to_float( + simulation_result.total_demand + ), + + "total_productions": to_float( + simulation_result.total_productions + ), + + # ========================================= + # JSONB + # ========================================= + "system_down_causes": ( + simulation_result.system_down_causes + or [] + ), + + # ========================================= + # RESULTS + # ========================================= + "asset_results": [ + serialize_asset(asset) + for asset in simulation_result.assets + ], + + "schema_results": [ + serialize_schema(schema) + for schema in simulation_result.schemas + ], + + "system_events": system_events, + } + + # remove null root keys + final_json = { + k: v + for k, v in final_json.items() + if v is not None + } + + return final_json + + + + diff --git a/src/dashboard_model/service.py b/src/dashboard_model/service.py index f4fa6a3..cdf9557 100644 --- a/src/dashboard_model/service.py +++ b/src/dashboard_model/service.py @@ -15,7 +15,7 @@ from src.aeros_simulation.service import ( from src.auth.service import CurrentUser from src.database.core import DbSession from src.database.service import search_filter_sort_paginate -from tkinter.constants import E +# from tkinter.constants import E from src.dashboard_model.model import RBDMasterData from src.dashboard_model.schema_masterdata import RBDMasterDataUpdate