Skip to content

Commit 590aa06

Browse files
committed
feat: mcda impact analysis by perspective
1 parent dbd13b7 commit 590aa06

12 files changed

Lines changed: 802 additions & 219 deletions

File tree

demo_mcda.ipynb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
{
1111
"cell_type": "code",
12-
"execution_count": 1,
12+
"execution_count": null,
1313
"id": "62a72ef0",
1414
"metadata": {},
1515
"outputs": [
@@ -203,7 +203,8 @@
203203
"from src.sum_impact_assessment.utils.tools import load_living_labs_from_file, load_measures_from_file, load_kpis_from_file, load_kpi_groups_from_file\n",
204204
"from src.sum_impact_assessment.models import KPIImpactAnalyzer, PrometheeGaiaAnalyzer\n",
205205
"from src.sum_impact_assessment.schemas.mcda import Goal, Alternative\n",
206-
"from src.sum_impact_assessment.schemas.impact_analysis import MeasureImpactCoefficient, LivingLabImpactError, KPIGroupImpactOutput\n",
206+
"from src.sum_impact_assessment.schemas.impact_analysis import KPIGroupImpactOutput\n",
207+
"from src.sum_impact_assessment.schemas.mcda import MCDAAnalysisOutput\n",
207208
"\n",
208209
"\n",
209210
"from pandas import DataFrame, concat\n",
@@ -296,7 +297,15 @@
296297
"\n",
297298
"alts_df = DataFrame(alts_data)\n",
298299
"alts_df.to_csv(\n",
299-
" f\"{file_dir}/promethee_goals_alternatives_matrix.csv\", index=False)"
300+
" f\"{file_dir}/promethee_goals_alternatives_matrix.csv\", index=False)\n",
301+
"\n",
302+
"analyzer = PrometheeGaiaAnalyzer(\n",
303+
" goals=goals, alternatives=business_alternatives)\n",
304+
"\n",
305+
"# Get complete structured output with standardized keys\n",
306+
"print(\"Running complete PROMETHEE-GAIA analysis with structured output...\")\n",
307+
"mcda_output: MCDAAnalysisOutput = analyzer.run_analysis(\n",
308+
" run_visualizations=False)"
300309
]
301310
},
302311
{

src/sum_impact_assessment/api/routes/jobs.py

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""
22
Job management API routes.
33
"""
4-
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, status as api_status, Query
4+
from fastapi import APIRouter, Body, Depends, HTTPException, BackgroundTasks, status as api_status, Query
55
from sqlalchemy.orm import Session
66
from typing import List, Optional
77
from datetime import datetime
88
from ...database.connection import get_db, get_db_session
99
from ...repositories.job_repository import JobRepository
10-
from ...schemas.job import JobNameEnum, JobRunResponse
10+
from ...schemas.job import JobNameEnum, JobRunResponse, TriggerJobRequest
1111
from ...jobs import get_job_class
1212
from ...utils.logger import get_logger
1313

@@ -18,19 +18,21 @@
1818
router = APIRouter(prefix="/jobs")
1919

2020

21-
def execute_job_in_background(job_name: JobNameEnum, job_id: str):
21+
def execute_job_in_background(job_name: JobNameEnum, job_id: str, params: Optional[dict] = None):
2222
"""
2323
Execute a job in the background.
2424
2525
Args:
2626
job_name: The name of the job to execute
2727
job_id: The UUID of the job run to track
28+
params: Optional parameters for the job
2829
"""
2930
logger.info(
3031
f"Background task started for job",
3132
extra={
3233
"job_name": job_name.value,
33-
"job_id": job_id
34+
"job_id": job_id,
35+
"params": params
3436
}
3537
)
3638

@@ -39,7 +41,7 @@ def execute_job_in_background(job_name: JobNameEnum, job_id: str):
3941
try:
4042
# Get the job class and execute
4143
job_class = get_job_class(job_name)
42-
job_class.run(job_id=job_id, db=db)
44+
job_class.run(job_id=job_id, db=db, params=params)
4345
except Exception as e:
4446
logger.error(
4547
f"Background job execution failed",
@@ -56,6 +58,65 @@ def execute_job_in_background(job_name: JobNameEnum, job_id: str):
5658
def trigger_job(
5759
job_name: JobNameEnum,
5860
background_tasks: BackgroundTasks,
61+
request: Optional[TriggerJobRequest] = Body(
62+
None,
63+
openapi_examples={
64+
"no_params": {
65+
"summary": "No parameters",
66+
"description": "Run job with default settings",
67+
"value": None
68+
},
69+
"kpi_group_filter": {
70+
"summary": "Filter by KPI group",
71+
"description": "Run MCDA analysis for specific KPI group only",
72+
"value": {
73+
"params": {
74+
"kpi_group_id": "MCDA_GOALS"
75+
}
76+
}
77+
},
78+
"mcda_regulatory_perspective": {
79+
"summary": "MCDA Analysis for regulatory perspective",
80+
"description": "Run MCDA analysis with regulatory stakeholder weights",
81+
"value": {
82+
"params": {
83+
"kpi_group_id": "MCDA_GOALS",
84+
"perspective": "regulatory"
85+
}
86+
}
87+
},
88+
"mcda_pto_perspective": {
89+
"summary": "MCDA Analysis for PTO perspective",
90+
"description": "Run MCDA analysis with PTO stakeholder weights",
91+
"value": {
92+
"params": {
93+
"kpi_group_id": "MCDA_GOALS",
94+
"perspective": "pto"
95+
}
96+
}
97+
},
98+
"mcda_citizens_users_perspective": {
99+
"summary": "MCDA Analysis for citizens/users perspective",
100+
"description": "Run MCDA analysis with citizens/users stakeholder weights",
101+
"value": {
102+
"params": {
103+
"kpi_group_id": "MCDA_GOALS",
104+
"perspective": "citizens_users"
105+
}
106+
}
107+
},
108+
"mcda_nsm_providers_perspective": {
109+
"summary": "MCDA Analysis for NSM providers perspective",
110+
"description": "Run MCDA analysis with NSM providers stakeholder weights",
111+
"value": {
112+
"params": {
113+
"kpi_group_id": "MCDA_GOALS",
114+
"perspective": "nsm_providers"
115+
}
116+
}
117+
}
118+
}
119+
),
59120
db: Session = Depends(get_db)
60121
):
61122
"""
@@ -67,6 +128,7 @@ def trigger_job(
67128
Args:
68129
job_name: Name of the job to trigger (must be a valid JobNameEnum value)
69130
background_tasks: FastAPI background tasks manager
131+
request: Optional request body with job parameters
70132
db: Database session (injected)
71133
72134
Returns:
@@ -76,8 +138,9 @@ def trigger_job(
76138
HTTPException 404: If the job name is not found in the registry
77139
HTTPException 500: If there's a database error
78140
"""
141+
params = request.params if request else None
79142
logger.info(f"Job trigger request received",
80-
extra={"job_name": job_name.value})
143+
extra={"job_name": job_name.value, "params": params})
81144

82145
try:
83146
# Verify the job exists in the registry
@@ -91,21 +154,29 @@ def trigger_job(
91154
)
92155

93156
# Create a new job run with PENDING status
157+
# For MCDA analysis, append perspective to job name if provided
94158
job_repo = JobRepository(db)
95-
job_run = job_repo.create_job_run(job_name=job_name.value)
159+
actual_job_name = job_name.value
160+
161+
if job_name == JobNameEnum.MCDA_ANALYSIS and params and "perspective" in params:
162+
perspective = params["perspective"]
163+
actual_job_name = f"{job_name.value}_{perspective}"
164+
logger.info(f"MCDA job with perspective: {perspective}")
165+
166+
job_run = job_repo.create_job_run(job_name=actual_job_name)
96167

97168
logger.info(
98169
f"Job run created",
99170
extra={
100-
"job_name": job_name.value,
171+
"job_name": actual_job_name,
101172
"job_id": job_run.id,
102173
"status": job_run.status
103174
}
104175
)
105176

106177
# Schedule the job for background execution
107178
background_tasks.add_task(
108-
execute_job_in_background, job_name, job_run.id)
179+
execute_job_in_background, job_name, job_run.id, params)
109180

110181
logger.info(
111182
f"Job scheduled for background execution",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"regulatory": {
3+
"Improve Safety": 0.4,
4+
"Improve Public Transport": 0.3,
5+
"Improve Accessibility": 0.3
6+
},
7+
"pto": {
8+
"Improve Safety": 0.3,
9+
"Improve Public Transport": 0.4,
10+
"Improve Accessibility": 0.3
11+
},
12+
"nsm_providers": {
13+
"Improve Safety": 0.4,
14+
"Improve Public Transport": 0.3,
15+
"Improve Accessibility": 0.3
16+
},
17+
"citizens_users": {
18+
"Improve Safety": 0.3,
19+
"Improve Public Transport": 0.3,
20+
"Improve Accessibility": 0.4
21+
}
22+
}

src/sum_impact_assessment/jobs/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
"""
44
from typing import Dict, Type
55
from ..services.kpi_measures_analysis_job import KpiMeasuresAnalysisJob
6+
from ..services.mcda_analysis_job import MCDAAnalysisJob
67
from ..schemas.job import JobNameEnum
78

89
# Job registry mapping job names to job classes
910
JOB_REGISTRY: Dict[JobNameEnum, Type] = {
10-
JobNameEnum.KPI_MEASURES_ANALYSIS: KpiMeasuresAnalysisJob
11+
JobNameEnum.KPI_MEASURES_ANALYSIS: KpiMeasuresAnalysisJob,
12+
JobNameEnum.MCDA_ANALYSIS: MCDAAnalysisJob
1113
}
1214

1315

src/sum_impact_assessment/schemas/job.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44
from pydantic import BaseModel, Field
55
from datetime import datetime
6-
from typing import Optional
6+
from typing import Optional, Dict
77
from enum import Enum
88

99

@@ -12,6 +12,7 @@ class JobNameEnum(str, Enum):
1212
Enumeration of valid job names.
1313
"""
1414
KPI_MEASURES_ANALYSIS = "kpi_measures_analysis"
15+
MCDA_ANALYSIS = "mcda_analysis"
1516

1617

1718
class JobStatusEnum(str, Enum):
@@ -24,6 +25,26 @@ class JobStatusEnum(str, Enum):
2425
FAILURE = "FAILURE"
2526

2627

28+
class TriggerJobRequest(BaseModel):
29+
"""
30+
Request schema for triggering a job with optional parameters.
31+
"""
32+
params: Optional[Dict] = Field(
33+
None,
34+
description="Optional job-specific parameters. For mcda_analysis: {'kpi_group_id': 'MCDA_GOALS'}"
35+
)
36+
37+
class Config:
38+
json_schema_extra = {
39+
"example": {
40+
"params": {
41+
"kpi_group_id": "MCDA_GOALS",
42+
"perspective": "regulatory"
43+
}
44+
}
45+
}
46+
47+
2748
class JobRunResponse(BaseModel):
2849
"""
2950
Response schema for job run information.

src/sum_impact_assessment/schemas/mcda/promethee_gaia_result.py

Lines changed: 0 additions & 88 deletions
This file was deleted.

0 commit comments

Comments
 (0)