Skip to content
Open
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
3 changes: 3 additions & 0 deletions docs/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ parts:
- file: examples/06_wind_and_hydrogen
- file: examples/07_open_cycle_gas_turbine
- file: examples/09_multiunit_thermal_plant
- caption: Post-Processing
chapters:
- file: herc_analysis
2 changes: 1 addition & 1 deletion docs/adding_components.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class MyComponent(ComponentBase):
| Requirement | Description |
|---|---|
| `component_category` | Must be `"generator"`, `"storage"`, or `"load"`. Generators contribute to `locally_generated_power`. |
| `super().__init__()` | Sets `self.component_name`, `self.component_type`, `self.dt`, `self.starttime`, and configures logging. |
| `super().__init__()` | Sets `self.component_name`, `self.component_type`, `self.component_group` (defaulting `component_group` in `h_dict` to `component_name` when omitted), `self.dt`, `self.starttime`, and configures logging. |
| `power` output | All components must write a `power` value to `h_dict[self.component_name]["power"]` in the `step` method. |
| Return `h_dict` | Both `get_initial_conditions_and_meta_data` and `step` must return the modified `h_dict`. |

Expand Down
13 changes: 11 additions & 2 deletions docs/component_types.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Component Names, Types, and Categories

Three related but distinct concepts govern how plant components are identified in Hercules: `component_name`, `component_type`, and `component_category`. Understanding the distinction is important for writing YAML input files and for programmatically working with `h_dict`.
Related concepts govern how plant components are identified in Hercules: `component_name`, `component_type`, `component_category`, and optionally `component_group`. Understanding the distinction is important for writing YAML input files and for programmatically working with `h_dict`.

## The Three Concepts
## The concepts

### `component_name`

Expand Down Expand Up @@ -33,13 +33,22 @@ The **component category** is a class-level attribute defined in each component

Every `ComponentBase` subclass **must** define `component_category`; a `TypeError` is raised at class-definition time if it is missing.

### `component_group`

The **component group** is an optional string users may set in YAML to label several distinct components as one logical unit for post-processing (for example, summing their powers before reporting).

- **Source**: optional `component_group:` field in the component's YAML block
- **Default**: If omitted, Hercules sets `component_group` equal to `component_name` (the YAML key)
- **Used by**: Not used inside Hercules physics or control; the resolved value is stored on each component as `self.component_group` and written into `h_dict[component_name]["component_group"]` (including in HDF5 metadata). Companion tools such as [herc_analysis](herc_analysis.md) use it to aggregate outputs across instances that share the same group label.

### Summary

| Concept | Set by | Example value | Used for |
|---|---|---|---|
| `component_name` | User (YAML key) | `"battery_unit_1"` | Accessing `h_dict[name]`; unique instance ID |
| `component_type` | User (`component_type:` field) | `"BatterySimple"` | Registry lookup to select the Python class |
| `component_category` | Developer (class variable) | `"storage"` | Generator classification; sign convention |
| `component_group` | User (`component_group:` field, optional) | `"stage_a_storage"` | Post-processing grouping (defaults to `component_name`) |

---

Expand Down
2 changes: 1 addition & 1 deletion docs/h_dict.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The `h_dict` is a Python dictionary that contains all the configurations for eac
| `controller` | dict | Controller configuration | - |
| **Hybrid Plant Components** |

Any top-level `h_dict` entry whose value is a dict containing a `component_type` key is auto-discovered as a plant component. The key is a user-chosen `component_name` (e.g. `wind_farm`, `battery_unit_1`) — it does not need to match the category name. See [Component Names, Types, and Categories](component_types.md) for details.
Any top-level `h_dict` entry whose value is a dict containing a `component_type` key is auto-discovered as a plant component. The key is a user-chosen `component_name` (e.g. `wind_farm`, `battery_unit_1`) — it does not need to match the category name. Each such dict may optionally include `component_group` (string); if absent, Hercules sets it to `component_name` when the component is constructed. See [Component Names, Types, and Categories](component_types.md) for details.

### Wind Farm
| `component_type` | str | Must be "WindFarm" or "WindFarmSCADAPower" |
Expand Down
13 changes: 13 additions & 0 deletions docs/herc_analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# herc_analysis (companion post-processing)

[**herc_analysis**](https://github.com/NatLabRockies/herc_analysis) is a companion Python repository for analyzing Hercules hybrid plant simulations. Install it into the same environment you use for Hercules when you want reusable loaders, plots, and summaries over HDF5 outputs (see the herc_analysis README and docs for features and API).

## Relationship to Hercules examples

The **examples** in this Hercules repository often include small, self-contained scripts that demonstrate basic post-processing (e.g. reading output files and plotting in place). **herc_analysis** builds on that idea with pre-built post-processing utilities tailored to Hercules HDF5 layout—use whichever fits your workflow; for full capability lists and usage, refer to herc_analysis documentation.

## `component_group`

Users may set an optional string `component_group` in each plant component block of the Hercules YAML input. If it is omitted, Hercules sets `component_group` equal to `component_name` (the YAML section key). The field does not change simulation behavior in Hercules; it is recorded in `h_dict` (including serialized metadata in output files) so tools such as herc_analysis can treat multiple named components as one logical entity—for example, summing their signals before downstream reporting.

See [Component Names, Types, and Categories](component_types.md) for the full picture of names, types, categories, and groups.
2 changes: 1 addition & 1 deletion docs/hercules_input.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The input file structure mirrors the `h_dict` structure documented in the [h_dic

- **Top level parameters**: `dt`, `starttime_utc`, `endtime_utc` (see [timing](timing.md) for details)
- **Plant configuration**: `interconnect_limit`
- **Plant component sections**: any number of user-named sections, each containing a `component_type` key that identifies the component class to use (see [Component Names, Types, and Categories](component_types.md))
- **Plant component sections**: any number of user-named sections, each containing a `component_type` key that identifies the component class to use (see [Component Names, Types, and Categories](component_types.md)). Optionally include `component_group` (string) to tag instances for post-processing in [herc_analysis](herc_analysis.md); if omitted, it defaults to the section name (`component_name`).
- **External data**: `external_data` for external time series data (e.g., LMP prices, weather forecasts)
- **Optional settings**: `verbose`, `name`, `description`, `output_file`

Expand Down
2 changes: 1 addition & 1 deletion docs/hybrid_plant.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The `HybridPlant` class manages all plant components in Hercules. It handles ini

`HybridPlant` auto-discovers components from the [h_dict](h_dict.md) at initialization time. Any top-level `h_dict` entry whose value is a dict containing a `component_type` key is treated as a plant component. The YAML key becomes the component's `component_name` (a user-chosen instance identifier), and the `component_type` value determines which Python class is instantiated.

See [Component Names, Types, and Categories](component_types.md) for a full explanation of how `component_name`, `component_type`, and `component_category` relate to each other.
See [Component Names, Types, and Categories](component_types.md) for a full explanation of how `component_name`, `component_type`, `component_category`, and optional `component_group` relate to each other.

## Available Components

Expand Down
2 changes: 2 additions & 0 deletions examples/hercules_input_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ plant:
# Wind farm configuration (comment out if not using wind)
wind_farm:
component_type: WindFarm # Options: WindFarm, WindFarmSCADAPower
# Optional: tag for post-processing (herc_analysis); defaults to this section key if omitted
# component_group: wind_assets_north
wake_method: dynamic # Options: dynamic, precomputed, no_added_wakes
floris_input_file: ../inputs/floris_input.yaml # Path to FLORIS farm configuration file
wind_input_filename: ../inputs/wind_input.ftr # Path to wind resource data file (CSV, pickle, or feather format)
Expand Down
19 changes: 19 additions & 0 deletions hercules/plant_components/component_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class ComponentBase:
``component_name`` (the unique YAML key chosen by the user) is passed into ``__init__``
and may differ from the category when multiple instances of the same type are present.
``component_type`` is always set automatically to the concrete class name.

Optional YAML field ``component_group`` defaults to ``component_name`` when omitted.
Hercules does not use it for physics or control; it is written back into ``h_dict`` so
companion tools (e.g. herc_analysis) can aggregate multiple instances under one label.
"""

# Subclasses must override this with one of: "generator", "load", "storage"
Expand Down Expand Up @@ -51,6 +55,11 @@ def __init__(self, h_dict, component_name):
key). For single-instance plants this is typically the category name (e.g.
``"battery"``); for multi-instance plants it may be any user-chosen string
(e.g. ``"battery_unit_1"``).

Note:
Optional ``h_dict[component_name]["component_group"]`` (str) defaults to
``component_name`` and is stored on ``self.component_group`` and written back to
``h_dict`` for downstream metadata consumers.
"""

# Store the component name (unique instance identifier from the YAML key)
Expand All @@ -59,6 +68,16 @@ def __init__(self, h_dict, component_name):
# Derive component_type from the concrete class name — no hardcoding needed
self.component_type = type(self).__name__

# Optional tag for post-processing (defaults to component_name)
component_group = h_dict[component_name].get("component_group", component_name)
if not isinstance(component_group, str):
raise TypeError(
f"component_group for '{component_name}' must be a string, "
f"got {type(component_group).__name__}: {component_group!r}"
)
self.component_group = component_group
h_dict[component_name]["component_group"] = component_group

# Set up logging
# Check if log_file_name is defined in the h_dict[component_name]
if "log_file_name" in h_dict[component_name]:
Expand Down
58 changes: 58 additions & 0 deletions tests/component_base_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Tests for ComponentBase behavior shared by all plant components."""

import copy

import pytest
from hercules.plant_components.battery_simple import BatterySimple

from .test_inputs.h_dict import battery


def _battery_h_dict(**battery_overrides):
"""Build a minimal h_dict with one BatterySimple component.

Args:
**battery_overrides: Keys merged into the battery component dict.

Returns:
dict: Copy suitable for BatterySimple(h_dict, "battery").
"""
batt = copy.deepcopy(battery)
batt.update(battery_overrides)
return {
"dt": 1.0,
"starttime": 0.0,
"endtime": 10.0,
"verbose": False,
"plant": {"interconnect_limit": 30000.0},
"battery": batt,
}


def test_component_group_defaults_to_component_name():
"""When component_group is omitted, it equals component_name and is written to h_dict."""
h_dict = _battery_h_dict()
assert "component_group" not in h_dict["battery"]

battery_obj = BatterySimple(h_dict, "battery")

assert battery_obj.component_group == "battery"
assert h_dict["battery"]["component_group"] == "battery"


def test_component_group_explicit_value():
"""Explicit component_group is stored on the instance and echoed into h_dict."""
h_dict = _battery_h_dict(component_group="hybrid_unit_a")

battery_obj = BatterySimple(h_dict, "battery")

assert battery_obj.component_group == "hybrid_unit_a"
assert h_dict["battery"]["component_group"] == "hybrid_unit_a"


def test_component_group_must_be_string():
"""Non-string component_group raises TypeError."""
h_dict = _battery_h_dict(component_group=42)

with pytest.raises(TypeError, match="component_group"):
BatterySimple(h_dict, "battery")
Loading