"""Inertia-Free QSM power estimation wrapper for the InertiaFree-QSM package.
This wrapper adapts the Inertia-Free Quasi-Steady Model to the AWESPA
modular architecture. The underlying model accepts awesIO format configuration
files directly and supports power curve generation, while single-wind-speed
simulations can be either direct or optimized."""
import numpy as np
from pathlib import Path
from typing import Dict, Any, Optional, List
from .base import PowerEstimationModel
try:
from awesio.validator import validate as awesio_validate # type: ignore
except ImportError:
awesio_validate = None
try:
from inertiafree_qsm import PowerCurveConstructor # type: ignore
except ImportError as e:
print(f"Import error for InertiaFree-QSM: {e}")
PowerCurveConstructor = None
[docs]
class InertiaFreeQSMPowerModel(PowerEstimationModel):
"""Wrapper for the Inertia-Free Quasi-Steady Model power curve constructor.
This wrapper adapts the vendored InertiaFree-QSM to the AWESPA modular
architecture. Power curves are generated from optimized cycle parameters
per wind speed. Single-wind-speed simulations can still be run either
directly or with optimization.
The model requires three configuration files in awesIO format:
- System configuration (kite, tether, ground station properties)
- Wind resource (altitude profiles, profiles, probability matrix)
- Simulation settings (cycle parameters, optimizer bounds, phase settings)
"""
[docs]
def __init__(self):
"""Initialize the Inertia-Free QSM power estimation model."""
self.constructor: Optional[Any] = None
self.systemPath: Optional[Path] = None
self.windResourcePath: Optional[Path] = None
self.simulationSettingsPath: Optional[Path] = None
[docs]
def load_configuration(
self,
system_path: Path,
simulation_settings_path: Path,
wind_resource_path: Path = None,
validate: bool = True,
) -> None:
"""Load power model configuration from YAML files.
Creates a ``PowerCurveConstructor`` instance from the vendored
InertiaFree-QSM package using the provided configuration files.
Args:
system_path (Path): Path to the system configuration YAML file
(awesIO format with wing, tether, ground_station components).
simulation_settings_path (Path): Path to simulation settings YAML
file containing cycle, phase, optimizer, and solver parameters.
wind_resource_path (Path): Path to wind resource YAML file
containing altitude profiles, profiles, and probability matrix.
validate (bool): If True, validate configuration files using
the awesIO validator. Defaults to True.
Raises:
ImportError: If the vendored PowerCurveConstructor cannot be imported.
FileNotFoundError: If any required file does not exist.
"""
if PowerCurveConstructor is None:
raise ImportError(
"PowerCurveConstructor could not be imported from InertiaFree-QSM"
)
self.systemPath = Path(system_path)
self.simulationSettingsPath = Path(simulation_settings_path)
self.windResourcePath = Path(wind_resource_path) if wind_resource_path else None
# Validate that required files exist
for label, path in [
("System config", self.systemPath),
("Simulation settings", self.simulationSettingsPath),
]:
if not path.exists():
raise FileNotFoundError(f"{label} file not found: {path}")
if self.windResourcePath is not None and not self.windResourcePath.exists():
raise FileNotFoundError(
f"Wind resource file not found: {self.windResourcePath}"
)
# Validate configuration files
if validate:
if awesio_validate is None:
raise ImportError("awesIO validator not available")
awesio_validate(input=self.systemPath)
if self.windResourcePath is not None:
awesio_validate(input=self.windResourcePath)
print("Configuration files validated.")
# Create the PowerCurveConstructor
self.constructor = PowerCurveConstructor(
system_config_path=self.systemPath,
wind_resource_path=self.windResourcePath,
simulation_settings_path=self.simulationSettingsPath,
)
print(f"Loaded InertiaFree-QSM configuration:")
self.constructor.print_summary()
[docs]
def compute_power_curves(
self,
wind_speeds: Optional[np.ndarray] = None,
profile_ids: Optional[List[int]] = None,
output_path: Path = None,
verbose: bool = True,
showplot: bool = False,
saveplot: bool = False,
validate: bool = True,
) -> Dict[str, Any]:
"""Compute power curves.
Args:
output_path (Path): Path where power curve YAML will be written.
If None, no export is performed. Defaults to None.
wind_speeds (np.ndarray): Custom wind speeds to evaluate [m/s].
If None, uses wind speeds from simulation settings.
Defaults to None.
profile_ids (list): profile IDs (1-indexed) to calculate. If None,
calculates all profiles. Defaults to None.
verbose (bool): Whether to print progress output.
Defaults to True.
showplot (bool): Whether to display plots after generation.
Defaults to False.
saveplot (bool): Whether to save plots to file. Requires
``output_path``. Defaults to False.
validate (bool): If True, validate the output YAML file using
the awesIO validator. Defaults to True.
Returns:
dict: Power curve data in awesIO format.
Raises:
ValueError: If model is not initialized.
"""
if self.constructor is None:
raise ValueError(
"Power model not initialized. Call load_configuration first."
)
print("Computing power curves...")
data = self.constructor.generate_power_curves(
wind_speeds=wind_speeds,
profile_ids=profile_ids,
output_path=output_path,
verbose=verbose,
show_plot=showplot,
save_plot=saveplot,
)
if output_path is not None:
print(f"Power curves exported to: {output_path}")
if validate:
if awesio_validate is None:
raise ImportError("awesIO validator not available")
awesio_validate(input=output_path)
if verbose:
print(f"Output validated: {output_path}")
return data
[docs]
def calculate_power_at_wind_speed(
self,
wind_speed: float,
method: str = "direct",
profile_id: int = 1,
output_path: Path = None,
verbose: bool = True,
showplot: bool = False,
saveplot: bool = False,
validate: bool = True,
) -> float:
"""Calculate power output at a single wind speed.
Simulates a single pumping cycle at the given wind speed and returns
the average cycle power.
Args:
wind_speed (float): Wind speed at reference height [m/s].
method (str): Simulation method, either ``'direct'`` or
``'optimization'``. Defaults to ``'direct'``.
profile_id (int): profile ID (1-indexed) for wind profile
selection. Defaults to 1.
output_path (Path): Path where results YAML will be written.
If None, no export is performed. Defaults to None.
verbose (bool): Whether to print verbose output.
Defaults to True.
showplot (bool): Whether to display cycle detail plot.
Defaults to False.
saveplot (bool): Whether to save cycle detail plot. Requires
``output_path``. Defaults to False.
validate (bool): If True, validate the output YAML file using
the awesIO validator. Defaults to True.
Returns:
float: Average cycle power output [W].
Raises:
ValueError: If model is not initialized.
"""
if self.constructor is None:
raise ValueError(
"Power model not initialized. Call load_configuration first."
)
result = self.constructor.simulate_single_wind_speed(
wind_speed=wind_speed,
profile_id=profile_id,
method=method,
output_path=output_path,
verbose=verbose,
show_plot=showplot,
save_plot=saveplot,
)
# Extract average cycle power from result
powerCurves = result.get("power_curves", [])
if powerCurves:
windSpeedData = powerCurves[0].get("wind_speed_data", [])
if windSpeedData:
power = windSpeedData[0]["performance"]["power"][
"average_cycle_power"
]
if verbose:
print(
f"\nPower at {wind_speed:.1f} m/s ({method}): "
f"{power:.2f} W ({power / 1000:.2f} kW)"
)
return float(power)
print(f"Warning: Could not extract power from result at {wind_speed:.1f} m/s")
return 0.0