Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/" # Location of package manifests
target-branch: "develop"
schedule:
interval: "monthly"
labels:
- "package"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration-workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Run ruff
run: |
ruff check .
ruff format
ruff format --check
- name: Run tests and collect coverage
run: |
# -rA displays the captured output for all tests after they're run
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/deploy-pages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ jobs:

# Build the book
- name: Build the book
working-directory: ${{runner.workspace}}/hycon/docs/
run: |
jupyter-book build .
jupyter-book build docs/

# Push the book's HTML to github-pages
- name: GitHub Pages action
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
BSD 3-Clause License

Copyright (c) 2025 Alliance for Sustainable Energy, LLC and Colorado School of Mines.
Copyright (c) 2025 Alliance for Energy Innovation, LLC and Colorado School of Mines.

Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@ and creates an entry point for the development of more advanced controllers.

Hycon will interface with various simulation testbeds and lower level
controllers, including:
- [Hercules](https://github.com/NREL/hercules)
- [Hercules](https://github.com/NatLabRockies/hercules)
- [FAST.Farm](https://github.com/OpenFAST/openfast)
- [ROSCO](https://github.com/NREL/rosco)
- [ROSCO](https://github.com/NatLabRockies/rosco)

Hycon controllers will also call on design tools such as
[FLORIS](https://github.com/NREL/floris).
[FLORIS](https://github.com/NatLabRockies/floris).

## WETO software

Hycon is primarily developed with the support from the U.S. Department of Energy and
is part of the [WETO Software Stack](https://nrel.github.io/WETOStack).
is part of the [WETO Software Stack](https://natlabrockies.github.io/WETOStack).
For more information and other integrated modeling software, see:

- [Portfolio Overview](https://nrel.github.io/WETOStack/portfolio_analysis/overview.html)
- [Entry Guide](https://nrel.github.io/WETOStack/_static/entry_guide/index.html)
- [Portfolio Overview](https://natlabrockies.github.io/WETOStack/portfolio_analysis/overview.html)
- [Entry Guide](https://natlabrockies.github.io/WETOStack/_static/entry_guide/index.html)
- [Wind Farm Controls Workshop](https://www.youtube.com/watch?v=f-w6whxIBrA&list=PL6ksUtsZI1dwRXeWFCmJT6cEN1xijsHJz)

NLR's software record for Hycon is SWR-25-54.

## Documentation

Documentation for Hycon, including installation instructions, can be found
[here](https://nrel.github.io/hycon/intro.html).
[here](https://natlabrockies.github.io/hycon/intro.html).
2 changes: 1 addition & 1 deletion docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ bibtex_bibfiles:

# Information about where the book exists on the web
repository:
url: https://github.com/NREL/hycon
url: https://github.com/NatLabRockies/hycon
path_to_book: docs
branch: main

Expand Down
2 changes: 1 addition & 1 deletion docs/code_development.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Code development
To contribute to Hycon, please consider forking the main github repository,
with the [NLR repo](https://github.com/NREL/hycon) as an
with the [NLR repo](https://github.com/NatLabRockies/hycon) as an
upstream remote. See the [Installation instructions](install_instructions)
for details about how to set up your repository as a developer.

Expand Down
3 changes: 1 addition & 2 deletions docs/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ However, is a useful comparison case for the WindFarmPowerTrackingController
Closed-loop wind farm-level power controller that distributes a farm-level
power reference among the wind turbines in a farm and adjusts the requests made
from each turbine depending on whether the power reference has been met.
Developed under the [A2e2g project](https://github.com/NREL/a2e2g), with
further details provided in
Further details provided in
[Sinner et al.](https://pubs.aip.org/aip/jrse/article/15/5/053304/2913100).

Integral action, as well as gain scheduling based on turbine saturation, has been disabled as
Expand Down
12 changes: 6 additions & 6 deletions docs/install_instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

Hycon is _not_ designed to be used as a stand-alone package. Most likely,
you'll want to add Hycon to an existing conda environment that contains your
simulation testbed, such as [Hercules](https://github.com/NREL/hercules).
simulation testbed, such as [Hercules](https://github.com/NatLabRockies/hercules).
For example, see the
[Hercules installation instructions](https://nrel.github.io/hercules/install_instructions.html)
[Hercules installation instructions](https://natlabrockies.github.io/hercules/install_instructions.html)
for how to set up an appropriate conda environment.

(installation_general_users)=
Expand All @@ -16,7 +16,7 @@ be sufficient to install Hycon (presumably, after activating your conda
environment):

```
git clone https://github.com/NREL/hycon
git clone https://github.com/NatLabRockies/hycon
pip install hycon/
```

Expand All @@ -32,7 +32,7 @@ git clone https://github.com/your-github-id/hycon
pip install -e "hycon/[develop]"
```
To contribute back to the base repository
https://github.com/NREL/hycon, please do the following:
https://github.com/NatLabRockies/hycon, please do the following:
- Create a branch from the base repository's `develop` branch on your fork
containing your code changes (e.g. `your-github-id:feature/your-new-feature`)
- Open a pull request into the base repository's `NREL:develop` branch, and provide
Expand All @@ -49,7 +49,7 @@ For more information on what your pull request should contain, see
(installation_examples)=
## To run examples

All Hycon examples run in the [Hercules](https://github.com/NREL/hercules) simulation environment.
All Hycon examples run in the [Hercules](https://github.com/NatLabRockies/hercules) simulation environment.
To run the examples, you will need to additionally install Hercules. See the
[Hercules installation instructions](https://nrel.github.io/hercules/install_instructions.html)
[Hercules installation instructions](https://natlabrockies.github.io/hercules/install_instructions.html)
for details.
4 changes: 2 additions & 2 deletions docs/wake_steering_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
The `hycon.design_tools.wake_steering_design` module provides various tools for the design of yaw
offset lookup tables for "open-loop" wake steering. The two primary functions are `build_simple_wake_steering_lookup_table` and `build_uncertain_wake_steering_lookup_table`, both of
which take an instantiated
[`FlorisModel`](https://nrel.github.io/floris/_autosummary/floris.floris_model.html),
[`FlorisModel`](https://natlabrockies.github.io/floris/_autosummary/floris.floris_model.html),
along with various design parameters, and return a pandas DataFrame `df_opt` containing the optimal
yaw offset angles for each wind turbine. Under the hood, both functions run an optimization using
FLORIS'
[`YawOptimizerSR`](https://nrel.github.io/floris/_autosummary/floris.optimization.yaw_optimization.yaw_optimizer_sr.html) methodology. The `uncertain` version takes into account wind direction
[`YawOptimizerSR`](https://natlabrockies.github.io/floris/_autosummary/floris.optimization.yaw_optimization.yaw_optimizer_sr.html) methodology. The `uncertain` version takes into account wind direction
uncertainty via the second required argument `wd_std`, representing the wind direction standard
deviation.

Expand Down
81 changes: 46 additions & 35 deletions examples/battery_control_comparison/runscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
df = pd.read_csv("../example_inputs/lmp_rt.csv")
df = df.rename(columns={"interval_start_utc": "time_utc"}).drop(columns=["market", "lmp"])
# Create reference that steps up and down each five minutes
reference_input_sequence = np.tile(np.array([20000, 0]), int(len(df)/2))
reference_input_sequence = np.tile(np.array([20000, 0]), int(len(df) / 2))
df["battery_power_reference"] = reference_input_sequence
# Add end of step info
df["time_utc"] = pd.to_datetime(df["time_utc"])
Expand All @@ -26,9 +26,9 @@
df = pd.merge(df, df_2, how="outer").sort_values("time_utc").reset_index(drop=True)
df.to_csv("power_reference.csv", index=False)


# Create some functions for simulating for simplicity
def simulate(soc_0, clipping_thresholds, gain):

h_dict = load_hercules_input("hercules_input.yaml")
h_dict["battery"]["initial_conditions"]["SOC"] = soc_0

Expand All @@ -42,11 +42,9 @@ def simulate(soc_0, clipping_thresholds, gain):
controller_parameters={"k_batt": gain, "clipping_thresholds": clipping_thresholds},
)
controller = HybridSupervisoryControllerMultiRef(
battery_controller=battery_controller,
interface=interface,
input_dict=hmodel.h_dict
battery_controller=battery_controller, interface=interface, input_dict=hmodel.h_dict
)

hmodel.assign_controller(controller)

# Run the simulation
Expand All @@ -61,16 +59,19 @@ def simulate(soc_0, clipping_thresholds, gain):

return time, power_sequence, soc_sequence, reference_sequence


def plot_results_soc(ax, color, time, power_sequence, soc_sequence):
ax[0].plot(time, power_sequence, color=color,
label="SOC initial: {:.3f}".format(soc_sequence[0]))
ax[0].plot(
time, power_sequence, color=color, label="SOC initial: {:.3f}".format(soc_sequence[0])
)
ax[1].plot(time, soc_sequence, color=color, label="SOC")


def plot_results_gain(ax, color, time, power_sequence, soc_sequence, gain):
ax[0].plot(time, power_sequence, color=color,
label="Gain: {:.3f}".format(gain))
ax[0].plot(time, power_sequence, color=color, label="Gain: {:.3f}".format(gain))
ax[1].plot(time, soc_sequence, color=color, label="SOC")


### SOC clipping

# Establish simulation options for demonstrating SOC clipping
Expand All @@ -79,66 +80,76 @@ def plot_results_gain(ax, color, time, power_sequence, soc_sequence, gain):
clipping_thresholds = [0.1, 0.2, 0.8, 0.9]

# Run simulations and create plots for SOC clipping
fig, ax = plt.subplots(2,1,sharex=True)
fig.set_size_inches(10,5)
fig, ax = plt.subplots(2, 1, sharex=True)
fig.set_size_inches(10, 5)
for soc_0, col in zip(starting_socs, colors):
time, pow, soc, ref = simulate(soc_0, clipping_thresholds, 0.01)
plot_results_soc(ax, col, time/60, pow, soc)
plot_results_soc(ax, col, time / 60, pow, soc)

# Add references and plot aesthetics
ax[0].plot(time/60, ref, color="black", linestyle="dashed", label="Reference")
ax[0].plot(time / 60, ref, color="black", linestyle="dashed", label="Reference")
ax[0].set_ylabel("Power [kW]")
ax[0].legend()

ax[1].set_ylabel("SOC [-]")
ax[1].set_xlabel("Time [min]")
ax[1].set_xlim([time[0]/60, time[-1]/60])
ax[1].set_xlim([time[0] / 60, time[-1] / 60])
ax[0].grid()
ax[1].grid()
ax[0].plot([time[0]/60, time[-1]/60], [20000, 20000], color="black", linestyle="dotted")
ax[0].plot([time[0]/60, time[-1]/60], [-20000, -20000], color="black", linestyle="dotted")
ax[0].plot([time[0] / 60, time[-1] / 60], [20000, 20000], color="black", linestyle="dotted")
ax[0].plot([time[0] / 60, time[-1] / 60], [-20000, -20000], color="black", linestyle="dotted")

# Add shading for the different clipping regions
ax[1].fill_between(time/60, 0, clipping_thresholds[0], color="black", alpha=0.2, edgecolor=None)
ax[1].fill_between(time/60, clipping_thresholds[0], clipping_thresholds[1], color="black",
alpha=0.1, edgecolor=None)
ax[1].fill_between(time/60, clipping_thresholds[2], clipping_thresholds[3], color="black",
alpha=0.1, edgecolor=None)
ax[1].fill_between(time/60, clipping_thresholds[3], 1, color="black", alpha=0.2, edgecolor=None)
ax[1].set_ylim([0,1])
ax[1].fill_between(time / 60, 0, clipping_thresholds[0], color="black", alpha=0.2, edgecolor=None)
ax[1].fill_between(
time / 60,
clipping_thresholds[0],
clipping_thresholds[1],
color="black",
alpha=0.1,
edgecolor=None,
)
ax[1].fill_between(
time / 60,
clipping_thresholds[2],
clipping_thresholds[3],
color="black",
alpha=0.1,
edgecolor=None,
)
ax[1].fill_between(time / 60, clipping_thresholds[3], 1, color="black", alpha=0.2, edgecolor=None)
ax[1].set_ylim([0, 1])
if save_figs:
fig.savefig(
"../../docs/graphics/battery-soc-clipping.png",
format="png", bbox_inches="tight", dpi=300
"../../docs/graphics/battery-soc-clipping.png", format="png", bbox_inches="tight", dpi=300
)

### k_batt gain

# Demonstrate different gains
gains = [0.001, 0.01, 0.1]
fig, ax = plt.subplots(2,1,sharex=True)
fig.set_size_inches(10,5)
fig, ax = plt.subplots(2, 1, sharex=True)
fig.set_size_inches(10, 5)
for gain, col in zip(gains, colors):
time, pow, soc, ref = simulate(0.5, clipping_thresholds, gain)
plot_results_gain(ax, col, time/60, pow, soc, gain)
plot_results_gain(ax, col, time / 60, pow, soc, gain)

# Add references and plot aesthetics
ax[0].plot(time/60, ref, color="black", linestyle="dashed", label="Reference")
ax[0].plot(time / 60, ref, color="black", linestyle="dashed", label="Reference")
ax[0].set_ylabel("Power [kW]")
ax[0].legend()

ax[1].set_ylabel("SOC [-]")
ax[1].set_xlabel("Time [min]")
ax[1].set_xlim([0, 15]) # Show only the first 15 to highlight differences
ax[1].set_xlim([0, 15]) # Show only the first 15 to highlight differences
ax[1].set_ylim([0.45, 0.51])
ax[0].grid()
ax[1].grid()
ax[0].plot([time[0]/60, time[-1]/60], [20000, 20000], color="black", linestyle="dotted")
ax[0].plot([time[0]/60, time[-1]/60], [-20000, -20000], color="black", linestyle="dotted")
ax[0].plot([time[0] / 60, time[-1] / 60], [20000, 20000], color="black", linestyle="dotted")
ax[0].plot([time[0] / 60, time[-1] / 60], [-20000, -20000], color="black", linestyle="dotted")
if save_figs:
fig.savefig(
"../../docs/graphics/battery-varying-gains.png",
format="png", bbox_inches="tight", dpi=300
"../../docs/graphics/battery-varying-gains.png", format="png", bbox_inches="tight", dpi=300
)

plt.show()
17 changes: 5 additions & 12 deletions examples/battery_market_revenue_control/plot_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,14 @@ def plot_outputs():
ax = axarr[1]
ax.plot(
df["time"],
df["battery.power"]/1e3, # Base unit: kW
df["battery.power"] / 1e3, # Base unit: kW
label="Battery output",
color="k",
linewidth=1.0,
)
ax.plot(
df["time"],
df["battery.power_setpoint"]/1e3, # Base unit: kW
df["battery.power_setpoint"] / 1e3, # Base unit: kW
label="Battery setpoint",
color="k",
linestyle=":",
Expand All @@ -129,27 +129,20 @@ def plot_outputs():

color = "C0"
ax2.set_ylabel("State of charge [-]", color=color)
ax2.plot(
df["time"],
df["battery.soc"],
color=color
)
ax2.plot(df["time"], df["battery.soc"], color=color)
ax2.tick_params(axis="y", labelcolor=color)

for ax in axarr:
ax.grid(True)
ax.legend(loc="upper right")

# Compute total revenue on real-time market
df["revenue_rt"] = (
df["battery.power"]/1e3
* df["external_signals.lmp_rt"]
/ 3600
)
df["revenue_rt"] = df["battery.power"] / 1e3 * df["external_signals.lmp_rt"] / 3600
print("Real-time revenue over simulation: ${:.1f}".format(df["revenue_rt"].sum()))

return fig


if __name__ == "__main__":
fig = plot_outputs()
# fig.savefig("../../docs/graphics/battery-market.png", dpi=300, format="png")
Expand Down
9 changes: 3 additions & 6 deletions examples/battery_market_revenue_control/runscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,17 @@

# Generate the LMP data needed for the simulation
df_lmp = generate_locational_marginal_price_dataframe_from_gridstatus(
pd.read_csv("../example_inputs/lmp_da.csv"),
pd.read_csv("../example_inputs/lmp_rt.csv")
pd.read_csv("../example_inputs/lmp_da.csv"), pd.read_csv("../example_inputs/lmp_rt.csv")
)
df_lmp.to_csv("lmp_data.csv", index=False)

# Load the input file and establish the Hercules model
hmodel = HerculesModel("hercules_input.yaml")

# Establish the interface and controller, assign to the Hercules model
interface=HerculesInterface(hmodel.h_dict)
interface = HerculesInterface(hmodel.h_dict)
controller = HybridSupervisoryControllerMultiRef(
battery_controller=BatteryPriceSOCController(
interface=interface, input_dict=hmodel.h_dict
),
battery_controller=BatteryPriceSOCController(interface=interface, input_dict=hmodel.h_dict),
interface=HerculesInterface(hmodel.h_dict),
input_dict=hmodel.h_dict,
)
Expand Down
Loading