Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problems modelling PV/T panels #521

Open
yiqiaowang-arch opened this issue Dec 4, 2023 · 5 comments
Open

Problems modelling PV/T panels #521

yiqiaowang-arch opened this issue Dec 4, 2023 · 5 comments
Labels
v0.6 Version 0.6

Comments

@yiqiaowang-arch
Copy link

yiqiaowang-arch commented Dec 4, 2023

Problem description

PV/T panels generate heat and electricity at the same time, but their yield is not correlated via a constant. For this, I prepared two dataframes, one for electricity generation, another one for heat generation.
I need to use supply_plus as parent of my PV/T panels, because the total area is limited and PV/T has to compete with other solar technologies (PV, Solar Collector), and conversion_plus doesn't allow the constraint of resource_unit: energy_per_area.
However, with supply_plus, I cannot define a different heat yield, creating an inaccuracy of the model.

Steps to reproduce the problem

Here's my configuration of PV/T panel in yaml, where I set the input resource to be the same as electricity yield (supply_PVT_e) and set the electricity output to 1, so it always gives me the correct electricity yield; for heat output, I calculated the relative ratio of heat comparing to electricity.
For example, if at 12:00 my PVT generates 1kWh electricity and 4kWh heat, and at 13:00 1.5kWh electricity and 5kWh heat, I will need a time-series output ratio for heat, which is 4 at 12:00 and 3.33 at 13:00, and my supply_PVT_h dataframe contains this ratio.

    PVT:
        essentials:
            name: 'PVT'
            color: '#E37A72'
            parent: supply_plus
            carrier_out: electricity
            carrier_out_2: heat
            primary_carrier_out: electricity
        constraints:
            export_carrier: electricity
            resource: df=supply_PVT_e
            resource_unit: energy_per_area
            carrier_ratios:
                carrier_out:
                    electricity: 1
                carrier_out_2:
                    heat: df=supply_PVT_h
            energy_eff: 1
            resource_area_per_energy_cap: 10 # 10m2 per kWp electricity
            lifetime: 15
        costs:
            monetary:
                interest_rate: 0.05
                energy_cap: 2600 # CHF per kW
                om_annual_investment_fraction: 0.01 # fraction of purchase cost
                export: -0.05 # CHF per kWh, feed-in tariff

And this is my calliope error traceback:

---------------------------------------------------------------------------
ModelError                                Traceback (most recent call last)
Cell In[5], line 1
----> 1 model = calliope.Model(building_specific_config, timeseries_dataframes = dict_timeseries_df)

File ~\OneDrive\Experiments\re_optimization\venv\lib\site-packages\calliope\core\model.py:81, in Model.__init__(self, config, model_data, *args, **kwargs)
     79     self._init_from_model_run(model_run, debug_data)
     80 elif isinstance(config, dict):
---> 81     model_run, debug_data = model_run_from_dict(config, *args, **kwargs)
     82     self._init_from_model_run(model_run, debug_data)
     83 elif model_data is not None and config is None:

File ~\OneDrive\Experiments\re_optimization\venv\lib\site-packages\calliope\preprocess\model_run.py:113, in model_run_from_dict(config_dict, timeseries_dataframes, scenario, override_dict)
    107 config.config_path = None
    109 config_with_overrides, debug_comments, overrides, scenario = apply_overrides(
    110     config, scenario=scenario, override_dict=override_dict
    111 )
--> 113 return generate_model_run(
    114     config_with_overrides,
    115     timeseries_dataframes,
    116     debug_comments,
    117     overrides,
    118     scenario,
    119 )

File ~\OneDrive\Experiments\re_optimization\venv\lib\site-packages\calliope\preprocess\model_run.py:742, in generate_model_run(config, timeseries_dataframes, debug_comments, applied_overrides, scenario)
    740 final_check_comments, warning_messages, errors = checks.check_final(model_run)
    741 debug_comments.union(final_check_comments)
--> 742 exceptions.print_warnings_and_raise_errors(warnings=warning_messages, errors=errors)
    744 # 9) Build a debug data dict with comments and the original configs
    745 debug_data = AttrDict(
    746     {
    747         "comments": debug_comments,
    748         "config_initial": config,
    749     }
    750 )

File ~\OneDrive\Experiments\re_optimization\venv\lib\site-packages\calliope\exceptions.py:80, in print_warnings_and_raise_errors(warnings, errors)
     74     warn(
     75         "Possible issues found during model processing:\n"
     76         + textwrap.indent("\n".join(sorted(list(set(warnings)))), " * ")
     77     )
     79 if errors:
---> 80     raise ModelError(
     81         "Errors during model processing:\n"
     82         + textwrap.indent("\n".join(sorted(list(set(errors)))), " * ")
     83     )
     85 return None

ModelError: Errors during model processing:
 * `PVT` at `B162298` defines non-allowed constraint `carrier_ratios`

Calliope version

0.6.10

@yiqiaowang-arch
Copy link
Author

yiqiaowang-arch commented Dec 4, 2023

I couldn't use group constraint here in my PV/T to split them into two technologies, because otherwise they will count as double area.


Speaking of group constraints, I had another similar issue with my geothermal heatpump.
geothermal heatpump can provide either heating or cooling, and it always couples with a geothermal storage.
When heating, it consumes (electricity + storage), produces heating;
when cooling, it consumes electricity, and produces (cooling + storage).

However, according to the modelling of complex conversion technology, I cannot set up a technology that switch between two modes while input/output multiple energy carriers per mode. The logic here is to use AND at the top level, but what I need is to use OR at the top level:
(in: electricity and storage, out: heating) OR (in: electricity, out: cooling and storage)
image

In the end, I set two technologies, one called GSHP_heat, another one is GSHP_cool, and force them to have the same capacity, and set one of them with no cost:

group_constraints:
    GSHP_heat_cooling:
        techs: [GSHP_heat, GSHP_cooling]
        energy_cap_equals: true

techs:
    GSHP_heat:
        essentials:
            name: 'Ground source heat pump mode heating'
            color: '#F9CF22'
            carrier_in: electricity
            carrier_in_2: geothermal_storage
            primary_carrier_in: electricity
            parent: conversion_plus
            carrier_out: heat
        constraints:
            energy_eff: 4.5
            energy_cap_min: 1 # kW
            energy_cap_max: 100000 # kW
            carrier_ratios:
                carrier_in.electricity: 1 # electricity
                carrier_in_2.geothermal_storage: 3.5 # geothermal storage
            lifetime: 25
        costs:
            monetary:
                interest_rate: 0.05
                purchase: 29204 # USD per device
                energy_cap: 750 # USD per kW
                om_annual_investment_fraction: 0.01 # fraction of purchase cost
    GSHP_cooling: # to be defined in group constraints to be strictly coupled with GSHP_heat
        essentials:
            name: 'Ground source heat pump mode cooling'
            color: '#F9CF22'
            carrier_in: electricity
            carrier_out: cooling
            carrier_out_2: geothermal_storage
            primary_carrier_out: cooling
            parent: conversion_plus
        constraints:
            energy_eff: 4
            energy_cap_min: 1 # kW
            energy_cap_max: 100000 # kW
            carrier_ratios:
                carrier_out_2.geothermal_storage: 1.25 # geothermal storage
            lifetime: 25
        costs:
            monetary: # to avoid double counting, only the cost of the heating mode is considered
                interest_rate: 0.05
                purchase: 0 # USD per device
                energy_cap: 0 # USD per kW
                om_annual_investment_fraction: 0 # fraction of purchase cost

It would be nice if I could choose two modes for a technology, which allows more flexibility and better readability in modelling.

@brynpickering
Copy link
Member

You're right that both your use-cases aren't possible, mostly because defining a generalised approach to enable them is quite hard. Instead, we are moving to a simplified approach that you can read more about in #518 and its associated discussions. This would allow you to have a supply technology like a PV/T that has multiple output carriers and to set up the geothermal problem you define. However, you would need to define the math yourself in our new custom math syntax to make it happen.

@brynpickering
Copy link
Member

In the meantime, setting up multiple technologies that represent a single technology is the way to go.

@yiqiaowang-arch
Copy link
Author

yiqiaowang-arch commented Dec 4, 2023

In the meantime, setting up multiple technologies that represent a single technology is the way to go.

Thanks for the quick answer!! I will look into your reference carefully.
I have a following-up question: in the GSHP case, I want my GSHP_heat and GSHP_cooling to be the same in capacity, what kind of group constraint can I use? I browsed through the list of constraints but I'm still not sure if I understand them correctly. Previously, I set my group constraints like this:

group_constraints:
    GSHP_heat_cooling:
        techs: [GSHP_heat, GSHP_cooling]
        energy_cap_equals: true

When I set the group constraint energy_cap_equals, do I need to specify a number? In the discription of this constriant: "Exact installed capacity from a set of technologies across a set of locations." I assume there needs to be a real number rather than a boolean and it will fix both the capacity (in kW) of my GSHP_heat and GSHP_cooling to that number, am I thinking correctly?

Right now my calliope gives me this error when I try to solve the problem (I constructed the model successfully):

ERROR: Constructing component 'group_energy_cap_equals' from data=None failed:
    ValueError: Default value (None) is not valid for Param
    group_energy_cap_equals domain Boolean

and traceback:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[13], line 1
----> 1 model.run()

File ~\miniforge3\envs\calliope\lib\site-packages\calliope\core\model.py:266, in Model.run(self, force_rerun, **kwargs)
    257 if (
    258     self.run_config["mode"] == "operate"
    259     and not self._model_data.attrs["allow_operate_mode"]
    260 ):
    261     raise exceptions.ModelError(
    262         "Unable to run this model in operational mode, probably because "
    263         "there exist non-uniform timesteps (e.g. from time masking)"
    264     )
--> 266 results, self._backend_model, self._backend_model_opt, interface = run_backend(
    267     self._model_data, self._timings, **kwargs
    268 )
    270 # Add additional post-processed result variables to results
    271 if results.attrs.get("termination_condition", None) in ["optimal", "feasible"]:

File ~\miniforge3\envs\calliope\lib\site-packages\calliope\backend\run.py:46, in run(model_data, timings, build_only)
     43 run_config = AttrDict.from_yaml_string(model_data.attrs["run_config"])
     45 if run_config["mode"] == "plan":
---> 46     results, backend, opt = run_plan(
     47         model_data,
     48         timings,
     49         backend=BACKEND[run_config.backend],
     50         build_only=build_only,
     51     )
     53 elif run_config["mode"] == "operate":
     54     results, backend, opt = run_operate(
     55         model_data,
     56         timings,
     57         backend=BACKEND[run_config.backend],
     58         build_only=build_only,
     59     )

File ~\miniforge3\envs\calliope\lib\site-packages\calliope\backend\run.py:88, in run_plan(model_data, timings, backend, build_only, backend_rerun, allow_warmstart, persistent, opt)
     86 warmstart = False
     87 if not backend_rerun:
---> 88     backend_model = backend.generate_model(model_data)
     89     log_time(
     90         logger,
     91         timings,
   (...)
     94         comment="Backend: model generated",
     95     )
     97 else:

File ~\miniforge3\envs\calliope\lib\site-packages\calliope\backend\pyomo\model.py:104, in generate_model(model_data)
    102     else:
    103         dims = [getattr(backend_model, i) for i in model_data_dict["dims"][k]]
--> 104     setattr(backend_model, k, po.Param(*dims, **_kwargs))
    106 for option_name, option_val in backend_model.__calliope_run_config[
    107     "objective_options"
    108 ].items():
    109     if option_name == "cost_class":

File ~\miniforge3\envs\calliope\lib\site-packages\pyomo\core\base\block.py:649, in _BlockData.__setattr__(self, name, val)
    644 if name not in self.__dict__:
    645     if isinstance(val, Component):
    646         #
    647         # Pyomo components are added with the add_component method.
    648         #
--> 649         self.add_component(name, val)
    650     else:
    651         #
    652         # Other Python objects are added with the standard __setattr__
    653         # method.
    654         #
    655         super(_BlockData, self).__setattr__(name, val)

File ~\miniforge3\envs\calliope\lib\site-packages\pyomo\core\base\block.py:1219, in _BlockData.add_component(self, name, val)
   1215     logger.debug("Constructing %s '%s' on %s from data=%s",
   1216                  val.__class__.__name__, name,
   1217                  _blockName, str(data))
   1218 try:
-> 1219     val.construct(data)
   1220 except:
   1221     err = sys.exc_info()[1]

File ~\miniforge3\envs\calliope\lib\site-packages\pyomo\core\base\param.py:745, in Param.construct(self, data)
    741 val = self._default_val
    742 if val is not Param.NoValue \
    743    and type(val) in native_types \
    744    and val not in self.domain:
--> 745     raise ValueError(
    746         "Default value (%s) is not valid for Param %s domain %s" %
    747         (str(val), self.name, self.domain.name))
    748 #
    749 # Flag that we are in the "during construction" phase
    750 #
    751 self._constructed = None

ValueError: Default value (None) is not valid for Param group_energy_cap_equals domain Boolean

@brynpickering
Copy link
Member

To fix the capacity of the two technologies (that really represent one technology), you'll need to add your own constraint to the model.

See here

@brynpickering brynpickering added the v0.6 Version 0.6 label Jan 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
v0.6 Version 0.6
Projects
None yet
Development

No branches or pull requests

2 participants