Skip to content
Draft
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
99 changes: 77 additions & 22 deletions windwatts-api/app/controllers/wind_data_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from app.power_curve.global_power_curve_manager import power_curve_manager
from app.schemas import (
AvailableTurbinesResponse,
WindSpeedResponse,
AvailablePowerCurvesResponse,
EnergyProductionResponse,
Expand Down Expand Up @@ -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",
Expand All @@ -169,37 +175,100 @@ 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
"""
try:
# 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
except Exception:
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",
Expand All @@ -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")

Expand Down
15 changes: 15 additions & 0 deletions windwatts-api/app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
5 changes: 5 additions & 0 deletions windwatts-api/app/utils/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", [])
Expand Down
2 changes: 1 addition & 1 deletion windwatts-ui/src/components/resultPane/RightPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
];
Expand Down
38 changes: 36 additions & 2 deletions windwatts-ui/src/components/settings/HubHeightSettings.tsx
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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 (
<Box sx={{ mt: 2 }}>
<Typography variant="h6" gutterBottom>
Expand All @@ -50,6 +63,26 @@ export function HubHeightSettings() {
<Typography variant="body1" gutterBottom>
Choose a closest value (in meters) to the considered hub height:
</Typography>

{turbineData && (
<Paper
sx={{
p: 1.5,
mb: 2,
backgroundColor: `${validationColor}.light`,
borderLeft: `4px solid`,
borderLeftColor: `${validationColor}.main`,
}}
>
<Typography variant="body2">
<strong>
{isHeightInRange ? "Within" : "Outside"} recommended range - (
{turbineData.minHeight}m - {turbineData.maxHeight}m)
</strong>
</Typography>
</Paper>
)}

<Slider
value={hubHeight}
onChange={handleHubHeightChange}
Expand All @@ -60,6 +93,7 @@ export function HubHeightSettings() {
marks={hubHeightMarks}
min={Math.min(...availableHeights)}
max={Math.max(...availableHeights)}
color={validationColor}
/>
</Box>
);
Expand Down
29 changes: 18 additions & 11 deletions windwatts-ui/src/components/settings/PowerCurveSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 (
<Box sx={{ mt: 4 }}>
<Typography variant="h6" gutterBottom>
Power Curve
Turbine
</Typography>
<Typography variant="body1" gutterBottom>
Select a power curve option:
Select a turbine option:
</Typography>

<FormControl component="fieldset" sx={{ width: "100%" }}>
{powerCurveOptions.length > 0 ? (
<>
{/* <InputLabel id="power-curve-label">Power Curve</InputLabel> */}
{/* <InputLabel id="power-curve-label">Turbine</InputLabel> */}
<Select
labelId="power-curve-label"
id="power-curve-select"
value={powerCurve}
// label="Power Curve"
// label="Turbine"
onChange={handlePowerCurveChange}
fullWidth
size="small"
>
{powerCurveOptions.map((option, idx) => (
<MenuItem key={"power_curve_option_" + idx} value={option}>
{POWER_CURVE_LABEL[option] || option}
{getTurbineLabel(option)}
</MenuItem>
))}
</Select>
</>
) : (
<Typography variant="body2">
Loading power curve options...
</Typography>
<Typography variant="body2">Loading turbine options...</Typography>
)}

<Typography variant="body2" marginTop={2} gutterBottom>
* 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.
</Typography>
</FormControl>
</Box>
Expand Down
2 changes: 1 addition & 1 deletion windwatts-ui/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
15 changes: 0 additions & 15 deletions windwatts-ui/src/constants/powerCurves.ts

This file was deleted.

Loading