|
| 1 | +# This software is open source software available under the BSD-3 license. |
| 2 | +# |
| 3 | +# Copyright (c) 2022 Triad National Security, LLC. All rights reserved. |
| 4 | +# Copyright (c) 2022 Lawrence Livermore National Security, LLC. All rights |
| 5 | +# reserved. |
| 6 | +# Copyright (c) 2022 UT-Battelle, LLC. All rights reserved. |
| 7 | +# |
| 8 | +# Additional copyright and license information can be found in the LICENSE file |
| 9 | +# distributed with this code, or at |
| 10 | +# https://raw.githubusercontent.com/MPAS-Dev/MPAS-Analysis/main/LICENSE |
| 11 | + |
| 12 | +import xarray as xr |
| 13 | + |
| 14 | +from mpas_tools.ocean.streamfunction.vorticity import ( |
| 15 | + compute_vertically_integrated_vorticity, |
| 16 | +) |
| 17 | + |
| 18 | +from mpas_analysis.ocean.utility import ( |
| 19 | + vector_cell_to_edge_isotropic, |
| 20 | + vector_to_edge_normal, |
| 21 | +) |
| 22 | +from mpas_analysis.shared import AnalysisTask |
| 23 | +from mpas_analysis.shared.climatology import RemapMpasClimatologySubtask |
| 24 | +from mpas_analysis.shared.plot import PlotClimatologyMapSubtask |
| 25 | + |
| 26 | + |
| 27 | +class ClimatologyMapWindStressCurl(AnalysisTask): |
| 28 | + """ |
| 29 | + An analysis task for computing and plotting maps of the wind stress curl. |
| 30 | + """ |
| 31 | + |
| 32 | + def __init__(self, config, mpas_climatology_task, control_config=None): |
| 33 | + """ |
| 34 | + Construct the analysis task. |
| 35 | +
|
| 36 | + Parameters |
| 37 | + ---------- |
| 38 | + config : mpas_tools.config.MpasConfigParser |
| 39 | + Configuration options |
| 40 | +
|
| 41 | + mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask |
| 42 | + The task that produced the climatology to be remapped and plotted |
| 43 | +
|
| 44 | + control_config : mpas_tools.config.MpasConfigParser, optional |
| 45 | + Configuration options for a control run (if any) |
| 46 | + """ # noqa: E501 |
| 47 | + |
| 48 | + field_name = 'windStressCurl' |
| 49 | + super().__init__( |
| 50 | + config=config, |
| 51 | + taskName='climatologyMapWindStressCurl', |
| 52 | + componentName='ocean', |
| 53 | + tags=[ |
| 54 | + 'climatology', 'horizontalMap', field_name, 'publicObs' |
| 55 | + ], |
| 56 | + ) |
| 57 | + |
| 58 | + section_name = self.taskName |
| 59 | + |
| 60 | + # read in what seasons we want to plot |
| 61 | + seasons = config.getexpression(section_name, 'seasons') |
| 62 | + if len(seasons) == 0: |
| 63 | + raise ValueError( |
| 64 | + f'config section {section_name} does not contain ' |
| 65 | + f'valid list of seasons' |
| 66 | + ) |
| 67 | + |
| 68 | + comparison_grid_names = config.getexpression( |
| 69 | + section_name, 'comparisonGrids' |
| 70 | + ) |
| 71 | + |
| 72 | + if len(comparison_grid_names) == 0: |
| 73 | + raise ValueError( |
| 74 | + f'config section {section_name} does not contain ' |
| 75 | + f'valid list of comparison grids' |
| 76 | + ) |
| 77 | + |
| 78 | + variable_list = list(RemapMpasWindStressCurl.VARIABLES) |
| 79 | + remap_climatology_subtask = RemapMpasWindStressCurl( |
| 80 | + mpasClimatologyTask=mpas_climatology_task, |
| 81 | + parentTask=self, |
| 82 | + climatologyName=field_name, |
| 83 | + variableList=variable_list, |
| 84 | + seasons=seasons, |
| 85 | + comparisonGridNames=comparison_grid_names, |
| 86 | + subtaskName='remapWindStressCurl', |
| 87 | + vertices=True, |
| 88 | + ) |
| 89 | + |
| 90 | + self.add_subtask(remap_climatology_subtask) |
| 91 | + |
| 92 | + out_file_label = field_name |
| 93 | + field_title = 'Wind Stress Curl' |
| 94 | + remap_observations_subtask = None |
| 95 | + |
| 96 | + mpas_field_name = field_name |
| 97 | + if control_config is None: |
| 98 | + ref_field_name = None |
| 99 | + ref_title_label = None |
| 100 | + diff_title_label = 'Model - Observations' |
| 101 | + |
| 102 | + else: |
| 103 | + control_run_name = control_config.get('runs', 'mainRunName') |
| 104 | + ref_field_name = mpas_field_name |
| 105 | + ref_title_label = f'Control: {control_run_name}' |
| 106 | + diff_title_label = 'Main - Control' |
| 107 | + |
| 108 | + for comparison_grid_name in comparison_grid_names: |
| 109 | + for season in seasons: |
| 110 | + # make a new subtask for this season and comparison grid |
| 111 | + subtask_name = f'plot{season}_{comparison_grid_name}' |
| 112 | + |
| 113 | + subtask = PlotClimatologyMapSubtask( |
| 114 | + self, season, comparison_grid_name, |
| 115 | + remap_climatology_subtask, remap_observations_subtask, |
| 116 | + controlConfig=control_config, subtaskName=subtask_name) |
| 117 | + subtask.set_plot_info( |
| 118 | + outFileLabel=out_file_label, |
| 119 | + fieldNameInTitle=field_title, |
| 120 | + mpasFieldName=mpas_field_name, |
| 121 | + refFieldName=ref_field_name, |
| 122 | + refTitleLabel=ref_title_label, |
| 123 | + diffTitleLabel=diff_title_label, |
| 124 | + unitsLabel=r'N m$^{-3}$', |
| 125 | + imageCaption=field_title, |
| 126 | + galleryGroup='Wind Stress Curl', |
| 127 | + groupSubtitle=None, |
| 128 | + groupLink='wsc', |
| 129 | + galleryName=None, |
| 130 | + configSectionName=section_name) |
| 131 | + |
| 132 | + self.add_subtask(subtask) |
| 133 | + |
| 134 | + |
| 135 | +class RemapMpasWindStressCurl(RemapMpasClimatologySubtask): |
| 136 | + """ |
| 137 | + A subtask for computing climatologies of the wind stress curl before |
| 138 | + it gets remapped to a comparison grid. |
| 139 | + """ |
| 140 | + |
| 141 | + VARIABLES = ( |
| 142 | + 'timeMonthly_avg_windStressZonal', |
| 143 | + 'timeMonthly_avg_windStressMeridional', |
| 144 | + ) |
| 145 | + |
| 146 | + def setup_and_check(self): |
| 147 | + """ |
| 148 | + Add the variables needed for computing wind stress curl to the |
| 149 | + climatology task |
| 150 | + """ |
| 151 | + super().setup_and_check() |
| 152 | + |
| 153 | + # Add the variables and seasons, now that we have the variable list |
| 154 | + self.mpasClimatologyTask.add_variables( |
| 155 | + list(self.VARIABLES), self.seasons |
| 156 | + ) |
| 157 | + |
| 158 | + def customize_masked_climatology(self, climatology, season): |
| 159 | + """ |
| 160 | + Compute the wind stress curl and add it to the climatology. |
| 161 | +
|
| 162 | + Parameters |
| 163 | + ---------- |
| 164 | + climatology : xarray.Dataset |
| 165 | + the climatology data set |
| 166 | +
|
| 167 | + season : str |
| 168 | + The name of the season to be masked |
| 169 | +
|
| 170 | + Returns |
| 171 | + ------- |
| 172 | + climatology : xarray.Dataset |
| 173 | + the modified climatology data set |
| 174 | + """ |
| 175 | + logger = self.logger |
| 176 | + |
| 177 | + ds_mesh = xr.open_dataset(self.restartFileName) |
| 178 | + var_list = [ |
| 179 | + 'verticesOnEdge', |
| 180 | + 'cellsOnVertex', |
| 181 | + 'kiteAreasOnVertex', |
| 182 | + 'angleEdge', |
| 183 | + 'areaTriangle', |
| 184 | + 'dcEdge', |
| 185 | + 'edgesOnVertex', |
| 186 | + 'verticesOnEdge', |
| 187 | + 'latVertex', |
| 188 | + ] |
| 189 | + ds_mesh = ds_mesh[var_list] |
| 190 | + |
| 191 | + ws_zonal_cell = climatology['timeMonthly_avg_windStressZonal'] |
| 192 | + ws_meridional_cell = ( |
| 193 | + climatology['timeMonthly_avg_windStressMeridional'] |
| 194 | + ) |
| 195 | + ws_zonal_edge, ws_meridional_edge = vector_cell_to_edge_isotropic( |
| 196 | + ds_mesh, ws_zonal_cell, ws_meridional_cell |
| 197 | + ) |
| 198 | + ws_normal_edge = vector_to_edge_normal( |
| 199 | + ds_mesh, ws_zonal_edge, ws_meridional_edge |
| 200 | + ) |
| 201 | + |
| 202 | + # despite the name, this is the curl operator |
| 203 | + wind_sress_curl, _ = compute_vertically_integrated_vorticity( |
| 204 | + ds_mesh, ws_normal_edge, logger |
| 205 | + ) |
| 206 | + climatology['windStressCurl'] = wind_sress_curl |
| 207 | + climatology['windStressCurl'].attrs['units'] = 'N m-3' |
| 208 | + |
| 209 | + # drop the original wind stress variables |
| 210 | + climatology = climatology.drop_vars(list(self.VARIABLES)) |
| 211 | + |
| 212 | + return climatology |
0 commit comments