|
58 | 58 |
|
59 | 59 | # Map and copy Exodus data to MPAS data |
60 | 60 |
|
61 | | -# Create dictionary of variables that are supported by the script |
62 | | -mpas_exodus_var_dic = {"beta":"basal_friction_log", |
63 | | - "muFriction":"mu_log", \ |
64 | | - "stiffnessFactor":"stiffening_factor", \ |
65 | | - "uReconstructX":"solution_1", \ |
66 | | - "uReconstructY":"solution_2", \ |
67 | | - "temperature":"temperature", \ |
68 | | - "basalTemperature":"temperature", \ |
69 | | - "surfaceTemperature":"temperature", \ |
70 | | - "thickness":"ice_thickness"} |
71 | | - #"surfaceAirTemperature":"surface_air_temperature", \ #use with caution! |
72 | | -# A mapping between mpas and exodus file. Add one if you need to manipulate a different variable! |
| 61 | +# Create dictionary mapping exodus variable names to MPAS variable names. |
| 62 | +# This allows us to check what fields are present in the exodus file and |
| 63 | +# map them to the correct MALI field name. |
| 64 | +exodus_to_mpas_dic = {"basal_friction_log":"beta", |
| 65 | + "mu_log":"muFriction", |
| 66 | + "stiffening_factor":"stiffnessFactor", |
| 67 | + "stiffening_factor_log":"stiffnessFactor", |
| 68 | + "solution_1":"uReconstructX", |
| 69 | + "solution_2":"uReconstructY", |
| 70 | + "temperature":"temperature", |
| 71 | + "ice_thickness":"thickness"} |
| 72 | + |
| 73 | +# Build reverse lookup: MPAS variable name -> list of possible exodus names |
| 74 | +mpas_to_exodus_candidates = {} |
| 75 | +for exo_name, mpas_name in exodus_to_mpas_dic.items(): |
| 76 | + if mpas_name not in mpas_to_exodus_candidates: |
| 77 | + mpas_to_exodus_candidates[mpas_name] = [] |
| 78 | + mpas_to_exodus_candidates[mpas_name].append(exo_name) |
| 79 | +# surfaceTemperature and basalTemperature also use the "temperature" exodus field |
| 80 | +mpas_to_exodus_candidates["surfaceTemperature"] = ["temperature"] |
| 81 | +mpas_to_exodus_candidates["basalTemperature"] = ["temperature"] |
73 | 82 | ice_density = 910.0 |
74 | 83 | ocean_density = 1028.0 |
75 | 84 |
|
|
104 | 113 | # Albany layering is in reverse order from mpas |
105 | 114 | exo_layer_thick = np.flip(np.reshape(np.array(exo_layer_thick), [len(exo_layer_thick),])) |
106 | 115 | mpas_layer_thick = np.ma.filled(dataset.variables['layerThicknessFractions'][:]) |
107 | | -if mpas_layer_thick.any() != exo_layer_thick.any(): |
| 116 | +nLayers_exo = len(exo_layer_thick) |
| 117 | +nLayers_mpas = len(mpas_layer_thick) |
| 118 | + |
| 119 | +if nLayers_exo <= 1 and nLayers_mpas > 1: |
| 120 | + print("WARNING: Albany exodus file has {} layer(s) but MPAS file has {} layers.".format(nLayers_exo, nLayers_mpas)) |
| 121 | + print(" Assuming MOLHO (mono-layer higher-order) Albany solve.") |
| 122 | + print(" 3D fields will be interpolated to MPAS vertical layers using sigma^4 profile.") |
| 123 | + molho_mode = True |
| 124 | +elif np.array_equal(mpas_layer_thick, exo_layer_thick) == False: |
108 | 125 | sys.exit("ERROR: Albany layer_thickness_ratio does not match MPAS layerThicknessFractions! Aborting") |
| 126 | +else: |
| 127 | + molho_mode = False |
109 | 128 |
|
110 | 129 | # Read cellID_array from ascii file |
111 | 130 | if not options.id_file: |
|
161 | 180 | extrapolation = None |
162 | 181 |
|
163 | 182 | print("\n---------------") |
164 | | - if not var_name in mpas_exodus_var_dic: |
165 | | - sys.exit("ERROR: variable '{}' not supported by this script. Supported variables are: {}".format(var_name, mpas_exodus_var_dic.keys())) |
166 | | - if mpas_exodus_var_dic[var_name] in exo.get_node_variable_names() and var_name in dataset.variables.keys(): |
167 | | - exo_var_name = mpas_exodus_var_dic[var_name] |
| 183 | + if not var_name in mpas_to_exodus_candidates: |
| 184 | + sys.exit("ERROR: variable '{}' not supported by this script. Supported variables are: {}".format(var_name, mpas_to_exodus_candidates.keys())) |
| 185 | + |
| 186 | + # Find the exodus variable name corresponding to this MPAS variable |
| 187 | + exo_var_name = None |
| 188 | + for candidate in mpas_to_exodus_candidates[var_name]: |
| 189 | + if candidate in exo.get_node_variable_names(): |
| 190 | + exo_var_name = candidate |
| 191 | + break |
| 192 | + |
| 193 | + # In MOLHO mode, temperature is derived from enthalpy (solution_4) in the exo file |
| 194 | + if molho_mode and var_name == "temperature": |
| 195 | + if "solution_4" in exo.get_node_variable_names(): |
| 196 | + exo_var_name = "solution_4" |
| 197 | + print("MOLHO mode: reading enthalpy (solution_4) from exo to derive temperature") |
| 198 | + else: |
| 199 | + sys.exit("ERROR: MOLHO mode requires 'solution_4' (enthalpy) in the exo file for temperature conversion.") |
| 200 | + |
| 201 | + if exo_var_name is not None and var_name in dataset.variables.keys(): |
168 | 202 | data_exo = np.array(exo.get_node_variable_values(exo_var_name,1)) |
169 | 203 |
|
170 | 204 | # Get number of vertical layers from mpas output file. |
171 | 205 | if len(np.shape(dataset.variables[var_name])) == 3: |
172 | | - if var_name == "temperature": |
| 206 | + if molho_mode: |
| 207 | + nVert_max = 2 # MOLHO exo has 2 interfaces (top and bottom) for 3D fields |
| 208 | + elif var_name == "temperature": |
173 | 209 | nVert_max = np.shape(dataset.variables[var_name])[2] + 1 #albany temperature is on diferent vertical grid than MPAS, and has one extra layer |
174 | 210 | else: |
175 | 211 | nVert_max = np.shape(dataset.variables[var_name])[2] |
176 | 212 | else: |
177 | 213 | nVert_max = 1 |
178 | 214 |
|
179 | 215 | albanyTemperature = np.zeros((nCells, nVert_max)) #initialize albanyTemperature array to fill in below |
| 216 | + albanyVelocity = np.zeros((nCells, nVert_max)) #initialize albanyVelocity array for MOLHO sigma^4 interpolation |
180 | 217 | dataset.variables[var_name][:] = 0 # Fill variable with zeros in order to ensure proper values in void |
181 | 218 |
|
182 | 219 | print("Begin {} conversion".format(var_name)) |
|
185 | 222 | print('Converting layer/level {} of {}'.format(nVert+1, nVert_max)) |
186 | 223 |
|
187 | 224 | #Albany has inverted layer/level ordering relative to MPAS. |
188 | | - if var_name == "surfaceTemperature": |
| 225 | + if molho_mode and var_name == "surfaceTemperature": |
| 226 | + nVert_albany = 1 # Top interface of MOLHO exo mesh (2 interfaces: 0=bed, 1=top) |
| 227 | + elif var_name == "surfaceTemperature": |
189 | 228 | nVert_albany = nVert_max - 1 |
190 | 229 | elif var_name == "basalTemperature": |
191 | 230 | nVert_albany = 0 # This not needed, because it will be 0 for basalTemperature, but adding this case to make that assumption explicit |
|
220 | 259 | elif var_name == "stiffnessFactor": |
221 | 260 | dataset.variables[var_name][0,cellID_array-1] = np.exp(data_exo_layer) |
222 | 261 | elif var_name == "uReconstructX" or var_name == "uReconstructY": |
223 | | - dataset.variables[var_name][0,cellID_array-1, nVert] = data_exo_layer / (60. * 60. * 24 * 365) |
| 262 | + if molho_mode: |
| 263 | + albanyVelocity[cellID_array-1, nVert] = data_exo_layer / (60. * 60. * 24 * 365) |
| 264 | + else: |
| 265 | + dataset.variables[var_name][0,cellID_array-1, nVert] = data_exo_layer / (60. * 60. * 24 * 365) |
224 | 266 | elif var_name == "thickness": |
225 | 267 | print("WARNING: thickness conversion is still experimental! Carefully check results before using.") |
226 | 268 | # We have to be careful to not change MPAS geometry in the extended layer of cells - only touch it where the MPAS file already had nonzero thickness |
|
242 | 284 | dataset.variables[var_name][0,cellID_array-1] = data_exo_layer |
243 | 285 |
|
244 | 286 | if var_name == "temperature": |
245 | | - # Make generic equally-spaced layers for albany and MPAS. This works |
246 | | - # even for unequally-spaced layers because this is a linear interpolation |
247 | | - # and MPAS temperature layers are always halfway between Albany temperature |
248 | | - # layers. |
249 | | - albany_layers = np.arange(0, nVert_max) |
250 | | - MPAS_layers = np.arange(1, nVert_max) - 0.5 # MPAS and albany temperature layers are staggered |
251 | | - temperatureInterpolant = interp1d(albany_layers, albanyTemperature, axis=1) |
252 | | - dataset.variables["temperature"][0,:,:] = temperatureInterpolant(MPAS_layers) |
| 287 | + if molho_mode: |
| 288 | + # In MOLHO mode, Albany has 2 temperature interfaces (top and bottom). |
| 289 | + # The sigma^4 profile applies to ENTHALPY, not temperature directly: |
| 290 | + # E(sigma) = E_bed + (E_top - E_bed) * (1 - sigma^4) |
| 291 | + # T_melt(sigma) = Tm - CCcoeff * ice_density * g * H * sigma |
| 292 | + # T(sigma) = min(1e6 / (ice_density * c_i) * E(sigma) + T0, T_melt(sigma)) |
| 293 | + # where sigma is 0 at the top surface and 1 at the bed. |
| 294 | + # albanyTemperature[:, 0] = T_top (extracted from Albany top interface) |
| 295 | + # albanyTemperature[:, 1] = T_bed (extracted from Albany bottom interface) |
| 296 | + |
| 297 | + # Physical constants (should match Albany input YAML "LandIce Physical Parameters") |
| 298 | + c_i = 2009.0 # "Heat capacity of ice" [J/(kg*K)] |
| 299 | + g = 9.80616 # "Gravity Acceleration" [m/s^2] |
| 300 | + T0 = 265.0 # "Reference Temperature" [K] |
| 301 | + CCcoeff = 7.9e-08 # "Clausius-Clapeyron Coefficient" [K/Pa] |
| 302 | + Tm = 273.15 # "Atmospheric Pressure Melting Temperature" [K] |
| 303 | + |
| 304 | + # albanyTemperature here actually contains enthalpy values |
| 305 | + # read directly from "solution_4" in the exo file |
| 306 | + E_top = albanyTemperature[:, 0] |
| 307 | + E_bed = albanyTemperature[:, 1] |
| 308 | + |
| 309 | + # Get ice thickness in meters (from MPAS file) |
| 310 | + H = dataset.variables['thickness'][0, :] |
| 311 | + |
| 312 | + # Verify enthalpy-to-temperature conversion against Albany's temperature field |
| 313 | + if "temperature" in exo.get_node_variable_names(): |
| 314 | + data_temp_exo = np.array(exo.get_node_variable_values("temperature", 1)) |
| 315 | + if ordering == 1.0: |
| 316 | + T_top_exo = data_temp_exo[1::int(stride)] # Albany top interface |
| 317 | + T_bed_exo = data_temp_exo[0::int(stride)] # Albany bottom interface |
| 318 | + elif ordering == 0.0: |
| 319 | + node_num_check = int(stride) |
| 320 | + T_bed_exo = data_temp_exo[0:node_num_check] |
| 321 | + T_top_exo = data_temp_exo[node_num_check:2*node_num_check] |
| 322 | + # Use Albany's thickness for pressure melting calculation (exo stores in km) |
| 323 | + if "ice_thickness" in exo.get_node_variable_names(): |
| 324 | + data_thk_exo = np.array(exo.get_node_variable_values("ice_thickness", 1)) |
| 325 | + if ordering == 1.0: |
| 326 | + H_albany = data_thk_exo[0::int(stride)] * 1000.0 # convert km to m; thickness same at all levels |
| 327 | + elif ordering == 0.0: |
| 328 | + H_albany = data_thk_exo[0:node_num_check] * 1000.0 |
| 329 | + else: |
| 330 | + print(" WARNING: 'ice_thickness' not found in exo; using MPAS thickness for verification.") |
| 331 | + H_albany = H[cellID_array-1] |
| 332 | + # Compute T from E at top (sigma=0) and bed (sigma=1) |
| 333 | + T_from_E_top = np.minimum(1.0e6 / (ice_density * c_i) * E_top[cellID_array-1] + T0, Tm) |
| 334 | + T_melt_bed = Tm - CCcoeff * ice_density * g * H_albany * 1.0 |
| 335 | + T_from_E_bed = np.minimum(1.0e6 / (ice_density * c_i) * E_bed[cellID_array-1] + T0, T_melt_bed) |
| 336 | + max_diff_top = np.max(np.abs(T_from_E_top - T_top_exo)) |
| 337 | + max_diff_bed = np.max(np.abs(T_from_E_bed - T_bed_exo)) |
| 338 | + print(" Verification: max |T_from_enthalpy - T_exo| at top = {:.6e} K".format(max_diff_top)) |
| 339 | + print(" Verification: max |T_from_enthalpy - T_exo| at bed = {:.6e} K".format(max_diff_bed)) |
| 340 | + if max_diff_top > 0.01 or max_diff_bed > 0.01: |
| 341 | + print(" WARNING: Enthalpy-derived temperature differs from Albany temperature by > 0.01 K.") |
| 342 | + print(" Check that physical constants match the Albany input YAML.") |
| 343 | + |
| 344 | + nVertLevels = dataset.dimensions["nVertLevels"].size |
| 345 | + layerThicknessFractions = np.ma.filled(dataset.variables['layerThicknessFractions'][:]) |
| 346 | + print("MOLHO mode: interpolating temperature via enthalpy sigma^4 profile") |
| 347 | + for nLayer in np.arange(0, nVertLevels): |
| 348 | + # sigma at layer midpoint |
| 349 | + sigma = np.sum(layerThicknessFractions[0:nLayer]) + layerThicknessFractions[nLayer] / 2.0 |
| 350 | + # Interpolate enthalpy |
| 351 | + E_sigma = E_bed[cellID_array-1] + (E_top[cellID_array-1] - E_bed[cellID_array-1]) * (1.0 - sigma**4) |
| 352 | + # Pressure melting temperature at this depth |
| 353 | + T_melt = Tm - CCcoeff * ice_density * g * H[cellID_array-1] * sigma |
| 354 | + # Convert enthalpy to temperature, capped at pressure melting point |
| 355 | + dataset.variables["temperature"][0,cellID_array-1,nLayer] = np.minimum( |
| 356 | + 1.0e6 / (ice_density * c_i) * E_sigma + T0, T_melt) |
| 357 | + else: |
| 358 | + # Make generic equally-spaced layers for albany and MPAS. This works |
| 359 | + # even for unequally-spaced layers because this is a linear interpolation |
| 360 | + # and MPAS temperature layers are always halfway between Albany temperature |
| 361 | + # layers. |
| 362 | + albany_layers = np.arange(0, nVert_max) |
| 363 | + MPAS_layers = np.arange(1, nVert_max) - 0.5 # MPAS and albany temperature layers are staggered |
| 364 | + temperatureInterpolant = interp1d(albany_layers, albanyTemperature, axis=1) |
| 365 | + dataset.variables["temperature"][0,:,:] = temperatureInterpolant(MPAS_layers) |
253 | 366 |
|
254 | 367 | # Add reasonable (non-zero) temperatures outside of ice mask. Make this a linear |
255 | 368 | # interpolation between min(273.15, surfaceAirTemperature) at the surface and 268.15K at the bed. |
|
263 | 376 |
|
264 | 377 | print('\nTemperature interpolation complete') |
265 | 378 |
|
| 379 | + # In MOLHO mode, interpolate velocity to all MPAS levels using sigma^4 profile |
| 380 | + if molho_mode and var_name in ("uReconstructX", "uReconstructY"): |
| 381 | + # albanyVelocity[:, 0] = u_top (extracted from Albany top interface) |
| 382 | + # albanyVelocity[:, 1] = u_bed (extracted from Albany bottom interface) |
| 383 | + # u(sigma) = u_bed + (1 - sigma^4) * (u_top - u_bed) |
| 384 | + # where sigma is 0 at the top surface and 1 at the bed. |
| 385 | + u_top = albanyVelocity[:, 0] |
| 386 | + u_bed = albanyVelocity[:, 1] |
| 387 | + nVertLevels = dataset.dimensions["nVertLevels"].size |
| 388 | + nVertInterfaces = nVertLevels + 1 |
| 389 | + layerThicknessFractions = np.ma.filled(dataset.variables['layerThicknessFractions'][:]) |
| 390 | + print("MOLHO mode: interpolating {} using sigma^4 profile".format(var_name)) |
| 391 | + for iInterface in np.arange(0, nVertInterfaces): |
| 392 | + # sigma at interface position (0 at top, 1 at bed) |
| 393 | + sigma = np.sum(layerThicknessFractions[0:iInterface]) |
| 394 | + dataset.variables[var_name][0,:,iInterface] = u_bed + (1.0 - sigma**4) * (u_top - u_bed) |
| 395 | + |
266 | 396 | # Extrapolate and smooth beta and stiffnessFactor fields |
267 | 397 | if var_name in ["beta", "muFriction", "stiffnessFactor"]: |
268 | 398 | # Extrapolation |
|
0 commit comments