diff --git a/app/static/scss/layouts/_moo.scss b/app/static/scss/layouts/_moo.scss new file mode 100644 index 00000000..c80afca0 --- /dev/null +++ b/app/static/scss/layouts/_moo.scss @@ -0,0 +1,22 @@ +.padding-1 { + padding: 1rem; + width: 35%; + align-self: center; +} + +.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..ca7c903c --- /dev/null +++ b/app/templates/wefe/steps/moo_setup.html @@ -0,0 +1,51 @@ +{% extends 'wefe/steps/step_progression.html' %} +{% load crispy_forms_tags %} +{% load i18n %} + +{% block progression_content %} +
+
+ + +
+
+ {% csrf_token %} +
+ {{ form | crispy }} +
+
+
+{% 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 2220401f..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 @@ -367,3 +367,38 @@ def clean(self): else: raise ValidationError("This form cannot be blank") return cleaned_data + + +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}) + ) + 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 round(cost + co2 + land + water, 4) != 1: + raise ValidationError("Weights must add up to 1") diff --git a/app/wefe/migrations/0004_mooweights.py b/app/wefe/migrations/0004_mooweights.py new file mode 100644 index 00000000..81034063 --- /dev/null +++ b/app/wefe/migrations/0004_mooweights.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.3 on 2025-11-04 14:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0027_alter_economicdata_currency"), + ("wefe", "0003_wefesimulation"), + ] + + 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 1a670d40..fe552358 100644 --- a/app/wefe/models.py +++ b/app/wefe/models.py @@ -80,3 +80,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 c00dc450..5d7d4cad 100644 --- a/app/wefe/views.py +++ b/app/wefe/views.py @@ -5,27 +5,26 @@ from django.conf import settings from django.contrib import messages 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 jsonview.decorators import json_view -from epa.settings import WEFESIM_GET_URL -from projects.constants import DONE, ERROR -from .forms import * -from .helpers import * from business_model.forms import * +from business_model.models import * +from projects.constants import DONE, ERROR +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 business_model.models import * -from projects.forms import UploadFileForm, ProjectShareForm, ProjectRevokeForm, UseCaseForm -from wefe.models import SurveyAnswer, WEFESimulation +from wefe.forms import * +from wefe.helpers import * +from wefe.models import MOOWeights, SurveyAnswer, WEFESimulation from wefe.requests import ( fetch_wefedemand_simulation_results, wefedemand_simulation_request, @@ -36,8 +35,6 @@ from wefe.survey import SURVEY_CATEGORIES, SURVEY_QUESTIONS_CATEGORIES, get_survey_question_by_id import logging - - logger = logging.getLogger(__name__) @@ -597,10 +594,28 @@ 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) + # weights might already exist -> maybe just update -> saving ModelForm does not work + MOOWeights.objects.update_or_create( + scenario=scenario, + defaults=form.cleaned_data, + ) return HttpResponseRedirect(reverse("wefe_steps", args=[proj_id, step_id + 1])) @@ -626,7 +641,7 @@ def wefe_simulation(request, proj_id, step_id=STEP_MAPPING["simulation"]): "step_id": step_id, "step_list": WEFE_STEP_VERBOSE, "page_information": page_information, - "WEFESIM_GET_URL": WEFESIM_GET_URL, + "WEFESIM_GET_URL": settings.WEFESIM_GET_URL, } qs = WEFESimulation.objects.filter(scenario=project.scenario, app="wefesim")