diff --git a/docs/battery.md b/docs/battery.md index befa564a..0ce6d68b 100644 --- a/docs/battery.md +++ b/docs/battery.md @@ -32,6 +32,7 @@ Battery parameters are defined in the hercules input yaml file used to initializ - `usage_calc_interval`: Interval for usage calculations in seconds (BatterySimple only) - `usage_lifetime`: Battery lifetime in years for time-based degradation (BatterySimple only) - `usage_cycles`: Number of cycles until replacement for cycle-based degradation (BatterySimple only) +- `log_channels`: List of output channels to log (see [Logging Configuration](#logging-configuration) below) Once initialized, the battery is only interacted with using the `step` method. @@ -55,12 +56,41 @@ Outputs are returned as a dict containing the following values: - `power`: Actual battery power in kW - `reject`: Rejected power due to constraints in kW (positive when power cannot be absorbed, negative when required power unavailable) - `soc`: Battery state of charge (0-1) +- `power_setpoint`: Requested power setpoint in kW #### Additional Outputs (BatterySimple only when track_usage=True) - `usage_in_time`: Time-based usage percentage - `usage_in_cycles`: Cycle-based usage percentage - `total_cycles`: Total equivalent cycles completed +### Logging Configuration + +The `log_channels` parameter controls which outputs are written to the HDF5 output file. This is a list of channel names. The `power` channel is always logged, even if not explicitly specified. + +**Available Channels:** +- `power`: Actual battery power output in kW (always logged) +- `soc`: State of charge (0-1) +- `power_setpoint`: Requested power setpoint in kW + +**Example:** +```yaml +battery: + component_type: BatterySimple + energy_capacity: 100.0 # kWh + charge_rate: 50.0 # kW + discharge_rate: 50.0 # kW + max_SOC: 0.9 + min_SOC: 0.1 + log_channels: + - power + - soc + - power_setpoint + initial_conditions: + SOC: 0.5 +``` + +If `log_channels` is not specified, only `power` will be logged. + ## `BatterySimple` diff --git a/docs/h_dict.md b/docs/h_dict.md index 3ac499fa..8c6d313f 100644 --- a/docs/h_dict.md +++ b/docs/h_dict.md @@ -31,12 +31,13 @@ The `h_dict` is a Python dictionary that contains all the configurations for eac | **Hybrid Plant Components** | ### Wind Farm (`wind_farm`) -| `component_type` | str | Must be "Wind_MesoToPower" | +| `component_type` | str | Must be "Wind_MesoToPower" or "Wind_MesoToPowerPrecomFloris" | | `floris_input_file` | str | FLORIS input file path | | `wind_input_filename` | str | Wind data input file | | `turbine_file_name` | str | Turbine configuration file | | `log_file_name` | str | Wind farm log file path | -| `logging_option` | str | Logging level: "base", "turb_subset", or "all" | +| `log_channels` | list | List of channels to log (e.g., ["power", "wind_speed_mean_background", "turbine_powers"]) | +| `floris_update_time_s` | float | How often to update FLORIS wake calculations in seconds | ### Solar Farm (`solar_farm`) | `component_type` | str | "SolarPySAMPVWatts" | @@ -48,6 +49,7 @@ The `h_dict` is a Python dictionary that contains all the configurations for eac | `lat` | float | Latitude | | `lon` | float | Longitude | | `elev` | float | Elevation in meters | +| `log_channels` | list | List of channels to log (e.g., ["power", "dni", "poa", "aoi"]) | | `initial_conditions` | dict | Initial power, DNI, POA | ### Battery (`battery`) @@ -61,6 +63,7 @@ The `h_dict` is a Python dictionary that contains all the configurations for eac | `min_SOC` | float | Minimum state of charge (0-1) | Required | | `initial_conditions` | dict | Contains initial SOC | Required | | `allow_grid_power_consumption` | bool | Allow grid power consumption | False | +| `log_channels` | list | List of channels to log (e.g., ["power", "soc", "power_setpoint"]) | ["power"] | | `roundtrip_efficiency` | float | Roundtrip efficiency (BatterySimple only) | 1.0 | | `self_discharge_time_constant` | float | Self-discharge time constant in seconds (BatterySimple only) | inf | | `track_usage` | bool | Enable usage tracking (BatterySimple only) | False | diff --git a/docs/hercules_input.md b/docs/hercules_input.md index ace576af..16270b42 100644 --- a/docs/hercules_input.md +++ b/docs/hercules_input.md @@ -48,7 +48,11 @@ wind_farm: wind_input_filename: inputs/wind_input.csv turbine_file_name: inputs/turbine_filter_model.yaml log_file_name: outputs/log_wind_sim.log - logging_option: all + log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean floris_update_time_s: 30.0 solar_farm: @@ -59,6 +63,11 @@ solar_farm: elev: 1829 system_capacity: 10000 # kW (10 MW) tilt: 0 # degrees + log_channels: + - power + - dni + - poa + - aoi initial_conditions: power: 2000 # kW dni: 1000 @@ -71,6 +80,10 @@ battery: discharge_rate: 50.0 # MW max_SOC: 0.95 min_SOC: 0.05 + log_channels: + - power + - soc + - power_setpoint initial_conditions: SOC: 0.5 @@ -126,7 +139,11 @@ wind_farm: floris_input_file: inputs/floris_input.yaml wind_input_filename: inputs/wind_input.csv turbine_file_name: inputs/turbine_filter_model.yaml - logging_option: all + log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean floris_update_time_s: 30.0 controller: @@ -141,6 +158,6 @@ The `load_hercules_input()` function performs strict validation on input files t - Numeric validation for timing and power parameters - File existence checks for referenced input files - Output configuration validation (`log_every_n` must be a positive integer) -- Component-specific validation (e.g., wind farm `logging_option` must be "base", "turb_subset", or "all") +- Component-specific validation (e.g., `log_channels` must be a list of valid channel names) Invalid configurations will raise descriptive `ValueError` exceptions to help with debugging. diff --git a/docs/output_files.md b/docs/output_files.md index 84f8c951..53fbccc4 100644 --- a/docs/output_files.md +++ b/docs/output_files.md @@ -18,10 +18,18 @@ hercules_output.h5 │ ├── plant_power # Total plant power output │ ├── plant_locally_generated_power # Locally generated power │ ├── components/ -│ │ ├── wind_farm.power # Wind farm power output -│ │ ├── wind_farm.wind_speed # Wind speed at hub height -│ │ ├── solar_farm.power # Solar farm power output -│ │ └── ... # Other component outputs +│ │ ├── wind_farm.power # Wind farm power output +│ │ ├── wind_farm.wind_speed_mean_background # Farm-average background wind speed +│ │ ├── wind_farm.wind_speed_mean_withwakes # Farm-average with-wakes wind speed +│ │ ├── wind_farm.wind_direction_mean # Farm-average wind direction +│ │ ├── wind_farm.turbine_powers.000 # Turbine 0 power (if logged) +│ │ ├── wind_farm.turbine_powers.001 # Turbine 1 power (if logged) +│ │ ├── solar_farm.power # Solar farm power output +│ │ ├── solar_farm.dni # Direct normal irradiance (if logged) +│ │ ├── solar_farm.poa # Plane-of-array irradiance (if logged) +│ │ ├── battery.power # Battery power (if present) +│ │ ├── battery.soc # Battery state of charge (if logged) +│ │ └── ... # Other component outputs │ └── external_signals/ │ └── ... # Other external signals └── metadata/ @@ -60,7 +68,7 @@ from hercules.utilities import read_hercules_hdf5_subset # Read specific columns df_subset = read_hercules_hdf5_subset( "outputs/hercules_output.h5", - columns=["wind_farm.power", "solar_farm.power", "external_signals.wind_speed"] + columns=["wind_farm.power", "wind_farm.wind_speed_mean_background", "solar_farm.power"] ) # Read specific time range (seconds) diff --git a/docs/solar_pv.md b/docs/solar_pv.md index 9c1e0d82..7cb9eac8 100644 --- a/docs/solar_pv.md +++ b/docs/solar_pv.md @@ -35,7 +35,30 @@ The PVWatts model is configured with the following hardcoded parameters for util The array tilt angle must be specified in the input configuration file. -When `log_extra_outputs` is set to `True` in the input .yaml file, the solar modules also output plane-of-array irradiance (`poa`) in W/m^2, direct normal irradiance (`dni`) in W/m^2, and the angle of incidence (`aoi`) in degrees. +### Logging Configuration + +The `log_channels` parameter controls which outputs are written to the HDF5 output file. This is a list of channel names. The `power` channel is always logged, even if not explicitly specified. + +**Available Channels:** +- `power`: DC power output in kW (always logged) +- `poa`: Plane-of-array irradiance in W/m² +- `dni`: Direct normal irradiance in W/m² +- `aoi`: Angle of incidence in degrees + +**Example:** +```yaml +solar_farm: + component_type: SolarPySAMPVWatts + solar_input_filename: inputs/solar_input.csv + log_channels: + - power + - dni + - poa + - aoi + # ... other parameters +``` + +If `log_channels` is not specified, only `power` will be logged. ### Efficiency and Loss Parameters diff --git a/docs/wind.md b/docs/wind.md index 5f4c7bde..95e896f8 100644 --- a/docs/wind.md +++ b/docs/wind.md @@ -37,21 +37,13 @@ Required parameters for both components in [h_dict](h_dict.md) (see [timing](tim Required parameters for Wind_MesoToPower: - `floris_update_time_s`: How often to update FLORIS (the last `floris_update_time_s` seconds are averaged as input) - -Required parameters for Wind_MesoToPower: -- `logging_option`: Logging level. Options are: - - `"base"`: Log basic outputs (power, wind_speed, wind_direction, wind_speed_waked) - - `"turb_subset"`: Base outputs plus 3 random turbines' waked_velocities, turbine_powers, and turbine_power_setpoints - - `"all"`: All available outputs including floris_wind_speed, floris_wind_direction, floris_ti, unwaked_velocities, waked_velocities, turbine_powers, turbine_power_setpoints +- `log_channels`: List of output channels to log. See [Logging Configuration](#logging-configuration) section below for details. ### Wind_MesoToPowerPrecomFloris Specific Parameters Required parameters for Wind_MesoToPowerPrecomFloris: - `floris_update_time_s`: Determines the cadence of wake precomputation. At each cadence tick, the last `floris_update_time_s` seconds are averaged and used to evaluate FLORIS. The computed wake deficits are then applied until the next cadence tick. -- `logging_option`: Logging level. Options are: - - `"base"`: Log basic outputs (power, wind_speed, wind_direction, wind_speed_waked) - - `"turb_subset"`: Base outputs plus 3 random turbines' waked_velocities, turbine_powers, and turbine_power_setpoints - - `"all"`: All available outputs including floris_wind_speed, floris_wind_direction, floris_ti, unwaked_velocities, waked_velocities, turbine_powers, turbine_power_setpoints +- `log_channels`: List of output channels to log. See [Logging Configuration](#logging-configuration) section below for details. ## Turbine Models @@ -65,33 +57,81 @@ Advanced model with rotor dynamics, pitch control, and generator torque control. ### Common Outputs -Both components provide these outputs: -- `power`: Total wind farm power -- `turbine_powers`: Individual turbine power outputs -- `turbine_power_setpoints`: Current power setpoint values -- `wind_speed`, `wind_direction`: Farm-level wind conditions - -### Logging Options - -The logging behavior depends on the `logging_option` setting: - -#### Base Logging (`logging_option: "base"`) -- `power`: Total wind farm power -- `wind_speed`, `wind_direction`: Farm-level wind conditions -- `wind_speed_waked`: Average waked wind speed across the farm - -#### Turbine Subset Logging (`logging_option: "turb_subset"`) -Includes all base outputs plus: -- `waked_velocities_turb_XXX`: Waked velocities for 3 randomly selected turbines -- `turbine_powers_turb_XXX`: Power outputs for 3 randomly selected turbines -- `turbine_power_setpoints_turb_XXX`: Power setpoints for 3 randomly selected turbines - -#### Full Logging (`logging_option: "all"`) -Includes all base outputs plus: -- `turbine_powers`: Individual turbine power outputs -- `turbine_power_setpoints`: Current power setpoint values -- `floris_wind_speed`: Wind speed used in FLORIS calculations -- `floris_wind_direction`: Wind direction used in FLORIS calculations -- `floris_ti`: Turbulence intensity values -- `unwaked_velocities`: Wind speeds without wake effects -- `waked_velocities`: Wind speeds with wake effects applied \ No newline at end of file +Both components provide these outputs in the h_dict at each simulation step: +- `power`: Total wind farm power (kW) +- `turbine_powers`: Individual turbine power outputs (array, kW) +- `turbine_power_setpoints`: Current power setpoint values (array, kW) +- `wind_speed_mean_background`: Farm-average background wind speed (m/s) +- `wind_speed_mean_withwakes`: Farm-average with-wakes wind speed (m/s) +- `wind_direction_mean`: Farm-average wind direction (degrees) +- `wind_speeds_background`: Per-turbine background wind speeds (array, m/s) +- `wind_speeds_withwakes`: Per-turbine with-wakes wind speeds (array, m/s) + +## Logging Configuration + +The `log_channels` parameter controls which outputs are written to the HDF5 output file. This is a list of channel names. The `power` channel is always logged, even if not explicitly specified. + +### Available Channels + +**Scalar Channels:** +- `power`: Total wind farm power output (kW) +- `wind_speed_mean_background`: Farm-average background wind speed (m/s) +- `wind_speed_mean_withwakes`: Farm-average with-wakes wind speed (m/s) +- `wind_direction_mean`: Farm-average wind direction (degrees) + +**Array Channels:** +- `turbine_powers`: Power output for all turbines (creates datasets like `wind_farm.turbine_powers.000`, `wind_farm.turbine_powers.001`, etc.) +- `turbine_power_setpoints`: Power setpoints for all turbines +- `wind_speeds_background`: Background wind speeds for all turbines +- `wind_speeds_withwakes`: With-wakes wind speeds for all turbines + +### Selective Array Element Logging + +For large wind farms, logging all turbine data can significantly increase file size and slow down the simulation. You can log specific turbine indices by appending a 3-digit turbine index to the channel name: + +```yaml +# Log only turbines 0, 5, and 10 +log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean + - turbine_powers.000 + - turbine_powers.005 + - turbine_powers.010 +``` + +### Example Configurations + +**Minimal Logging:** +```yaml +log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean +``` + +**Detailed Logging (all turbines):** +```yaml +log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean + - turbine_powers + - wind_speeds_withwakes +``` + +**Selected Turbine Logging:** +```yaml +# Log first 3 turbines only +log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean + - turbine_powers.000 + - turbine_powers.001 + - turbine_powers.002 +``` \ No newline at end of file diff --git a/examples/00_wind_farm_only/hercules_input.yaml b/examples/00_wind_farm_only/hercules_input.yaml index d874fe15..1192ddb1 100644 --- a/examples/00_wind_farm_only/hercules_input.yaml +++ b/examples/00_wind_farm_only/hercules_input.yaml @@ -5,7 +5,7 @@ name: example_00 ### # Describe this emulator setup -description: Wind Farm Only +description: Wind Farm Only, Logging All Turbine Data dt: 1.0 starttime: 0.0 @@ -22,7 +22,14 @@ wind_farm: wind_input_filename: ../inputs/wind_input_small.ftr turbine_file_name: ../inputs/turbine_filter_model.yaml log_file_name: outputs/log_wind_sim.log - logging_option: all + log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean + - turbine_powers + - wind_speeds_withwakes + - wind_speeds_background floris_update_time_s: 30.0 # Update wakes every 30 seconds diff --git a/examples/00_wind_farm_only/plot_outputs.py b/examples/00_wind_farm_only/plot_outputs.py index 82a8880d..56cced26 100644 --- a/examples/00_wind_farm_only/plot_outputs.py +++ b/examples/00_wind_farm_only/plot_outputs.py @@ -25,33 +25,42 @@ # Plot the wind speeds ax = axarr[0] for t_idx in range(3): - if f"wind_farm.unwaked_velocities.{t_idx:03}" in df.columns: + if f"wind_farm.wind_speeds_background.{t_idx:03}" in df.columns: ax.plot( df["time"], - df[f"wind_farm.unwaked_velocities.{t_idx:03}"], - label=f"Unwaked {t_idx}", + df[f"wind_farm.wind_speeds_background.{t_idx:03}"], + label=f"Background {t_idx}", color=colors[t_idx], ) for t_idx in range(3): - if f"wind_farm.waked_velocities.{t_idx:03}" in df.columns: + if f"wind_farm.wind_speeds_withwakes.{t_idx:03}" in df.columns: ax.plot( df["time"], - df[f"wind_farm.waked_velocities.{t_idx:03}"], - label=f"Waked {t_idx}", + df[f"wind_farm.wind_speeds_withwakes.{t_idx:03}"], + label=f"With wakes {t_idx}", linestyle="--", color=colors[t_idx], ) # Plot the FLORIS wind speed if available -if "wind_farm.floris_wind_speed" in df.columns: +if "wind_farm.wind_speed_mean_background" in df.columns: ax.plot( df["time"], - df["wind_farm.floris_wind_speed"], - label="FLORIS", + df["wind_farm.wind_speed_mean_background"], + label="Mean Background Wind Speed", color="black", lw=2, ) +if "wind_farm.wind_speed_mean_withwakes" in df.columns: + ax.plot( + df["time"], + df["wind_farm.wind_speed_mean_withwakes"], + label="Mean With-Wakes Wind Speed", + color="red", + lw=2, + ) + ax.grid(True) ax.legend() ax.set_ylabel("Wind Speed [m/s]") diff --git a/examples/01_wind_farm_dof1_model/hercules_input.yaml b/examples/01_wind_farm_dof1_model/hercules_input.yaml index 518fb181..0fc192da 100644 --- a/examples/01_wind_farm_dof1_model/hercules_input.yaml +++ b/examples/01_wind_farm_dof1_model/hercules_input.yaml @@ -22,7 +22,12 @@ wind_farm: wind_input_filename: inputs/wind_input.csv turbine_file_name: inputs/turbine_dof_1.yaml log_file_name: outputs/log_wind_sim.log - logging_option: all + log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean + - turbine_powers floris_update_time_s: 30.0 # Update wakes every 30 seconds diff --git a/examples/01_wind_farm_dof1_model/plot_outputs.py b/examples/01_wind_farm_dof1_model/plot_outputs.py index 8ce5fdae..54d0400e 100644 --- a/examples/01_wind_farm_dof1_model/plot_outputs.py +++ b/examples/01_wind_farm_dof1_model/plot_outputs.py @@ -27,14 +27,14 @@ for t_idx in range(3): ax.plot( df["time"], - df[f"wind_farm.unwaked_velocities.{t_idx:03}"], + df[f"wind_farm.wind_speeds_background.{t_idx:03}"], label=f"Unwaked {t_idx}", color=colors[t_idx], ) for t_idx in range(3): ax.plot( df["time"], - df[f"wind_farm.waked_velocities.{t_idx:03}"], + df[f"wind_farm.wind_speeds_withwakes.{t_idx:03}"], label=f"Waked {t_idx}", linestyle="--", color=colors[t_idx], @@ -43,7 +43,7 @@ # Plot the FLORIS wind speed ax.plot( df["time"], - df["wind_farm.floris_wind_speed"], + df["wind_farm.wind_speed_mean"], label="FLORIS", color="black", lw=2, diff --git a/examples/02_wind_farm_realistic_inflow/hercules_input.yaml b/examples/02_wind_farm_realistic_inflow/hercules_input.yaml index d442e973..e8ddcd04 100644 --- a/examples/02_wind_farm_realistic_inflow/hercules_input.yaml +++ b/examples/02_wind_farm_realistic_inflow/hercules_input.yaml @@ -22,7 +22,15 @@ wind_farm: wind_input_filename: ../inputs/wind_input_large.ftr turbine_file_name: ../inputs/turbine_filter_model.yaml log_file_name: outputs/log_wind_sim.log - logging_option: all + log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean + - turbine_powers + - wind_speeds_withwakes + - wind_speeds_background + - turbine_power_setpoints floris_update_time_s: 300.0 # Update wakes every 5 minutes diff --git a/examples/02_wind_farm_realistic_inflow/plot_outputs.py b/examples/02_wind_farm_realistic_inflow/plot_outputs.py index a9db1248..90f3d136 100644 --- a/examples/02_wind_farm_realistic_inflow/plot_outputs.py +++ b/examples/02_wind_farm_realistic_inflow/plot_outputs.py @@ -40,27 +40,19 @@ for t_idx in turbines_to_plot: ax.plot( df["time_utc"], - df[f"wind_farm.unwaked_velocities.{t_idx:03}"], + df[f"wind_farm.wind_speeds_background.{t_idx:03}"], label=f"Unwaked {t_idx}", color=colors[t_idx], ) for t_idx in turbines_to_plot: ax.plot( df["time_utc"], - df[f"wind_farm.waked_velocities.{t_idx:03}"], + df[f"wind_farm.wind_speeds_withwakes.{t_idx:03}"], label=f"Waked {t_idx}", linestyle="--", color=colors[t_idx], ) -# # Plot the FLORIS wind speed -# ax.plot( -# df["time"], -# df["wind_farm.floris_wind_speed"], -# label="FLORIS", -# color="black", -# lw=2, -# ) ax.grid(True) ax.legend() diff --git a/examples/02b_wind_farm_realistic_inflow_precom_floris/hercules_input.yaml b/examples/02b_wind_farm_realistic_inflow_precom_floris/hercules_input.yaml index 1e4c0669..2cb0982a 100644 --- a/examples/02b_wind_farm_realistic_inflow_precom_floris/hercules_input.yaml +++ b/examples/02b_wind_farm_realistic_inflow_precom_floris/hercules_input.yaml @@ -22,7 +22,15 @@ wind_farm: wind_input_filename: ../inputs/wind_input_large.ftr turbine_file_name: ../inputs/turbine_filter_model.yaml log_file_name: outputs/log_wind_sim.log - logging_option: all + log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean + - turbine_powers + - wind_speeds_withwakes + - wind_speeds_background + - turbine_power_setpoints floris_update_time_s: 300.0 # Update wakes every 5 minutes diff --git a/examples/02b_wind_farm_realistic_inflow_precom_floris/plot_outputs.py b/examples/02b_wind_farm_realistic_inflow_precom_floris/plot_outputs.py index a91d017e..dddbd118 100644 --- a/examples/02b_wind_farm_realistic_inflow_precom_floris/plot_outputs.py +++ b/examples/02b_wind_farm_realistic_inflow_precom_floris/plot_outputs.py @@ -40,14 +40,14 @@ for t_idx in turbines_to_plot: ax.plot( df["time"], - df[f"wind_farm.unwaked_velocities.{t_idx:03}"], + df[f"wind_farm.wind_speeds_background.{t_idx:03}"], label=f"Unwaked {t_idx}", color=colors[t_idx], ) for t_idx in turbines_to_plot: ax.plot( df["time"], - df[f"wind_farm.waked_velocities.{t_idx:03}"], + df[f"wind_farm.wind_speeds_withwakes.{t_idx:03}"], label=f"Waked {t_idx}", linestyle="--", color=colors[t_idx], @@ -56,8 +56,8 @@ # Plot the FLORIS wind speed ax.plot( df["time"], - df["wind_farm.floris_wind_speed"], - label="FLORIS", + df["wind_farm.wind_speed_mean_background"], + label="Mean Unwaked Wind Speed", color="black", lw=2, ) diff --git a/examples/03_wind_and_solar/hercules_input.yaml b/examples/03_wind_and_solar/hercules_input.yaml index 660bb719..eabdb342 100644 --- a/examples/03_wind_and_solar/hercules_input.yaml +++ b/examples/03_wind_and_solar/hercules_input.yaml @@ -23,7 +23,8 @@ wind_farm: # The name of the Wind_MesoToPower wind farm wind_input_filename: ../inputs/wind_input_large.ftr turbine_file_name: ../inputs/turbine_filter_model.yaml log_file_name: outputs/log_wind_sim.log - logging_option: all + log_channels: + - power floris_update_time_s: 300.0 # Update wakes every 5 minutes solar_farm: # The name of component object 1 @@ -35,6 +36,8 @@ solar_farm: # The name of component object 1 system_capacity: 30000 # kW (30 MW) tilt: 0 # degrees losses: 0 + log_channels: + - power initial_conditions: diff --git a/examples/04_wind_and_storage/hercules_input.yaml b/examples/04_wind_and_storage/hercules_input.yaml index 946ebc78..adf31a79 100644 --- a/examples/04_wind_and_storage/hercules_input.yaml +++ b/examples/04_wind_and_storage/hercules_input.yaml @@ -23,8 +23,9 @@ wind_farm: # The name of the Wind_MesoToPower wind farm wind_input_filename: ../inputs/wind_input_large.ftr turbine_file_name: ../inputs/turbine_filter_model.yaml log_file_name: outputs/log_wind_sim.log + log_channels: + - power floris_update_time_s: 300.0 # Update wakes every 5 minutes - logging_option: all battery: component_type: BatterySimple @@ -34,6 +35,10 @@ battery: discharge_rate: 10000 # discharge rate in kW (10 MW) max_SOC: 0.9 min_SOC: 0.1 + log_channels: + - power + - soc + - power_setpoint allow_grid_power_consumption: False initial_conditions: SOC: 0.5 diff --git a/hercules/emulator.py b/hercules/emulator.py index 2c3d807e..60786f52 100644 --- a/hercules/emulator.py +++ b/hercules/emulator.py @@ -251,33 +251,65 @@ def numpy_serializer(obj): components_group = data_group.create_group("components") for component_name in self.hybrid_plant.component_names: component_obj = self.hybrid_plant.component_objects[component_name] - log_outputs = getattr(component_obj, "log_outputs", ["power"]) - for output_name in log_outputs: - if output_name in self.h_dict[component_name]: - output_value = self.h_dict[component_name][output_name] - - if isinstance(output_value, (list, np.ndarray)): - # Handle arrays by creating individual datasets - arr = np.asarray(output_value) - for i in range(len(arr)): - dataset_name = f"{component_name}.{output_name}.{i:03d}" + for c in component_obj.log_channels: + # First check if channel name ends with a 3-digit number after a period + if len(c) >= 4 and c[-4] == "." and c[-3:].isdigit(): + # In this case, we want a single index from within an array output + # For example, wind_farm.turbine_powers.000 + # We want to create a dataset for this index + index = int(c[-3:]) + channel_name = c[:-4] + channel_obj = self.h_dict[component_name][channel_name] + if isinstance(channel_obj, (list, np.ndarray)): + if index < len(channel_obj): + dataset_name = f"{component_name}.{channel_name}.{index:03d}" self.hdf5_datasets[dataset_name] = components_group.create_dataset( dataset_name, shape=(total_rows,), dtype=hercules_float_type, **compression_params, ) + else: + raise ValueError( + ( + f"Index {index} is out of range for {channel_name} " + f"in {component_name}" + ) + ) else: - # Handle scalar values - dataset_name = f"{component_name}.{output_name}" - self.hdf5_datasets[dataset_name] = components_group.create_dataset( - dataset_name, - shape=(total_rows,), - dtype=hercules_float_type, - **compression_params, + raise ValueError( + f"Channel {channel_name} is not an array in {component_name}" ) + else: + # In this case, either the value is a scalar, or we want to log the entire array + if c in self.h_dict[component_name]: + output_value = self.h_dict[component_name][c] + + if isinstance(output_value, (list, np.ndarray)): + # Handle arrays by creating individual datasets + arr = np.asarray(output_value) + for i in range(len(arr)): + dataset_name = f"{component_name}.{c}.{i:03d}" + self.hdf5_datasets[dataset_name] = components_group.create_dataset( + dataset_name, + shape=(total_rows,), + dtype=hercules_float_type, + **compression_params, + ) + else: + # Handle scalar values + dataset_name = f"{component_name}.{c}" + self.hdf5_datasets[dataset_name] = components_group.create_dataset( + dataset_name, + shape=(total_rows,), + dtype=hercules_float_type, + **compression_params, + ) + else: + raise ValueError(f"Output {c} not found in {component_name}") + # Create external signals datasets if "external_signals" in self.h_dict and self.h_dict["external_signals"]: external_signals_group = data_group.create_group("external_signals") @@ -545,24 +577,44 @@ def _log_data_to_hdf5(self): # Buffer component outputs for component_name in self.hybrid_plant.component_names: component_obj = self.hybrid_plant.component_objects[component_name] - log_outputs = getattr(component_obj, "log_outputs", ["power"]) - for output_name in log_outputs: - if output_name in self.h_dict[component_name]: - output_value = self.h_dict[component_name][output_name] - - if isinstance(output_value, (list, np.ndarray)): - # Handle arrays by buffering to individual datasets - arr = np.asarray(output_value) - for i in range(len(arr)): - dataset_name = f"{component_name}.{output_name}.{i:03d}" + for c in component_obj.log_channels: + # First check if channel ends in with a 3-digit number after a period + if len(c) >= 4 and c[-4] == "." and c[-3:].isdigit(): + # In this case, we want a single index from within an array output + # For example, wind_farm.turbine_powers.000 + # We want to create a dataset for this index + index = int(c[-3:]) + channel_name = c[:-4] + channel_obj = self.h_dict[component_name][channel_name] + if isinstance(channel_obj, (list, np.ndarray)): + if index < len(channel_obj): + dataset_name = f"{component_name}.{channel_name}.{index:03d}" if dataset_name in self.data_buffers: - self.data_buffers[dataset_name][self.buffer_row] = arr[i] + self.data_buffers[dataset_name][self.buffer_row] = channel_obj[ + index + ] else: - # Handle scalar values - dataset_name = f"{component_name}.{output_name}" - if dataset_name in self.data_buffers: - self.data_buffers[dataset_name][self.buffer_row] = output_value + raise ValueError( + f"Channel {channel_name} is not an array in {component_name}" + ) + else: + # In this case, either the value is a scalar, or we want to log the entire array + if c in self.h_dict[component_name]: + output_value = self.h_dict[component_name][c] + + if isinstance(output_value, (list, np.ndarray)): + # Handle arrays by buffering to individual datasets + arr = np.asarray(output_value) + for i in range(len(arr)): + dataset_name = f"{component_name}.{c}.{i:03d}" + if dataset_name in self.data_buffers: + self.data_buffers[dataset_name][self.buffer_row] = arr[i] + else: + # Handle scalar values + dataset_name = f"{component_name}.{c}" + if dataset_name in self.data_buffers: + self.data_buffers[dataset_name][self.buffer_row] = output_value # Buffer external signals if "external_signals" in self.h_dict and self.h_dict["external_signals"]: diff --git a/hercules/plant_components/battery_lithium_ion.py b/hercules/plant_components/battery_lithium_ion.py index 35ef1fe2..5f9c8d36 100644 --- a/hercules/plant_components/battery_lithium_ion.py +++ b/hercules/plant_components/battery_lithium_ion.py @@ -113,10 +113,6 @@ def __init__(self, h_dict): # Call the base class init super().__init__(h_dict, self.component_name) - # Add to the log outputs with specific outputs - # Note that power is assumed in the base class - self.log_outputs = self.log_outputs + ["soc", "power_setpoint"] - self.V_cell_nom = 3.3 # [V] self.C_cell = 15.756 # [Ah] mean value from [1] Table 1 diff --git a/hercules/plant_components/battery_simple.py b/hercules/plant_components/battery_simple.py index 520eba7a..8e77bf22 100644 --- a/hercules/plant_components/battery_simple.py +++ b/hercules/plant_components/battery_simple.py @@ -109,10 +109,6 @@ def __init__(self, h_dict): # Call the base class init super().__init__(h_dict, self.component_name) - # Add to the log outputs with specific outputs - # Note that power is assumed in the base class - self.log_outputs = self.log_outputs + ["soc", "power_setpoint"] - # size = h_dict[self.component_name]["size"] self.energy_capacity = h_dict[self.component_name]["energy_capacity"] # [kWh] initial_conditions = h_dict[self.component_name]["initial_conditions"] diff --git a/hercules/plant_components/component_base.py b/hercules/plant_components/component_base.py index 4e87a9cf..88dee272 100644 --- a/hercules/plant_components/component_base.py +++ b/hercules/plant_components/component_base.py @@ -30,8 +30,27 @@ def __init__(self, h_dict, component_name): self.logger = self._setup_logging(self.log_file_name) - # Initialize the outputs to log - self.log_outputs = ["power"] + # Parse log_channels from the h_dict + if "log_channels" in h_dict[component_name]: + log_channels_input = h_dict[component_name]["log_channels"] + # Require list format + if isinstance(log_channels_input, list): + self.log_channels = log_channels_input + else: + raise TypeError( + f"log_channels must be a list, got {type(log_channels_input)}. " + f"Use YAML list format:\n" + f" log_channels:\n" + f" - power\n" + f" - channel_name" + ) + + # If power is not in the list, add it + if "power" not in self.log_channels: + self.log_channels.append("power") + else: + # Default to just power if not specified + self.log_channels = ["power"] # Save the time information self.dt = h_dict["dt"] diff --git a/hercules/plant_components/electrolyzer_plant.py b/hercules/plant_components/electrolyzer_plant.py index 0ae6b94b..1d4b20e2 100644 --- a/hercules/plant_components/electrolyzer_plant.py +++ b/hercules/plant_components/electrolyzer_plant.py @@ -43,6 +43,7 @@ def __init__(self, h_dict): # Remove keys not expected by Supervisor elec_config = dict(electrolyzer_dict["electrolyzer"]) elec_config.pop("allow_grid_power_consumption", None) + elec_config.pop("log_channels", None) # Initialize electrolyzer plant self.elec_sys = Supervisor.from_dict(elec_config) diff --git a/hercules/plant_components/solar_pysam_base.py b/hercules/plant_components/solar_pysam_base.py index 2ddcb051..7c80e8a9 100644 --- a/hercules/plant_components/solar_pysam_base.py +++ b/hercules/plant_components/solar_pysam_base.py @@ -30,25 +30,6 @@ def __init__(self, h_dict): # Call the base class init super().__init__(h_dict, self.component_name) - # Add to the log outputs with specific outputs - # Note that power is assumed in the base class - self.log_outputs = self.log_outputs - - # If "log_extra_outputs" is in h_dict[self.component_name], - # Save this value to self.log_extra_outputs - if "log_extra_outputs" in h_dict[self.component_name]: - self.log_extra_outputs = h_dict[self.component_name]["log_extra_outputs"] - else: - self.log_extra_outputs = False - - # If log_extra_outputs is True, add the extra outputs to the log outputs - if self.log_extra_outputs: - self.log_outputs = self.log_outputs + [ - "dni", - "poa", - "aoi", - ] - # Load and process solar data self._load_solar_data(h_dict) diff --git a/hercules/plant_components/wind_meso_to_power.py b/hercules/plant_components/wind_meso_to_power.py index 78cd00b8..b5b4b31e 100644 --- a/hercules/plant_components/wind_meso_to_power.py +++ b/hercules/plant_components/wind_meso_to_power.py @@ -39,15 +39,6 @@ def __init__(self, h_dict): # Call the base class init super().__init__(h_dict, self.component_name) - # Confirm that logging_option is in h_dict[self.component_name] - if "logging_option" not in h_dict[self.component_name]: - raise ValueError(f"logging_option must be in the h_dict for {self.component_name}") - self.logging_option = h_dict[self.component_name]["logging_option"] - if self.logging_option not in ["base", "turb_subset", "all"]: - raise ValueError( - f"logging_option must be one of: base, turb_subset, all for {self.component_name}" - ) - # Track the number of FLORIS calculations self.num_floris_calcs = 0 @@ -137,40 +128,6 @@ def __init__(self, h_dict): self.layout_y = self.fmodel.layout_y self.n_turbines = self.fmodel.n_turbines - # Set the logging outputs based on the logging_option - # First add outputs included in every logging option - self.log_outputs = [ - "power", - "wind_speed", - "wind_direction", - "wind_speed_waked", - ] - - # If including subset of turbines, add the turbine indices - if self.logging_option == "turb_subset": - self.random_turbine_indices = np.random.choice(self.n_turbines, size=3, replace=False) - self.log_outputs = self.log_outputs + [ - f"waked_velocities_turb_{t_idx:03d}" for t_idx in self.random_turbine_indices - ] - self.log_outputs = self.log_outputs + [ - f"turbine_powers_turb_{t_idx:03d}" for t_idx in self.random_turbine_indices - ] - self.log_outputs = self.log_outputs + [ - f"turbine_power_setpoints_turb_{t_idx:03d}" for t_idx in self.random_turbine_indices - ] - - # If including all data add these data points - elif self.logging_option == "all": - self.log_outputs = self.log_outputs + [ - "turbine_powers", - "turbine_power_setpoints", - "floris_wind_speed", - "floris_wind_direction", - "floris_ti", - "unwaked_velocities", - "waked_velocities", - ] - # How often to update the wake deficits self.floris_update_steps = int(self.floris_update_time_s / self.dt) self.floris_update_steps = max(1, self.floris_update_steps) @@ -201,7 +158,7 @@ def __init__(self, h_dict): self.ws_mat_mean = np.mean(self.ws_mat, axis=1, dtype=hercules_float_type) self.initial_wind_speeds = self.ws_mat[0, :] - self.floris_wind_speed = self.ws_mat_mean[0] + self.wind_speed_mean_background = self.ws_mat_mean[0] # For now require "wd_mean" to be in the df_wi if "wd_mean" not in df_wi.columns: @@ -255,15 +212,15 @@ def __init__(self, h_dict): # Initialize the turbine powers to nan self.turbine_powers = np.zeros(self.n_turbines, dtype=hercules_float_type) * np.nan - # Get the initial unwaked velocities + # Get the initial background wind speeds # TODO: This is more a debugging thing, not really necessary - self.unwaked_velocities = self.ws_mat[0, :] + self.wind_speeds_background = self.ws_mat[0, :] - # # Compute the initial waked velocities + # # Compute the initial waked wind speeds self.update_wake_deficits(step=0) - # Compute waked velocities - self.waked_velocities = self.ws_mat[0, :] - self.floris_wake_deficits + # Compute withwakes wind speeds + self.wind_speeds_withwakes = self.ws_mat[0, :] - self.floris_wake_deficits # Get the turbine information self.turbine_dict = load_yaml(self.turbine_file_name) @@ -273,13 +230,13 @@ def __init__(self, h_dict): if self.turbine_model_type == "filter_model": # Use vectorized implementation for improved performance self.turbine_array = TurbineFilterModelVectorized( - self.turbine_dict, self.dt, self.fmodel, self.waked_velocities + self.turbine_dict, self.dt, self.fmodel, self.wind_speeds_withwakes ) self.use_vectorized_turbines = True elif self.turbine_model_type == "dof1_model": self.turbine_array = [ Turbine1dofModel( - self.turbine_dict, self.dt, self.fmodel, self.waked_velocities[t_idx] + self.turbine_dict, self.dt, self.fmodel, self.wind_speeds_withwakes[t_idx] ) for t_idx in range(self.n_turbines) ] @@ -322,8 +279,8 @@ def get_initial_conditions_and_meta_data(self, h_dict): h_dict["wind_farm"]["n_turbines"] = self.n_turbines h_dict["wind_farm"]["capacity"] = self.capacity h_dict["wind_farm"]["rated_turbine_power"] = self.rated_turbine_power - h_dict["wind_farm"]["wind_direction"] = self.wd_mat_mean[0] - h_dict["wind_farm"]["wind_speed"] = self.ws_mat_mean[0] + h_dict["wind_farm"]["wind_direction_mean"] = self.wd_mat_mean[0] + h_dict["wind_farm"]["wind_speed_mean_background"] = self.ws_mat_mean[0] h_dict["wind_farm"]["turbine_powers"] = self.turbine_powers h_dict["wind_farm"]["power"] = np.sum(self.turbine_powers) @@ -425,70 +382,47 @@ def step(self, h_dict): turbine_power_setpoints = h_dict[self.component_name]["turbine_power_setpoints"] self.update_power_setpoints_buffer(turbine_power_setpoints) - # Get the unwaked velocities + # Get the background wind speeds # TODO: This is more a debugging thing, not really necessary - self.unwaked_velocities = self.ws_mat[step, :] + self.wind_speeds_background = self.ws_mat[step, :] - # Check if it is time to update the waked velocities + # Check if it is time to update the withwakes wind speeds if step % self.floris_update_steps == 0: self.update_wake_deficits(step) - # Compute waked velocities - self.waked_velocities = self.ws_mat[step, :] - self.floris_wake_deficits + # Compute withwakes wind speeds + self.wind_speeds_withwakes = self.ws_mat[step, :] - self.floris_wake_deficits # Update the turbine powers if self.use_vectorized_turbines: # Vectorized calculation for all turbines at once self.turbine_powers = self.turbine_array.step( - self.waked_velocities, + self.wind_speeds_withwakes, turbine_power_setpoints, ) else: # Original loop-based calculation for t_idx in range(self.n_turbines): self.turbine_powers[t_idx] = self.turbine_array[t_idx].step( - self.waked_velocities[t_idx], + self.wind_speeds_withwakes[t_idx], power_setpoint=turbine_power_setpoints[t_idx], ) # Update instantaneous wind direction and wind speed - self.wind_direction = self.wd_mat_mean[step] - self.wind_speed = self.ws_mat_mean[step] + self.wind_direction_mean = self.wd_mat_mean[step] + self.wind_speed_mean_background = self.ws_mat_mean[step] # Update the h_dict with outputs h_dict[self.component_name]["power"] = np.sum(self.turbine_powers) h_dict[self.component_name]["turbine_powers"] = self.turbine_powers h_dict[self.component_name]["turbine_power_setpoints"] = turbine_power_setpoints - h_dict[self.component_name]["wind_direction"] = self.wind_direction - h_dict[self.component_name]["wind_speed"] = self.wind_speed - h_dict[self.component_name]["wind_speed_waked"] = np.mean( - self.waked_velocities, dtype=hercules_float_type + h_dict[self.component_name]["wind_direction_mean"] = self.wind_direction_mean + h_dict[self.component_name]["wind_speed_mean_background"] = self.wind_speed_mean_background + h_dict[self.component_name]["wind_speed_mean_withwakes"] = np.mean( + self.wind_speeds_withwakes, dtype=hercules_float_type ) - - # If logging_option is "turb_subset", add the turbine indices - if self.logging_option == "turb_subset": - for t_idx in self.random_turbine_indices: - h_dict[self.component_name][f"waked_velocities_turb_{t_idx:03d}"] = ( - self.waked_velocities[t_idx] - ) - h_dict[self.component_name][f"turbine_powers_turb_{t_idx:03d}"] = ( - self.turbine_powers[t_idx] - ) - h_dict[self.component_name][f"turbine_power_setpoints_turb_{t_idx:03d}"] = ( - turbine_power_setpoints[t_idx] - ) - - # Else if logging_option is "all", add the turbine powers - elif self.logging_option == "all": - h_dict[self.component_name]["floris_wind_speed"] = self.floris_wind_speed - h_dict[self.component_name]["floris_wind_direction"] = self.floris_wind_direction - h_dict[self.component_name]["floris_ti"] = self.floris_ti - h_dict[self.component_name]["floris_turbine_power_setpoints"] = ( - self.floris_turbine_power_setpoints - ) - h_dict[self.component_name]["unwaked_velocities"] = self.unwaked_velocities - h_dict[self.component_name]["waked_velocities"] = self.waked_velocities - + h_dict[self.component_name]["wind_speeds_withwakes"] = self.wind_speeds_withwakes + h_dict[self.component_name]["wind_speeds_background"] = self.wind_speeds_background return h_dict diff --git a/hercules/plant_components/wind_meso_to_power_precom_floris.py b/hercules/plant_components/wind_meso_to_power_precom_floris.py index 8c290622..a0eb387f 100644 --- a/hercules/plant_components/wind_meso_to_power_precom_floris.py +++ b/hercules/plant_components/wind_meso_to_power_precom_floris.py @@ -69,15 +69,6 @@ def __init__(self, h_dict): # Call the base class init super().__init__(h_dict, self.component_name) - # Confirm that logging_option is in h_dict[self.component_name] - if "logging_option" not in h_dict[self.component_name]: - raise ValueError(f"logging_option must be in the h_dict for {self.component_name}") - self.logging_option = h_dict[self.component_name]["logging_option"] - if self.logging_option not in ["base", "turb_subset", "all"]: - raise ValueError( - f"logging_option must be one of: base, turb_subset, all for {self.component_name}" - ) - self.logger.info("Completed base class init...") # Track the number of FLORIS calculations @@ -196,7 +187,7 @@ def __init__(self, h_dict): self.ws_mat_mean = np.mean(self.ws_mat, axis=1, dtype=hercules_float_type) self.initial_wind_speeds = self.ws_mat[0, :] - self.floris_wind_speed = self.ws_mat_mean[0] + self.wind_speed_mean_background = self.ws_mat_mean[0] # For now require "wd_mean" to be in the df_wi if "wd_mean" not in df_wi.columns: @@ -307,20 +298,20 @@ def window_circmean(arr_1d, idx, win): # Use deficits from this evaluation time for the whole block deficits_all[start_idx : end_idx + 1, :] = floris_wake_deficits_eval[block_idx, :] - # Compute all the waked velocities from unwaked minus deficits - self.waked_velocities_all = self.ws_mat - deficits_all + # Compute all the withwakes wind speeds from background minus deficits + self.wind_speeds_withwakes_all = self.ws_mat - deficits_all # Initialize the turbine powers to nan self.turbine_powers = np.zeros(self.n_turbines, dtype=hercules_float_type) * np.nan - # Get the initial unwaked velocities - self.unwaked_velocities = self.ws_mat[0, :] + # Get the initial background wind speeds + self.wind_speeds_background = self.ws_mat[0, :] - # Compute initial waked velocities - self.waked_velocities = self.waked_velocities_all[0, :] + # Compute initial withwakes wind speeds + self.wind_speeds_withwakes = self.wind_speeds_withwakes_all[0, :] # Get the initial FLORIS wake deficits - self.floris_wake_deficits = self.unwaked_velocities - self.waked_velocities + self.floris_wake_deficits = self.wind_speeds_background - self.wind_speeds_withwakes # Get the turbine information self.turbine_dict = load_yaml(self.turbine_file_name) @@ -330,13 +321,13 @@ def window_circmean(arr_1d, idx, win): if self.turbine_model_type == "filter_model": # Use vectorized implementation for improved performance self.turbine_array = TurbineFilterModelVectorized( - self.turbine_dict, self.dt, self.fmodel, self.waked_velocities + self.turbine_dict, self.dt, self.fmodel, self.wind_speeds_withwakes ) self.use_vectorized_turbines = True elif self.turbine_model_type == "dof1_model": self.turbine_array = [ Turbine1dofModel( - self.turbine_dict, self.dt, self.fmodel, self.waked_velocities[t_idx] + self.turbine_dict, self.dt, self.fmodel, self.wind_speeds_withwakes[t_idx] ) for t_idx in range(self.n_turbines) ] @@ -362,40 +353,6 @@ def window_circmean(arr_1d, idx, win): # Get the capacity of the farm self.capacity = self.n_turbines * self.rated_turbine_power - # Set the logging outputs based on the logging_option - # First add outputs included in every logging option - self.log_outputs = [ - "power", - "wind_speed", - "wind_direction", - "wind_speed_waked", - ] - - # If including subset of turbines, add the turbine indices - if self.logging_option == "turb_subset": - self.random_turbine_indices = np.random.choice(self.n_turbines, size=3, replace=False) - self.log_outputs = self.log_outputs + [ - f"waked_velocities_turb_{t_idx:03d}" for t_idx in self.random_turbine_indices - ] - self.log_outputs = self.log_outputs + [ - f"turbine_powers_turb_{t_idx:03d}" for t_idx in self.random_turbine_indices - ] - self.log_outputs = self.log_outputs + [ - f"turbine_power_setpoints_turb_{t_idx:03d}" for t_idx in self.random_turbine_indices - ] - - # If including all data add these data points - elif self.logging_option == "all": - self.log_outputs = self.log_outputs + [ - "turbine_powers", - "turbine_power_setpoints", - "floris_wind_speed", - "floris_wind_direction", - "floris_ti", - "unwaked_velocities", - "waked_velocities", - ] - # Update the user self.logger.info( f"Initialized Wind_MesoToPowerPrecomFloris with {self.n_turbines} turbines" @@ -416,8 +373,8 @@ def get_initial_conditions_and_meta_data(self, h_dict): h_dict["wind_farm"]["n_turbines"] = self.n_turbines h_dict["wind_farm"]["capacity"] = self.capacity h_dict["wind_farm"]["rated_turbine_power"] = self.rated_turbine_power - h_dict["wind_farm"]["wind_direction"] = self.wd_mat_mean[0] - h_dict["wind_farm"]["wind_speed"] = self.ws_mat_mean[0] + h_dict["wind_farm"]["wind_direction_mean"] = self.wd_mat_mean[0] + h_dict["wind_farm"]["wind_speed_mean_background"] = self.ws_mat_mean[0] h_dict["wind_farm"]["turbine_powers"] = self.turbine_powers h_dict["wind_farm"]["power"] = np.sum(self.turbine_powers) @@ -452,60 +409,41 @@ def step(self, h_dict): # Grab the instantaneous turbine power setpoint signal and update the power_setpoints buffer turbine_power_setpoints = h_dict[self.component_name]["turbine_power_setpoints"] - # Update all the velocities - self.unwaked_velocities = self.ws_mat[step, :] - self.waked_velocities = self.waked_velocities_all[step, :] - self.floris_wake_deficits = self.unwaked_velocities - self.waked_velocities + # Update all the wind speeds + self.wind_speeds_background = self.ws_mat[step, :] + self.wind_speeds_withwakes = self.wind_speeds_withwakes_all[step, :] + self.floris_wake_deficits = self.wind_speeds_background - self.wind_speeds_withwakes # Update the turbine powers if self.use_vectorized_turbines: # Vectorized calculation for all turbines at once self.turbine_powers = self.turbine_array.step( - self.waked_velocities, + self.wind_speeds_withwakes, turbine_power_setpoints, ) else: # Original loop-based calculation for t_idx in range(self.n_turbines): self.turbine_powers[t_idx] = self.turbine_array[t_idx].step( - self.waked_velocities[t_idx], + self.wind_speeds_withwakes[t_idx], power_setpoint=turbine_power_setpoints[t_idx], ) # Update instantaneous wind direction and wind speed - self.wind_direction = self.wd_mat_mean[step] - self.wind_speed = self.ws_mat_mean[step] + self.wind_direction_mean = self.wd_mat_mean[step] + self.wind_speed_mean_background = self.ws_mat_mean[step] # Update the h_dict with outputs h_dict[self.component_name]["power"] = np.sum(self.turbine_powers) h_dict[self.component_name]["turbine_powers"] = self.turbine_powers h_dict[self.component_name]["turbine_power_setpoints"] = turbine_power_setpoints - h_dict[self.component_name]["wind_direction"] = self.wind_direction - h_dict[self.component_name]["wind_speed"] = self.wind_speed - h_dict[self.component_name]["wind_speed_waked"] = np.mean( - self.waked_velocities, dtype=hercules_float_type + h_dict[self.component_name]["wind_direction_mean"] = self.wind_direction_mean + h_dict[self.component_name]["wind_speed_mean_background"] = self.wind_speed_mean_background + h_dict[self.component_name]["wind_speed_mean_withwakes"] = np.mean( + self.wind_speeds_withwakes, dtype=hercules_float_type ) - - # If logging_option is "turb_subset", add the turbine indices - if self.logging_option == "turb_subset": - for t_idx in self.random_turbine_indices: - h_dict[self.component_name][f"waked_velocities_turb_{t_idx:03d}"] = ( - self.waked_velocities[t_idx] - ) - h_dict[self.component_name][f"turbine_powers_turb_{t_idx:03d}"] = ( - self.turbine_powers[t_idx] - ) - h_dict[self.component_name][f"turbine_power_setpoints_turb_{t_idx:03d}"] = ( - turbine_power_setpoints[t_idx] - ) - - # Else if logging_option is "all", add the turbine powers - elif self.logging_option == "all": - h_dict[self.component_name]["floris_wind_speed"] = self.wind_speed - h_dict[self.component_name]["floris_wind_direction"] = self.wind_direction - h_dict[self.component_name]["floris_ti"] = self.ti_mat_mean[step] - h_dict[self.component_name]["unwaked_velocities"] = self.unwaked_velocities - h_dict[self.component_name]["waked_velocities"] = self.waked_velocities + h_dict[self.component_name]["wind_speeds_withwakes"] = self.wind_speeds_withwakes + h_dict[self.component_name]["wind_speeds_background"] = self.wind_speeds_background return h_dict diff --git a/tests/emulator_test.py b/tests/emulator_test.py index 76ba8a39..635e4e3f 100644 --- a/tests/emulator_test.py +++ b/tests/emulator_test.py @@ -470,3 +470,72 @@ def test_log_every_n_option(): assert df_hdf5["step"].iloc[0] == 0 assert df_hdf5["step"].iloc[1] == 3 assert df_hdf5["step"].iloc[2] == 6 + + +def test_log_selective_array_element(): + """Test that selective array element logging (e.g., turbine_powers.001) works correctly. + + This test verifies that when log_channels specifies a specific array element + (e.g., turbine_powers.001), only that element is logged and not other elements. + """ + import copy + + # Use h_dict_wind as base for testing + test_h_dict = copy.deepcopy(h_dict_wind) + + # Modify log_channels to only include turbine_powers.001 (not the full array) + test_h_dict["wind_farm"]["log_channels"] = ["power", "turbine_powers.001"] + + # Set up logger for testing + logger = setup_logging(console_output=False) + + controller = SimpleControllerWind(test_h_dict) + hybrid_plant = HybridPlant(test_h_dict) + + emulator = Emulator(controller, hybrid_plant, test_h_dict, logger) + + # Set up the simulation state + emulator.time = 5.0 + emulator.step = 5 + emulator.h_dict["time"] = 5.0 + emulator.h_dict["step"] = 5 + + # Run controller and hybrid_plant steps to generate plant-level outputs + emulator.h_dict = controller.step(emulator.h_dict) + emulator.h_dict = hybrid_plant.step(emulator.h_dict) + + # Call the new HDF5 logging function + emulator._log_data_to_hdf5() + + # Check that ONLY turbine_powers.001 is logged (not .000 or .002) + actual_datasets = set(emulator.hdf5_datasets.keys()) + + # turbine_powers.001 SHOULD be present + assert ( + "wind_farm.turbine_powers.001" in actual_datasets + ), "Expected wind_farm.turbine_powers.001 to be logged" + + # turbine_powers.000 should NOT be present + assert ( + "wind_farm.turbine_powers.000" not in actual_datasets + ), "wind_farm.turbine_powers.000 should NOT be logged when only .001 is specified" + + # turbine_powers.002 should NOT be present + assert ( + "wind_farm.turbine_powers.002" not in actual_datasets + ), "wind_farm.turbine_powers.002 should NOT be logged when only .001 is specified" + + # Verify that basic datasets are still present + assert "time" in actual_datasets + assert "step" in actual_datasets + assert "wind_farm.power" in actual_datasets + + # Flush buffer to write data to HDF5 + if hasattr(emulator, "data_buffers") and emulator.data_buffers and emulator.buffer_row > 0: + emulator._flush_buffer_to_hdf5() + + # Verify that turbine_powers.001 has a valid value + assert emulator.hdf5_datasets["wind_farm.turbine_powers.001"][0] > 0 + + # Clean up + emulator.close() diff --git a/tests/example_regression_tests/example_00b_regression_precom_test.py b/tests/example_regression_tests/example_00b_regression_precom_test.py index b0cb53e6..2c44e631 100644 --- a/tests/example_regression_tests/example_00b_regression_precom_test.py +++ b/tests/example_regression_tests/example_00b_regression_precom_test.py @@ -4,7 +4,7 @@ import tempfile import yaml -from hercules.utilities_examples import ensure_example_inputs_exist +from hercules.utilities_examples import generate_example_inputs from test_example_utilities import ( copy_example_files, generate_input_data, @@ -52,8 +52,6 @@ def modify_input_file_for_precom_floris(temp_dir, input_file): h_dict["wind_farm"]["floris_update_time_s"] = h_dict["wind_farm"].get( "floris_update_time_s", 300.0 ) - # Add logging_option for the new logging system - h_dict["wind_farm"]["logging_option"] = "all" # Write the modified YAML file back with open(input_path, "w") as f: @@ -108,7 +106,7 @@ def test_example_00b_precom_floris_limited_time_regression(): outputs are reasonable and consistent. """ # Ensure centralized example inputs exist - ensure_example_inputs_exist() + generate_example_inputs() # Create a temporary directory for this test with tempfile.TemporaryDirectory() as temp_dir: diff --git a/tests/example_regression_tests/example_03_regression_test.py b/tests/example_regression_tests/example_03_regression_test.py index 5395c1fd..cabd5d15 100644 --- a/tests/example_regression_tests/example_03_regression_test.py +++ b/tests/example_regression_tests/example_03_regression_test.py @@ -5,7 +5,7 @@ import numpy as np import pandas as pd -from hercules.utilities_examples import ensure_example_inputs_exist +from hercules.utilities_examples import generate_example_inputs from test_example_utilities import ( copy_example_files, run_simulation, @@ -201,7 +201,7 @@ def test_example_03_limited_time_regression(): and verifies that the final outputs are reasonable and consistent. """ # Ensure centralized example inputs exist - ensure_example_inputs_exist() + generate_example_inputs() # Create a temporary directory for this test with tempfile.TemporaryDirectory() as temp_dir: diff --git a/tests/example_regression_tests/test_example_utilities.py b/tests/example_regression_tests/test_example_utilities.py index 994b7b4f..12ee4f19 100644 --- a/tests/example_regression_tests/test_example_utilities.py +++ b/tests/example_regression_tests/test_example_utilities.py @@ -9,7 +9,7 @@ from hercules.emulator import Emulator from hercules.hybrid_plant import HybridPlant from hercules.utilities import load_hercules_input, setup_logging -from hercules.utilities_examples import ensure_example_inputs_exist +from hercules.utilities_examples import generate_example_inputs def copy_example_files(example_dir, temp_dir, input_file, inputs_dir, notebook_file): @@ -216,7 +216,13 @@ def verify_outputs( turbine_power_cols = [ col for col in df.columns if col.startswith("wind_farm.turbine_powers.") ] - assert len(turbine_power_cols) > 0, "Should have turbine power columns" + # Only check turbine power columns if they were logged by the example + # Some examples configure log_channels to only include aggregate power + if len(turbine_power_cols) > 0: + # Ensure values are non-negative and finite when present + for col in turbine_power_cols: + assert all(df[col] >= 0), f"{col} should be non-negative" + assert all(np.isfinite(df[col])), f"{col} should be finite" # Test that the final wind power has not changed much np.testing.assert_allclose( @@ -304,7 +310,7 @@ def run_example_regression_test( Defaults to "plot_outputs.py". """ # Ensure centralized example inputs exist - ensure_example_inputs_exist() + generate_example_inputs() # Create a temporary directory for this test with tempfile.TemporaryDirectory() as temp_dir: diff --git a/tests/test_inputs/h_dict.py b/tests/test_inputs/h_dict.py index bf6afb24..0d54a112 100644 --- a/tests/test_inputs/h_dict.py +++ b/tests/test_inputs/h_dict.py @@ -8,8 +8,14 @@ "wind_input_filename": "tests/test_inputs/wind_input.csv", "turbine_file_name": "tests/test_inputs/turbine_filter_model.yaml", "log_file_name": "outputs/wind_farm.log", + "log_channels": [ + "power", + "wind_speed_mean_background", + "wind_speed_mean_withwakes", + "wind_direction_mean", + "turbine_powers", + ], "floris_update_time_s": 30.0, # Required parameter for FLORIS updates - "logging_option": "all", # Required parameter for logging configuration } @@ -22,6 +28,7 @@ "lon": -105.179, "elev": 1828.8, "losses": 0, + "log_channels": ["power", "dni", "poa", "aoi"], "initial_conditions": {"power": 0.0, "dni": 0.0, "poa": 0.0}, } @@ -34,6 +41,7 @@ "system_capacity": 100000.0, # kW (100 MW) "tilt": 0, # degrees "losses": 0, + "log_channels": ["power", "dni", "poa", "aoi"], "initial_conditions": {"power": 25, "dni": 1000, "poa": 1000}, } @@ -44,6 +52,7 @@ "discharge_rate": 50.0, "max_SOC": 0.9, "min_SOC": 0.1, + "log_channels": ["power", "soc", "power_setpoint"], "initial_conditions": {"SOC": 0.5}, } @@ -55,6 +64,7 @@ "discharge_rate": 2000, # discharge rate in kW (2 MW) "max_SOC": 0.9, # upper boundary on battery SOC "min_SOC": 0.1, # lower boundary on battery SOC + "log_channels": ["power", "soc", "power_setpoint"], "initial_conditions": {"SOC": 0.102}, } @@ -66,6 +76,7 @@ "discharge_rate": 2000, # discharge rate in kW (2 MW) "max_SOC": 0.9, # upper boundary on battery SOC "min_SOC": 0.1, # lower boundary on battery SOC + "log_channels": ["power", "soc", "power_setpoint"], "initial_conditions": {"SOC": 0.102}, } @@ -73,6 +84,7 @@ # 'component_type': 'ElectrolyzerPlant', # Removed for Supervisor compatibility "initialize": True, "initial_power_kW": 3000, + "log_channels": ["power"], "supervisor": { "n_stacks": 10, }, diff --git a/tests/test_inputs/hercules_input_test.yaml b/tests/test_inputs/hercules_input_test.yaml index 7d58dd28..f4f58f3b 100644 --- a/tests/test_inputs/hercules_input_test.yaml +++ b/tests/test_inputs/hercules_input_test.yaml @@ -21,7 +21,13 @@ wind_farm: # The name of the Wind_MesoToPower wind farm wind_input_filename: inputs/wind_input.p turbine_file_name: inputs/turbine_filter_model.yaml log_file_name: outputs/log_wind_sim.log - logging_option: base + log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean + - turbine_powers + floris_update_time_s: 30.0 solar_farm: # The name of component object 1 component_type: SolarPySAMPVWatts @@ -30,6 +36,11 @@ solar_farm: # The name of component object 1 lat: 39.7442 lon: -105.1778 elev: 1829 + log_channels: + - power + - dni + - poa + - aoi initial_conditions: diff --git a/tests/wind_meso_to_power_precom_floris_test.py b/tests/wind_meso_to_power_precom_floris_test.py index 2ec5781e..4d16b876 100644 --- a/tests/wind_meso_to_power_precom_floris_test.py +++ b/tests/wind_meso_to_power_precom_floris_test.py @@ -16,9 +16,8 @@ # Create a base test dictionary for Wind_MesoToPowerPrecomFloris h_dict_wind_precom_floris = copy.deepcopy(h_dict_wind) -# Update component type and add logging_option for precom_floris tests +# Update component type h_dict_wind_precom_floris["wind_farm"]["component_type"] = "Wind_MesoToPowerPrecomFloris" -h_dict_wind_precom_floris["wind_farm"]["logging_option"] = "all" def test_wind_meso_to_power_precom_floris_initialization(): @@ -38,14 +37,15 @@ def test_wind_meso_to_power_precom_floris_initialization(): == h_dict_wind_precom_floris["wind_farm"]["floris_update_time_s"] ) + def test_wind_meso_to_power_precom_floris_ws_mean(): """Test that invalid component_type raises ValueError.""" current_dir = os.path.dirname(__file__) - df_input = pd.read_csv(current_dir+"/test_inputs/wind_input.csv") + df_input = pd.read_csv(current_dir + "/test_inputs/wind_input.csv") df_input["ws_mean"] = 10.0 - df_input.to_csv(current_dir+"/test_inputs/wind_input_temp.csv") + df_input.to_csv(current_dir + "/test_inputs/wind_input_temp.csv") test_h_dict = copy.deepcopy(h_dict_wind_precom_floris) test_h_dict["wind_farm"]["wind_input_filename"] = "tests/test_inputs/wind_input_temp.csv" @@ -54,26 +54,26 @@ def test_wind_meso_to_power_precom_floris_ws_mean(): # Note that h_dict_wind_precom_floris specifies an end time of 10. wind_sim = Wind_MesoToPowerPrecomFloris(test_h_dict) assert ( - wind_sim.ws_mat[:, 0] - == df_input["ws_000"].to_numpy(dtype=hercules_float_type)[:10] + wind_sim.ws_mat[:, 0] == df_input["ws_000"].to_numpy(dtype=hercules_float_type)[:10] ).all() assert np.allclose( wind_sim.ws_mat_mean, - (df_input[["ws_000", "ws_001", "ws_002"]].mean(axis=1)).to_numpy( - dtype=hercules_float_type - )[:10] + (df_input[["ws_000", "ws_001", "ws_002"]].mean(axis=1)).to_numpy(dtype=hercules_float_type)[ + :10 + ], ) # Drop individual speeds and test that ws_mean is used instead df_input = df_input.drop(columns=["ws_000", "ws_001", "ws_002"]) - df_input.to_csv(current_dir+"/test_inputs/wind_input_temp.csv") + df_input.to_csv(current_dir + "/test_inputs/wind_input_temp.csv") wind_sim = Wind_MesoToPowerPrecomFloris(test_h_dict) assert (wind_sim.ws_mat_mean == 10.0).all() assert (wind_sim.ws_mat[:, :] == 10.0).all() # Delete temp file - os.remove(current_dir+"/test_inputs/wind_input_temp.csv") + os.remove(current_dir + "/test_inputs/wind_input_temp.csv") + def test_wind_meso_to_power_precom_floris_requires_floris_update_time(): """Test that missing floris_update_time_s raises ValueError.""" @@ -152,16 +152,16 @@ def test_wind_meso_to_power_precom_floris_get_initial_conditions_and_meta_data() assert "n_turbines" in result["wind_farm"] assert "capacity" in result["wind_farm"] assert "rated_turbine_power" in result["wind_farm"] - assert "wind_direction" in result["wind_farm"] - assert "wind_speed" in result["wind_farm"] + assert "wind_direction_mean" in result["wind_farm"] + assert "wind_speed_mean_background" in result["wind_farm"] assert "turbine_powers" in result["wind_farm"] # Verify the values match the wind_sim attributes assert result["wind_farm"]["n_turbines"] == wind_sim.n_turbines assert result["wind_farm"]["capacity"] == wind_sim.capacity assert result["wind_farm"]["rated_turbine_power"] == wind_sim.rated_turbine_power - assert result["wind_farm"]["wind_direction"] == wind_sim.wd_mat_mean[0] - assert result["wind_farm"]["wind_speed"] == wind_sim.ws_mat_mean[0] + assert result["wind_farm"]["wind_direction_mean"] == wind_sim.wd_mat_mean[0] + assert result["wind_farm"]["wind_speed_mean_background"] == wind_sim.ws_mat_mean[0] # Verify turbine_powers is a numpy array with correct length assert isinstance(result["wind_farm"]["turbine_powers"], np.ndarray) @@ -179,13 +179,13 @@ def test_wind_meso_to_power_precom_floris_precomputed_wake_deficits(): """Test that wake deficits are precomputed and stored correctly.""" wind_sim = Wind_MesoToPowerPrecomFloris(h_dict_wind_precom_floris) - # Verify that precomputed wake velocities exist - assert hasattr(wind_sim, "waked_velocities_all") - assert isinstance(wind_sim.waked_velocities_all, np.ndarray) + # Verify that precomputed wake wind speeds exist + assert hasattr(wind_sim, "wind_speeds_withwakes_all") + assert isinstance(wind_sim.wind_speeds_withwakes_all, np.ndarray) # Check shape: should be (n_time_steps, n_turbines) expected_shape = (wind_sim.n_steps, wind_sim.n_turbines) - assert wind_sim.waked_velocities_all.shape == expected_shape + assert wind_sim.wind_speeds_withwakes_all.shape == expected_shape # Verify that initial wake deficits are calculated assert hasattr(wind_sim, "floris_wake_deficits") @@ -197,7 +197,7 @@ def test_wind_meso_to_power_precom_floris_precomputed_wake_deficits(): def test_wind_meso_to_power_precom_floris_velocities_update_correctly(): - """Test that velocities are updated correctly from precomputed arrays during simulation.""" + """Test that wind speeds are updated correctly from precomputed arrays during simulation.""" # Create a temporary wind input file with varying conditions wind_data = { "time": [0, 1, 2, 3], @@ -224,9 +224,9 @@ def test_wind_meso_to_power_precom_floris_velocities_update_correctly(): # Initialize wind simulation wind_sim = Wind_MesoToPowerPrecomFloris(test_h_dict) - # Store initial velocities - initial_unwaked = wind_sim.unwaked_velocities.copy() - initial_waked = wind_sim.waked_velocities.copy() + # Store initial wind speeds + initial_background = wind_sim.wind_speeds_background.copy() + initial_withwakes = wind_sim.wind_speeds_withwakes.copy() # Run a step step_h_dict = {"step": 1} @@ -236,20 +236,20 @@ def test_wind_meso_to_power_precom_floris_velocities_update_correctly(): wind_sim.step(step_h_dict) - # Verify that velocities have been updated + # Verify that wind speeds have been updated assert not np.array_equal( - wind_sim.unwaked_velocities, initial_unwaked - ), "Unwaked velocities should have been updated" + wind_sim.wind_speeds_background, initial_background + ), "Background wind speeds should have been updated" assert not np.array_equal( - wind_sim.waked_velocities, initial_waked - ), "Waked velocities should have been updated" + wind_sim.wind_speeds_withwakes, initial_withwakes + ), "Withwakes wind speeds should have been updated" - # Verify the velocities match the expected values from the input data - expected_unwaked = np.array([9.0, 9.5, 10.0]) # ws values for step 1 - np.testing.assert_array_equal(wind_sim.unwaked_velocities, expected_unwaked) + # Verify the wind speeds match the expected values from the input data + expected_background = np.array([9.0, 9.5, 10.0]) # ws values for step 1 + np.testing.assert_array_equal(wind_sim.wind_speeds_background, expected_background) # Verify that wake deficits are recalculated - expected_wake_deficits = wind_sim.unwaked_velocities - wind_sim.waked_velocities + expected_wake_deficits = wind_sim.wind_speeds_background - wind_sim.wind_speeds_withwakes np.testing.assert_array_equal(wind_sim.floris_wake_deficits, expected_wake_deficits) finally: diff --git a/tests/wind_meso_to_power_test.py b/tests/wind_meso_to_power_test.py index 6bbd6ed8..7bd124f2 100644 --- a/tests/wind_meso_to_power_test.py +++ b/tests/wind_meso_to_power_test.py @@ -26,14 +26,15 @@ def test_wind_meso_to_power_initialization(): assert wind_sim.num_floris_calcs == 1 # FLORIS is called during initialization assert wind_sim.floris_update_time_s == 30.0 + def test_wind_meso_to_power_precom_floris_ws_mean(): """Test that invalid component_type raises ValueError.""" current_dir = os.path.dirname(__file__) - df_input = pd.read_csv(current_dir+"/test_inputs/wind_input.csv") + df_input = pd.read_csv(current_dir + "/test_inputs/wind_input.csv") df_input["ws_mean"] = 10.0 - df_input.to_csv(current_dir+"/test_inputs/wind_input_temp.csv") + df_input.to_csv(current_dir + "/test_inputs/wind_input_temp.csv") test_h_dict = copy.deepcopy(h_dict_wind) test_h_dict["wind_farm"]["wind_input_filename"] = "tests/test_inputs/wind_input_temp.csv" @@ -42,26 +43,26 @@ def test_wind_meso_to_power_precom_floris_ws_mean(): # Note that h_dict_wind specifies an end time of 10. wind_sim = Wind_MesoToPower(test_h_dict) assert ( - wind_sim.ws_mat[:, 0] - == df_input["ws_000"].to_numpy(dtype=hercules_float_type)[:10] + wind_sim.ws_mat[:, 0] == df_input["ws_000"].to_numpy(dtype=hercules_float_type)[:10] ).all() assert np.allclose( wind_sim.ws_mat_mean, - (df_input[["ws_000", "ws_001", "ws_002"]].mean(axis=1)).to_numpy( - dtype=hercules_float_type - )[:10] + (df_input[["ws_000", "ws_001", "ws_002"]].mean(axis=1)).to_numpy(dtype=hercules_float_type)[ + :10 + ], ) # Drop individual speeds and test that ws_mean is used instead df_input = df_input.drop(columns=["ws_000", "ws_001", "ws_002"]) - df_input.to_csv(current_dir+"/test_inputs/wind_input_temp.csv") + df_input.to_csv(current_dir + "/test_inputs/wind_input_temp.csv") wind_sim = Wind_MesoToPower(test_h_dict) assert (wind_sim.ws_mat_mean == 10.0).all() assert (wind_sim.ws_mat[:, :] == 10.0).all() # Delete temp file - os.remove(current_dir+"/test_inputs/wind_input_temp.csv") + os.remove(current_dir + "/test_inputs/wind_input_temp.csv") + def test_wind_meso_to_power_missing_floris_update_time(): """Test that missing floris_update_time_s raises ValueError.""" @@ -243,16 +244,16 @@ def test_wind_meso_to_power_get_initial_conditions_and_meta_data(): assert "n_turbines" in result["wind_farm"] assert "capacity" in result["wind_farm"] assert "rated_turbine_power" in result["wind_farm"] - assert "wind_direction" in result["wind_farm"] - assert "wind_speed" in result["wind_farm"] + assert "wind_direction_mean" in result["wind_farm"] + assert "wind_speed_mean_background" in result["wind_farm"] assert "turbine_powers" in result["wind_farm"] # Verify the values match the wind_sim attributes assert result["wind_farm"]["n_turbines"] == wind_sim.n_turbines assert result["wind_farm"]["capacity"] == wind_sim.capacity assert result["wind_farm"]["rated_turbine_power"] == wind_sim.rated_turbine_power - assert result["wind_farm"]["wind_direction"] == wind_sim.wd_mat_mean[0] - assert result["wind_farm"]["wind_speed"] == wind_sim.ws_mat_mean[0] + assert result["wind_farm"]["wind_direction_mean"] == wind_sim.wd_mat_mean[0] + assert result["wind_farm"]["wind_speed_mean_background"] == wind_sim.ws_mat_mean[0] # Verify turbine_powers is a numpy array with correct length assert isinstance(result["wind_farm"]["turbine_powers"], np.ndarray) diff --git a/timing_tests/hercules_input_solar.yaml b/timing_tests/hercules_input_solar.yaml index 70d65972..3735dda0 100644 --- a/timing_tests/hercules_input_solar.yaml +++ b/timing_tests/hercules_input_solar.yaml @@ -24,6 +24,11 @@ solar_farm: system_capacity: 100000 # kW (100 MW) tilt: 0 # degrees losses: 0 + log_channels: + - power + - dni + - poa + - aoi initial_conditions: diff --git a/timing_tests/hercules_input_wind.yaml b/timing_tests/hercules_input_wind.yaml index f172832d..114a3f1b 100644 --- a/timing_tests/hercules_input_wind.yaml +++ b/timing_tests/hercules_input_wind.yaml @@ -22,7 +22,11 @@ wind_farm: wind_input_filename: inputs/wind_input.p turbine_file_name: inputs/turbine_filter_model.yaml log_file_name: outputs/log_wind_sim.log - logging_option: base + log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean floris_update_time_s: 300.0 # Update wakes every 5 minutes diff --git a/timing_tests/hercules_input_wind_precom.yaml b/timing_tests/hercules_input_wind_precom.yaml index 41a193b4..ac7d342e 100644 --- a/timing_tests/hercules_input_wind_precom.yaml +++ b/timing_tests/hercules_input_wind_precom.yaml @@ -23,7 +23,11 @@ wind_farm: wind_input_filename: inputs/wind_input.p turbine_file_name: inputs/turbine_filter_model.yaml log_file_name: outputs/log_wind_sim.log - logging_option: base + log_channels: + - power + - wind_speed_mean_background + - wind_speed_mean_withwakes + - wind_direction_mean floris_update_time_s: 300.0 # FLORIS updates every 5 minutes diff --git a/timing_tests/timing_results.csv b/timing_tests/timing_results.csv index 993afd10..61ba4f17 100644 --- a/timing_tests/timing_results.csv +++ b/timing_tests/timing_results.csv @@ -60,3 +60,8 @@ wind_precom,1.4046452045440674,,d0b65f8e,v2,2025-08-28T14:52:40.968804,arm wind_precom,1.4467580318450928,,d0b65f8e,v2,2025-08-28T15:16:33.303801,arm wind_precom,1.4958651065826416,,d0b65f8e,v2,2025-08-28T15:22:49.645404,arm wind_precom,1.2841532230377197,,229519c4,v2,2025-09-03T09:09:49.964979,arm +wind_precom,1.3157711029052734,,c13d1e47,feature/reorg_log_channels_v2,2025-10-29T13:18:30.472552,arm +wind_precom,1.4151639938354492,,2dc18b42,feature/reorg_log_channels_v2,2025-10-29T13:44:40.264003,arm +wind_precom,1.3030691146850586,,c829b976,feature/reorg_log_channels_v2,2025-10-29T14:31:48.111826,arm +wind_precom,2.321347951889038,,c829b976,feature/reorg_log_channels_v2,2025-10-29T15:04:32.015310,arm +wind_precom,1.3096461296081543,,c829b976,feature/reorg_log_channels_v2,2025-10-29T15:04:59.714680,arm