|
| 1 | +from pathlib import Path |
| 2 | +import hdf5storage |
| 3 | +import numpy as np |
| 4 | +import xarray as xr |
| 5 | +from . import variable_conversion as vc |
| 6 | + |
| 7 | + |
| 8 | +def _write_matlab_ascii(fname, data, ncols): |
| 9 | + """Internal function to handle writing data in the Matlab ascii format. Equivalent |
| 10 | + to `save([-], '-ascii')` in Matlab. |
| 11 | +
|
| 12 | + Args: |
| 13 | + fname (path or str): Path to the file that should be written |
| 14 | + data (np.array): Array with data to write to file |
| 15 | + ncols (int, optional): The number of data columns, required to correctly format |
| 16 | + the ascii file when writing multiple variables. |
| 17 | + """ |
| 18 | + matlab_fmt = ' %14.7e' |
| 19 | + multi_fmt = [matlab_fmt]*ncols |
| 20 | + multi_fmt[0] = f' {multi_fmt[0]}' |
| 21 | + np.savetxt(fname, data, multi_fmt) |
| 22 | + |
| 23 | + |
| 24 | +def read_forcing_data(forcing_file): |
| 25 | + """Reads the forcing data from the provided netCDF file, and applies the required |
| 26 | + unit conversions before returning the read data. |
| 27 | +
|
| 28 | + Args: |
| 29 | + forcing_file (Path): Path to the netCDF file containing the forcing data |
| 30 | +
|
| 31 | + Returns: |
| 32 | + dict: Dictionary containing the different variables required by STEMMUS_SCOPE |
| 33 | + for the different forcing files. |
| 34 | + """ |
| 35 | + ds_forcing = xr.open_dataset(forcing_file) |
| 36 | + |
| 37 | + # remove the x and y coordinates from the data variables to make the numpy arrays 1D |
| 38 | + ds_forcing = ds_forcing.squeeze(['x', 'y']) |
| 39 | + |
| 40 | + data = {} |
| 41 | + |
| 42 | + # Expected time format is days (as floating point) since Jan 1st 00:00. |
| 43 | + data['doy_float'] = ( |
| 44 | + ds_forcing['time'].dt.dayofyear - 1 + |
| 45 | + ds_forcing['time'].dt.hour/24 + |
| 46 | + ds_forcing['time'].dt.minute/60/24 |
| 47 | + ) |
| 48 | + data['year'] = ds_forcing['time'].dt.year.astype(float) |
| 49 | + |
| 50 | + data['t_air_celcius'] = vc.kelvin_to_celcius(ds_forcing['Tair']) |
| 51 | + data['psurf_hpa'] = vc.pa_to_hpa(ds_forcing['Psurf']) |
| 52 | + 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']) |
| 54 | + data['lw_down'] = ds_forcing['LWdown'] |
| 55 | + data['sw_down'] = ds_forcing['SWdown'] |
| 56 | + data['wind_speed'] = ds_forcing['Wind'] |
| 57 | + data['rh'] = ds_forcing['RH'] |
| 58 | + data['vpd'] = ds_forcing['VPD'] |
| 59 | + 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'])) |
| 61 | + |
| 62 | + # Load in non-timedependent variables |
| 63 | + data['sitename'] = forcing_file.name.split('_')[0] |
| 64 | + # Forcing data timestep size in seconds |
| 65 | + time_delta = (ds_forcing.time.values[1] - |
| 66 | + ds_forcing.time.values[0]) / np.timedelta64(1, 's') |
| 67 | + data['DELT'] = time_delta.astype(float) |
| 68 | + data['total_timesteps'] = ds_forcing.time.size |
| 69 | + |
| 70 | + data['latitude'] = ds_forcing['latitude'].values |
| 71 | + data['longitude'] = ds_forcing['longitude'].values |
| 72 | + data['elevation'] = ds_forcing['elevation'].values |
| 73 | + data['IGBP_veg_long'] = ds_forcing['IGBP_veg_long'].values |
| 74 | + data['reference_height'] = ds_forcing['reference_height'].values |
| 75 | + data['canopy_height'] = ds_forcing['canopy_height'].values |
| 76 | + |
| 77 | + return data |
| 78 | + |
| 79 | + |
| 80 | +def write_dat_files(data, input_dir): |
| 81 | + """Fuction to write the single-data .dat files for the STEMMUS_SCOPE matlab model. |
| 82 | +
|
| 83 | + Args: |
| 84 | + data (dict): Data dictionary generated by read_forcing |
| 85 | + input_dir (path or str): Directory to which the different single-column .dat |
| 86 | + files should be written to. |
| 87 | + """ |
| 88 | + write_info = { |
| 89 | + 'doy_float': 't_.dat', |
| 90 | + 't_air_celcius': 'Ta_.dat', |
| 91 | + 'sw_down': 'Rin_.dat', |
| 92 | + 'lw_down': 'Rli_.dat', |
| 93 | + 'psurf_hpa': 'p_.dat', |
| 94 | + 'wind_speed': 'u_.dat', |
| 95 | + 'co2_conv': 'CO2_.dat', |
| 96 | + 'ea': 'ea_.dat', |
| 97 | + 'year': 'year_.dat' |
| 98 | + } |
| 99 | + for var, fname in write_info.items(): |
| 100 | + fpath = Path(input_dir) / fname |
| 101 | + _write_matlab_ascii(fpath, data[var], ncols=1) |
| 102 | + |
| 103 | + |
| 104 | +def write_lai_file(data, fname): |
| 105 | + """Function to write the ascii LAI_.dat file for STEMMUS_SCOPE. |
| 106 | +
|
| 107 | + Args: |
| 108 | + data (dict): Dictionary containing the required variables. Generated by the |
| 109 | + function `read_forcing_data`. |
| 110 | + fname (Path): Full path, including filename, to which the file should be |
| 111 | + written to. |
| 112 | + """ |
| 113 | + lai_file_data = np.vstack([data['doy_float'], data['lai']]).T |
| 114 | + _write_matlab_ascii(fname, lai_file_data, ncols=2) |
| 115 | + |
| 116 | + |
| 117 | +def write_meteo_file(data, fname): |
| 118 | + """Function to write the ascii Mdata.txt meteo file for STEMMUS_SCOPE. |
| 119 | +
|
| 120 | + Args: |
| 121 | + data (dict): Dictionary containing the required variables. Generated by the |
| 122 | + function `read_forcing_data`. |
| 123 | + fname (Path): Full path, including filename, to which the file should be |
| 124 | + written to. |
| 125 | + """ |
| 126 | + meteo_data_vars = ['doy_float', 't_air_celcius', 'rh', |
| 127 | + 'wind_speed', 'psurf_hpa', 'precip_conv', 'sw_down', |
| 128 | + 'lw_down', 'vpd', 'lai'] |
| 129 | + meteo_file_data = np.vstack([data[var] for var in meteo_data_vars]).T |
| 130 | + _write_matlab_ascii(fname, meteo_file_data, ncols=len(meteo_data_vars)) |
| 131 | + |
| 132 | + |
| 133 | +def prepare_global_variables(data, input_path, config): |
| 134 | + """Function to read and calculate global variables for STEMMUS_SCOPE from the |
| 135 | + forcing data. Data will be written to a Matlab binary file (v7.3), under the name |
| 136 | + 'forcing_globals.mat' in the specified input directory. |
| 137 | +
|
| 138 | + Args: |
| 139 | + data (dict): Dictionary containing the required variables. Generated by the |
| 140 | + function `read_forcing_data`. |
| 141 | + input_path (Path): Path to which the file should be written to. |
| 142 | + config (dict): The PyStemmusScope configuration dictionary. |
| 143 | + """ |
| 144 | + if int(config['NumberOfTimeSteps']) > data['total_timesteps']: |
| 145 | + total_duration = data['total_timesteps'] |
| 146 | + else: |
| 147 | + total_duration = int(config['NumberOfTimeSteps']) |
| 148 | + |
| 149 | + matfile_vars = ['latitude', 'longitude', 'elevation', 'IGBP_veg_long', |
| 150 | + 'reference_height', 'canopy_height', 'DELT', 'sitename'] |
| 151 | + matfiledata = {key: data[key] for key in matfile_vars} |
| 152 | + |
| 153 | + matfiledata['Dur_tot'] = float(total_duration) # Matlab expects a 'double' |
| 154 | + |
| 155 | + hdf5storage.savemat(input_path / 'forcing_globals.mat', matfiledata, appendmat=False) |
| 156 | + |
| 157 | + |
| 158 | +def prepare_forcing(input_dir, forcing_file, config): |
| 159 | + """Function to prepare the forcing files required by STEMMUS_SCOPE. The input |
| 160 | + directory should be taken from the model configuration file. |
| 161 | +
|
| 162 | + Args: |
| 163 | + input_dir (path or str): Path to the input directory that will be read by |
| 164 | + STEMMUS_SCOPE. |
| 165 | + forcing_file (path or str): Path to the netCDF forcing file that will be used |
| 166 | + to generate the input data. |
| 167 | + config (dict): The PyStemmusScope configuration dictionary. |
| 168 | + """ |
| 169 | + input_path = Path(input_dir) |
| 170 | + |
| 171 | + # Read the required data from the forcing file into a dictionary |
| 172 | + data = read_forcing_data(forcing_file) |
| 173 | + |
| 174 | + # Write the single-column ascii '.dat' files to the input directory |
| 175 | + write_dat_files(data, input_path) |
| 176 | + |
| 177 | + # Write the two-column LAI_.dat file to the input directory. |
| 178 | + write_lai_file(data, input_path / 'LAI_.dat') |
| 179 | + |
| 180 | + # Write the multi-column Mdata.txt ascii file to the input directory |
| 181 | + write_meteo_file(data, input_path / 'Mdata.txt') |
| 182 | + |
| 183 | + # Write the remaining variables (without time dependency) to the matlab v7.3 |
| 184 | + # file 'forcing_globals.mat' |
| 185 | + prepare_global_variables(data, input_path, config) |
0 commit comments