Skip to content

Commit 8b17130

Browse files
authored
Merge pull request #104 from open-plan-tool/feature/sensitivity-analysis
Implement sensitivity analysis
2 parents a68d12d + d28d919 commit 8b17130

File tree

16 files changed

+797
-146
lines changed

16 files changed

+797
-146
lines changed

app/dashboard/helpers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
EMPTY_SUBCAT = "none"
2626

2727
KPI_PARAMETERS = {}
28+
KPI_PARAMETERS_ASSETS = {}
2829

2930
if os.path.exists(staticfiles_storage.path("MVS_kpis_list.csv")) is True:
3031
with open(staticfiles_storage.path("MVS_kpis_list.csv")) as csvfile:
@@ -65,17 +66,24 @@
6566
for i, row in enumerate(csvreader):
6667
if i == 0:
6768
hdr = [el.replace(" ", "_").replace(":", "").lower() for el in row]
68-
print(hdr)
6969
label_idx = hdr.index("label")
7070
cat_idx = hdr.index("category")
71+
scope_idx = hdr.index("scope")
7172
else:
7273
label = row[label_idx]
7374
category = row[cat_idx]
75+
scope = row[scope_idx]
76+
7477
if category != "files":
7578
KPI_PARAMETERS[label] = {
7679
k: _(v) if k == "verbose" or k == "definition" else v
7780
for k, v in zip(hdr, row)
7881
}
82+
if "asset" in scope:
83+
KPI_PARAMETERS_ASSETS[label] = {
84+
k: _(v) if k == "verbose" or k == "definition" else v
85+
for k, v in zip(hdr, row)
86+
}
7987

8088

8189
#### FUNCTIONS ####

app/epa/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@
184184
MVS_POST_URL = f"{MVS_API_HOST}/sendjson/openplan"
185185
MVS_GET_URL = f"{MVS_API_HOST}/check/"
186186
MVS_LP_FILE_URL = f"{MVS_API_HOST}/get_lp_file/"
187+
MVS_SA_POST_URL = f"{MVS_API_HOST}/sendjson/openplan/sensitivity-analysis"
188+
MVS_SA_GET_URL = f"{MVS_API_HOST}/check-sensitivity-analysis/"
187189

188190
# Allow iframes to show in page
189191
X_FRAME_OPTIONS = "SAMEORIGIN"

app/projects/dtos.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@
44
from numpy.core import long
55
from datetime import date, datetime, time
66

7-
from projects.models import *
7+
from projects.models import (
8+
ConnectionLink,
9+
Scenario,
10+
Project,
11+
EconomicData,
12+
Asset,
13+
Bus,
14+
Constraint,
15+
ValueType,
16+
)
817

918

1019
class ProjectDataDto:

app/projects/forms.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
from projects.models import *
2424
from projects.constants import MAP_EPA_MVS, RENEWABLE_ASSETS
2525

26+
from dashboard.helpers import KPI_PARAMETERS_ASSETS
27+
2628
from django.utils.translation import ugettext_lazy as _
2729
from django.conf import settings as django_settings
2830

@@ -36,7 +38,11 @@
3638
label_idx = hdr.index("label")
3739
else:
3840
label = row[label_idx]
39-
PARAMETERS[label] = {k: v for k, v in zip(hdr, row)}
41+
PARAMETERS[label] = {}
42+
for k, v in zip(hdr, row):
43+
if k == "sensitivity_analysis":
44+
v = bool(int(v))
45+
PARAMETERS[label][k] = v
4046

4147

4248
def gettext_variables(some_string, lang="de"):
@@ -71,7 +77,7 @@ def set_parameter_info(param_name, field, parameters=PARAMETERS):
7177
verbose = None
7278
default_value = None
7379
if param_name in PARAMETERS:
74-
help_text = PARAMETERS[param_name][":Definition:"]
80+
help_text = PARAMETERS[param_name][":Definition_Short:"]
7581
unit = PARAMETERS[param_name][":Unit:"]
7682
verbose = PARAMETERS[param_name]["verbose"]
7783
default_value = PARAMETERS[param_name][":Default:"]
@@ -481,6 +487,54 @@ class Meta:
481487
exclude = ["scenario", "value"]
482488

483489

490+
class SensitivityAnalysisForm(ModelForm):
491+
output_parameters_names = forms.MultipleChoiceField(
492+
choices=[
493+
(v, _(KPI_PARAMETERS_ASSETS[v]["verbose"])) for v in KPI_PARAMETERS_ASSETS
494+
]
495+
)
496+
497+
class Meta:
498+
model = SensitivityAnalysis
499+
fields = [
500+
"variable_name",
501+
"variable_min",
502+
"variable_max",
503+
"variable_step",
504+
"variable_reference",
505+
"output_parameters_names",
506+
]
507+
508+
def __init__(self, *args, **kwargs):
509+
scen_id = kwargs.pop("scen_id", None)
510+
super().__init__(*args, **kwargs)
511+
512+
forbidden_parameters_for_sa = ("name", "input_timeseries")
513+
514+
if scen_id is not None:
515+
scenario = Scenario.objects.get(id=scen_id)
516+
asset_parameters = []
517+
for asset in scenario.asset_set.all():
518+
asset_parameters += [
519+
(f"{asset.name}.{p}", _(p) + f" ({asset.name})")
520+
for p in asset.visible_fields
521+
if p not in forbidden_parameters_for_sa
522+
]
523+
self.fields["variable_name"] = forms.ChoiceField(choices=asset_parameters)
524+
# self.fields["output_parameters_names"] = forms.MultipleChoiceField(choices = [(v, _(KPI_PARAMETERS_ASSETS[v]["verbose"])) for v in KPI_PARAMETERS_ASSETS])
525+
# TODO restrict possible parameters here
526+
self.fields["output_parameters_names"].choices = [
527+
(v, _(KPI_PARAMETERS_ASSETS[v]["verbose"]))
528+
for v in KPI_PARAMETERS_ASSETS
529+
]
530+
531+
def clean_output_parameters_names(self):
532+
"""method which gets called upon form validation"""
533+
data = self.cleaned_data["output_parameters_names"]
534+
data_js = json.dumps(data)
535+
return data_js
536+
537+
484538
class BusForm(OpenPlanModelForm):
485539
def __init__(self, *args, **kwargs):
486540
bus_type_name = kwargs.pop("asset_type", None) # always = bus

app/projects/helpers.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import json
2+
from projects.dtos import convert_to_dto
3+
4+
5+
# Helper method to clean dict data from empty values
6+
def remove_empty_elements(d):
7+
def empty(x):
8+
return x is None or x == {} or x == []
9+
10+
if not isinstance(d, (dict, list)):
11+
return d
12+
elif isinstance(d, list):
13+
return [v for v in (remove_empty_elements(v) for v in d) if not empty(v)]
14+
else:
15+
return {
16+
k: v
17+
for k, v in ((k, remove_empty_elements(v)) for k, v in d.items())
18+
if not empty(v)
19+
}
20+
21+
22+
# Helper to convert Scenario data to MVS importable json
23+
def format_scenario_for_mvs(scenario_to_convert):
24+
mvs_request_dto = convert_to_dto(scenario_to_convert)
25+
dumped_data = json.loads(
26+
json.dumps(mvs_request_dto.__dict__, default=lambda o: o.__dict__)
27+
)
28+
29+
# format the constraints in MVS format directly, thus avoiding the need to maintain MVS-EPA
30+
# parser in multi-vector-simulator package
31+
constraint_dict = {}
32+
for constraint in dumped_data["constraints"]:
33+
constraint_dict[constraint["label"]] = constraint["value"]
34+
dumped_data["constraints"] = constraint_dict
35+
36+
# Remove None values
37+
return remove_empty_elements(dumped_data)
38+
39+
40+
def sensitivity_analysis_payload(
41+
variable_parameter_name="",
42+
variable_parameter_range="",
43+
variable_parameter_ref_val="",
44+
output_parameter_names=None,
45+
):
46+
"""format the parameters required to request a sensitivity analysis in a specific JSON"""
47+
if output_parameter_names is None:
48+
output_parameter_names = []
49+
return {
50+
"sensitivity_analysis_settings": {
51+
"variable_parameter_name": variable_parameter_name,
52+
"variable_parameter_range": variable_parameter_range,
53+
"variable_parameter_ref_val": variable_parameter_ref_val,
54+
"output_parameter_names": output_parameter_names,
55+
}
56+
}
57+
58+
59+
SA_RESPONSE_SCHEMA = {
60+
"type": "object",
61+
"required": ["server_info", "mvs_version", "id", "status", "results"],
62+
"properties": {
63+
"server_info": {"type": "string"},
64+
"mvs_version": {"type": "string"},
65+
"id": {"type": "string"},
66+
"status": {"type": "string"},
67+
"results": {
68+
"type": "object",
69+
"required": ["reference_simulation_id", "sensitivity_analysis_steps"],
70+
"properties": {
71+
"reference_simulation_id": {"type": "string"},
72+
"sensitivity_analysis_steps": {
73+
"type": "array",
74+
"items": {"type": "object"},
75+
},
76+
},
77+
"additionalProperties": False,
78+
},
79+
"ref_sim_id": {"type": "string"},
80+
"sensitivity_analysis_ids": {"type": "array", "items": {"type": "string"}},
81+
},
82+
"additionalProperties": False,
83+
}
84+
85+
86+
# Used to proof the json objects stored as text in the db
87+
SA_OUPUT_NAMES_SCHEMA = {"type": "array", "items": {"type": "string"}}
88+
89+
90+
def sa_output_values_schema_generator(output_names):
91+
return {
92+
"type": "object",
93+
"required": output_names,
94+
"properties": {
95+
output_name: {
96+
"type": "object",
97+
"required": ["value", "path"],
98+
"properties": {
99+
"value": {
100+
"oneOf": [
101+
{"type": "null"},
102+
{
103+
"type": "array",
104+
"items": {
105+
"anyOf": [{"type": "number"}, {"type": "null"}]
106+
},
107+
},
108+
]
109+
},
110+
"path": {
111+
"oneOf": [
112+
{"type": "string"},
113+
{"type": "array", "items": {"type": "string"}},
114+
]
115+
},
116+
},
117+
}
118+
for output_name in output_names
119+
},
120+
"additionalProperties": False,
121+
}

app/projects/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .base_models import *
2+
from .simulation_models import Simulation, SensitivityAnalysis
Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import uuid
22
import json
3-
from django.core.validators import MinValueValidator, MaxValueValidator
43
from django.conf import settings
54
from django.db import models
65
from django.core.validators import MaxValueValidator, MinValueValidator
76
from datetime import timedelta
87
from django.forms.models import model_to_dict
98
from django.utils.translation import gettext_lazy as _
109
from django.core.exceptions import ValidationError
11-
from .constants import (
10+
from projects.constants import (
1211
ASSET_CATEGORY,
1312
ASSET_TYPE,
1413
COUNTRY,
@@ -17,6 +16,7 @@
1716
FLOW_DIRECTION,
1817
MVS_TYPE,
1918
SIMULATION_STATUS,
19+
PENDING,
2020
TRUE_FALSE_CHOICES,
2121
BOOL_CHOICES,
2222
USER_RATING,
@@ -174,6 +174,10 @@ def export(self):
174174
dm = model_to_dict(self, exclude=["id"])
175175
return dm
176176

177+
@property
178+
def visible_fields(self):
179+
return self.asset_fields.replace("[", "").replace("]", "").split(",")
180+
177181

178182
class TopologyNode(models.Model):
179183
name = models.CharField(max_length=60, null=False, blank=False)
@@ -279,6 +283,28 @@ def save(self, *args, **kwargs):
279283
def fields(self):
280284
return [f.name for f in self._meta.fields + self._meta.many_to_many]
281285

286+
@property
287+
def visible_fields(self):
288+
return self.asset_type.visible_fields
289+
290+
def has_parameter(self, param_name):
291+
return param_name in self.visible_fields
292+
293+
def parameter_path(self, param_name):
294+
# TODO for storage
295+
if self.has_parameter(param_name):
296+
# TODO if (unit, value) formatting, add "value" at the end
297+
if self.asset_type.asset_category == "energy_provider":
298+
asset_category = "energy_providers"
299+
else:
300+
asset_category = self.asset_type.asset_category
301+
if param_name == "optimize_cap":
302+
param_name = "optimize_capacity"
303+
answer = (asset_category, self.name, param_name)
304+
else:
305+
answer = None
306+
return answer
307+
282308
@property
283309
def timestamps(self):
284310
return self.scenario.get_timestamps()
@@ -393,15 +419,17 @@ class ScenarioFile(models.Model):
393419
file = models.FileField(upload_to="tempFiles/", null=True, blank=True)
394420

395421

396-
class Simulation(models.Model):
422+
class AbstractSimulation(models.Model):
423+
397424
start_date = models.DateTimeField(auto_now_add=True, null=False)
398425
end_date = models.DateTimeField(null=True)
399426
elapsed_seconds = models.FloatField(null=True)
400427
mvs_token = models.CharField(max_length=200, null=True)
401-
status = models.CharField(max_length=20, choices=SIMULATION_STATUS, null=False)
402-
scenario = models.OneToOneField(Scenario, on_delete=models.CASCADE, null=False)
403-
user_rating = models.PositiveSmallIntegerField(
404-
null=True, choices=USER_RATING, default=None
428+
status = models.CharField(
429+
max_length=20, choices=SIMULATION_STATUS, null=False, default=PENDING
405430
)
406431
results = models.TextField(null=True, max_length=30e6)
407432
errors = models.TextField(null=True)
433+
434+
class Meta:
435+
abstract = True

0 commit comments

Comments
 (0)