Skip to content

Commit

Permalink
Merge pull request #349 from kbrunik/develop-merge
Browse files Browse the repository at this point in the history
Backmerge develop into dev/refactor
  • Loading branch information
kbrunik authored Sep 27, 2024
2 parents 94282b8 + 1a7b7b9 commit ea35682
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 53 deletions.
81 changes: 44 additions & 37 deletions examples/04-load-following-battery.ipynb

Large diffs are not rendered by default.

41 changes: 29 additions & 12 deletions hopp/simulation/technologies/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,29 +118,46 @@ def simulate_grid_connection(
"""
if self.site.follow_desired_schedule:
# Desired schedule sets the upper bound of the system output, any over generation is curtailed
lifetime_schedule: NDArrayFloat = np.tile([
x * 1e3 for x in self.site.desired_schedule],
int(project_life / (len(self.site.desired_schedule) // self.site.n_timesteps))
)
self.generation_profile = list(np.minimum(total_gen, lifetime_schedule)) # TODO: remove list() cast once parent class uses numpy

self.missed_load = np.array([schedule - gen if gen > 0 else schedule for (schedule, gen) in
zip(lifetime_schedule, self.generation_profile)])
self.missed_load_percentage = sum(self.missed_load)/sum(lifetime_schedule)

if self.site.curtailment_value_type == "interconnect_kw":
lifetime_schedule: NDArrayFloat = np.tile([self.interconnect_kw],
len(total_gen))
desired_schedule = np.tile(
[x * 1e3 for x in self.site.desired_schedule],
int(project_life / (len(self.site.desired_schedule) // self.site.n_timesteps))
)
elif self.site.curtailment_value_type == "desired_schedule":
lifetime_schedule: NDArrayFloat = np.tile([
x * 1e3 for x in self.site.desired_schedule],
int(project_life / (len(self.site.desired_schedule) // self.site.n_timesteps))
)
desired_schedule = lifetime_schedule

# Generate the final generation profile by curtailing over-generation
self.generation_profile = np.minimum(total_gen, lifetime_schedule)

# Calculate missed load and missed load percentage
self.missed_load = np.array([
max(schedule - gen, 0)
for schedule, gen in zip(desired_schedule, self.generation_profile)
])
self.missed_load_percentage = sum(self.missed_load)/sum(desired_schedule)

# Calculate curtailed schedule and curtailed schedule percentage
self.schedule_curtailed = np.array([gen - schedule if gen > schedule else 0. for (gen, schedule) in
zip(total_gen, lifetime_schedule)])
self.schedule_curtailed_percentage = sum(self.schedule_curtailed)/sum(lifetime_schedule)

# NOTE: This is currently only happening for load following, would be good to make it more general
# i.e. so that this analysis can be used when load following isn't being used (without storage)
# for comparison
# Hybrid power production for load following
N_hybrid = len(self.generation_profile)

final_power_production = total_gen
schedule = [x for x in lifetime_schedule]
schedule = [x for x in desired_schedule]
hybrid_power = [(final_power_production[x] - (schedule[x]*0.95)) for x in range(len(final_power_production))]

# Count the instances where load is met
load_met = len([i for i in hybrid_power if i >= 0])
self.time_load_met = 100 * load_met/N_hybrid

Expand Down Expand Up @@ -189,7 +206,7 @@ def simulate_grid_connection(

logger.info('Total number of hours available for ERS: ', np.round(self.total_number_hours,2))
else:
self.generation_profile = total_gen
self.generation_profile = total_gen #actual

self.total_gen_max_feasible_year1 = np.array(total_gen_max_feasible_year1)
self.system_capacity_kw = hybrid_size_kw # TODO: Should this be interconnection limit?
Expand Down
6 changes: 5 additions & 1 deletion hopp/simulation/technologies/sites/site_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class SiteInfo(BaseClass):
hub_height: Turbine hub height for resource download in meters. Defaults to 97.0.
capacity_hours: Boolean list indicating hours for capacity payments. Defaults to [].
desired_schedule: Absolute desired load profile in MWe. Defaults to [].
curtailment_value_type: whether to curtail power above grid interconnection limit or desired schedule.
Options "interconnect_kw" or "desired_schedule". Defaults to "interconnect_kw".
solar: Whether to set solar data for this site. Defaults to True.
wind: Whether to set wind data for this site. Defaults to True.
wave: Whether to set wave data for this site. Defaults to True.
Expand All @@ -66,6 +68,8 @@ class SiteInfo(BaseClass):
hub_height: hopp_float_type = field(default=97., converter=hopp_float_type)
capacity_hours: NDArray = field(default=[], converter=converter(bool))
desired_schedule: NDArrayFloat = field(default=[], converter=converter())
curtailment_value_type: str = field(default="interconnect_kw", validator=contains(["interconnect_kw", "desired_schedule"]))

solar: bool = field(default=True)
wind: bool = field(default=True)
wave: bool = field(default=False)
Expand Down Expand Up @@ -140,7 +144,7 @@ def __attrs_post_init__(self):
if self.wind:
# TODO: allow hub height to be used as an optimization variable
self.wind_resource = WindResource(data['lat'], data['lon'], data['year'], wind_turbine_hub_ht=self.hub_height,
filepath=self.wind_resource_file, source=self.wind_resource_origin)
filepath=self.wind_resource_file, source=self.wind_resource_origin)
n_timesteps = len(self.wind_resource.data['data']) // 8760 * 8760
if self.n_timesteps is None:
self.n_timesteps = n_timesteps
Expand Down
4 changes: 2 additions & 2 deletions tests/greenheart/test_greenheart_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,12 @@ def test_simulation_wind_wave_solar_battery(subtests):

with subtests.test("lcoh"):
# TODO base this test value on something. Currently just based on output at writing.
assert results.lcoh == approx(17.11063907134404, rel=rtol)
assert results.lcoh == approx(17.088961197626638, rel=rtol)

# TODO base this test value on something. Currently just based on output at writing.
with subtests.test("lcoe"):
# TODO base this test value on something. Currently just based on output at writing.
assert results.lcoe == approx(0.1294193054583137, rel=rtol)
assert results.lcoe == approx(0.1288450595098765, rel=rtol)

with subtests.test("no conflict in om cost does not raise warning"):
with warnings.catch_warnings():
Expand Down
35 changes: 34 additions & 1 deletion tests/hopp/test_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ def test_simulate_grid_connection(mock_simulate_power, site, subtests):
with subtests.test("follow desired schedule: curtailment"):
desired_schedule = np.repeat([3], site.n_timesteps)
site2 = create_default_site_info(
desired_schedule=desired_schedule
desired_schedule=desired_schedule,
curtailment_value_type = "desired_schedule"
)
config = GridConfig.from_dict({"interconnect_kw": interconnect_kw})
grid = Grid(site2, config=config)
Expand Down Expand Up @@ -128,3 +129,35 @@ def test_simulate_grid_connection(mock_simulate_power, site, subtests):

assert_array_equal(grid.schedule_curtailed, np.repeat([2000], timesteps))
assert_approx_equal(grid.schedule_curtailed_percentage, 2/3)

with subtests.test("follow desired schedule: curtailment interconnection"):
desired_schedule = np.repeat([3], site.n_timesteps)
site2 = create_default_site_info(
desired_schedule=desired_schedule,
)
config = GridConfig.from_dict({"interconnect_kw": interconnect_kw})
grid = Grid(site2, config=config)
grid.simulate_grid_connection(
hybrid_size_kw,
total_gen,
project_life,
lifetime_sim,
total_gen_max_feasible_year1
)

timesteps = site.n_timesteps * project_life
assert_array_equal(
grid.generation_profile,
np.repeat([5000], timesteps),
"gen profile should be reduced"
)

msg = "no load should be missed"
assert_array_equal(
grid.missed_load,
np.repeat([0], timesteps),
msg
)
assert grid.missed_load_percentage == 0., msg

assert_array_equal(grid.schedule_curtailed, np.repeat([0], timesteps))

0 comments on commit ea35682

Please sign in to comment.