Skip to content

Comments

PV with Profiles#399

Open
tolga-akan wants to merge 25 commits intomainfrom
pv-with-profile
Open

PV with Profiles#399
tolga-akan wants to merge 25 commits intomainfrom
pv-with-profile

Conversation

@tolga-akan
Copy link
Collaborator

@tolga-akan tolga-akan commented Jan 12, 2026

DONE:

  • Check if maximum_electricity_source is already availabe for solar_PV asset
  • Maximum profile constraint for PV asset is taken into account in solar_PV sizing similar to heat producer sizing with maximum profile constraint
  • Test is added for solar_PV asset with maximum profile constraint is defined via csv
  • Test that profile constraint is scaled by the asset capacity.

@tolga-akan tolga-akan changed the title initial commit PV with Profiles Jan 12, 2026
@tolga-akan tolga-akan marked this pull request as ready for review January 28, 2026 10:48
@tolga-akan
Copy link
Collaborator Author

@FJanssen-TNO Ready for review

Copy link
Collaborator

@FJanssen-TNO FJanssen-TNO left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't checked every thing yet, but here are some pointers

Comment on lines 50 to 52
for source in optimization_problem.energy_system_components.get("electricity_source", []):
if "PV_without_profile" in source:
sum = optimization_problem.state(f"{source}.Electricity_source")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you only want electricity production of pv added to the function, then you should only loop over over the pv asset.:
optimization_problem.energy_system_components.get("solarpv", [])

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the test is modified. There is no PV w/o profile constraint (but instead Electricity Producer). Hence, I only want electricity production of "Electricity Producer" added to the function. "Electricity Producer" has no component_subtype. Hence, I still keep if condition (if "ElectricityProducer_edde" in source:) after the for loop.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This for loop over all e-producers, but one with a positive and the other a negative sum is confusing. You could just loop over the e-production assets and only add to the sum if the asset is not solar_pv. Either solar_pv is a subgroup and you can check if it is not in that or the solarpv asset is already not included in this electricity_source group.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. my previous comment was outdated (I forgot to update that comment after I have modified the objective function. Hence, that comment does not explain what I did back then). Back then I actually intentionally made contribution of pv to objective function negative and electric generator positive (in that moment I thought it might be better to understand the aim of the objective function). But actually only using electric generator was also enough. Now, I made a new update in the objective function, i only take into account electricity producer in that part of the objective function.

Comment on lines 59 to 62
np.testing.assert_allclose(
results["PV__max_size"],
max(solution.get_timeseries("PV.maximum_electricity_source").values[1:]),
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually want to see that it can scale the profile.

Copy link
Collaborator Author

@tolga-akan tolga-akan Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the objective function in a way that we ensure that constraint profile sizing in asset_sizing works as expected

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm missing a check that the PV is not sized to its max size, but smaller.

(np_ones * max_power - electricity_source) / constraint_nominal,
0.0,
np.inf,
if (d in self.energy_system_components.get("solar_pv", [])) and (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we only want to allow this in case the asset is a solar_pv?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR was only related to solar_pv, I intentionally did not want to allow all electricity producers (other component subtypes of electricity_source). Otherwise, I would needed to add tests for the other electricity producers with profile constraints. If you like I can remove solar_pv condition and make this sizing valid for all electricity producers.

@tolga-akan
Copy link
Collaborator Author

@FJanssen-TNO Ready for review

@tolga-akan
Copy link
Collaborator Author

@FJanssen-TNO ready for the review

Comment on lines 92 to 93
for source in self.energy_system_components["electricity_source"]:
goals.append(MinimizeElecProductionSize(source, total_demand))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The total_demand seems unnecessary. You don't need a target here.
This goal should even not be a path goal but a normal goal. So you should move it to
def goals()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I agree. Constraint on max_size should not be done via path_goals() but goals().

Comment on lines 55 to 68
class MinimizeElecProductionSize(Goal):
priority = 2

order = 2

def __init__(self, source, total_demand):
self.source = source
self.target_min = total_demand
self.target_max = total_demand
self.function_range = (0.0, 2.0 * max(total_demand.values))
self.function_nominal = np.median(total_demand.values)

def function(self, optimization_problem, ensemble_member):
return optimization_problem.state(f"{self.source}__max_size") * 2.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a "normal goal". No target is assigned for sizing variable.
Furthermore in the function itself you also multiply it with "2" what is the reason for that?
f"{self.source}__max_size" is not state variable, it is a variable that only exists once. You have to extract it iusing optimization_problem.extra_variable()

if total_demand is None:
total_demand = target.copy() if hasattr(target, "copy") else target
else:
total_demand += target
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The total_demand is unnecessary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the total_demand vector, but I still use maximum of the electricity demand to define self.function_nominal in MinimizeElecProductionSize.

Comment on lines 71 to 80
class _GoalsAndOptionsPV:
def path_goals(self):
"""
Add goal to meet the specified power demands in the electricity network.

Returns
-------
Extended goals list.
"""
goals = super().path_goals().copy()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of another _GoalsAndOptions, check what can already be placed in the orginal one and else add them to the problem directly instead of creating a new class, there is no need for that right now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Makes it cleaner.

np.testing.assert_allclose(
results["PV__max_size"],
max(solution.get_timeseries("PV.maximum_electricity_source").values[1:]),
results["PV.Electricity_source"], profile_scaled * results["PV__max_size"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some checks are still missing like, check that the max PV size is actually the same as the max demand and thus much smaller than the optional size.
Also this check shouldn't be an all close but smaller than.

Comment on lines 2047 to 2062
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire if/else statement could be placed in a generic function instead of only the cap of the profile. All relevant information is already pass on.

self.source = source
self.target_min = 1e-6
self.function_range = (0.0, 2.0 * nominal)
self.function_nominal = nominal
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function_nominal has to be the same for all assets --> as otherwise the one assetsize is scaled larger/smaller than the other, thereby the optimization is not exactly on their size. If the sizes are in the magnitude of MW, then the function_nominal could be set to 1e6.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now function_nominal is defined same for all assets and equal to 1e6

def __init__(self, source, nominal):
self.source = source
self.target_min = 1e-6
self.function_range = (0.0, 2.0 * nominal)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function_range is typically set to 2*max value.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now it is nor nominal but max_value

Comment on lines 129 to 132
if source not in self.energy_system_components.get("solar_pv", []):
nominal = float(self.bounds()[f"{source}.Electricity_source"][1]) / 2.0
else:
nominal = max(self.bounds()[f"{source}.Electricity_source"][1].values) / 2.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the information you pass on can be:

Suggested change
if source not in self.energy_system_components.get("solar_pv", []):
nominal = float(self.bounds()[f"{source}.Electricity_source"][1]) / 2.0
else:
nominal = max(self.bounds()[f"{source}.Electricity_source"][1].values) / 2.0
max_val = self.bounds()[f"{source}.Electricity_source"][1]

which is indep of asset type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now I call the max_value. but I still keep if/else conditions. Because, on electricty producer has a time Reries as upper bound other has just a scalar

max_profile_non_scaled = max(profile_non_scaled)
profile_scaled = profile_non_scaled / max_profile_non_scaled

# Cap the electricity produced via a profile. Two profile options below.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Cap the electricity produced via a profile. Two profile options below.
# Cap the energy produced via a profile. Two profile options below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

word is replaced

Comment on lines 1861 to 1867
self,
asset,
variable_suffix, # i.e. maximum_electricity_source, maximum_heat_source
source, # i.e. heat_source, energy source
max_power, # i.e. max_heat, max_power
constraint_nominal,
ensemble_member,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typehinting should be added here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typehinting is now done as how you explained in the comment

Comment on lines +119 to +126
"""
Add goal to minimize max_size of electricity producers while ensuring
that they are equal to each other.

Returns
-------
Extended goals list.
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments from these goals are removed

)
results = solution.extract_results()

# Test demand matching
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is unnecessary, the demand_matching_test name is already describing it. same for electric_power_conservation_test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is removed


def __init__(self, source, nominal):
self.source = source
self.target_min = 1e-6
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you put in this target_min?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

order = 2 required to pass a target_min or target_max. I thought max value of self.function_range should be equal to self.target_max but as I defined both equal to each other I got error indicating that self.target_max should be smllaer than the upper bound of self.function_range. Hence, I decided to not to define self.target_max but self.target_min. But now I define self.target_max = max_value, whereas upper bound of self.function_range as 2*max_value. Hence, this is better now

Comment on lines 66 to 70
np.testing.assert_allclose(
solution.get_timeseries("ElectricityDemand_2af6.target_electricity_demand").values,
results["ElectricityProducer_edde.Electricity_source"]
+ results["PV.Electricity_source"],
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is unnecessary as it is already part of the electrical_power_conservation_test

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment on lines 59 to 62
np.testing.assert_allclose(
results["PV__max_size"],
max(solution.get_timeseries("PV.maximum_electricity_source").values[1:]),
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm missing a check that the PV is not sized to its max size, but smaller.

@tolga-akan
Copy link
Collaborator Author

@FJanssen-TNO I did the modification based on PR review. But, still I have issue with the problem formulation. The objective functions do not constraint the the problem in a way we want. For instance, I used "MinimizeElecProductionSize" to make PV max size equal to Electricity producer max size. But it does not work. results["ElectricityProducer_edde__max_size"] is not equal to max(results["ElectricityProducer_edde.Electricity_source"]). Also other checks that I placed inthe test does not pass. Can you have a look at the branch?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants