From ddcee9e71c709461b2220810da25eeeee5a81e8e Mon Sep 17 00:00:00 2001 From: stefansc1 Date: Tue, 4 Nov 2025 14:24:23 +0100 Subject: [PATCH 1/4] multi-objective weights --- app/static/scss/layouts/_moo.scss | 20 +++++++++ app/static/scss/main.scss | 1 + app/templates/wefe/steps/moo_setup.html | 56 +++++++++++++++++++++++++ app/wefe/forms.py | 30 +++++++++++++ app/wefe/migrations/0003_mooweights.py | 26 ++++++++++++ app/wefe/models.py | 9 ++++ app/wefe/views.py | 50 +++++++++++++++------- 7 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 app/static/scss/layouts/_moo.scss create mode 100644 app/templates/wefe/steps/moo_setup.html create mode 100644 app/wefe/migrations/0003_mooweights.py diff --git a/app/static/scss/layouts/_moo.scss b/app/static/scss/layouts/_moo.scss new file mode 100644 index 00000000..683a502a --- /dev/null +++ b/app/static/scss/layouts/_moo.scss @@ -0,0 +1,20 @@ +.padding-1 { + padding: 1rem; +} + +.center-text { + text-align: center; +} + +.inp-default { + width: initial; +} + +.no-bullet ul { + list-style-type: none; +} + +.error { + color: red; + font-weight: bold; +} diff --git a/app/static/scss/main.scss b/app/static/scss/main.scss index 681eba20..84b8d82e 100644 --- a/app/static/scss/main.scss +++ b/app/static/scss/main.scss @@ -21,6 +21,7 @@ @import 'layouts/general-layout'; @import 'layouts/gui'; @import 'layouts/homepage'; +@import 'layouts/moo'; @import 'layouts/project'; @import 'layouts/scenario-create'; @import 'layouts/signup'; diff --git a/app/templates/wefe/steps/moo_setup.html b/app/templates/wefe/steps/moo_setup.html new file mode 100644 index 00000000..134a66c5 --- /dev/null +++ b/app/templates/wefe/steps/moo_setup.html @@ -0,0 +1,56 @@ +{% extends 'wefe/steps/step_progression.html' %} +{% load i18n %} + +{% block progression_content %} +
+
+ + +
+
+ {% csrf_token %} +
+ {{ form.total_cost.label }}{{ form.total_cost }} + {{ form.co2_emissions.label }}{{ form.co2_emissions }} + {{ form.land_requirements.label }}{{ form.land_requirements }} + {{ form.water_footprint.label }}{{ form.water_footprint }} +
+
+ {{ form.non_field_errors }} +
+
+
+{% endblock progression_content %} + +{% block next_btn %} + +{% endblock next_btn %} + +{% block end_body_scripts %} + +{% endblock end_body_scripts %} diff --git a/app/wefe/forms.py b/app/wefe/forms.py index ed4a84d9..f3551d58 100644 --- a/app/wefe/forms.py +++ b/app/wefe/forms.py @@ -368,3 +368,33 @@ def clean(self): raise ValidationError("This form cannot be blank") return cleaned_data + +class MOOForm(forms.Form): + # multi-objective optimization setup + total_cost = forms.FloatField( + min_value=0, max_value=1, initial=1, + widget=forms.NumberInput(attrs={'step': 0.1, 'default': 1}) + ) + co2_emissions = forms.FloatField( + min_value=0, max_value=1, initial=0, + widget=forms.NumberInput(attrs={'step': 0.1, 'default': 0}) + ) + land_requirements = forms.FloatField( + min_value=0, max_value=1, initial=0, + widget=forms.NumberInput(attrs={'step': 0.1, 'default': 0}) + ) + water_footprint = forms.FloatField( + min_value=0, max_value=1, initial=0, + widget=forms.NumberInput(attrs={'step': 0.1, 'default': 0}) + ) + + def clean(self): + # check that weights add up to 1 + cleaned_data = super().clean() + cost = cleaned_data.get("total_cost") + co2 = cleaned_data.get("co2_emissions") + land = cleaned_data.get("land_requirements") + water = cleaned_data.get("water_footprint") + if cost is not None and co2 is not None and land is not None and water is not None: + if cost + co2 + land + water != 1: + raise ValidationError("Weights must add up to 1") diff --git a/app/wefe/migrations/0003_mooweights.py b/app/wefe/migrations/0003_mooweights.py new file mode 100644 index 00000000..276f1d0d --- /dev/null +++ b/app/wefe/migrations/0003_mooweights.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.3 on 2025-11-04 14:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0025_project_kobo_survey_id_project_kobo_survey_url"), + ("wefe", "0002_surveyquestion_matrix_answers"), + ] + + operations = [ + migrations.CreateModel( + name="MOOWeights", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("total_cost", models.FloatField(default=1)), + ("co2_emissions", models.FloatField(default=0)), + ("land_requirements", models.FloatField(default=0)), + ("water_footprint", models.FloatField(default=0)), + ("scenario", models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="projects.scenario")), + ], + ), + ] diff --git a/app/wefe/models.py b/app/wefe/models.py index 1c3bfe54..82bc4283 100644 --- a/app/wefe/models.py +++ b/app/wefe/models.py @@ -75,3 +75,12 @@ def copy_energy_system_from_usecase(usecase_name, scenario): # assign the assets and busses to the given scenario assign_assets(scenario, assets) assign_busses(scenario, busses) + + +class MOOWeights(models.Model): + # multi-objective optimization weights + scenario = models.OneToOneField(Scenario, on_delete=models.CASCADE) + total_cost = models.FloatField(default=1) + co2_emissions = models.FloatField(default=0) + land_requirements = models.FloatField(default=0) + water_footprint = models.FloatField(default=0) diff --git a/app/wefe/views.py b/app/wefe/views.py index cfd20142..9c06e961 100644 --- a/app/wefe/views.py +++ b/app/wefe/views.py @@ -1,28 +1,28 @@ import io from pathlib import Path + from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db.models import Q, F, Avg, Max from django.http import JsonResponse -from django.utils.translation import gettext_lazy as _ from django.shortcuts import * from django.urls import reverse -from django.core.exceptions import PermissionDenied +from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_http_methods -from django.db.models import Q, F, Avg, Max -from .forms import * -from .helpers import * + from business_model.forms import * -from projects.models import * -from projects.views import project_duplicate, project_delete from business_model.models import * from projects.forms import UploadFileForm, ProjectShareForm, ProjectRevokeForm, UseCaseForm +from projects.models import * +from projects.models.base_models import Timeseries +from projects.views import project_duplicate, project_delete -from .models import SurveyAnswer +from .forms import * +from .helpers import * +from .models import SurveyAnswer, MOOWeights from .survey import SURVEY_CATEGORIES, SURVEY_QUESTIONS_CATEGORIES, get_survey_question_by_id import logging - -from projects.models.base_models import Timeseries - logger = logging.getLogger(__name__) @@ -271,7 +271,7 @@ def request_wefedemand_simulation(request, proj_id=None): def get_wefedemand_data(request, proj_id): proj = get_object_or_404(Project, id=proj_id) ts_qs = Timeseries.objects.filter(scenario=proj.scenario, name__contains="ramp_demand") - index = pd.date_range(start="2025-01-01 00:00:00", end="2025-12-31 23:00:00", freq="H") + index = pd.date_range(start="2025-01-01 00:00:00", end="2025-12-31 23:00:00", freq="h") index = index.strftime("%Y-%m-%dT%H:%M:%S").tolist() data = {} @@ -447,10 +447,32 @@ def wefe_optimization_weighting(request, proj_id, step_id=STEP_MAPPING["optimiza } if request.method == "GET": - return render(request, "wefe/steps/step_progression.html", context) + try: + # weights already defined: get values + weights = scenario.mooweights + context["form"] = MOOForm(initial=vars(weights)) + context["custom_weights"] = weights.total_cost != 1 + except ObjectDoesNotExist: + # no weights yet: use defaults + context["form"] = MOOForm() + return render(request, "wefe/steps/moo_setup.html", context) if request.method == "POST": - # TODO + form = MOOForm(request.POST) + if not form.is_valid(): + context["form"] = form + return render(request, "wefe/steps/moo_setup.html", context) + + # save form data (1 to 1 relation between scenario and weights) + MOOWeights.objects.update_or_create( + scenario=scenario, + defaults={ + "total_cost": form.cleaned_data["total_cost"], + "co2_emissions": form.cleaned_data["co2_emissions"], + "land_requirements": form.cleaned_data["land_requirements"], + "water_footprint": form.cleaned_data["water_footprint"], + } + ) return HttpResponseRedirect(reverse("wefe_steps", args=[proj_id, step_id + 1])) From c2e9324faf9b26c0d2a671acc547830848642e71 Mon Sep 17 00:00:00 2001 From: paulapreuss Date: Mon, 1 Dec 2025 15:03:10 +0100 Subject: [PATCH 2/4] Make form crispy and slightly adjust layout --- app/static/scss/layouts/_moo.scss | 2 ++ app/templates/wefe/steps/moo_setup.html | 9 ++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/static/scss/layouts/_moo.scss b/app/static/scss/layouts/_moo.scss index 683a502a..c80afca0 100644 --- a/app/static/scss/layouts/_moo.scss +++ b/app/static/scss/layouts/_moo.scss @@ -1,5 +1,7 @@ .padding-1 { padding: 1rem; + width: 35%; + align-self: center; } .center-text { diff --git a/app/templates/wefe/steps/moo_setup.html b/app/templates/wefe/steps/moo_setup.html index 134a66c5..ca7c903c 100644 --- a/app/templates/wefe/steps/moo_setup.html +++ b/app/templates/wefe/steps/moo_setup.html @@ -1,4 +1,5 @@ {% extends 'wefe/steps/step_progression.html' %} +{% load crispy_forms_tags %} {% load i18n %} {% block progression_content %} @@ -10,13 +11,7 @@
{% csrf_token %}
- {{ form.total_cost.label }}{{ form.total_cost }} - {{ form.co2_emissions.label }}{{ form.co2_emissions }} - {{ form.land_requirements.label }}{{ form.land_requirements }} - {{ form.water_footprint.label }}{{ form.water_footprint }} -
-
- {{ form.non_field_errors }} + {{ form | crispy }}
From a8e3892838bffe7ab13b7738df62cb5837f974d6 Mon Sep 17 00:00:00 2001 From: stefansc1 Date: Wed, 3 Dec 2025 09:53:21 +0100 Subject: [PATCH 3/4] moo weights: change to ModelForm --- app/wefe/forms.py | 10 +++++++--- app/wefe/views.py | 8 ++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/wefe/forms.py b/app/wefe/forms.py index 3629e1d8..acddf3ca 100644 --- a/app/wefe/forms.py +++ b/app/wefe/forms.py @@ -9,7 +9,7 @@ from projects.forms import OpenPlanForm, OpenPlanModelForm from projects.models import Project, EconomicData, Scenario from projects.requests import request_exchange_rate -from wefe.models import SurveyQuestion +from wefe.models import MOOWeights, SurveyQuestion from wefe.survey import SURVEY_STRUCTURE, SURVEY_CATEGORIES, TYPE_STRING @@ -369,8 +369,12 @@ def clean(self): return cleaned_data -class MOOForm(forms.Form): +class MOOForm(forms.ModelForm): # multi-objective optimization setup + class Meta: + model = MOOWeights + exclude = ["scenario"] + total_cost = forms.FloatField( min_value=0, max_value=1, initial=1, widget=forms.NumberInput(attrs={'step': 0.1, 'default': 1}) @@ -396,5 +400,5 @@ def clean(self): land = cleaned_data.get("land_requirements") water = cleaned_data.get("water_footprint") if cost is not None and co2 is not None and land is not None and water is not None: - if cost + co2 + land + water != 1: + if round(cost + co2 + land + water, 4) != 1: raise ValidationError("Weights must add up to 1") diff --git a/app/wefe/views.py b/app/wefe/views.py index d54f2268..7b56503a 100644 --- a/app/wefe/views.py +++ b/app/wefe/views.py @@ -510,14 +510,10 @@ def wefe_optimization_weighting(request, proj_id, step_id=STEP_MAPPING["optimiza return render(request, "wefe/steps/moo_setup.html", context) # save form data (1 to 1 relation between scenario and weights) + # weights might already exist -> maybe just update -> saving ModelForm does not work MOOWeights.objects.update_or_create( scenario=scenario, - defaults={ - "total_cost": form.cleaned_data["total_cost"], - "co2_emissions": form.cleaned_data["co2_emissions"], - "land_requirements": form.cleaned_data["land_requirements"], - "water_footprint": form.cleaned_data["water_footprint"], - }, + defaults=form.cleaned_data, ) return HttpResponseRedirect(reverse("wefe_steps", args=[proj_id, step_id + 1])) From a876ca976e84dcd4f0ddde8f9125e3d5100753b6 Mon Sep 17 00:00:00 2001 From: stefansc1 Date: Tue, 4 Nov 2025 14:24:23 +0100 Subject: [PATCH 4/4] multi-objective weights --- app/wefe/migrations/0004_mooweights.py | 2 +- app/wefe/views.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/wefe/migrations/0004_mooweights.py b/app/wefe/migrations/0004_mooweights.py index 4319e696..81034063 100644 --- a/app/wefe/migrations/0004_mooweights.py +++ b/app/wefe/migrations/0004_mooweights.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ("projects", "0025_project_kobo_survey_id_project_kobo_survey_url"), + ("projects", "0027_alter_economicdata_currency"), ("wefe", "0003_wefesimulation"), ] diff --git a/app/wefe/views.py b/app/wefe/views.py index 9fbe876b..5d7d4cad 100644 --- a/app/wefe/views.py +++ b/app/wefe/views.py @@ -8,9 +8,9 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db.models import Q, F, Avg, Max from django.http import JsonResponse -from django.utils.translation import gettext_lazy as _ from django.shortcuts import * from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_http_methods from jsonview.decorators import json_view @@ -35,7 +35,6 @@ from wefe.survey import SURVEY_CATEGORIES, SURVEY_QUESTIONS_CATEGORIES, get_survey_question_by_id import logging - logger = logging.getLogger(__name__)