diff --git a/windwatts-api/app/controllers/wind_data_controller.py b/windwatts-api/app/controllers/wind_data_controller.py
index bf8f212..ac0790b 100644
--- a/windwatts-api/app/controllers/wind_data_controller.py
+++ b/windwatts-api/app/controllers/wind_data_controller.py
@@ -21,6 +21,7 @@
from app.power_curve.global_power_curve_manager import power_curve_manager
from app.schemas import (
+ AvailableTurbinesResponse,
WindSpeedResponse,
AvailablePowerCurvesResponse,
EnergyProductionResponse,
@@ -156,8 +157,13 @@ def get_production(
lat: float = Query(..., description="Latitude of the location"),
lng: float = Query(..., description="Longitude of the location"),
height: int = Query(..., description="Height in meters"),
- powercurve: str = Query(
- ..., description="Power curve identifier (e.g., nrel-reference-100kW)"
+ turbine: Optional[str] = Query(
+ None, description="Turbine model identifier (e.g., nrl-reference-100kW)"
+ ),
+ powercurve: Optional[str] = Query(
+ None,
+ deprecated=True,
+ description="Deprecated: use 'turbine' instead. Power curve identifier.",
),
period: str = Query(
"all",
@@ -169,13 +175,14 @@ def get_production(
),
):
"""
- Retrieve energy production estimates for a specific location, height, and power curve.
+ Retrieve energy production estimates for a specific location, height, and turbine.
- **model**: Data model (era5, wtk, ensemble)
- **lat**: Latitude (varies by model, refer info endpoint for coordinate bounds)
- **lng**: Longitude (varies by model, refer info endpoint for coordinate bounds)
- **height**: Height in meters (varies by model, refer info endpoint for the available heights)
- - **powercurve**: Power curve to use for calculations
+ - **turbine**: Turbine model to use for calculations
+ - **powercurve**: Deprecated parameter, use 'turbine' instead
- **period**: Time aggregation period (default: all)
- **source**: Optional data source override
"""
@@ -183,12 +190,20 @@ def get_production(
# Catch invalid model before core function call
model = validate_model(model)
+ # Backward compatibility for 'powercurve'
+ turbine = turbine or powercurve
+ if not turbine:
+ raise HTTPException(
+ status_code=400,
+ detail="Either 'turbine' or 'powercurve' parameter is required",
+ )
+
# Use default source if not provided
if source is None:
source = MODEL_CONFIG.get(model, {}).get("default_source", "athena")
return get_production_core(
- model, lat, lng, height, powercurve, period, source, data_fetcher_router
+ model, lat, lng, height, turbine, period, source, data_fetcher_router
)
except HTTPException:
raise
@@ -196,10 +211,64 @@ def get_production(
raise HTTPException(status_code=500, detail="Internal server error")
+def _get_available_turbines(field_name: str = "turbines"):
+ """
+ Retrieve all available turbines/power curves.
+
+ Returns sorted list with NLR reference turbines first (by capacity),
+ followed by other turbines alphabetically.
+
+ Args:
+ field_name: "turbines" or "power curves"
+ """
+ all_curves = list(power_curve_manager.power_curves.keys())
+
+ def extract_kw(curve_name: str):
+ # Extracts the kw value from nlr curves, e.g. "nlr-reference-2.5kW" -> 2.5
+ match = re.search(r"nlr-reference-([0-9.]+)kW", curve_name)
+ if match:
+ return float(match.group(1))
+ return float("inf")
+
+ nlr_curves = [c for c in all_curves if c.startswith("nlr-reference-")]
+ other_curves = [c for c in all_curves if not c.startswith("nlr-reference-")]
+
+ nlr_curves_sorted = sorted(nlr_curves, key=extract_kw)
+ other_curves_sorted = sorted(other_curves)
+
+ ordered_curves = nlr_curves_sorted + other_curves_sorted
+ return {f"available_{field_name}": ordered_curves}
+
+
+@router.get(
+ "/turbines",
+ summary="Fetch all available turbines",
+ response_model=AvailableTurbinesResponse,
+ responses={
+ 200: {
+ "description": "Available turbines retrieved successfully",
+ "model": AvailableTurbinesResponse,
+ },
+ 500: {"description": "Internal server error"},
+ },
+)
+def get_turbines():
+ """
+ Retrieve a list of all available turbines.
+
+ Turbines are model-agnostic and can be used with any dataset (era5, wtk, ensemble).
+ """
+ try:
+ return _get_available_turbines("turbines")
+ except Exception:
+ raise HTTPException(status_code=500, detail="Internal server error")
+
+
@router.get(
"/powercurves",
summary="Fetch all available power curves",
response_model=AvailablePowerCurvesResponse,
+ deprecated=True,
responses={
200: {
"description": "Available power curves retrieved successfully",
@@ -212,26 +281,12 @@ def get_powercurves():
"""
Retrieve a list of all available power curves.
+ Deprecated: Use /turbines endpoint instead.
+
Power curves are model-agnostic and can be used with any dataset (era5, wtk, ensemble).
"""
try:
- all_curves = list(power_curve_manager.power_curves.keys())
-
- def extract_kw(curve_name: str):
- # Extracts the kw value from nrel curves, e.g. "nrel-reference-2.5kW" -> 2.5
- match = re.search(r"nrel-reference-([0-9.]+)kW", curve_name)
- if match:
- return float(match.group(1))
- return float("inf")
-
- nrel_curves = [c for c in all_curves if c.startswith("nrel-reference-")]
- other_curves = [c for c in all_curves if not c.startswith("nrel-reference-")]
-
- nrel_curves_sorted = sorted(nrel_curves, key=extract_kw)
- other_curves_sorted = sorted(other_curves)
-
- ordered_curves = nrel_curves_sorted + other_curves_sorted
- return {"available_power_curves": ordered_curves}
+ return _get_available_turbines("power_curves")
except Exception:
raise HTTPException(status_code=500, detail="Internal server error")
diff --git a/windwatts-api/app/schemas.py b/windwatts-api/app/schemas.py
index aa6f4b4..1e32d36 100644
--- a/windwatts-api/app/schemas.py
+++ b/windwatts-api/app/schemas.py
@@ -73,6 +73,21 @@ class HourlyWindSpeedResponse(BaseModel):
]
+class AvailableTurbinesResponse(BaseModel):
+ available_turbines: List[str]
+
+ model_config = {
+ "json_schema_extra": {
+ "example": {
+ "available_turbines": [
+ "nlr-reference-2.5kW",
+ "nlr-reference-100kW",
+ ]
+ }
+ }
+ }
+
+
class AvailablePowerCurvesResponse(BaseModel):
available_power_curves: List[str]
diff --git a/windwatts-api/app/utils/validation.py b/windwatts-api/app/utils/validation.py
index 41b1039..14d8d56 100644
--- a/windwatts-api/app/utils/validation.py
+++ b/windwatts-api/app/utils/validation.py
@@ -85,6 +85,11 @@ def validate_powercurve(powercurve: str) -> str:
return powercurve
+def validate_turbine(turbine: str) -> str:
+ """Validate turbine name (alias for validate_powercurve for clarity)"""
+ return validate_powercurve(turbine)
+
+
def validate_year(year: int, model: str) -> int:
"""Validate year for given model"""
valid_years = MODEL_CONFIG[model]["years"].get("full", [])
diff --git a/windwatts-ui/src/components/resultPane/RightPane.tsx b/windwatts-ui/src/components/resultPane/RightPane.tsx
index 9c82f48..5f75477 100644
--- a/windwatts-ui/src/components/resultPane/RightPane.tsx
+++ b/windwatts-ui/src/components/resultPane/RightPane.tsx
@@ -46,7 +46,7 @@ export const RightPane = () => {
data: hubHeight ? `${hubHeight} meters` : "Not selected",
},
{
- title: "Power curve",
+ title: "Turbine",
data: powerCurve ? `${POWER_CURVE_LABEL[powerCurve]}` : "Not selected",
},
];
diff --git a/windwatts-ui/src/components/settings/HubHeightSettings.tsx b/windwatts-ui/src/components/settings/HubHeightSettings.tsx
index b2ed6c1..ff6a8c9 100644
--- a/windwatts-ui/src/components/settings/HubHeightSettings.tsx
+++ b/windwatts-ui/src/components/settings/HubHeightSettings.tsx
@@ -1,13 +1,14 @@
import { useContext, useEffect, useMemo } from "react";
import { SettingsContext } from "../../providers/SettingsContext";
-import { Box, Slider, Typography } from "@mui/material";
-import { HUB_HEIGHTS } from "../../constants";
+import { Box, Slider, Typography, Paper } from "@mui/material";
+import { HUB_HEIGHTS, TURBINE_DATA, TurbineInfo } from "../../constants";
export function HubHeightSettings() {
const {
hubHeight,
setHubHeight,
preferredModel: dataModel,
+ powerCurve,
} = useContext(SettingsContext);
const { values: availableHeights, interpolation: step } = useMemo(() => {
@@ -42,6 +43,18 @@ export function HubHeightSettings() {
}
};
+ const turbineData: TurbineInfo | undefined = TURBINE_DATA[powerCurve];
+
+ const isHeightInRange: boolean = turbineData
+ ? hubHeight >= turbineData.minHeight && hubHeight <= turbineData.maxHeight
+ : true;
+
+ const validationColor: "primary" | "success" | "error" = turbineData
+ ? isHeightInRange
+ ? "success"
+ : "error"
+ : "primary";
+
return (
@@ -50,6 +63,26 @@ export function HubHeightSettings() {
Choose a closest value (in meters) to the considered hub height:
+
+ {turbineData && (
+
+
+
+ {isHeightInRange ? "Within" : "Outside"} recommended range - (
+ {turbineData.minHeight}m - {turbineData.maxHeight}m)
+
+
+
+ )}
+
);
diff --git a/windwatts-ui/src/components/settings/PowerCurveSettings.tsx b/windwatts-ui/src/components/settings/PowerCurveSettings.tsx
index d77e711..85c9c85 100644
--- a/windwatts-ui/src/components/settings/PowerCurveSettings.tsx
+++ b/windwatts-ui/src/components/settings/PowerCurveSettings.tsx
@@ -4,7 +4,7 @@ import useSWR from "swr";
import { SettingsContext } from "../../providers/SettingsContext";
import { useContext } from "react";
import { getAvailablePowerCurves } from "../../services/api";
-import { POWER_CURVE_LABEL } from "../../constants";
+import { POWER_CURVE_LABEL, TURBINE_DATA } from "../../constants";
const DefaultPowerCurveOptions = [
"nlr-reference-2.5kW",
@@ -27,44 +27,51 @@ export function PowerCurveSettings() {
setPowerCurve(event.target.value as string);
};
+ const getTurbineLabel = (turbineId: string): string => {
+ const turbineInfo = TURBINE_DATA[turbineId];
+ const baseName = POWER_CURVE_LABEL[turbineId] || turbineId;
+
+ if (turbineInfo) {
+ return `${baseName} (${turbineInfo.minHeight}-${turbineInfo.maxHeight}m)`;
+ }
+ return baseName;
+ };
+
return (
- Power Curve
+ Turbine
- Select a power curve option:
+ Select a turbine option:
{powerCurveOptions.length > 0 ? (
<>
- {/* Power Curve */}
+ {/* Turbine */}
>
) : (
-
- Loading power curve options...
-
+ Loading turbine options...
)}
- * Make sure the selected turbine class matches the hub height (higher
- hub heights should be chosen for larger turbines).
+ * The height range shows recommended hub height for this turbine.
diff --git a/windwatts-ui/src/constants/index.ts b/windwatts-ui/src/constants/index.ts
index 0230d7d..3be29be 100644
--- a/windwatts-ui/src/constants/index.ts
+++ b/windwatts-ui/src/constants/index.ts
@@ -1,6 +1,6 @@
// Export all constants from this directory
export * from "./coordinates";
-export * from "./powerCurves";
+export * from "./turbines"; // formerly powerCurves
export * from "./ui";
export * from "./dataModelInfo";
export * from "./hubSettings";
diff --git a/windwatts-ui/src/constants/powerCurves.ts b/windwatts-ui/src/constants/powerCurves.ts
deleted file mode 100644
index 5a2a348..0000000
--- a/windwatts-ui/src/constants/powerCurves.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export const POWER_CURVE_LABEL: Record = {
- "nlr-reference-2.5kW": "NLR Reference 2.5kW",
- "nlr-reference-100kW": "NLR Reference 100kW",
- "nlr-reference-250kW": "NLR Reference 250kW",
- "nlr-reference-2000kW": "NLR Reference 2000kW",
- "bergey-excel-15": "Bergey Excel 15kW",
- "eocycle-25": "Eocycle 25kW",
- "northern-100": "Northern Power 100kW",
- siva_250kW_30m_rotor_diameter: "Siva 250kW (30m rotor diameter)",
- siva_250kW_32m_rotor_diameter: "Siva 250kW (32m rotor diameter)",
- siva_750_u50: "Siva 750kW (50m rotor diameter)",
- siva_750_u57: "Siva 750kW (57m rotor diameter)",
-};
-
-export const VALID_POWER_CURVES = Object.keys(POWER_CURVE_LABEL);
diff --git a/windwatts-ui/src/constants/turbines.ts b/windwatts-ui/src/constants/turbines.ts
new file mode 100644
index 0000000..9b18337
--- /dev/null
+++ b/windwatts-ui/src/constants/turbines.ts
@@ -0,0 +1,75 @@
+export interface TurbineInfo {
+ label: string; // Displayed Label
+ minHeight: number; // Preferred min height
+ maxHeight: number; // Preferred max height
+}
+
+export const TURBINE_DATA: Record = {
+ "nlr-reference-2.5kW": {
+ label: "NLR Reference 2.5kW",
+ minHeight: 20,
+ maxHeight: 40,
+ },
+ "nlr-reference-100kW": {
+ label: "NLR Reference 100kW",
+ minHeight: 30,
+ maxHeight: 80,
+ },
+ "nlr-reference-250kW": {
+ label: "NLR Reference 250kW",
+ minHeight: 40,
+ maxHeight: 100,
+ },
+ "nlr-reference-2000kW": {
+ label: "NLR Reference 2000kW",
+ minHeight: 60,
+ maxHeight: 140,
+ },
+ "bergey-excel-15": {
+ label: "Bergey Excel 15kW",
+ minHeight: 20,
+ maxHeight: 50,
+ },
+ "eocycle-25": {
+ label: "Eocycle 25kW",
+ minHeight: 25,
+ maxHeight: 60,
+ },
+ "northern-100": {
+ label: "Northern Power 100kW",
+ minHeight: 30,
+ maxHeight: 80,
+ },
+ siva_250kW_30m_rotor_diameter: {
+ label: "Siva 250kW (30m rotor diameter)",
+ minHeight: 40,
+ maxHeight: 100,
+ },
+ siva_250kW_32m_rotor_diameter: {
+ label: "Siva 250kW (32m rotor diameter)",
+ minHeight: 40,
+ maxHeight: 100,
+ },
+ siva_750_u50: {
+ label: "Siva 750kW (50m rotor diameter)",
+ minHeight: 50,
+ maxHeight: 120,
+ },
+ siva_750_u57: {
+ label: "Siva 750kW (57m rotor diameter)",
+ minHeight: 50,
+ maxHeight: 140,
+ },
+};
+
+export const POWER_CURVE_LABEL: Record = Object.entries(
+ TURBINE_DATA
+).reduce(
+ (acc, [key, turbine]) => {
+ acc[key] = turbine.label;
+ return acc;
+ },
+ {} as Record
+);
+
+export const VALID_POWER_CURVES = Object.keys(TURBINE_DATA);
diff --git a/windwatts-ui/src/hooks/useEnsembleTilesData.ts b/windwatts-ui/src/hooks/useEnsembleTilesData.ts
index bdacc30..c267a47 100644
--- a/windwatts-ui/src/hooks/useEnsembleTilesData.ts
+++ b/windwatts-ui/src/hooks/useEnsembleTilesData.ts
@@ -51,7 +51,7 @@ export const useEnsembleTilesData = () => {
lat: lat!,
lng: lng!,
hubHeight,
- powerCurve,
+ turbine: powerCurve,
dataModel,
period: "all",
}),
diff --git a/windwatts-ui/src/hooks/useProductionData.ts b/windwatts-ui/src/hooks/useProductionData.ts
index 1574714..3765b32 100644
--- a/windwatts-ui/src/hooks/useProductionData.ts
+++ b/windwatts-ui/src/hooks/useProductionData.ts
@@ -41,7 +41,7 @@ export const useProductionData = () => {
lat: lat!,
lng: lng!,
hubHeight,
- powerCurve,
+ turbine: powerCurve,
dataModel,
period: "full",
}),
diff --git a/windwatts-ui/src/services/api.ts b/windwatts-ui/src/services/api.ts
index e18eaab..0e837ff 100644
--- a/windwatts-ui/src/services/api.ts
+++ b/windwatts-ui/src/services/api.ts
@@ -44,11 +44,11 @@ export const getEnergyProduction = async ({
lat,
lng,
hubHeight,
- powerCurve,
+ turbine,
dataModel,
period = "all",
}: EnergyProductionRequest) => {
- const url = `/api/v1/${dataModel}/production?lat=${lat}&lng=${lng}&height=${hubHeight}&powercurve=${powerCurve}&period=${period}`;
+ const url = `/api/v1/${dataModel}/production?lat=${lat}&lng=${lng}&height=${hubHeight}&turbine=${turbine}&period=${period}`;
const options = {
method: "GET",
headers: {
diff --git a/windwatts-ui/src/types/Requests.ts b/windwatts-ui/src/types/Requests.ts
index ac5515c..c17018a 100644
--- a/windwatts-ui/src/types/Requests.ts
+++ b/windwatts-ui/src/types/Requests.ts
@@ -12,7 +12,7 @@ export interface EnergyProductionRequest {
lat: number;
lng: number;
hubHeight: number;
- powerCurve: string;
+ turbine: string;
dataModel: DataModel;
period?: string;
}