Skip to content

Commit c6934ef

Browse files
Add support for ERA5 Metadata (#710)
* Change Copernicus to GLORYS and add support for ERA5 * add tests * fix pipeline names * try to configure cdsapi * update config to correctly download files --------- Co-authored-by: Simone Silvestri <silvestri.simone0@gmail.com>
1 parent 08ccc76 commit c6934ef

File tree

12 files changed

+608
-52
lines changed

12 files changed

+608
-52
lines changed

.github/workflows/ci.yml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ jobs:
5757
files: lcov.info
5858
token: ${{ secrets.CODECOV_TOKEN }}
5959

60-
copernicus_downloading:
61-
name: Copernicus Marine Store Downloading - Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
60+
cds_downloading:
61+
name: CDS Downloading - Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
6262
runs-on: ${{ matrix.os }}
6363
timeout-minutes: 60
6464
strategy:
@@ -83,11 +83,19 @@ jobs:
8383
arch: ${{ matrix.arch }}
8484
- uses: julia-actions/cache@v2
8585
- uses: julia-actions/julia-buildpkg@v1
86+
- name: Configure era5cli
87+
env:
88+
CDS_API_KEY: ${{ secrets.CDS_API_KEY }}
89+
run: |
90+
mkdir -p ~/.config/era5cli
91+
echo "url: https://cds.climate.copernicus.eu/api" > ~/.config/era5cli/cds_key.txt
92+
echo "key: $CDS_API_KEY" >> ~/.config/era5cli/cds_key.txt
93+
shell: bash
8694
- uses: julia-actions/julia-runtest@v1
8795
env:
88-
TEST_GROUP: "copernicus_downloading"
89-
COPERNICUSMARINE_SERVICE_USERNAME: ${{ secrets.COPERNICUSMARINE_SERVICE_USERNAME }}
90-
COPERNICUSMARINE_SERVICE_PASSWORD: ${{ secrets.COPERNICUSMARINE_SERVICE_PASSWORD }}
96+
TEST_GROUP: "cds_downloading"
97+
CDSAPI_URL: "https://cds.climate.copernicus.eu/api"
98+
CDSAPI_KEY: ${{ secrets.CDS_API_KEY }}
9199
- uses: julia-actions/julia-processcoverage@v1
92100
- uses: codecov/codecov-action@v5
93101
with:

Project.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
99
CFTime = "179af706-886a-5703-950a-314cd64e0468"
1010
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
1111
ClimaSeaIce = "6ba0ff68-24e6-4315-936c-2e99227c95a4"
12+
CopernicusClimateDataStore = "bce3f73f-acea-4481-bd86-df89ecc2cb46"
1213
CubicSplines = "9c784101-8907-5a6d-9be6-98f00873c89b"
1314
DataDeps = "124859b0-ceae-595e-8997-d05f6a7a8dfe"
1415
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
@@ -39,6 +40,7 @@ SpeedyWeather = "9e226e20-d153-4fed-8a5b-493def4f21a9"
3940
XESMF = "2e0b0046-e7a1-486f-88de-807ee8ffabe5"
4041

4142
[extensions]
43+
ClimaOceanCopernicusClimateDataStoreExt = "CopernicusClimateDataStore"
4244
ClimaOceanCopernicusMarineExt = "CopernicusMarine"
4345
ClimaOceanReactantExt = "Reactant"
4446
ClimaOceanSpeedyWeatherExt = ["SpeedyWeather", "XESMF"]
@@ -48,6 +50,7 @@ Adapt = "4"
4850
CFTime = "0.1, 0.2"
4951
CUDA = "4, 5"
5052
ClimaSeaIce = "0.4.2"
53+
CopernicusClimateDataStore = "0.1"
5154
CopernicusMarine = "0.1.1"
5255
CubicSplines = "0.2"
5356
DataDeps = "0.7"
@@ -76,9 +79,10 @@ julia = "1.10"
7679

7780
[extras]
7881
CUDA_Runtime_jll = "76a88914-d11a-5bdc-97e0-2f5a05c973a2"
82+
CopernicusClimateDataStore = "bce3f73f-acea-4481-bd86-df89ecc2cb46"
7983
Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037"
8084
MPIPreferences = "3da0fdf6-3ccc-4f1b-acd9-58baa6c99267"
8185
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
8286

8387
[targets]
84-
test = ["Coverage", "Test", "MPIPreferences", "CUDA_Runtime_jll", "Reactant", "CopernicusMarine", "XESMF", "SpeedyWeather"]
88+
test = ["Coverage", "Test", "MPIPreferences", "CUDA_Runtime_jll", "Reactant", "CopernicusClimateDataStore", "CopernicusMarine", "XESMF", "SpeedyWeather"]
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
module ClimaOceanCopernicusClimateDataStoreExt
2+
3+
using ClimaOcean
4+
using CopernicusClimateDataStore
5+
6+
using Oceananigans
7+
using Oceananigans.DistributedComputations: @root
8+
9+
using Dates
10+
using ClimaOcean.DataWrangling.ERA5: ERA5Metadata, ERA5Metadatum, ERA5_dataset_variable_names
11+
12+
import ClimaOcean.DataWrangling: download_dataset
13+
14+
"""
15+
download_dataset(metadata::ERA5Metadata; kwargs...)
16+
17+
Download ERA5 data for each date in the metadata, returning paths to downloaded files.
18+
"""
19+
function download_dataset(metadata::ERA5Metadata; kwargs...)
20+
paths = Array{String}(undef, length(metadata))
21+
for (m, metadatum) in enumerate(metadata)
22+
paths[m] = download_dataset(metadatum; kwargs...)
23+
end
24+
return paths
25+
end
26+
27+
"""
28+
download_dataset(meta::ERA5Metadatum; skip_existing=true, kwargs...)
29+
30+
Download ERA5 data for a single date/time using the CopernicusClimateDataStore package.
31+
32+
The download is performed using `era5cli` through the CopernicusClimateDataStore package.
33+
34+
# Keyword Arguments
35+
- `skip_existing`: Skip download if the file already exists (default: `true`).
36+
- Additional keyword arguments are passed to `CopernicusClimateDataStore.hourly`.
37+
38+
# Environment Setup
39+
Before downloading, you must:
40+
1. Create an account at https://cds.climate.copernicus.eu/
41+
2. Accept the Terms of Use for the ERA5 dataset on the dataset page
42+
3. Set up your API credentials in `~/.cdsapirc`
43+
44+
See https://cds.climate.copernicus.eu/how-to-api for details.
45+
"""
46+
function download_dataset(meta::ERA5Metadatum;
47+
skip_existing = true,
48+
threads = 1,
49+
additional_kw...)
50+
51+
output_directory = meta.dir
52+
output_filename = ClimaOcean.DataWrangling.metadata_filename(meta)
53+
output_path = joinpath(output_directory, output_filename)
54+
55+
# Skip if file already exists
56+
if skip_existing && isfile(output_path)
57+
return output_path
58+
end
59+
60+
# Ensure output directory exists
61+
mkpath(output_directory)
62+
63+
# Get the ERA5 variable name
64+
variable_name = ERA5_dataset_variable_names[meta.name]
65+
66+
# Extract date information
67+
date = meta.dates
68+
year = Dates.year(date)
69+
month = Dates.month(date)
70+
day = Dates.day(date)
71+
hour = Dates.hour(date)
72+
73+
# Build area constraint from bounding box
74+
area = build_era5_area(meta.bounding_box)
75+
76+
# Build output prefix (filename without extension)
77+
output_prefix = first(splitext(output_filename))
78+
79+
# Perform the download using era5cli via CopernicusClimateDataStore
80+
@root begin
81+
downloaded_files = CopernicusClimateDataStore.hourly(;
82+
variables = variable_name,
83+
startyear = year,
84+
months = month,
85+
days = day,
86+
hours = hour,
87+
area = area,
88+
format = "netcdf",
89+
outputprefix = output_prefix,
90+
overwrite = !skip_existing,
91+
threads = threads,
92+
splitmonths = false,
93+
directory = output_directory,
94+
additional_kw...
95+
)
96+
97+
# era5cli generates its own filename suffix, so rename to our expected name
98+
if !isempty(downloaded_files)
99+
downloaded_file = first(downloaded_files)
100+
if downloaded_file != output_path && isfile(downloaded_file)
101+
mv(downloaded_file, output_path; force=true)
102+
end
103+
end
104+
end
105+
106+
return output_path
107+
end
108+
109+
#####
110+
##### Area/bounding box utilities
111+
#####
112+
113+
build_era5_area(::Nothing) = nothing
114+
115+
const BBOX = ClimaOcean.DataWrangling.BoundingBox
116+
117+
function build_era5_area(bbox::BBOX)
118+
# ERA5/era5cli uses (lat_max, lon_min, lat_min, lon_max) ordering
119+
# BoundingBox has longitude = (west, east), latitude = (south, north)
120+
121+
lon = bbox.longitude
122+
lat = bbox.latitude
123+
124+
if isnothing(lon) || isnothing(lat)
125+
return nothing
126+
end
127+
128+
lon_min = lon[1] # west
129+
lon_max = lon[2] # east
130+
lat_min = lat[1] # south
131+
lat_max = lat[2] # north
132+
133+
# Return in era5cli order: (lat_max, lon_min, lat_min, lon_max)
134+
return (lat = (lat_min, lat_max), lon = (lon_min, lon_max))
135+
end
136+
137+
end # module ClimaOceanCopernicusClimateDataStoreExt
138+

ext/ClimaOceanCopernicusMarineExt.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@ using Oceananigans
77
using Oceananigans.DistributedComputations: @root
88

99
using Dates: DateTime
10-
using ClimaOcean.DataWrangling.Copernicus: CopernicusMetadata, CopernicusMetadatum
10+
using ClimaOcean.DataWrangling.GLORYS: GLORYSMetadata, GLORYSMetadatum
1111

1212
import ClimaOcean.DataWrangling: download_dataset
1313

1414

1515
# Download each date individually, instead of downloading the entire dataset at once.
1616
# This is useful for a possible extension of the temporal horizon of the dataset.
17-
function download_dataset(metadata::CopernicusMetadata; kwargs...)
17+
function download_dataset(metadata::GLORYSMetadata; kwargs...)
1818
paths = Array{String}(undef, length(metadata))
1919
for (m, metadatum) in enumerate(metadata)
2020
paths[m] = download_dataset(metadatum; kwargs...)
2121
end
2222
return paths
2323
end
2424

25-
function download_dataset(meta::CopernicusMetadatum;
25+
function download_dataset(meta::GLORYSMetadatum;
2626
skip_existing=true,
2727
username=get(ENV, "COPERNICUS_USERNAME", nothing),
2828
password=get(ENV, "COPERNICUS_PASSWORD", nothing),
@@ -35,15 +35,15 @@ function download_dataset(meta::CopernicusMetadatum;
3535

3636
toolbox = CopernicusMarine.copernicusmarine
3737

38-
variable_name = ClimaOcean.DataWrangling.Copernicus.copernicus_dataset_variable_names[meta.name]
38+
variable_name = ClimaOcean.DataWrangling.GLORYS.GLORYS_dataset_variable_names[meta.name]
3939
variables = CopernicusMarine.pylist([variable_name])
4040

41-
dataset_id = ClimaOcean.DataWrangling.Copernicus.copernicusmarine_dataset_id(meta.dataset)
42-
datetime_kw = if meta.dataset isa ClimaOcean.DataWrangling.Copernicus.GLORYSStatic
41+
dataset_id = ClimaOcean.DataWrangling.GLORYS.copernicusmarine_dataset_id(meta.dataset)
42+
datetime_kw = if meta.dataset isa ClimaOcean.DataWrangling.GLORYS.GLORYSStatic
4343
NamedTuple()
4444
else
45-
start_datetime = ClimaOcean.DataWrangling.Copernicus.start_date_str(meta.dates)
46-
end_datetime = ClimaOcean.DataWrangling.Copernicus.end_date_str(meta.dates)
45+
start_datetime = ClimaOcean.DataWrangling.GLORYS.start_date_str(meta.dates)
46+
end_datetime = ClimaOcean.DataWrangling.GLORYS.end_date_str(meta.dates)
4747
(; start_datetime, end_datetime)
4848
end
4949

src/ClimaOcean.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ include("Bathymetry.jl")
9494
include("Diagnostics/Diagnostics.jl")
9595

9696
using .DataWrangling
97-
using .DataWrangling: ETOPO, ECCO, Copernicus, EN4, JRA55
97+
using .DataWrangling: ETOPO, ECCO, GLORYS, EN4, JRA55
9898
using .Bathymetry
9999
using .InitialConditions
100100
using .OceanSeaIceModels
@@ -105,7 +105,7 @@ using .SeaIces
105105
using ClimaOcean.OceanSeaIceModels: ComponentInterfaces, MomentumRoughnessLength, ScalarRoughnessLength
106106
using ClimaOcean.DataWrangling.ETOPO
107107
using ClimaOcean.DataWrangling.ECCO
108-
using ClimaOcean.DataWrangling.Copernicus
108+
using ClimaOcean.DataWrangling.GLORYS
109109
using ClimaOcean.DataWrangling.EN4
110110
using ClimaOcean.DataWrangling.JRA55
111111
using ClimaOcean.DataWrangling.JRA55: JRA55NetCDFBackend

src/DataWrangling/DataWrangling.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export Metadata, Metadatum, ECCOMetadatum, EN4Metadatum, all_dates, first_date,
88
export metadata_time_step, metadata_epoch
99
export LinearlyTaperedPolarMask
1010
export DatasetRestoring
11+
export ERA5Hourly, ERA5Monthly
1112

1213
using Oceananigans
1314
using Downloads
@@ -219,13 +220,15 @@ end
219220
# Datasets
220221
include("ETOPO/ETOPO.jl")
221222
include("ECCO/ECCO.jl")
222-
include("Copernicus/Copernicus.jl")
223+
include("GLORYS/GLORYS.jl")
224+
include("ERA5/ERA5.jl")
223225
include("EN4/EN4.jl")
224226
include("JRA55/JRA55.jl")
225227

226228
using .ETOPO
227229
using .ECCO
228-
using .Copernicus
230+
using .GLORYS
231+
using .ERA5
229232
using .EN4
230233
using .JRA55
231234

0 commit comments

Comments
 (0)