Skip to content

Commit 12d79a8

Browse files
committed
Refactored soil_io code based on PR #17 review
1 parent 0b542a3 commit 12d79a8

5 files changed

Lines changed: 115 additions & 212 deletions

File tree

PyStemmusScope/forcing_io.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def read_forcing_data(forcing_file):
3838
ds_forcing = ds_forcing.squeeze(['x', 'y'])
3939

4040
data = {}
41-
41+
4242
# Expected time format is days (as floating point) since Jan 1st 00:00.
4343
data['doy_float'] = (
4444
ds_forcing['time'].dt.dayofyear - 1 +
@@ -47,17 +47,18 @@ def read_forcing_data(forcing_file):
4747
)
4848
data['year'] = ds_forcing['time'].dt.year.astype(float)
4949

50-
data['t_air_celcius'] = vc.kelvin_to_celcius(ds_forcing['Tair'])
51-
data['psurf_hpa'] = vc.pa_to_hpa(ds_forcing['Psurf'])
50+
data['t_air_celcius'] = ds_forcing['Tair'] - 273.15 # conversion from K to degC.
51+
data['psurf_hpa'] = ds_forcing['Psurf'] / 100 # conversion from Pa to hPa
5252
data['co2_conv'] = vc.co2_molar_fraction_to_kg_per_m3(ds_forcing['CO2air'])
53-
data['precip_conv'] = vc.precipitation_mm_s_to_cm_s(ds_forcing['Precip'])
53+
data['precip_conv'] = ds_forcing['Precip'] / 10 # conversion from mm/s to cm/s
5454
data['lw_down'] = ds_forcing['LWdown']
5555
data['sw_down'] = ds_forcing['SWdown']
5656
data['wind_speed'] = ds_forcing['Wind']
5757
data['rh'] = ds_forcing['RH']
5858
data['vpd'] = ds_forcing['VPD']
5959
data['lai'] = vc.mask_data(ds_forcing['LAI'], min_value=0.01)
60-
data['ea'] = vc.kpa_to_hpa(vc.calculate_ea(data['t_air_celcius'], data['rh']))
60+
# calculate ea, conversion from kPa to hPa:
61+
data['ea'] = vc.calculate_ea(data['t_air_celcius'], data['rh']) * 10
6162

6263
# Load in non-timedependent variables
6364
data['sitename'] = forcing_file.name.split('_')[0]
@@ -167,7 +168,7 @@ def prepare_forcing(input_dir, forcing_file, config):
167168
config (dict): The PyStemmusScope configuration dictionary.
168169
"""
169170
input_path = Path(input_dir)
170-
171+
171172
# Read the required data from the forcing file into a dictionary
172173
data = read_forcing_data(forcing_file)
173174

PyStemmusScope/soil_io.py

Lines changed: 68 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import hdf5storage
33
import numpy as np
44
import xarray as xr
5+
from . import utils
56
from . import variable_conversion as vc
67

78

@@ -29,40 +30,44 @@ def _open_multifile_datasets(paths, lat, lon, lat_key='lat', lon_key='lon'):
2930
return ds
3031

3132

32-
def _read_lambda_coef(lambda_directory, lat, lon):
33+
def _read_lambda_coef(lambda_directory, lat, lon, depth_indices):
3334
"""Internal function that reads the lambda coefficient files and returns the data
3435
of interest in a dictionary.
3536
3637
Args:
3738
lambda_directory (Path): Path to the directory which contains the lambda data.
3839
lat (float): Latitude of the site of interest (in degrees North)
3940
lon (float): Longitude of the site of interest (in degrees East)
41+
depth_indices (list): List of which indices (0 - 7) should be selected from the
42+
lambda variable dataset.
4043
4144
Returns:
4245
dict: Dictionary containing the lambda coefficient data.
4346
"""
47+
if not np.all([d in range(0, 8) for d in depth_indices]):
48+
raise ValueError("Incorrect depth indices provided. Indices range from 0 to 7")
49+
4450
lambda_files = sorted(lambda_directory.glob("lambda_l*.nc"))
4551

4652
ds = _open_multifile_datasets(lambda_files, lat, lon)
4753

4854
# which depth indices the STEMMUS_SCOPE model expects
49-
indices = [0, 2, 4, 5, 6, 7]
50-
coef_lambda = ds['lambda'].isel(depth=indices).values
51-
52-
lambda_matfile = {'Coef_Lamda': coef_lambda}
55+
ds = ds.sortby("depth") # make sure that the depths are sorted in increasing order
56+
coef_lambda = ds['lambda'].isel(depth=depth_indices).values
5357

54-
return lambda_matfile
58+
return {'Coef_Lamda': coef_lambda}
5559

5660

57-
def _read_soil_composition(soil_data_path, lat, lon):
61+
def _read_soil_composition(soil_data_path, lat, lon, depth_indices):
5862
"""Internal function that reads the soil composition files and returns them in a
5963
dictionary.
6064
6165
Args:
6266
soil_data_path (Path): Path to the directory which contains the soil data.
6367
lat (float): Latitude of the site of interest (in degrees North)
6468
lon (float): Longitude of the site of interest (in degrees East)
65-
69+
depth_indices (list): List of which indices (0 - 7) should be selected from the
70+
soil composition dataset.
6671
Returns:
6772
dict: Dictionary containing the soil composition data.
6873
"""
@@ -73,30 +78,29 @@ def _read_soil_composition(soil_data_path, lat, lon):
7378

7479
ds = _open_multifile_datasets(soil_comp_paths, lat, lon)
7580

76-
depths_indices = [0, 2, 4, 5, 6, 7]
77-
ds = ds.isel(depth=depths_indices)
81+
if not np.all([d in range(0, 8) for d in depth_indices]):
82+
raise ValueError("Incorrect depth indices provided. Indices range from 0 to 7")
7883

79-
clay_fraction = vc.percent_to_fraction(ds['CLAY'].values)
80-
sand_fraction = vc.percent_to_fraction(ds['SAND'].values)
81-
organic_fraction = vc.hundredth_percent_to_fraction(ds['OC'].values)
84+
ds = ds.sortby("depth") # make sure that the depths are sorted in increasing order
85+
ds = ds.isel(depth=depth_indices)
8286

83-
soil_comp_matfiledata = {
84-
'FOC': clay_fraction,
85-
'FOS': sand_fraction,
86-
'MSOC': organic_fraction,
87-
}
87+
clay_fraction = ds['CLAY'].values / 100 # convert % to fraction
88+
sand_fraction = ds['SAND'].values / 100 # convert % to fraction
89+
organic_fraction = ds['OC'].values / 10000 # convert from 1/100th % to fraction.
8890

89-
return soil_comp_matfiledata
91+
return {'FOC': clay_fraction, 'FOS': sand_fraction, 'MSOC': organic_fraction}
9092

9193

92-
def _read_hydraulic_parameters(soil_data_path, lat, lon):
94+
def _read_hydraulic_parameters(soil_data_path, lat, lon, depths):
9395
"""Internal function that reads the soil hydraulic parameters from the Schaap
9496
dataset and returns them in a dictionary.
9597
9698
Args:
9799
soil_data_path (Path): Path to the directory which contains the soil data.
98100
lat (float): Latitude of the site of interest (in degrees North)
99101
lon (float): Longitude of the site of interest (in degrees East)
102+
depths (list): List of depths which should be selected from the dataset. The
103+
valid depths are: 0, 5, 15, 30, 60, 100 and 200 cm.
100104
101105
Returns:
102106
dict: Dictionary containing the hydraulic parameters.
@@ -106,31 +110,31 @@ def _read_hydraulic_parameters(soil_data_path, lat, lon):
106110
ptf_files, lat, lon, lat_key='latitude', lon_key='longitude'
107111
)
108112

109-
depths = [0, 5, 30, 60, 100, 200]
113+
valid_depths = [0, 5, 15, 30, 60, 100, 200]
114+
if not np.all([d in valid_depths for d in depths]):
115+
raise ValueError("Incorrect depth value(s) provided. Available depths are"
116+
f"{valid_depths}")
110117

111-
alpha = np.zeros(len(depths))
112-
Ks = np.zeros(len(depths))
113-
theta_s = np.zeros(len(depths))
114-
theta_r = np.zeros(len(depths))
115-
coef_n = np.zeros(len(depths))
118+
schaap_vars = ['alpha', 'Ks', 'thetas', 'thetar', 'n']
119+
schaap_data = {key: np.zeros(len(depths)) for key in schaap_vars}
116120

117121
for i, depth in enumerate(depths):
118-
alpha[i] = ds[f"alpha_{depth}cm"]
119-
Ks[i] = ds[f"Ks_{depth}cm"]
120-
theta_s[i] = ds[f"thetas_{depth}cm"]
121-
theta_r[i] = ds[f"thetar_{depth}cm"]
122-
coef_n[i] = ds[f"n_{depth}cm"]
122+
for var in schaap_vars:
123+
schaap_data[var][i] = ds[f"{var}_{depth}cm"]
124+
125+
fieldmc = vc.field_moisture_content(schaap_data['thetar'], schaap_data['thetas'],
126+
schaap_data['alpha'], schaap_data['n'])
123127

124128
hydraulic_matfiledata = {
125-
'SaturatedMC': theta_s,
126-
'ResidualMC': theta_r,
127-
'Coefficient_n': coef_n,
128-
'Coefficient_Alpha': alpha,
129-
'porosity': theta_s,
130-
'Ks0': Ks[0],
131-
'SaturatedK': vc.per_day_to_per_second(Ks),
132-
'fieldMC': vc.field_moisture_content(theta_r, theta_s, alpha, coef_n),
133-
'theta_s0': theta_s[0]
129+
'SaturatedMC': schaap_data['thetas'],
130+
'ResidualMC': schaap_data['thetar'],
131+
'Coefficient_n': schaap_data['n'],
132+
'Coefficient_Alpha': schaap_data['alpha'],
133+
'porosity': schaap_data['thetas'],
134+
'Ks0': schaap_data['Ks'][0],
135+
'SaturatedK': schaap_data['Ks'] / (24 * 3600), # convert 1/day -> 1/s
136+
'fieldMC': fieldmc,
137+
'theta_s0': schaap_data['thetas'][0]
134138
}
135139

136140
return hydraulic_matfiledata
@@ -149,14 +153,13 @@ def _read_surface_data(soil_data_path, lat, lon):
149153
dict: Dictionary containing the `fmax` value (maximum fractional saturated area)
150154
"""
151155
ds = xr.open_dataset(soil_data_path / 'surfdata.nc')
156+
lat, lon = utils.convert_to_lsm_coordinates(lat, lon)
152157
ds = ds.sel(
153-
lsmlat=vc.lat_to_lsmlat(lat), lsmlon=vc.lon_to_lsmlon(lon))
158+
lsmlat=lat, lsmlon=lon)
154159

155160
fmax = ds['FMAX'].values
156161

157-
surface_matfiledata = {'fmax': fmax}
158-
159-
return surface_matfiledata
162+
return {'fmax': fmax}
160163

161164

162165
def _collect_soil_data(soil_data_path, lat, lon):
@@ -171,35 +174,34 @@ def _collect_soil_data(soil_data_path, lat, lon):
171174
Returns:
172175
dict: Dictionary containing all the processed soil property data.
173176
"""
177+
lambda_directory = soil_data_path / "lambda"
174178

175-
matfiledata = {}
176-
177-
lambda_directory = soil_data_path / 'lambda'
179+
schaap_depths = [0, 5, 30, 60, 100, 200]
180+
depth_indices = [0, 2, 4, 5, 6, 7]
178181

179-
matfiledata.update(_read_lambda_coef(lambda_directory, lat, lon))
180-
matfiledata.update(_read_hydraulic_parameters(soil_data_path, lat, lon))
181-
matfiledata.update(_read_soil_composition(soil_data_path, lat, lon))
182+
matfiledata = _read_lambda_coef(lambda_directory, lat, lon, depth_indices)
183+
matfiledata.update(_read_hydraulic_parameters(soil_data_path, lat, lon,
184+
schaap_depths))
185+
matfiledata.update(_read_soil_composition(soil_data_path, lat, lon, depth_indices))
182186
matfiledata.update(_read_surface_data(soil_data_path, lat, lon))
183187

184188
return matfiledata
185189

186190

187-
def _retrieve_latlon(plumber2_file):
188-
"""Retrieves the latitude and longitude coordinates from the PLUMBER2 dataset file,
189-
to allow for the
191+
def _retrieve_latlon(file):
192+
"""Retrieves the latitude and longitude coordinates from the dataset file.
190193
191194
Args:
192-
plumber2_file (Path): Full path to the netCDF file containing the site latitude
195+
file (Path): Full path to the netCDF file containing the site latitude
193196
and longitude
194197
195198
Returns:
196199
tuple(float, float): Tuple containing the latitude and longitude values.
197200
Latitude in degrees N, longitude in degrees E.
198201
"""
199-
ds = xr.open_dataset(plumber2_file)
200-
ds = ds.squeeze()
201-
lat = float(ds.latitude.values)
202-
lon = float(ds.longitude.values)
202+
ds = xr.open_dataset(file)
203+
lon = ds.longitude.values.flatten()
204+
lat = ds.latitude.values.flatten()
203205
return lat, lon
204206

205207

@@ -215,15 +217,16 @@ def prepare_soil_data(soil_data_dir, matfile_path, run_config):
215217
run_config (dict): Dictionary containing the configuration for the current
216218
STEMMUS_SCOPE run.
217219
"""
218-
forcing_file = Path(run_config['ForcingPath']) / run_config['ForcingFileName']
219-
lat, lon = _retrieve_latlon(forcing_file)
220+
forcing_file = Path(run_config["ForcingPath"]) / run_config["ForcingFileName"]
220221

221-
# Ensure pathlib Paths are used
222-
soil_data_path = Path(soil_data_dir)
223-
matfile_path = Path(matfile_path)
222+
# Data missing at ID-Pag site. See github.com/EcoExtreML/STEMMUS_SCOPE/issues/77
223+
if run_config["ForcingFileName"].startswith("ID"):
224+
lat, lon = -1., 112.
225+
else:
226+
lat, lon = _retrieve_latlon(forcing_file)
224227

225-
matfiledata = _collect_soil_data(soil_data_path, lat, lon)
228+
matfiledata = _collect_soil_data(Path(soil_data_dir), lat, lon)
226229

227230
hdf5storage.savemat(
228-
matfile_path / 'soil_parameters.mat', mdict=matfiledata, appendmat=False,
231+
Path(matfile_path) / "soil_parameters.mat", mdict=matfiledata, appendmat=False,
229232
)

PyStemmusScope/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import numpy as np
2+
3+
4+
def convert_to_lsm_coordinates(lat, lon):
5+
"""Converts latitude in degrees North to NCAR's LSM coordinate system: Grid with lat
6+
values ranging from 0 -- 360, where 0 is the South Pole, and lon values ranging
7+
from 0 -- 720, where 0 is the prime meridian. Both representing a 0.5 degree
8+
resolution.
9+
10+
Args:
11+
lat (float): Latitude in degrees North
12+
lon (float): longitude in degrees East
13+
14+
Returns:
15+
(int, int): (nearest) latitude grid coordinates
16+
"""
17+
lat += 90
18+
lat *= 2
19+
20+
lon *= 2
21+
if lon < 0:
22+
lon += 720
23+
24+
return np.round(lat), np.round(lon)

0 commit comments

Comments
 (0)