calliope
calliope copied to clipboard
Problems modelling PV/T panels
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
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)
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.
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.
In the meantime, setting up multiple technologies that represent a single technology is the way to go.
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
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