From 4a399b5b386f227bd9d08dff81b182db742777ca Mon Sep 17 00:00:00 2001 From: Roy Li Date: Thu, 15 Jan 2026 10:06:36 -0700 Subject: [PATCH 1/5] power curve rename and preferred range --- .../src/components/resultPane/RightPane.tsx | 2 +- .../components/settings/HubHeightSettings.tsx | 33 +++++++- .../settings/PowerCurveSettings.tsx | 27 ++++--- windwatts-ui/src/constants/index.ts | 2 +- windwatts-ui/src/constants/powerCurves.ts | 15 ---- windwatts-ui/src/constants/turbines.ts | 75 +++++++++++++++++++ 6 files changed, 126 insertions(+), 28 deletions(-) delete mode 100644 windwatts-ui/src/constants/powerCurves.ts create mode 100644 windwatts-ui/src/constants/turbines.ts diff --git a/windwatts-ui/src/components/resultPane/RightPane.tsx b/windwatts-ui/src/components/resultPane/RightPane.tsx index 9c82f48d..5f754777 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 b2ed6c10..5d9ba6d7 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,14 @@ 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 +59,25 @@ 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 d77e7111..814925d4 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,53 @@ 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 0230d7da..3be29be8 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 5a2a3485..00000000 --- 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 00000000..a2294170 --- /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); \ No newline at end of file From cc9ac5d8e73232a7513a33310f25f4b1108ff04b Mon Sep 17 00:00:00 2001 From: Roy Li Date: Tue, 20 Jan 2026 13:42:14 -0700 Subject: [PATCH 2/5] prefer turbine to powercurve - production endpoint --- .../app/controllers/wind_data_controller.py | 24 +++++++++++++++---- windwatts-api/app/utils/validation.py | 5 ++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/windwatts-api/app/controllers/wind_data_controller.py b/windwatts-api/app/controllers/wind_data_controller.py index bf8f2122..e6e5defa 100644 --- a/windwatts-api/app/controllers/wind_data_controller.py +++ b/windwatts-api/app/controllers/wind_data_controller.py @@ -156,8 +156,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 +174,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 +189,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 diff --git a/windwatts-api/app/utils/validation.py b/windwatts-api/app/utils/validation.py index 41b1039b..14d8d56f 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", []) From 9c12caf7d7e44411991ae8c99907107a3263f16b Mon Sep 17 00:00:00 2001 From: Roy Li Date: Tue, 20 Jan 2026 14:17:06 -0700 Subject: [PATCH 3/5] add turbines/ endpoint --- .../app/controllers/wind_data_controller.py | 75 ++++++++++++++----- windwatts-api/app/schemas.py | 15 ++++ 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/windwatts-api/app/controllers/wind_data_controller.py b/windwatts-api/app/controllers/wind_data_controller.py index e6e5defa..cbcfef24 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, @@ -210,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", @@ -226,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 aa6f4b49..1e32d362 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] From 7dac797e60a46df0e095503858a26a40c0b2baac Mon Sep 17 00:00:00 2001 From: Roy Li Date: Tue, 20 Jan 2026 14:18:13 -0700 Subject: [PATCH 4/5] make format --- .../app/controllers/wind_data_controller.py | 6 +++--- .../src/components/settings/HubHeightSettings.tsx | 15 ++++++++++----- .../components/settings/PowerCurveSettings.tsx | 6 ++---- windwatts-ui/src/constants/turbines.ts | 2 +- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/windwatts-api/app/controllers/wind_data_controller.py b/windwatts-api/app/controllers/wind_data_controller.py index cbcfef24..ac0790b2 100644 --- a/windwatts-api/app/controllers/wind_data_controller.py +++ b/windwatts-api/app/controllers/wind_data_controller.py @@ -214,10 +214,10 @@ def get_production( 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" """ @@ -286,7 +286,7 @@ def get_powercurves(): Power curves are model-agnostic and can be used with any dataset (era5, wtk, ensemble). """ try: - return _get_available_turbines("power curves") + return _get_available_turbines("power_curves") except Exception: raise HTTPException(status_code=500, detail="Internal server error") diff --git a/windwatts-ui/src/components/settings/HubHeightSettings.tsx b/windwatts-ui/src/components/settings/HubHeightSettings.tsx index 5d9ba6d7..ff6a8c90 100644 --- a/windwatts-ui/src/components/settings/HubHeightSettings.tsx +++ b/windwatts-ui/src/components/settings/HubHeightSettings.tsx @@ -44,12 +44,16 @@ export function HubHeightSettings() { }; const turbineData: TurbineInfo | undefined = TURBINE_DATA[powerCurve]; - - const isHeightInRange: boolean = turbineData + + const isHeightInRange: boolean = turbineData ? hubHeight >= turbineData.minHeight && hubHeight <= turbineData.maxHeight : true; - const validationColor: "primary" | "success" | "error" = turbineData ? (isHeightInRange ? "success" : "error") : "primary"; + const validationColor: "primary" | "success" | "error" = turbineData + ? isHeightInRange + ? "success" + : "error" + : "primary"; return ( @@ -59,7 +63,7 @@ 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) + {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 814925d4..85c9c859 100644 --- a/windwatts-ui/src/components/settings/PowerCurveSettings.tsx +++ b/windwatts-ui/src/components/settings/PowerCurveSettings.tsx @@ -30,7 +30,7 @@ export function PowerCurveSettings() { 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)`; } @@ -67,9 +67,7 @@ export function PowerCurveSettings() { ) : ( - - Loading turbine options... - + Loading turbine options... )} diff --git a/windwatts-ui/src/constants/turbines.ts b/windwatts-ui/src/constants/turbines.ts index a2294170..9b183374 100644 --- a/windwatts-ui/src/constants/turbines.ts +++ b/windwatts-ui/src/constants/turbines.ts @@ -72,4 +72,4 @@ export const POWER_CURVE_LABEL: Record = Object.entries( {} as Record ); -export const VALID_POWER_CURVES = Object.keys(TURBINE_DATA); \ No newline at end of file +export const VALID_POWER_CURVES = Object.keys(TURBINE_DATA); From 41e31dc70e259a38da7d056ed1088119efc1f7bb Mon Sep 17 00:00:00 2001 From: Roy Li Date: Tue, 20 Jan 2026 14:28:19 -0700 Subject: [PATCH 5/5] update to ui for turbine renaming --- windwatts-ui/src/hooks/useEnsembleTilesData.ts | 2 +- windwatts-ui/src/hooks/useProductionData.ts | 2 +- windwatts-ui/src/services/api.ts | 4 ++-- windwatts-ui/src/types/Requests.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/windwatts-ui/src/hooks/useEnsembleTilesData.ts b/windwatts-ui/src/hooks/useEnsembleTilesData.ts index bdacc300..c267a470 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 15747141..3765b325 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 e18eaabb..0e837ff0 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 ac5515c9..c17018aa 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; }