Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
143a474
Set webapp Dockerfile in captain-definition
paulapreuss Dec 9, 2025
3657550
Clean json response
paulapreuss Dec 9, 2025
6a7142b
Rename open_plan queue to prod
Bachibouzouk Feb 25, 2026
b0f9687
Update the landing page
Bachibouzouk Feb 25, 2026
2311551
Allow zipfiles of datapackage to be uploaded (caution zip bombs)
Bachibouzouk Feb 28, 2026
29e8bef
Add oemof-datapackage
Bachibouzouk Feb 28, 2026
646c5b2
Edit instruction for upload
Bachibouzouk Feb 28, 2026
ace6964
Set the structure of the eesyplan worker
Bachibouzouk Feb 25, 2026
d5ff838
Update Dockerfile
Bachibouzouk Feb 25, 2026
6c0279c
First draft of eesyplan simulation task
Bachibouzouk Feb 27, 2026
85c24f1
Update the requirements
Bachibouzouk Feb 27, 2026
f269a11
Move gunicorn install to Dockerfile
Bachibouzouk Feb 27, 2026
398e94e
Add oemof.visio in requirements.txt
Bachibouzouk Feb 27, 2026
d26f321
Trigger rebuild
Bachibouzouk Feb 28, 2026
2ac41c1
Remove oemof.visio as now optional
Bachibouzouk Feb 28, 2026
f0beb33
Retrigger install of eesyplan
Bachibouzouk Feb 28, 2026
797e51b
Set no cache for eesyplan and oemof-datapackage
Bachibouzouk Feb 28, 2026
3b823db
Change eesyplan and datapackage versions
Bachibouzouk Mar 2, 2026
8c8702f
Update python version
Bachibouzouk Mar 2, 2026
d4b763d
Update oemof-eesyplan imports
Bachibouzouk Mar 2, 2026
a76a0f5
Omit the plot argument when creating energy system
Bachibouzouk Mar 24, 2026
71f24a4
Change oemof-datapackage version for bugfixing
Bachibouzouk Jun 3, 2026
a2559f0
Fix docker-compose config
paulapreuss Jun 23, 2026
8dfb6ab
Add oemof-visio requirements
paulapreuss Jun 23, 2026
c459b7e
Update TemplateResponse to new starlette function signature
paulapreuss Jun 23, 2026
1224e13
Update dockerfile requirements
MaaJoo13 Jul 1, 2026
2819f76
Enable local script execution
MaaJoo13 Jun 23, 2026
9e5c2d5
Include sankey figure in server response
MaaJoo13 Jul 7, 2026
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
2 changes: 1 addition & 1 deletion captain-definition
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"schemaVersion": 2,
"dockerfilePath": "task_queue/Dockerfile_dev"
"dockerfilePath": "task_queue/Dockerfile"
}
19 changes: 12 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
version: "3"
services:
web:
environment:
- MVS_DEV_VERSION=${MVS_DEV_VERSION}
- MVS_OPEN_PLAN_VERSION=${MVS_OPEN_PLAN_VERSION}
- CELERY_BROKER_URL=redis://redis:6379
- CELERY_RESULT_BACKEND=redis://redis:6379
build:
args:
# this should be in a .env file in the root of the repository
mvs_version: ${MVS_DEV_VERSION}
# `context` should be a path to a directory containing a Dockerfile
context: ./fastapi_app
dockerfile: Dockerfile
context: .
dockerfile: ./fastapi_app/Dockerfile
restart: unless-stopped
ports:
- "5001:5001"
Expand All @@ -22,27 +23,31 @@ services:
worker-dev:
environment:
- CELERY_TASK_NAME=dev
- CELERY_BROKER_URL=redis://redis:6379
- CELERY_RESULT_BACKEND=redis://redis:6379
build:
args:
# this should be in a .env file in the root of the repository
mvs_version: ${MVS_DEV_VERSION}
# context should be the name of the folder which define the tasks
context: task_queue
dockerfile: Dockerfile_dev
context: .
dockerfile: ./task_queue/Dockerfile
depends_on:
- redis
networks:
- sim_network
worker-open-plan:
environment:
- CELERY_TASK_NAME=open_plan
- CELERY_BROKER_URL=redis://redis:6379
- CELERY_RESULT_BACKEND=redis://redis:6379
build:
args:
# this should be in a .env file in the root of the repository
mvs_version: ${MVS_OPEN_PLAN_VERSION}
# context should be the name of the folder which define the tasks
context: task_queue
dockerfile: Dockerfile
context: .
dockerfile: ./task_queue/Dockerfile
depends_on:
- redis
networks:
Expand Down
3 changes: 2 additions & 1 deletion fastapi_app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ COPY fastapi_app/requirements.txt /tmp/
RUN python -m pip install --upgrade pip
RUN pip install -r /tmp/requirements.txt
# install multi-vector-simulator's version pinned in docker-compose.yml file
RUN pip install multi-vector-simulator==1.0.7rc3 gunicorn
RUN pip install gunicorn
RUN pip install "oemof-datapackage @ git+https://github.com/oemof/oemof-datapackage.git" --no-cache-dir



Expand Down
26 changes: 14 additions & 12 deletions fastapi_app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="description" content="MVS simulation server" />
<meta name="description" content="EesyPlan simulation server" />
<link rel="icon" href="{{ url_for('static', path='/favicon.ico') }}" />
<link
rel="stylesheet"
Expand All @@ -15,37 +15,39 @@
crossorigin="anonymous"
/>
<link href="{{ url_for('static', path='/style.css') }}" rel="stylesheet" />
<title>MVS SimServer</title>
<title>OpenPlan SimServer</title>

</head>
<body>
<h1>Welcome to MVS simulation server! </h1>
<h1>Welcome to OpenPlan simulation server! </h1>
<div>
<p>This simulation server uses oemof-eesyplan as a backend, it recieves request as jsonified oemof-datapackage and returns a JSON with the oemof-solph results</p>
<h2>Test simulation with custom json</h2>
<h3>Actual multi-vector-simulator's version for development: {{ mvs_dev_version }}</h3>
<h3>Actual oemof-eesyplan's version for development: {{ dev_version }}</h3>

<p>Upload a mvs input json file and click on "Run simulation"</p>
<p>Upload a JSONIFIED datapackage or a zipped datapackage and click on "Run simulation"</p>
<form action="/uploadjson/dev" enctype="multipart/form-data" method="post">
<!-- the value of `name` property should be used in the uploadjson endpoint function -->
<input name="json_file" type="file" multiple>
<input value="Run simulation" type="submit">
</form>

<h3>Actual multi-vector-simulator's version for open_plan: {{ mvs_open_plan_version }}</h3>
<h3>Actual oemof-eesyplan's version for OpenPlan production: {{ prod_version }}</h3>

<p>Upload a mvs input json file and click on "Run simulation"</p>
<form action="/uploadjson/open_plan" enctype="multipart/form-data" method="post">
<!-- the value of `name` property should be used in the uploadjson endpoint function -->
<input name="json_file" type="file" multiple>
<input value="Run simulation" type="submit">
<p>Upload a datapackage in JSON format and click on "Run simulation"</p>
<form action="/uploadjson/prod" enctype="multipart/form-data" method="post">
<!-- the value of `name` property should be used in the uploadjson endpoint function -->
<input name="json_file" type="file" multiple>
<input value="Run simulation" type="submit">
</form>
</div>


<div>
<h2>More information</h2>
<ul>
<li><a href="https://github.com/rl-institut/multi-vector-simulator" target="_blank">More about MVS simulation</a></li>
<li><a href="https://github.com/oemof/oemof-eesyplan" target="_blank">More about oemof-eesyplan</a></li>
<li><a href="" target="_blank">More about OpenPlan Simulation server</a></li>
</ul>
</div>
{% block body %}{% endblock body%}
Expand Down
136 changes: 95 additions & 41 deletions fastapi_app/webapp.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
import math
import os
import json
import io
from fastapi import FastAPI, Request, Response, File, UploadFile
from fastapi import FastAPI, Request, Response, File, UploadFile, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import StreamingResponse
from celery.exceptions import TimeoutError
import tempfile
import zipfile
from oemof.datapackage import datapackage # noqa


from multi_vector_simulator import version as mvs_version

from multi_vector_simulator.utils.constants_json_strings import (
SIMULATION_SETTINGS,
OUTPUT_LP_FILE,
VALUE,
UNIT,
)

MVS_DEV_VERSION = os.environ.get("MVS_DEV_VERSION", mvs_version.version_num)
MVS_OPEN_PLAN_VERSION = os.environ.get("MVS_OPEN_PLAN_VERSION", mvs_version.version_num)

MVS_SERVER_VERSIONS = {"dev": MVS_DEV_VERSION, "open_plan": MVS_OPEN_PLAN_VERSION}

try:
from worker import app as celery_app
except ModuleNotFoundError:
Expand Down Expand Up @@ -55,6 +46,7 @@

# Test Driven Development --> https://fastapi.tiangolo.com/tutorial/testing/


@app.get("/debug/ping")
async def debug_ping():
result = celery_app.send_task("dev.ping", queue="dev")
Expand All @@ -77,19 +69,30 @@ async def debug_ping():

@app.get("/")
def index(request: Request) -> Response:

return templates.TemplateResponse(
"index.html",
{
"request": request,
"mvs_dev_version": MVS_DEV_VERSION,
"mvs_open_plan_version": MVS_OPEN_PLAN_VERSION,
request=request,
name="index.html",
context={
"dev_version": get_worker_version("dev"),
"prod_version": get_worker_version("prod"),
},
)


def get_worker_version(queue):
task = celery_app.send_task(f"{queue}.get_version", queue=queue, kwargs={})
try:
version = task.get(timeout=0.5)
except TimeoutError:
version = f"{queue} worker not available"
return version


async def simulate_json_variable(request: Request, queue: str = "dev"):
"""Receive mvs simulation parameter in json post request and send it to simulator"""

# TODO use jsonschema to verify metadata and data and then use the content of metadata to check data

input_dict = await request.json()

# send the task to celery
Expand All @@ -106,9 +109,9 @@ async def simulate_json_variable_dev(request: Request):
return await simulate_json_variable(request, queue="dev")


@app.post("/sendjson/openplan")
async def simulate_json_variable_open_plan(request: Request):
return await simulate_json_variable(request, queue="open_plan")
@app.post("/sendjson/prod")
async def simulate_json_variable_prod(request: Request):
return await simulate_json_variable(request, queue="prod")


@app.post("/uploadjson/dev")
Expand All @@ -119,20 +122,54 @@ def simulate_uploaded_json_files_dev(
the value of `name` property of the input html tag should be `json_file` as the second
argument of this function
"""
json_content = jsonable_encoder(json_file.file.read())

if "json" in json_file.filename:
json_content = jsonable_encoder(json_file.file.read())
elif "zip" in json_file.filename:
# TODO do the same with only pathlib and temp files
import uuid
import shutil

ZIP_DIR = "zipped"
UNZIP_DIR = "unzipped"
zip_id = str(uuid.uuid4())
from pathlib import Path

unzip_path = os.path.join(UNZIP_DIR, zip_id)
os.makedirs(unzip_path)
zip_path = os.path.join(UNZIP_DIR, f"{zip_id}.zip")
with open(zip_path, "wb") as f:
shutil.copyfileobj(json_file.file, f)
with zipfile.ZipFile(zip_path, "r") as zipf:
zipf.extractall(unzip_path)
os.remove(zip_path)
unzip_path = Path(unzip_path)
extracted_name = [f for f in unzip_path.glob("*")]
extracted_name = extracted_name[0]
json_content = datapackage.export_dp_to_json(extracted_name)

# with tempfile.TemporaryDirectory() as td:
# # Open the zip file
# with zipfile.ZipFile(json_file.file, 'r') as zip_ref:
# # Extract all the contents into the specified directory
# zip_ref.extractall(td)
#
# json_content = json.dumps(datapackage.export_dp_to_json(td))
# json_content = datapackage.export_dp_to_json(td)

return run_simulation(request, input_json=json_content)


@app.post("/uploadjson/open_plan")
def simulate_uploaded_json_files_open_plan(
@app.post("/uploadjson/prod")
def simulate_uploaded_json_files_prod(
request: Request, json_file: UploadFile = File(...)
):
"""Receive mvs simulation parameter in json post request and send it to simulator
the value of `name` property of the input html tag should be `json_file` as the second
argument of this function
"""
json_content = jsonable_encoder(json_file.file.read())
return run_simulation_open_plan(request, input_json=json_content)
return run_simulation_prod(request, input_json=json_content)


def run_simulation(request: Request, input_json=None, queue="dev") -> Response:
Expand All @@ -152,7 +189,9 @@ def run_simulation(request: Request, input_json=None, queue="dev") -> Response:
)

return templates.TemplateResponse(
"submitted_task.html", {"request": request, "task_id": task.id}
request=request,
name="submitted_task.html",
context={"task_id": task.id},
)


Expand All @@ -161,17 +200,17 @@ def run_simulation_dev(request: Request, input_json=None) -> Response:
return run_simulation(request, input_json, queue="dev")


@app.post("/run_simulation_open_plan")
def run_simulation_open_plan(request: Request, input_json=None) -> Response:
return run_simulation(request, input_json, queue="open_plan")
@app.post("/run_simulation_prod")
def run_simulation_prod(request: Request, input_json=None) -> Response:
return run_simulation(request, input_json, queue="prod")


@app.get("/check/{task_id}")
async def check_task(task_id: str) -> JSONResponse:
res = celery_app.AsyncResult(task_id)
task = {
"server_info": None,
"mvs_version": None,
"simulation_version": None,
"id": task_id,
"status": res.state,
"results": None,
Expand All @@ -181,14 +220,30 @@ async def check_task(task_id: str) -> JSONResponse:
else:
task["status"] = "DONE"
results_as_dict = json.loads(res.result)
server_info = results_as_dict.pop("SERVER")
task["server_info"] = server_info
task["mvs_version"] = MVS_SERVER_VERSIONS.get(server_info, "unknown")
task["results"] = json.dumps(results_as_dict)
try:
task["server_info"] = results_as_dict.pop("SERVER")
task["simulation_version"] = results_as_dict.pop("VERSION")
except KeyError:
for k in ["server_info", "simulation_version"]:
task[k] = "Error: Could not retrieve simulation metadata"
task["results"] = results_as_dict
if "ERROR" in task["results"]:
task["status"] = "ERROR"
task["results"] = results_as_dict

def clean_floats(obj):
if isinstance(obj, float):
if math.isnan(obj) or math.isinf(obj):
return None
if isinstance(obj, dict):
return {k: clean_floats(v) for k, v in obj.items()}
if isinstance(obj, list):
return [clean_floats(i) for i in obj]
return obj

cleaned = clean_floats(task)

return JSONResponse(content=jsonable_encoder(cleaned))
return JSONResponse(content=jsonable_encoder(task))


Expand All @@ -197,7 +252,7 @@ async def get_lp_file(task_id: str) -> Response:
res = celery_app.AsyncResult(task_id)
task = {
"server_info": None,
"mvs_version": mvs_version,
"simulation_version": None,
"id": task_id,
"status": res.state,
"results": None,
Expand All @@ -208,9 +263,8 @@ async def get_lp_file(task_id: str) -> Response:
else:
task["status"] = "DONE"
results_as_dict = json.loads(res.result)
server_info = results_as_dict.pop("SERVER")
task["server_info"] = server_info
task["mvs_version"] = MVS_SERVER_VERSIONS.get(server_info, "unknown")
task["server_info"] = results_as_dict.pop("SERVER")
task["simulation_version"] = results_as_dict.pop("VERSION")
task["results"] = json.dumps(results_as_dict)
if "ERROR" in task["results"]:
task["status"] = "ERROR"
Expand Down
2 changes: 1 addition & 1 deletion fastapi_app/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
app = Celery("tasks", broker=CELERY_BROKER_URL, backend=CELERY_RESULT_BACKEND)
app.conf.task_queues = (
Queue("dev", routing_key="dev.#"),
Queue("open_plan", routing_key="open_plan.#"),
Queue("prod", routing_key="prod.#"),
)
Loading