open-wether / Sources /App /Controllers /EnsembleController.swift
soiz1's picture
Migrated from GitHub
6ee917b verified
import Foundation
import Vapor
/**
API controller to return ensemble models data from ICON, GFS, IFS and GEM ensemble models
Endpoint https://ensemble-api.open-meteo.com/v1/ensemble?latitude=52.52&longitude=13.41&models=icon_seamless&hourly=temperature_2m
*/
public struct EnsembleApiController {
func query(_ req: Request) async throws -> Response {
_ = try await req.ensureSubdomain("ensemble-api")
let params = req.method == .POST ? try req.content.decode(ApiQueryParameter.self) : try req.query.decode(ApiQueryParameter.self)
let numberOfLocationsMaximum = try await req.ensureApiKey("ensemble-api", apikey: params.apikey)
let currentTime = Timestamp.now()
let allowedRange = Timestamp(2023, 4, 1) ..< currentTime.add(86400 * 36)
let domains = try EnsembleMultiDomains.load(commaSeparatedOptional: params.models) ?? [.gfs_seamless]
let prepared = try GenericReaderMulti<EnsembleVariable, EnsembleMultiDomains>.prepareReaders(domains: domains, params: params, currentTime: currentTime, forecastDayDefault: 7, forecastDaysMax: 36, pastDaysMax: 92, allowedRange: allowedRange)
let paramsHourly = try EnsembleVariableWithoutMember.load(commaSeparatedOptional: params.hourly)
let nVariables = (paramsHourly?.count ?? 0) * domains.reduce(0, {$0 + $1.countEnsembleMember})
let locations: [ForecastapiResult<EnsembleMultiDomains>.PerLocation] = try prepared.map { prepared in
let timezone = prepared.timezone
let time = prepared.time
let timeLocal = TimerangeLocal(range: time.dailyRead.range, utcOffsetSeconds: timezone.utcOffsetSeconds)
let readers: [ForecastapiResult<EnsembleMultiDomains>.PerModel] = try prepared.perModel.compactMap { readerAndDomain in
guard let reader = try readerAndDomain.reader() else {
return nil
}
let hourlyDt = (params.temporal_resolution ?? .hourly).dtSeconds ?? reader.modelDtSeconds
let timeHourlyRead = time.hourlyRead.with(dtSeconds: hourlyDt)
let timeHourlyDisplay = time.hourlyDisplay.with(dtSeconds: hourlyDt)
let domain = readerAndDomain.domain
return .init(
model: domain,
latitude: reader.modelLat,
longitude: reader.modelLon,
elevation: reader.targetElevation,
prefetch: {
if let hourlyVariables = paramsHourly {
for variable in hourlyVariables {
for member in 0..<reader.domain.countEnsembleMember {
try reader.prefetchData(variable: variable, time: timeHourlyRead.toSettings(ensembleMemberLevel: member))
}
}
}
},
current: nil,
hourly: paramsHourly.map { variables in
return {
return .init(name: "hourly", time: timeHourlyDisplay, columns: try variables.map { variable in
var unit: SiUnit? = nil
let allMembers: [ApiArray] = try (0..<reader.domain.countEnsembleMember).compactMap { member in
guard let d = try reader.get(variable: variable, time: timeHourlyRead.toSettings(ensembleMemberLevel: member))?.convertAndRound(params: params) else {
return nil
}
unit = d.unit
assert(timeHourlyRead.count == d.data.count)
return ApiArray.float(d.data)
}
guard allMembers.count > 0 else {
return ApiColumn(variable: variable.resultVariable, unit: .undefined, variables: .init(repeating: ApiArray.float([Float](repeating: .nan, count: timeHourlyRead.count)), count: reader.domain.countEnsembleMember))
}
return .init(variable: variable.resultVariable, unit: unit ?? .undefined, variables: allMembers)
})
}
},
daily: nil,
sixHourly: nil,
minutely15: nil
)
}
guard !readers.isEmpty else {
throw ForecastapiError.noDataAvilableForThisLocation
}
return .init(timezone: timezone, time: timeLocal, locationId: prepared.locationId, results: readers)
}
let result = ForecastapiResult<EnsembleMultiDomains>(timeformat: params.timeformatOrDefault, results: locations)
await req.incrementRateLimiter(weight: result.calculateQueryWeight(nVariablesModels: nVariables), apikey: numberOfLocationsMaximum.apikey)
return try await result.response(format: params.format ?? .json, numberOfLocationsMaximum: numberOfLocationsMaximum)
}
}
extension EnsembleVariableWithoutMember {
var resultVariable: ForecastapiResult<EnsembleMultiDomains>.SurfacePressureAndHeightVariable {
switch self {
case .pressure(let p):
return .pressure(.init(p.variable, p.level))
case .surface(let s):
return .surface(s)
case .height(let s):
return .height(.init(s.variable, s.level))
}
}
}
/**
List of ensemble models. "Seamless" models combine global with local models. A best_match model is not possible, as all models are too different to give any advice
*/
enum EnsembleMultiDomains: String, RawRepresentableString, CaseIterable, MultiDomainMixerDomain {
case icon_seamless
case icon_global
case icon_eu
case icon_d2
case ecmwf_ifs04
case ecmwf_ifs025
case gem_global
case bom_access_global_ensemble
case gfs_seamless
case gfs025
case gfs05
case ukmo_global_ensemble_20km
/// Return the required readers for this domain configuration
/// Note: last reader has highes resolution data
func getReader(lat: Float, lon: Float, elevation: Float, mode: GridSelectionMode, options: GenericReaderOptions) throws -> [any GenericReaderProtocol] {
switch self {
case .icon_seamless:
/// Note: ICON D2 EPS has been excluded, because it only provides 20 members and noticable different results compared to ICON EU EPS
/// See: https://github.com/open-meteo/open-meteo/issues/876
return try IconMixer(domains: [.iconEps, .iconEuEps], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? []
case .icon_global:
return try IconReader(domain: .iconEps, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .icon_eu:
return try IconReader(domain: .iconEuEps, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .icon_d2:
return try IconReader(domain: .iconD2Eps, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .ecmwf_ifs04:
return try EcmwfReader(domain: .ifs04_ensemble, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .ecmwf_ifs025:
return try EcmwfReader(domain: .ifs025_ensemble, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .gfs025:
return try GfsReader(domains: [.gfs025_ens], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .gfs05:
return try GfsReader(domains: [.gfs05_ens], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .gfs_seamless:
return try GfsReader(domains: [.gfs05_ens, .gfs025_ens], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .gem_global:
return try GemReader(domain: .gem_global_ensemble, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .bom_access_global_ensemble:
return try BomReader(domain: .access_global_ensemble, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
case .ukmo_global_ensemble_20km:
return try UkmoReader(domain: .global_ensemble_20km, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []
}
}
/// Number of ensenble members including control
var countEnsembleMember: Int {
switch self {
case .icon_seamless:
return IconDomains.iconEps.ensembleMembers
case .icon_global:
return IconDomains.iconEps.ensembleMembers
case .icon_eu:
return IconDomains.iconEuEps.ensembleMembers
case .icon_d2:
return IconDomains.iconD2Eps.ensembleMembers
case .ecmwf_ifs04:
return EcmwfDomain.ifs04_ensemble.ensembleMembers
case .ecmwf_ifs025:
return EcmwfDomain.ifs025_ensemble.ensembleMembers
case .gfs025:
return GfsDomain.gfs025_ens.ensembleMembers
case .gfs05:
return GfsDomain.gfs05_ens.ensembleMembers
case .gfs_seamless:
return GfsDomain.gfs05_ens.ensembleMembers
case .gem_global:
return GemDomain.gem_global_ensemble.ensembleMembers
case .bom_access_global_ensemble:
return BomDomain.access_global_ensemble.ensembleMembers
case .ukmo_global_ensemble_20km:
return UkmoDomain.global_ensemble_20km.ensembleMembers
}
}
var genericDomain: (any GenericDomain)? {
return nil
}
func getReader(gridpoint: Int, options: GenericReaderOptions) throws -> (any GenericReaderProtocol)? {
return nil
}
}
/// Define all available surface weather variables
enum EnsembleSurfaceVariable: String, GenericVariableMixable, Equatable, RawRepresentableString {
case weathercode
case weather_code
case temperature_2m
case temperature_80m
case temperature_120m
case cloudcover
case cloud_cover
case pressure_msl
case relativehumidity_2m
case relative_humidity_2m
case precipitation
//case showers
case rain
case windgusts_10m
case wind_gusts_10m
case dewpoint_2m
case dew_point_2m
case diffuse_radiation
case direct_radiation
case apparent_temperature
case windspeed_10m
case winddirection_10m
case windspeed_80m
case winddirection_80m
case windspeed_120m
case winddirection_120m
case wind_speed_10m
case wind_direction_10m
case wind_speed_80m
case wind_direction_80m
case wind_speed_120m
case wind_direction_120m
case direct_normal_irradiance
case et0_fao_evapotranspiration
case vapour_pressure_deficit
case vapor_pressure_deficit
case shortwave_radiation
case snowfall
case snow_depth
case surface_pressure
case shortwave_radiation_instant
case diffuse_radiation_instant
case direct_radiation_instant
case direct_normal_irradiance_instant
case global_tilted_irradiance
case global_tilted_irradiance_instant
case is_day
case visibility
case freezinglevel_height
case freezing_level_height
case uv_index
case uv_index_clear_sky
case cape
case surface_temperature
case soil_temperature_0_to_10cm
case soil_temperature_10_to_40cm
case soil_temperature_40_to_100cm
case soil_temperature_100_to_200cm
case soil_moisture_0_to_10cm
case soil_moisture_10_to_40cm
case soil_moisture_40_to_100cm
case soil_moisture_100_to_200cm
case sunshine_duration
/// Soil moisture or snow depth are cumulative processes and have offests if mutliple models are mixed
var requiresOffsetCorrectionForMixing: Bool {
switch self {
case .soil_moisture_0_to_10cm, .soil_moisture_10_to_40cm, .soil_moisture_40_to_100cm, .soil_moisture_100_to_200cm:
return true
case .snow_depth:
return true
default:
return false
}
}
}
/// Available pressure level variables
enum EnsemblePressureVariableType: String, GenericVariableMixable {
case temperature
case geopotential_height
case relativehumidity
case relative_humidity
case windspeed
case wind_speed
case winddirection
case wind_direction
case dewpoint
case dew_point
case cloudcover
case cloud_cover
case vertical_velocity
var requiresOffsetCorrectionForMixing: Bool {
return false
}
}
struct EnsemblePressureVariable: PressureVariableRespresentable, GenericVariableMixable {
let variable: EnsemblePressureVariableType
let level: Int
var requiresOffsetCorrectionForMixing: Bool {
return false
}
}
typealias EnsembleVariableWithoutMember = SurfacePressureAndHeightVariable<EnsembleSurfaceVariable, EnsemblePressureVariable, ForecastHeightVariable>
typealias EnsembleVariable = EnsembleVariableWithoutMember
/// Available daily aggregations
/*enum EnsembleVariableDaily: String, DailyVariableCalculatable, RawRepresentableString {
case temperature_2m_max
case temperature_2m_min
case temperature_2m_mean
case apparent_temperature_max
case apparent_temperature_min
case apparent_temperature_mean
case precipitation_sum
/*case precipitation_probability_max
case precipitation_probability_min
case precipitation_probability_mean*/
case snowfall_sum
case rain_sum
case showers_sum
//case weathercode
case shortwave_radiation_sum
case windspeed_10m_max
case windspeed_10m_min
case windspeed_10m_mean
case windgusts_10m_max
case windgusts_10m_min
case windgusts_10m_mean
case winddirection_10m_dominant
case precipitation_hours
case sunrise
case sunset
case et0_fao_evapotranspiration
/*case visibility_max
case visibility_min
case visibility_mean*/
case pressure_msl_max
case pressure_msl_min
case pressure_msl_mean
case surface_pressure_max
case surface_pressure_min
case surface_pressure_mean
case cloudcover_max
case cloudcover_min
case cloudcover_mean
/*case uv_index_max
case uv_index_clear_sky_max*/
var aggregation: DailyAggregation<EnsembleVariable> {
switch self {
case .temperature_2m_max:
return .max(.surface(.temperature_2m))
case .temperature_2m_min:
return .min(.surface(.temperature_2m))
case .temperature_2m_mean:
return .mean(.surface(.temperature_2m))
case .apparent_temperature_max:
return .max(.surface(.apparent_temperature))
case .apparent_temperature_mean:
return .mean(.surface(.apparent_temperature))
case .apparent_temperature_min:
return .min(.surface(.apparent_temperature))
case .precipitation_sum:
return .sum(.surface(.precipitation))
case .snowfall_sum:
return .sum(.surface(.snowfall))
case .rain_sum:
return .sum(.surface(.rain))
case .showers_sum:
return .sum(.surface(.showers))
/*case .weathercode:
return .max(.surface(.weathercode))*/
case .shortwave_radiation_sum:
return .radiationSum(.surface(.shortwave_radiation))
case .windspeed_10m_max:
return .max(.surface(.windspeed_10m))
case .windspeed_10m_min:
return .min(.surface(.windspeed_10m))
case .windspeed_10m_mean:
return .mean(.surface(.windspeed_10m))
case .windgusts_10m_max:
return .max(.surface(.windgusts_10m))
case .windgusts_10m_min:
return .min(.surface(.windgusts_10m))
case .windgusts_10m_mean:
return .mean(.surface(.windgusts_10m))
case .winddirection_10m_dominant:
return .dominantDirection(velocity: .surface(.windspeed_10m), direction: .surface(.winddirection_10m))
case .precipitation_hours:
return .precipitationHours(.surface(.precipitation))
case .sunrise:
return .none
case .sunset:
return .none
case .et0_fao_evapotranspiration:
return .sum(.surface(.et0_fao_evapotranspiration))
/*case .visibility_max:
return .max(.surface(.visibility))
case .visibility_min:
return .min(.surface(.visibility))
case .visibility_mean:
return .mean(.surface(.visibility))*/
case .pressure_msl_max:
return .max(.surface(.pressure_msl))
case .pressure_msl_min:
return .min(.surface(.pressure_msl))
case .pressure_msl_mean:
return .mean(.surface(.pressure_msl))
case .surface_pressure_max:
return .max(.surface(.surface_pressure))
case .surface_pressure_min:
return .min(.surface(.surface_pressure))
case .surface_pressure_mean:
return .mean(.surface(.surface_pressure))
/*case .cape_max:
return .max(.surface(.cape))
case .cape_min:
return .min(.surface(.cape))
case .cape_mean:
return .mean(.surface(.cape))*/
case .cloudcover_max:
return .max(.surface(.cloudcover))
case .cloudcover_min:
return .min(.surface(.cloudcover))
case .cloudcover_mean:
return .mean(.surface(.cloudcover))
/*case .uv_index_max:
return .max(.surface(.uv_index))
case .uv_index_clear_sky_max:
return .max(.surface(.uv_index_clear_sky))
case .precipitation_probability_max:
return .max(.surface(.precipitation_probability))
case .precipitation_probability_min:
return .max(.surface(.precipitation_probability))
case .precipitation_probability_mean:
return .max(.surface(.precipitation_probability))*/
}
}
}
*/