zen-temple 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zen_temple-0.0.1/LICENSE +21 -0
- zen_temple-0.0.1/PKG-INFO +34 -0
- zen_temple-0.0.1/README.md +13 -0
- zen_temple-0.0.1/pyproject.toml +32 -0
- zen_temple-0.0.1/setup.cfg +17 -0
- zen_temple-0.0.1/src/zen_temple/__init__.py +0 -0
- zen_temple-0.0.1/src/zen_temple/config.py +23 -0
- zen_temple-0.0.1/src/zen_temple/main.py +44 -0
- zen_temple-0.0.1/src/zen_temple/models/__init__.py +1 -0
- zen_temple-0.0.1/src/zen_temple/models/solution.py +222 -0
- zen_temple-0.0.1/src/zen_temple/repositories/__init__.py +0 -0
- zen_temple-0.0.1/src/zen_temple/repositories/solution_repository.py +134 -0
- zen_temple-0.0.1/src/zen_temple/routers/__init__.py +0 -0
- zen_temple-0.0.1/src/zen_temple/routers/solutions.py +56 -0
- zen_temple-0.0.1/src/zen_temple.egg-info/PKG-INFO +34 -0
- zen_temple-0.0.1/src/zen_temple.egg-info/SOURCES.txt +18 -0
- zen_temple-0.0.1/src/zen_temple.egg-info/dependency_links.txt +1 -0
- zen_temple-0.0.1/src/zen_temple.egg-info/requires.txt +7 -0
- zen_temple-0.0.1/src/zen_temple.egg-info/top_level.txt +1 -0
zen_temple-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Reliability and Risk Engineering, ETH Zurich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: zen_temple
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The visualization plattform for ZEN Garden
|
|
5
|
+
Author-email: Vinzenz Muser <muserv@ethz.ch>
|
|
6
|
+
Project-URL: Homepage, https://github.com/ZEN-universe/ZEN-temple
|
|
7
|
+
Project-URL: Issues, https://github.com/ZEN-universe/ZEN-temple/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: uvicorn
|
|
15
|
+
Requires-Dist: fastapi
|
|
16
|
+
Requires-Dist: fastapi-cache2
|
|
17
|
+
Requires-Dist: python-dotenv
|
|
18
|
+
Requires-Dist: sqlmodel
|
|
19
|
+
Requires-Dist: python-multipart
|
|
20
|
+
Requires-Dist: zen-garden
|
|
21
|
+
|
|
22
|
+
## ZEN Suite
|
|
23
|
+
|
|
24
|
+
Run with: `uvicorn src.main:app`
|
|
25
|
+
Develop with: `uvicorn src.main:app --reload --host 0.0.0.0`
|
|
26
|
+
|
|
27
|
+
If chnages to the models are made, alembic is responsible for the migrations. Use the following steps:
|
|
28
|
+
|
|
29
|
+
Create change-scripts: `alembic revision --autogenerate -m "<message>"`
|
|
30
|
+
Commit changes: `alembic upgrade head`
|
|
31
|
+
|
|
32
|
+
Build Docker-Container with `docker build -t example --build-arg ssh_prv_key="$(cat ~/.ssh/id_rsa)" --build-arg ssh_pub_key="$(cat ~/.ssh/id_rsa.pub)" .`
|
|
33
|
+
|
|
34
|
+
Watch out, this copies your private ssh-key to the container because the required repository (ZEN garden) is private and therefore you need git-authentification.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## ZEN Suite
|
|
2
|
+
|
|
3
|
+
Run with: `uvicorn src.main:app`
|
|
4
|
+
Develop with: `uvicorn src.main:app --reload --host 0.0.0.0`
|
|
5
|
+
|
|
6
|
+
If chnages to the models are made, alembic is responsible for the migrations. Use the following steps:
|
|
7
|
+
|
|
8
|
+
Create change-scripts: `alembic revision --autogenerate -m "<message>"`
|
|
9
|
+
Commit changes: `alembic upgrade head`
|
|
10
|
+
|
|
11
|
+
Build Docker-Container with `docker build -t example --build-arg ssh_prv_key="$(cat ~/.ssh/id_rsa)" --build-arg ssh_pub_key="$(cat ~/.ssh/id_rsa.pub)" .`
|
|
12
|
+
|
|
13
|
+
Watch out, this copies your private ssh-key to the container because the required repository (ZEN garden) is private and therefore you need git-authentification.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zen_temple"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Vinzenz Muser", email="muserv@ethz.ch" },
|
|
10
|
+
]
|
|
11
|
+
description = "The visualization plattform for ZEN Garden"
|
|
12
|
+
requires-python= ">=3.10"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"uvicorn",
|
|
22
|
+
"fastapi",
|
|
23
|
+
"fastapi-cache2",
|
|
24
|
+
"python-dotenv",
|
|
25
|
+
"sqlmodel",
|
|
26
|
+
"python-multipart",
|
|
27
|
+
"zen-garden"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/ZEN-universe/ZEN-temple"
|
|
32
|
+
Issues = "https://github.com/ZEN-universe/ZEN-temple/issues"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[flake8]
|
|
2
|
+
max-line-length = 90
|
|
3
|
+
exclude = migrations,src/config.py
|
|
4
|
+
ignore = E203
|
|
5
|
+
|
|
6
|
+
[black]
|
|
7
|
+
max-line-length = 90
|
|
8
|
+
|
|
9
|
+
[mypy]
|
|
10
|
+
python_executable = "venv/bin/python"
|
|
11
|
+
exclude = venv|migrations|tests|public
|
|
12
|
+
strict = True
|
|
13
|
+
|
|
14
|
+
[egg_info]
|
|
15
|
+
tag_build =
|
|
16
|
+
tag_date = 0
|
|
17
|
+
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
|
|
4
|
+
load_dotenv()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Config:
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
self.SOLUTION_FOLDER: str = os.getenv("SOLUTION_FOLDER") # type: ignore
|
|
10
|
+
|
|
11
|
+
if self.SOLUTION_FOLDER is None:
|
|
12
|
+
self.SOLUTION_FOLDER = "../outputs"
|
|
13
|
+
print(f"Setting default solution folder {self.SOLUTION_FOLDER}")
|
|
14
|
+
|
|
15
|
+
self.check()
|
|
16
|
+
|
|
17
|
+
def check(self) -> None:
|
|
18
|
+
for key, val in self.__dict__.items():
|
|
19
|
+
if val is None:
|
|
20
|
+
raise Exception(f"Env-Variable {key} is missing!")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
config = Config()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from src.routers import solutions
|
|
2
|
+
from collections.abc import AsyncIterator
|
|
3
|
+
|
|
4
|
+
import uvicorn
|
|
5
|
+
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
|
|
8
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from fastapi.staticfiles import StaticFiles
|
|
11
|
+
|
|
12
|
+
from fastapi_cache import FastAPICache
|
|
13
|
+
from fastapi_cache.backends.inmemory import InMemoryBackend
|
|
14
|
+
import webbrowser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@asynccontextmanager
|
|
18
|
+
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
19
|
+
FastAPICache.init(InMemoryBackend())
|
|
20
|
+
yield
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
app = FastAPI(lifespan=lifespan)
|
|
24
|
+
|
|
25
|
+
origins = ["*"]
|
|
26
|
+
|
|
27
|
+
app.add_middleware(
|
|
28
|
+
CORSMiddleware,
|
|
29
|
+
allow_origins=origins, # type: ignore
|
|
30
|
+
allow_credentials=True, # type: ignore
|
|
31
|
+
allow_methods=["*"], # type: ignore
|
|
32
|
+
allow_headers=["*"], # type: ignore
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
app.include_router(solutions.router)
|
|
36
|
+
app.mount("/explorer", StaticFiles(directory="explorer", html=True), name="explorer")
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
config = uvicorn.Config("main:app", port=8000, log_level="info")
|
|
40
|
+
server = uvicorn.Server(config)
|
|
41
|
+
webbrowser.open('http://localhost:8000/explorer', new=2)
|
|
42
|
+
|
|
43
|
+
server.run()
|
|
44
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import solution # noqa: F401
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from sqlmodel import Field, Column, String
|
|
2
|
+
from sqlalchemy.dialects import postgresql
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from src.config import config
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any
|
|
10
|
+
from zen_garden.model.default_config import System # type: ignore
|
|
11
|
+
from zen_garden.postprocess.results import Results # type: ignore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SeriesBehaviour(Enum):
|
|
15
|
+
sum = "sum"
|
|
16
|
+
series = "series"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ScenarioDetail(BaseModel):
|
|
20
|
+
system: System
|
|
21
|
+
reference_carrier: dict[str, str]
|
|
22
|
+
carriers_import: list[str]
|
|
23
|
+
carriers_export: list[str]
|
|
24
|
+
carriers_input: dict[str, list[str]]
|
|
25
|
+
carriers_output: dict[str, list[str]]
|
|
26
|
+
carriers_demand: list[str]
|
|
27
|
+
edges: dict[str, str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SolutionDetail(BaseModel):
|
|
31
|
+
name: str
|
|
32
|
+
folder_name: str
|
|
33
|
+
scenarios: dict[str, ScenarioDetail]
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def from_name(name: str) -> "SolutionDetail":
|
|
37
|
+
results = Results(os.path.join(config.SOLUTION_FOLDER, name))
|
|
38
|
+
reference_carriers = results.get_df("set_reference_carriers")
|
|
39
|
+
|
|
40
|
+
scenario_details = {}
|
|
41
|
+
|
|
42
|
+
for scenario_name, scenario in results.solution_loader.scenarios.items():
|
|
43
|
+
system = scenario.system
|
|
44
|
+
reference_carrier = reference_carriers[scenario_name].to_dict()
|
|
45
|
+
df_import = results.get_df("availability_import")[scenario_name]
|
|
46
|
+
df_export = results.get_df("availability_export")[scenario_name]
|
|
47
|
+
df_demand = results.get_df("demand")[scenario_name]
|
|
48
|
+
df_input_carriers = results.get_df("set_input_carriers")[scenario_name]
|
|
49
|
+
df_output_carriers = results.get_df("set_output_carriers")[scenario_name]
|
|
50
|
+
edges = results.get_df("set_nodes_on_edges")[scenario_name]
|
|
51
|
+
edges_dict = edges.to_dict()
|
|
52
|
+
carriers_input_dict = {
|
|
53
|
+
key: val.split(",") for key, val in df_input_carriers.to_dict().items()
|
|
54
|
+
}
|
|
55
|
+
carriers_output_dict = {
|
|
56
|
+
key: val.split(",") for key, val in df_output_carriers.to_dict().items()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for key in carriers_output_dict:
|
|
60
|
+
if carriers_output_dict[key] == [""]:
|
|
61
|
+
carriers_output_dict[key] = []
|
|
62
|
+
|
|
63
|
+
for key in carriers_input_dict:
|
|
64
|
+
if carriers_input_dict[key] == [""]:
|
|
65
|
+
carriers_input_dict[key] = []
|
|
66
|
+
|
|
67
|
+
carriers_import = list(
|
|
68
|
+
df_import.loc[df_import != 0].index.get_level_values("carrier").unique()
|
|
69
|
+
)
|
|
70
|
+
carriers_export = list(
|
|
71
|
+
df_export.loc[df_export != 0].index.get_level_values("carrier").unique()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
carriers_demand = list(
|
|
75
|
+
df_demand.loc[df_demand != 0].index.get_level_values("carrier").unique()
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
scenario_details[scenario_name] = ScenarioDetail(
|
|
79
|
+
system=system,
|
|
80
|
+
reference_carrier=reference_carrier,
|
|
81
|
+
carriers_import=carriers_import,
|
|
82
|
+
carriers_export=carriers_export,
|
|
83
|
+
carriers_input=carriers_input_dict,
|
|
84
|
+
carriers_output=carriers_output_dict,
|
|
85
|
+
carriers_demand=carriers_demand,
|
|
86
|
+
edges=edges_dict
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return SolutionDetail(
|
|
90
|
+
name=name.split("/")[-1],
|
|
91
|
+
folder_name=name.split("/")[-1],
|
|
92
|
+
scenarios=scenario_details,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Solution(BaseModel):
|
|
97
|
+
folder_name: str
|
|
98
|
+
name: str
|
|
99
|
+
nodes: list[str] = Field(default=[], sa_column=Column(postgresql.ARRAY(String())))
|
|
100
|
+
total_hours_per_year: int
|
|
101
|
+
optimized_years: int
|
|
102
|
+
technologies: list[str] = Field(
|
|
103
|
+
default=[], sa_column=Column(postgresql.ARRAY(String()))
|
|
104
|
+
)
|
|
105
|
+
carriers: list[str] = Field(
|
|
106
|
+
default=[], sa_column=Column(postgresql.ARRAY(String()))
|
|
107
|
+
)
|
|
108
|
+
scenarios: list[str] = Field(
|
|
109
|
+
default=[], sa_column=Column(postgresql.ARRAY(String()))
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def from_name(name: str) -> "Solution":
|
|
114
|
+
scenarios = [
|
|
115
|
+
i
|
|
116
|
+
for i in os.listdir(os.path.join(config.SOLUTION_FOLDER, name))
|
|
117
|
+
if i.startswith("scenario_")
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
if len(scenarios) == 0:
|
|
121
|
+
scenarios = ["scenario_"]
|
|
122
|
+
|
|
123
|
+
system_path = os.path.join(config.SOLUTION_FOLDER, name, "system.json")
|
|
124
|
+
if not os.path.exists(system_path):
|
|
125
|
+
system_path = os.path.join(
|
|
126
|
+
config.SOLUTION_FOLDER, name, "scenario_", "system.json"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
with open(system_path) as f:
|
|
130
|
+
system: dict[str, Any] = json.load(f)
|
|
131
|
+
|
|
132
|
+
system["carriers"] = system["set_carriers"]
|
|
133
|
+
system["technologies"] = system["set_technologies"]
|
|
134
|
+
system["folder_name"] = name.split("/")[-1]
|
|
135
|
+
system["scenarios"] = scenarios
|
|
136
|
+
system["nodes"] = system["set_nodes"]
|
|
137
|
+
system["name"] = name.split("/")[-1]
|
|
138
|
+
solution = Solution(**system)
|
|
139
|
+
|
|
140
|
+
return solution
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class IndexSet(BaseModel):
|
|
144
|
+
index_title: str
|
|
145
|
+
behaviour: SeriesBehaviour = SeriesBehaviour.series
|
|
146
|
+
indices: list[str] = []
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class DataRequest(BaseModel):
|
|
150
|
+
default: SeriesBehaviour = SeriesBehaviour.series
|
|
151
|
+
index_sets: list[IndexSet] = []
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class CompleteDataRequest(BaseModel):
|
|
155
|
+
solution_name: str
|
|
156
|
+
component: str
|
|
157
|
+
scenario: str = "scenario_"
|
|
158
|
+
data_request: DataRequest
|
|
159
|
+
aggregate_years: bool = False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class DataResult(BaseModel):
|
|
163
|
+
data_csv: str
|
|
164
|
+
unit: Optional[str]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ResultsRequest(BaseModel):
|
|
168
|
+
component: str
|
|
169
|
+
yearly: bool = False
|
|
170
|
+
node_edit: Optional[str] = None
|
|
171
|
+
sum_techs: bool = False
|
|
172
|
+
tech_type: Optional[str] = None
|
|
173
|
+
reference_carrier: Optional[str] = None
|
|
174
|
+
scenario: Optional[str] = None
|
|
175
|
+
|
|
176
|
+
def to_data_request(self, solution_name: str) -> CompleteDataRequest:
|
|
177
|
+
data_request = DataRequest()
|
|
178
|
+
index_sets: list[IndexSet] = []
|
|
179
|
+
|
|
180
|
+
if self.node_edit is not None and self.node_edit != "all":
|
|
181
|
+
index_sets.append(
|
|
182
|
+
IndexSet(
|
|
183
|
+
index_title="node",
|
|
184
|
+
behaviour=SeriesBehaviour.series,
|
|
185
|
+
indices=[self.node_edit],
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
self.sum_techs is not None or self.tech_type
|
|
191
|
+
) is not None and self.tech_type != "all":
|
|
192
|
+
tech_index = IndexSet(
|
|
193
|
+
index_title="technology", behaviour=SeriesBehaviour.series
|
|
194
|
+
)
|
|
195
|
+
if self.sum_techs:
|
|
196
|
+
tech_index.behaviour = SeriesBehaviour.sum
|
|
197
|
+
if self.tech_type is not None:
|
|
198
|
+
tech_index.indices = [self.tech_type]
|
|
199
|
+
index_sets.append(tech_index)
|
|
200
|
+
|
|
201
|
+
if self.reference_carrier is not None and self.reference_carrier != "all":
|
|
202
|
+
index_sets.append(
|
|
203
|
+
IndexSet(
|
|
204
|
+
index_title="carrier",
|
|
205
|
+
behaviour=SeriesBehaviour.series,
|
|
206
|
+
indices=[self.reference_carrier],
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
data_request.index_sets = index_sets
|
|
211
|
+
|
|
212
|
+
request = CompleteDataRequest(
|
|
213
|
+
solution_name=solution_name,
|
|
214
|
+
component=self.component,
|
|
215
|
+
data_request=data_request,
|
|
216
|
+
aggregate_years=self.yearly,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if self.scenario is not None:
|
|
220
|
+
request.scenario = "scenario_" + self.scenario
|
|
221
|
+
|
|
222
|
+
return request
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from ..config import config
|
|
2
|
+
from zen_garden.postprocess.results import Results # type: ignore
|
|
3
|
+
from ..models.solution import (
|
|
4
|
+
Solution,
|
|
5
|
+
ResultsRequest,
|
|
6
|
+
CompleteDataRequest,
|
|
7
|
+
SolutionDetail,
|
|
8
|
+
DataResult,
|
|
9
|
+
)
|
|
10
|
+
import os
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from time import perf_counter
|
|
13
|
+
from fastapi import HTTPException, UploadFile
|
|
14
|
+
from zipfile import ZipFile
|
|
15
|
+
from typing import Optional, Any
|
|
16
|
+
from functools import cache
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SolutionRepository:
|
|
20
|
+
def get_list(self) -> list[Solution]:
|
|
21
|
+
ans = []
|
|
22
|
+
for folder in os.listdir(config.SOLUTION_FOLDER):
|
|
23
|
+
try:
|
|
24
|
+
ans.append(Solution.from_name(folder))
|
|
25
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
26
|
+
continue
|
|
27
|
+
return ans
|
|
28
|
+
|
|
29
|
+
@cache
|
|
30
|
+
def get_detail(self, solution_name: str) -> SolutionDetail:
|
|
31
|
+
return SolutionDetail.from_name(solution_name)
|
|
32
|
+
|
|
33
|
+
@cache
|
|
34
|
+
def get_total(
|
|
35
|
+
self, solution: str, component: str, scenario: Optional[str] = None
|
|
36
|
+
) -> DataResult:
|
|
37
|
+
solution_folder = os.path.join(config.SOLUTION_FOLDER, solution)
|
|
38
|
+
results = Results(solution_folder)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
unit: pd.Series[Any] | None = results.get_unit(
|
|
42
|
+
component, scenario_name=scenario
|
|
43
|
+
)
|
|
44
|
+
except:
|
|
45
|
+
unit = None
|
|
46
|
+
|
|
47
|
+
total: pd.DataFrame | pd.Series = results.get_total(
|
|
48
|
+
component, scenario_name=scenario
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if type(total) is not pd.Series:
|
|
52
|
+
total = total.loc[~(total == 0).all(axis=1)]
|
|
53
|
+
|
|
54
|
+
if unit is not None:
|
|
55
|
+
unit_csv = unit.to_csv()
|
|
56
|
+
else:
|
|
57
|
+
unit_csv = None
|
|
58
|
+
|
|
59
|
+
return DataResult(data_csv=str(total.to_csv()), unit=unit_csv)
|
|
60
|
+
|
|
61
|
+
def get_energy_balance(
|
|
62
|
+
self,
|
|
63
|
+
solution: str,
|
|
64
|
+
node: str,
|
|
65
|
+
carrier: str,
|
|
66
|
+
scenario: Optional[str] = None,
|
|
67
|
+
year: Optional[int] = None,
|
|
68
|
+
) -> dict[str, str]:
|
|
69
|
+
solution_folder = os.path.join(config.SOLUTION_FOLDER, solution)
|
|
70
|
+
results = Results(solution_folder)
|
|
71
|
+
|
|
72
|
+
if year is None:
|
|
73
|
+
year = 0
|
|
74
|
+
energy_balance: dict[str, pd.DataFrame] = results.get_energy_balance_dataframes(
|
|
75
|
+
node, carrier, year, scenario
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
ans = {key: val.drop_duplicates() for key, val in energy_balance.items()}
|
|
79
|
+
|
|
80
|
+
for key, series in ans.items():
|
|
81
|
+
if key == "demand":
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
if type(series) is not pd.Series:
|
|
85
|
+
ans[key] = series.loc[~(series == 0).all(axis=1)]
|
|
86
|
+
|
|
87
|
+
ans = {key: val.to_csv() for key, val in ans.items()}
|
|
88
|
+
return ans
|
|
89
|
+
|
|
90
|
+
def get_dataframe(self, solution_name: str, df_request: ResultsRequest) -> str:
|
|
91
|
+
path = os.path.join(config.SOLUTION_FOLDER, solution_name)
|
|
92
|
+
argument_dictionary = {
|
|
93
|
+
key: val for key, val in df_request.dict().items() if val is not None
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
start = perf_counter()
|
|
97
|
+
results = Results(path)
|
|
98
|
+
print(f"Loading results took {perf_counter() - start}")
|
|
99
|
+
|
|
100
|
+
if "scenario" in argument_dictionary:
|
|
101
|
+
request_scenario = "scenario_" + argument_dictionary["scenario"]
|
|
102
|
+
if request_scenario not in results.results:
|
|
103
|
+
argument_dictionary["scenario"] = None
|
|
104
|
+
else:
|
|
105
|
+
argument_dictionary["scenario"] = request_scenario
|
|
106
|
+
|
|
107
|
+
start = perf_counter()
|
|
108
|
+
res: pd.DataFrame = results.get_summary_df(**argument_dictionary)
|
|
109
|
+
res = res.reset_index()
|
|
110
|
+
years = [i for i in res.columns if isinstance(i, int)]
|
|
111
|
+
others = [i for i in res.columns if not isinstance(i, int)]
|
|
112
|
+
res = pd.melt(res, id_vars=others, var_name="year", value_vars=years)
|
|
113
|
+
|
|
114
|
+
return res.to_csv()
|
|
115
|
+
|
|
116
|
+
async def upload_file(self, in_file: UploadFile) -> str:
|
|
117
|
+
file_path = os.path.join("./", str(in_file.filename))
|
|
118
|
+
|
|
119
|
+
async def upload() -> None:
|
|
120
|
+
pass
|
|
121
|
+
# async with aiofiles.open(file_path, "wb") as out_file:
|
|
122
|
+
# while content := await in_file.read(): # async read chunk
|
|
123
|
+
# await out_file.write(content) # async write chunk
|
|
124
|
+
|
|
125
|
+
await upload()
|
|
126
|
+
|
|
127
|
+
with ZipFile(file_path, "r") as zip:
|
|
128
|
+
contents: list[str] = zip.namelist()
|
|
129
|
+
print(contents)
|
|
130
|
+
|
|
131
|
+
return "Success"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
solution_repository = SolutionRepository()
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
from ..repositories.solution_repository import solution_repository
|
|
3
|
+
from ..models.solution import Solution, CompleteDataRequest, SolutionDetail, DataResult
|
|
4
|
+
from ..models.solution import ResultsRequest
|
|
5
|
+
from fastapi import UploadFile, status
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from fastapi_cache.decorator import cache
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/solutions", tags=["Solutions"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/list")
|
|
14
|
+
async def get_list() -> list[Solution]:
|
|
15
|
+
return solution_repository.get_list()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/get_detail/{solution_name}")
|
|
19
|
+
async def get_detail(solution_name: str) -> SolutionDetail:
|
|
20
|
+
ans = solution_repository.get_detail(solution_name)
|
|
21
|
+
return ans
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.get("/get_total/{solution_name}/{component_name}")
|
|
25
|
+
async def get_total(
|
|
26
|
+
solution_name: str, component_name: str, scenario: Optional[str] = None
|
|
27
|
+
) -> DataResult:
|
|
28
|
+
ans = solution_repository.get_total(solution_name, component_name, scenario)
|
|
29
|
+
return ans
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get("/get_energy_balance/{solution_name}/{node_name}/{carrier_name}")
|
|
33
|
+
@cache(expire=30000)
|
|
34
|
+
async def get_energy_balance(
|
|
35
|
+
solution_name: str,
|
|
36
|
+
node_name: str,
|
|
37
|
+
carrier_name: str,
|
|
38
|
+
scenario: Optional[str] = None,
|
|
39
|
+
year: Optional[int] = 0,
|
|
40
|
+
) -> dict[str, str]:
|
|
41
|
+
ans = solution_repository.get_energy_balance(
|
|
42
|
+
solution_name, node_name, carrier_name, scenario, year
|
|
43
|
+
)
|
|
44
|
+
return ans
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.post("/upload", status_code=status.HTTP_201_CREATED)
|
|
48
|
+
async def upload(in_file: UploadFile) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Creates a dataset with files
|
|
51
|
+
:param in_file: zip file containing the files to be uploaded
|
|
52
|
+
:param title: title of the dataset
|
|
53
|
+
"""
|
|
54
|
+
ans = await solution_repository.upload_file(in_file)
|
|
55
|
+
|
|
56
|
+
return ans
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: zen_temple
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The visualization plattform for ZEN Garden
|
|
5
|
+
Author-email: Vinzenz Muser <muserv@ethz.ch>
|
|
6
|
+
Project-URL: Homepage, https://github.com/ZEN-universe/ZEN-temple
|
|
7
|
+
Project-URL: Issues, https://github.com/ZEN-universe/ZEN-temple/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: uvicorn
|
|
15
|
+
Requires-Dist: fastapi
|
|
16
|
+
Requires-Dist: fastapi-cache2
|
|
17
|
+
Requires-Dist: python-dotenv
|
|
18
|
+
Requires-Dist: sqlmodel
|
|
19
|
+
Requires-Dist: python-multipart
|
|
20
|
+
Requires-Dist: zen-garden
|
|
21
|
+
|
|
22
|
+
## ZEN Suite
|
|
23
|
+
|
|
24
|
+
Run with: `uvicorn src.main:app`
|
|
25
|
+
Develop with: `uvicorn src.main:app --reload --host 0.0.0.0`
|
|
26
|
+
|
|
27
|
+
If chnages to the models are made, alembic is responsible for the migrations. Use the following steps:
|
|
28
|
+
|
|
29
|
+
Create change-scripts: `alembic revision --autogenerate -m "<message>"`
|
|
30
|
+
Commit changes: `alembic upgrade head`
|
|
31
|
+
|
|
32
|
+
Build Docker-Container with `docker build -t example --build-arg ssh_prv_key="$(cat ~/.ssh/id_rsa)" --build-arg ssh_pub_key="$(cat ~/.ssh/id_rsa.pub)" .`
|
|
33
|
+
|
|
34
|
+
Watch out, this copies your private ssh-key to the container because the required repository (ZEN garden) is private and therefore you need git-authentification.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
src/zen_temple/__init__.py
|
|
6
|
+
src/zen_temple/config.py
|
|
7
|
+
src/zen_temple/main.py
|
|
8
|
+
src/zen_temple.egg-info/PKG-INFO
|
|
9
|
+
src/zen_temple.egg-info/SOURCES.txt
|
|
10
|
+
src/zen_temple.egg-info/dependency_links.txt
|
|
11
|
+
src/zen_temple.egg-info/requires.txt
|
|
12
|
+
src/zen_temple.egg-info/top_level.txt
|
|
13
|
+
src/zen_temple/models/__init__.py
|
|
14
|
+
src/zen_temple/models/solution.py
|
|
15
|
+
src/zen_temple/repositories/__init__.py
|
|
16
|
+
src/zen_temple/repositories/solution_repository.py
|
|
17
|
+
src/zen_temple/routers/__init__.py
|
|
18
|
+
src/zen_temple/routers/solutions.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zen_temple
|