From ddee264d196a010773ee2a0122df123bc4cd5244 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 28 Nov 2023 11:51:37 -0600 Subject: [PATCH 01/30] Initial port of ice_shelf_2d --- polaris/ocean/__init__.py | 2 + polaris/ocean/tasks/ice_shelf_2d/__init__.py | 74 +++++++ .../tasks/ice_shelf_2d/default/__init__.py | 41 ++++ polaris/ocean/tasks/ice_shelf_2d/forward.py | 136 ++++++++++++ polaris/ocean/tasks/ice_shelf_2d/forward.yaml | 83 ++++++++ .../ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg | 76 +++++++ polaris/ocean/tasks/ice_shelf_2d/init.py | 194 ++++++++++++++++++ .../tasks/ice_shelf_2d/ssh_adjustment.py | 149 ++++++++++++++ .../ocean/tasks/ice_shelf_2d/ssh_forward.py | 156 ++++++++++++++ polaris/ocean/tasks/ice_shelf_2d/viz.py | 194 ++++++++++++++++++ 10 files changed, 1105 insertions(+) create mode 100644 polaris/ocean/tasks/ice_shelf_2d/__init__.py create mode 100644 polaris/ocean/tasks/ice_shelf_2d/default/__init__.py create mode 100644 polaris/ocean/tasks/ice_shelf_2d/forward.py create mode 100644 polaris/ocean/tasks/ice_shelf_2d/forward.yaml create mode 100644 polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg create mode 100644 polaris/ocean/tasks/ice_shelf_2d/init.py create mode 100644 polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py create mode 100644 polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py create mode 100644 polaris/ocean/tasks/ice_shelf_2d/viz.py diff --git a/polaris/ocean/__init__.py b/polaris/ocean/__init__.py index aa7a8b083..c09979dc5 100644 --- a/polaris/ocean/__init__.py +++ b/polaris/ocean/__init__.py @@ -2,6 +2,7 @@ from polaris.ocean.tasks.baroclinic_channel import add_baroclinic_channel_tasks from polaris.ocean.tasks.cosine_bell import add_cosine_bell_tasks from polaris.ocean.tasks.geostrophic import add_geostrophic_tasks +from polaris.ocean.tasks.ice_shelf_2d import add_ice_shelf_2d_tasks from polaris.ocean.tasks.inertial_gravity_wave import ( add_inertial_gravity_wave_tasks, ) @@ -27,6 +28,7 @@ def __init__(self): # planar: please keep these in alphabetical order add_baroclinic_channel_tasks(component=self) + add_ice_shelf_2d_tasks(component=self) add_inertial_gravity_wave_tasks(component=self) add_internal_wave_tasks(component=self) add_isomip_plus_tasks(component=self, mesh_type='planar') diff --git a/polaris/ocean/tasks/ice_shelf_2d/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/__init__.py new file mode 100644 index 000000000..2ff8c1aeb --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/__init__.py @@ -0,0 +1,74 @@ +from typing import Dict + +from polaris import Step +from polaris.config import PolarisConfigParser +from polaris.ocean.resolution import resolution_to_subdir +from polaris.ocean.tasks.ice_shelf_2d.default import Default +from polaris.ocean.tasks.ice_shelf_2d.init import Init +from polaris.ocean.tasks.ice_shelf_2d.ssh_adjustment import SshAdjustment +from polaris.ocean.tasks.ice_shelf_2d.ssh_forward import SshForward + + +def add_ice_shelf_2d_tasks(component): + """ + Add tasks for different ice shelf 2-d tests to the ocean component + + component : polaris.ocean.Ocean + the ocean component that the tasks will be added to + """ + # TODO add vertical coordinate + # TODO add restart test + for resolution in [5., 2.]: + resdir = resolution_to_subdir(resolution) + resdir = f'planar/ice_shelf_2d/{resdir}' + + config_filename = 'ice_shelf_2d.cfg' + config = PolarisConfigParser(filepath=f'{resdir}/{config_filename}') + config.add_from_package('polaris.ocean.tasks.ice_shelf_2d', + 'ice_shelf_2d.cfg') + + shared_steps: Dict[str, Step] = dict() + + init = Init(component=component, resolution=resolution, indir=resdir) + init.set_shared_config(config, link=config_filename) + shared_steps['init'] = init + + num_iterations = 10 + + iteration = 0 + name = f'ssh_forward_{iteration}' + ssh_forward = SshForward( + component=component, resolution=resolution, indir=resdir, + mesh=init, init=shared_steps['init'], + name=name) + ssh_forward.set_shared_config(config, link=config_filename) + shared_steps[name] = ssh_forward + + for iteration in range(1, num_iterations): + name = f'ssh_adjust_{iteration - 1}' + ssh_adjust = SshAdjustment( + component=component, resolution=resolution, indir=resdir, + name=name, forward=ssh_forward) + ssh_adjust.set_shared_config(config, link=config_filename) + shared_steps[name] = ssh_adjust + name = f'ssh_forward_{iteration}' + ssh_forward = SshForward( + component=component, resolution=resolution, indir=resdir, + mesh=init, init=ssh_adjust, + name=name) + ssh_forward.set_shared_config(config, link=config_filename) + shared_steps[name] = ssh_forward + + iteration = num_iterations + name = f'ssh_adjust_{iteration - 1}' + ssh_adjust = SshAdjustment( + component=component, resolution=resolution, indir=resdir, + name=name, forward=ssh_forward) + ssh_adjust.set_shared_config(config, link=config_filename) + shared_steps[name] = ssh_adjust + + default = Default(component=component, resolution=resolution, + indir=resdir, mesh=init, init=ssh_adjust, + shared_steps=shared_steps) + default.set_shared_config(config, link=config_filename) + component.add_task(default) diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py new file mode 100644 index 000000000..e283bd5ec --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -0,0 +1,41 @@ +from polaris import Task +from polaris.ocean.tasks.ice_shelf_2d.forward import Forward +from polaris.ocean.tasks.ice_shelf_2d.viz import Viz + + +class Default(Task): + """ + The default ice shelf 2d test case simply creates the mesh and + initial condition, then performs a short forward run. + """ + + def __init__(self, component, resolution, indir, shared_steps, mesh, init): + """ + Create the test case + + Parameters + ---------- + component : polaris.ocean.Ocean + The ocean component that this task belongs to + + resolution : float + The resolution of the test case in km + + indir : str + The directory the task is in, to which ``name`` will be appended + + shared_steps : dict of dict of polaris.Steps + The shared steps to include as symlinks + """ + super().__init__(component=component, name='default', indir=indir) + + for name, shared_step in shared_steps.items(): + self.add_step(shared_step, symlink=name) + + self.add_step( + Forward(component=component, indir=self.subdir, ntasks=None, + min_tasks=None, openmp_threads=1, resolution=resolution, + mesh=mesh, init=init)) + + self.add_step( + Viz(component=component, indir=self.subdir, mesh=mesh, init=init)) diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.py b/polaris/ocean/tasks/ice_shelf_2d/forward.py new file mode 100644 index 000000000..b2e4e94c0 --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.py @@ -0,0 +1,136 @@ +import time + +from polaris.mesh.planar import compute_planar_hex_nx_ny +from polaris.ocean.model import OceanModelStep + + +class Forward(OceanModelStep): + """ + A step for performing forward ocean component runs as part of baroclinic + channel tasks. + + Attributes + ---------- + resolution : float + The resolution of the task in km + + dt : float + The model time step in seconds + + btr_dt : float + The model barotropic time step in seconds + """ + def __init__(self, component, resolution, mesh, init, name='forward', + subdir=None, indir=None, ntasks=None, min_tasks=None, + openmp_threads=1): + """ + Create a new task + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + resolution : km + The resolution of the task in km + + name : str + the name of the task + + subdir : str, optional + the subdirectory for the step. If neither this nor ``indir`` + are provided, the directory is the ``name`` + + indir : str, optional + the directory the step is in, to which ``name`` will be appended + + ntasks : int, optional + the number of tasks the step would ideally use. If fewer tasks + are available on the system, the step will run on all available + tasks as long as this is not below ``min_tasks`` + + min_tasks : int, optional + the number of tasks the step requires. If the system has fewer + than this number of tasks, the step will fail + + openmp_threads : int, optional + the number of OpenMP threads the step will use + """ + self.resolution = resolution + super().__init__(component=component, name=name, subdir=subdir, + indir=indir, ntasks=ntasks, min_tasks=min_tasks, + openmp_threads=openmp_threads) + + # make sure output is double precision + self.add_yaml_file('polaris.ocean.config', 'output.yaml') + + self.add_input_file(filename='initial_state.nc', + target='../../init/initial_state.nc') + self.add_input_file(filename='graph.info', + target='../../init/culled_graph.info') + + self.add_yaml_file('polaris.ocean.tasks.baroclinic_channel', + 'forward.yaml') + + self.add_output_file( + filename='output.nc', + validate_vars=['temperature', 'salinity', 'layerThickness', + 'normalVelocity']) + + self.dt = None + self.btr_dt = None + + def compute_cell_count(self): + """ + Compute the approximate number of cells in the mesh, used to constrain + resources + + Returns + ------- + cell_count : int or None + The approximate number of cells in the mesh + """ + section = self.config['ice_shelf_2d'] + lx = section.getfloat('lx') + ly = section.getfloat('ly') + nx, ny = compute_planar_hex_nx_ny(lx, ly, self.resolution) + cell_count = nx * ny + return cell_count + + def dynamic_model_config(self, at_setup): + """ + Add model config options, namelist, streams and yaml files using config + options or template replacements that need to be set both during step + setup and at runtime + + Parameters + ---------- + at_setup : bool + Whether this method is being run during setup of the step, as + opposed to at runtime + """ + super().dynamic_model_config(at_setup) + + config = self.config + + options = dict() + # TODO config_use_tidal_forcing + # TODO config_use_time_varying_forcing + + # dt is proportional to resolution: default 30 seconds per km + dt_per_km = config.getfloat('ice_shelf_2d', 'dt_per_km') + dt = dt_per_km * self.resolution + # https://stackoverflow.com/a/1384565/7728169 + options['config_dt'] = \ + time.strftime('%H:%M:%S', time.gmtime(dt)) + + # btr_dt is also proportional to resolution: default 1.5 seconds per km + btr_dt_per_km = config.getfloat('ice_shelf_2d', 'btr_dt_per_km') + btr_dt = btr_dt_per_km * self.resolution + options['config_btr_dt'] = \ + time.strftime('%H:%M:%S', time.gmtime(btr_dt)) + + self.dt = dt + self.btr_dt = btr_dt + + self.add_model_config_options(options=options) diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.yaml b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml new file mode 100644 index 000000000..8e4b10da6 --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml @@ -0,0 +1,83 @@ +omega: + io: + config_write_output_on_startup: true + hmix_del2: + config_use_mom_del2: true + config_mom_del2: 10.0 + bottom_drag: + config_implicit_bottom_drag_type: constant + config_implicit_constant_bottom_drag_coeff: 1.0e-3 + wetting_drying: + config_use_wetting_drying: true + config_zero_drying_velocity: true + config_zero_drying_velocity_ramp: true + config_check_ssh_consistency: false + config_drying_min_cell_height: {} + config_zero_drying_velocity_ramp_hmin: {} + config_zero_drying_velocity_ramp_hmax: {} + time_management: + config_run_duration: 0000-01-00_00:00:00 + eos: + config_eos_type: jm + pressure_gradient: + config_pressure_gradient_type: Jacobian_from_TS + tidal_forcing: + config_use_tidal_forcing_tau: 10000 + config_tidal_forcing_model: monochromatic + config_tidal_forcing_type: thickness_source + config_tidal_forcing_monochromatic_amp: 1.0 + config_tidal_forcing_monochromatic_period: 10.0 + time_varying_forcing: + config_time_varying_land_ice_forcing_start_time: 0001-01-01_00:00:00 + config_time_varying_land_ice_forcing_reference_time: 0001-01-01_00:00:00 + config_time_varying_land_ice_forcing_cycle_start: none + config_time_varying_land_ice_forcing_cycle_duration: 0002-00-00_00:00:00 + config_time_varying_land_ice_forcing_interval: 0001-00-00_00:00:00 + AM_globalStats: + config_AM_globalStats_enable: true + config_AM_globalStats_compute_on_startup: true + config_AM_globalStats_write_on_startup: true + streams: + mesh: + filename_template: init.nc + input: + filename_template: init.nc + restart: + output_interval: 0005_00:00:00 + forcing_data: + type: input + filename_template: forcing_data.nc + input_interval: initial_only + contents: + - tidalInputMask + land_ice_forcing: + filename_template: land_ice_forcing.nc + output: + type: output + filename_template: output.nc + output_interval: 0000_01:00:00 + clobber_mode: truncate + contents: + - tracers + - xtime + - daysSinceStartOfSim + - normalVelocity + - layerThickness + - wettingVelocityFactor + - zMid + - ssh + - velocityX + - velocityY + - tidalInputMask + - atmosphericPressure + - landIcePressure + - landIceDraft + - landIceFraction + - landIceMask + - landIceFreshwaterFlux + - landIceHeatFlux + - landIceFrictionVelocity + - landIceInterfaceTracers + - landIceBoundaryLayerTracers + globalStatsOutput: + output_interval: 0000_00:10:00 diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg new file mode 100644 index 000000000..deb8e9e6b --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg @@ -0,0 +1,76 @@ +# Options related to the vertical grid +[vertical_grid] + +# the type of vertical grid +grid_type = uniform + +# Number of vertical levels +vert_levels = 20 + +# Depth of the bottom of the ocean +bottom_depth = 1050.0 + +# The type of vertical coordinate (e.g. z-level, z-star) +coord_type = z-star + +# Whether to use "partial" or "full", or "None" to not alter the topography +partial_cell_type = partial + +# The minimum fraction of a layer for partial cells +min_pc_fraction = 0.1 + + +# config options for 2D ice-shelf testcases +[ice_shelf_2d] + +# width of domain in km +lx = 50 + +# length of domain in km +ly = 220 + +# How the land ice pressure at y (y_max - 0.6 * dc), 1.0, 0.0) + write_netcdf(ds_forcing, 'init_mode_forcing_data.nc') + + +def _compute_land_ice_pressure_from_draft(land_ice_draft, modify_mask, + ref_density=None): + """ + Compute the pressure from an overlying ice shelf from ice draft + + Parameters + ---------- + land_ice_draft : xarray.DataArray + The ice draft (sea surface height) + + modify_mask : xarray.DataArray + A mask that is 1 where ``landIcePressure`` can be deviate from 0 + + ref_density : float, optional + A reference density for seawater displaced by the ice shelf + + Returns + ------- + land_ice_pressure : xarray.DataArray + The pressure from the overlying land ice on the ocean + """ + gravity = constants['SHR_CONST_G'] + if ref_density is None: + ref_density = constants['SHR_CONST_RHOSW'] + land_ice_pressure = \ + modify_mask * np.maximum(-ref_density * gravity * land_ice_draft, 0.) + return land_ice_pressure diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py b/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py new file mode 100644 index 000000000..e013858bf --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py @@ -0,0 +1,149 @@ +import numpy as np +import xarray as xr +from mpas_tools.cime.constants import constants +from mpas_tools.io import write_netcdf + +from polaris import Step + + +class SshAdjustment(Step): + """ + A step for iteratively adjusting the pressure from the weight of the ice + shelf to match the sea-surface height as part of ice-shelf 2D test cases + """ + def __init__(self, component, resolution, forward, indir=None, + name='ssh_adjust', tidal_forcing=False): + """ + Create the step + + Parameters + ---------- + resolution : float + The resolution of the test case in m + + coord_type: str + The coordinate type (e.g., 'z-star', 'single_layer', etc.) + + iteration : int, optional + the iteration number + + tidal_forcing : bool, optional + """ + self.resolution = resolution + + super().__init__(component=component, name=name, indir=indir) + + self.add_input_file(filename='init.nc', + target=f'{forward.path}/output.nc') + self.add_input_file(filename='init_ssh.nc', + target=f'{forward.path}/output_ssh.nc') + self.add_output_file(filename='output.nc') + + # no setup() is needed + + def run(self): + """ + Adjust the sea surface height or land-ice pressure to be dynamically + consistent with one another. + + """ + logger = self.logger + # TODO get from config + # config = self.config + adjust_variable = 'landIcePressure' + in_filename = self.inputs[0] + ssh_filename = self.inputs[1] + out_filename = self.outputs[0] + + if adjust_variable not in ['ssh', 'landIcePressure']: + raise ValueError(f"Unknown variable to modify: {adjust_variable}") + + logger.info(" * Updating SSH or land-ice pressure") + + with xr.open_dataset(in_filename) as ds: + + # keep the data set with Time for output + ds_out = ds + + ds = ds.isel(Time=0) + + on_a_sphere = ds.attrs['on_a_sphere'].lower() == 'yes' + + initSSH = ds.ssh + if 'minLevelCell' in ds: + minLevelCell = ds.minLevelCell - 1 + else: + minLevelCell = xr.zeros_like(ds.maxLevelCell) + + with xr.open_dataset(ssh_filename) as ds_ssh: + # get the last time entry + ds_ssh = ds_ssh.isel(Time=ds_ssh.sizes['Time'] - 1) + finalSSH = ds_ssh.ssh + topDensity = ds_ssh.density.isel(nVertLevels=minLevelCell) + + mask = np.logical_and(ds.maxLevelCell > 0, + ds.modifyLandIcePressureMask == 1) + + deltaSSH = mask * (finalSSH - initSSH) + + # then, modify the SSH or land-ice pressure + if adjust_variable == 'ssh': + ssh = finalSSH.expand_dims(dim='Time', axis=0) + ds_out['ssh'] = ssh + # also update the landIceDraft variable, which will be used to + # compensate for the SSH due to land-ice pressure when + # computing sea-surface tilt + ds_out['landIceDraft'] = ssh + # we also need to stretch layerThickness to be compatible with + # the new SSH + stretch = ((finalSSH + ds.bottomDepth) / + (initSSH + ds.bottomDepth)) + ds_out['layerThickness'] = ds_out.layerThickness * stretch + landIcePressure = ds.landIcePressure.values + else: + # Moving the SSH up or down by deltaSSH would change the + # land-ice pressure by density(SSH)*g*deltaSSH. If deltaSSH is + # positive (moving up), it means the land-ice pressure is too + # small and if deltaSSH is negative (moving down), it means + # land-ice pressure is too large, the sign of the second term + # makes sense. + gravity = constants['SHR_CONST_G'] + deltaLandIcePressure = topDensity * gravity * deltaSSH + + landIcePressure = np.maximum( + 0.0, ds.landIcePressure + deltaLandIcePressure) + + ds_out['landIcePressure'] = \ + landIcePressure.expand_dims(dim='Time', axis=0) + + finalSSH = initSSH + + write_netcdf(ds_out, out_filename) + + # Write the largest change in SSH and its lon/lat to a file + with open('maxDeltaSSH.log', 'w') as log_file: + + mask = landIcePressure > 0. + i_cell = np.abs(deltaSSH.where(mask)).argmax().values + + ds_cell = ds.isel(nCells=i_cell) + deltaSSHMax = deltaSSH.isel(nCells=i_cell).values + + if on_a_sphere: + coords = (f'lon/lat: ' + f'{np.rad2deg(ds_cell.lonCell.values):f} ' + f'{np.rad2deg(ds_cell.latCell.values):f}') + else: + coords = (f'x/y: {1e-3 * ds_cell.xCell.values:f} ' + f'{1e-3 * ds_cell.yCell.values:f}') + string = (f'deltaSSHMax: ' + f'{deltaSSHMax:g}, {coords}') + logger.info(f' {string}') + log_file.write(f'{string}\n') + string = (f'ssh: {finalSSH.isel(nCells=i_cell).values:g}, ' + f'landIcePressure: ' + f'{landIcePressure.isel(nCells=i_cell).values:g}') + logger.info(f' {string}') + log_file.write(f'{string}\n') + + logger.info(" - Complete\n") diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py new file mode 100644 index 000000000..aa1261206 --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py @@ -0,0 +1,156 @@ +import time + +from polaris.mesh.planar import compute_planar_hex_nx_ny +from polaris.ocean.model import OceanModelStep + + +class SshForward(OceanModelStep): + """ + A step for performing forward ocean component runs as part of baroclinic + channel tasks. + + Attributes + ---------- + resolution : float + The resolution of the task in km + + dt : float + The model time step in seconds + + btr_dt : float + The model barotropic time step in seconds + """ + def __init__(self, component, resolution, mesh, init, + name='ssh_forward', subdir=None, + iteration=1, indir=None, + ntasks=None, min_tasks=None, openmp_threads=1): + """ + Create a new task + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + resolution : km + The resolution of the task in km + + name : str + the name of the task + + subdir : str, optional + the subdirectory for the step. If neither this nor ``indir`` + are provided, the directory is the ``name`` + + indir : str, optional + the directory the step is in, to which ``name`` will be appended + + iteration : int, optional + the iteration number + + ntasks : int, optional + the number of tasks the step would ideally use. If fewer tasks + are available on the system, the step will run on all available + tasks as long as this is not below ``min_tasks`` + + min_tasks : int, optional + the number of tasks the step requires. If the system has fewer + than this number of tasks, the step will fail + + openmp_threads : int, optional + the number of OpenMP threads the step will use + """ + self.resolution = resolution + + super().__init__(component=component, name=name, subdir=subdir, + indir=indir, ntasks=ntasks, min_tasks=min_tasks, + openmp_threads=openmp_threads) + + # make sure output is double precision + self.add_yaml_file('polaris.ocean.config', 'output.yaml') + + self.add_input_file(filename='init.nc', + target=f'{init.path}/initial_state.nc') + self.add_input_file(filename='graph.info', + target=f'{mesh.path}/culled_graph.info') + + self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', + 'forward.yaml') + + # config_run_duration: '0000_01:00:00' + # config_land_ice_flux_mode: 'standalone' + # we don't want the global stats AM for this run + # self.add_namelist_options( + # {'config_AM_globalStats_enable': '.false.'}) + + # we want a shorter run and no freshwater fluxes under the ice shelf + # from these namelist options + # self.add_namelist_file('compass.ocean.namelists', + # 'namelist.ssh_adjust') + + # self.add_streams_file('compass.ocean.streams', 'streams.ssh_adjust') + + self.add_output_file( + filename='output.nc', + validate_vars=['temperature', 'salinity', 'layerThickness', + 'normalVelocity']) + + self.dt = None + self.btr_dt = None + + def compute_cell_count(self): + """ + Compute the approximate number of cells in the mesh, used to constrain + resources + + Returns + ------- + cell_count : int or None + The approximate number of cells in the mesh + """ + section = self.config['ice_shelf_2d'] + lx = section.getfloat('lx') + ly = section.getfloat('ly') + nx, ny = compute_planar_hex_nx_ny(lx, ly, self.resolution) + cell_count = nx * ny + return cell_count + + def dynamic_model_config(self, at_setup): + """ + Add model config options, namelist, streams and yaml files using config + options or template replacements that need to be set both during step + setup and at runtime + + Parameters + ---------- + at_setup : bool + Whether this method is being run during setup of the step, as + opposed to at runtime + """ + super().dynamic_model_config(at_setup) + + config = self.config + + vert_levels = config.getfloat('vertical_grid', 'vert_levels') + if not at_setup and vert_levels == 1: + self.add_yaml_file('polaris.ocean.config', 'single_layer.yaml') + + options = dict() + + # dt is proportional to resolution: default 30 seconds per km + dt_per_km = config.getfloat('ice_shelf_2d', 'dt_per_km') + dt = dt_per_km * self.resolution + # https://stackoverflow.com/a/1384565/7728169 + options['config_dt'] = \ + time.strftime('%H:%M:%S', time.gmtime(dt)) + + # btr_dt is also proportional to resolution: default 1.5 seconds per km + btr_dt_per_km = config.getfloat('ice_shelf_2d', 'btr_dt_per_km') + btr_dt = btr_dt_per_km * self.resolution + options['config_btr_dt'] = \ + time.strftime('%H:%M:%S', time.gmtime(btr_dt)) + + self.dt = dt + self.btr_dt = btr_dt + + self.add_model_config_options(options=options) diff --git a/polaris/ocean/tasks/ice_shelf_2d/viz.py b/polaris/ocean/tasks/ice_shelf_2d/viz.py new file mode 100644 index 000000000..2d1e4f357 --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/viz.py @@ -0,0 +1,194 @@ +import cmocean # noqa: F401 +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import xarray as xr + +from polaris import Step +from polaris.ocean.viz import compute_transect, plot_transect +from polaris.viz import plot_horiz_field + + +class Viz(Step): + """ + A step for visualizing a cross-section and horizontal planes through the + ice shelf + """ + def __init__(self, component, indir, mesh, init): + """ + Create the step + + Parameters + ---------- + test_case : compass.TestCase + The test case this step belongs to + """ + super().__init__(component=component, name='viz', indir=indir) + self.add_input_file( + filename='mesh.nc', + target=f'{mesh.path}/culled_mesh.nc') + self.add_input_file( + filename='init.nc', + target=f'{init.path}/init.nc') + self.add_input_file( + filename='adjusted_init.nc', + target='../forward/init.nc') + self.add_input_file( + filename='output.nc', + target='../forward/output.nc') + + def run(self): + """ + Run this step of the test case + """ + ds_mesh = xr.load_dataset('mesh.nc') + ds_init = xr.load_dataset('init.nc') + ds = xr.load_dataset('output.nc') + + x_mid = ds_mesh.xCell.median() + y_min = ds_mesh.yCell.min() + y_max = ds_mesh.yCell.max() + x = xr.DataArray(data=[x_mid, x_mid], dims=('nPoints',)) + y = xr.DataArray(data=[y_min, y_max], dims=('nPoints',)) + + # TODO add derived variable delSsh, delLandIcePressure to ds + # for tidx=0, ds.isel(Time=tidx) - ds_init.isel(Time=0) + # for tidx>0, ds.isel(Time=tidx) - ds.isel(Time=0) + # TODO add derived variable columnThickness to ds_init and ds + # Plot the time series of max velocity + plt.figure(figsize=[12, 6], dpi=100) + umax = np.amax(ds.velocityX[:, :, 0].values, axis=1) + vmax = np.amax(ds.velocityY[:, :, 0].values, axis=1) + t = ds.daysSinceStartOfSim.values + time = pd.to_timedelta(t) / 1.e9 + plt.plot(time, umax, 'k', label='u') + plt.plot(time, vmax, 'b', label='v') + plt.xlabel('Time (s)') + plt.ylabel('Velocity (m/s)') + plt.legend() + plt.savefig('velocity_max_t.png', dpi=200) + plt.close() + + vmin_del_ssh = np.min(ds.delSsh.values) + vmax_del_ssh = np.max(ds.delSsh.values) + vmin_del_p = np.min(ds.delLandIcePressure.values) + vmax_del_p = np.max(ds.delLandIcePressure.values) + vmin_temp = np.min(ds.temperature.values) + vmax_temp = np.max(ds.temperature.values) + vmin_salt = np.min(ds.salinity.values) + vmax_salt = np.max(ds.salinity.values) + vmax_uv = max(np.amax(ds.velocityX.values), + np.amax(ds.velocityY.values)) + + tidx = 0 # Plot the initial time + ds_transect = compute_transect( + x=x, y=y, ds_horiz_mesh=ds_mesh, + layer_thickness=ds_init.layerThickness.isel(Time=tidx), + bottom_depth=ds_mesh.bottomDepth, + min_level_cell=ds_mesh.minLevelCell - 1, + max_level_cell=ds_mesh.maxLevelCell - 1, + spherical=False) + + plot_transect(ds_transect, + mpas_field=ds_init.temperature.isel(Time=tidx), + out_filename='temperature_section_init.png', + title='temperature', + interface_color='grey', + vmin=vmin_temp, vmax=vmax_temp, + colorbar_label=r'$^{\circ}$C', cmap='cmo.thermal') + + plot_transect(ds_transect, + mpas_field=ds_init.salinity.isel(Time=tidx), + out_filename='salinity_section_init.png', + title='salinity', + interface_color='grey', + vmin=vmin_salt, vmax=vmax_salt, + colorbar_label=r'PSU', cmap='cmo.haline') + + # Plot water column thickness horizontal ds_init + cell_mask = ds_init.maxLevelCell >= 1 + ds_horiz = self._process_ds(ds_init, ds_init, ds_mesh.bottomDepth, + time_index=0) + plot_horiz_field(ds_init, ds_mesh, 'columnThickness', + 'H_horiz_init.png', t_index=0, + cell_mask=cell_mask) + # Plot land ice pressure horizontal ds_init + for tidx in [0, -1]: + ds_horiz = self._process_ds(ds, ds_init, ds_mesh.bottomDepth, + time_index=tidx) + day = ds.daysSinceStartOfSim.isel(Time=tidx) + # Plot water column thickness horizontal + plot_horiz_field(ds_horiz, ds_mesh, 'columnThickness', + f'H_horiz_day{int(day)}.png', t_index=tidx, + cell_mask=cell_mask) + plot_horiz_field(ds_horiz, ds_mesh, 'wettingVelocityFactor', + f'wet_horiz_day{int(day)}.png', t_index=tidx, + z_index=0, cell_mask=cell_mask, vmin=0, vmax=1, + cmap='cmo.ice') + # Plot difference in ssh + plot_horiz_field(ds_horiz, ds_mesh, 'delSsh', + f'del_ssh_horiz_day{int(day)}.png', t_index=tidx, + cell_mask=cell_mask, + vmin=vmin_del_ssh, vmax=vmax_del_ssh) + + # Plot difference in land ice pressure + plot_horiz_field(ds_horiz, ds_mesh, 'delLandIcePressure', + f'del_land_ice_pressure_horiz_day{int(day)}.png', + t_index=tidx, cell_mask=cell_mask, + vmin=vmin_del_p, vmax=vmax_del_p) + + # Plot transects + ds_transect = compute_transect( + x=x, y=y, ds_horiz_mesh=ds_mesh, + layer_thickness=ds.layerThickness.isel(Time=tidx), + bottom_depth=ds_mesh.bottomDepth, + min_level_cell=ds_mesh.minLevelCell - 1, + max_level_cell=ds_mesh.maxLevelCell - 1, + spherical=False) + + plot_transect(ds_transect, + mpas_field=ds.velocityX.isel(Time=tidx), + out_filename=f'u_section_day{int(day)}.png', + title='x-velocity', + interface_color='grey', + vmin=-vmax_uv, vmax=vmax_uv, + colorbar_label=r'm/s', cmap='cmo.balance') + + plot_transect(ds_transect, + mpas_field=ds.velocityY.isel(Time=tidx), + out_filename=f'v_section_day{int(day)}.png', + title='x-velocity', + interface_color='grey', + vmin=-vmax_uv, vmax=vmax_uv, + colorbar_label=r'm/s', cmap='cmo.balance') + + plot_transect( + ds_transect, + mpas_field=ds.temperature.isel(Time=tidx), + out_filename=f'temperature_section_day{int(day)}.png', + title='temperature', + interface_color='grey', + vmin=vmin_temp, vmax=vmax_temp, + colorbar_label=r'$^{\circ}$C', cmap='cmo.thermal') + + plot_transect(ds_transect, + mpas_field=ds.salinity.isel(Time=tidx), + out_filename=f'salinity_section_day{int(day)}.png', + title='salinity', + interface_color='grey', + vmin=vmin_salt, vmax=vmax_salt, + colorbar_label=r'PSU', cmap='cmo.haline') + + @staticmethod + def _process_ds(ds, ds_init, bottom_depth, time_index): + ds_out = ds.isel(Time=time_index, nVertLevels=0) + ds_out['columnThickness'] = ds_out.ssh + bottom_depth + if time_index == 0: + ds_out['delSsh'] = ds_out.ssh - ds_init.ssh.isel(Time=0) + ds_out['delLandIcePressure'] = ds_out.landIcePressure - \ + ds_init.landIcePressure.isel(Time=0) + else: + ds_out['delSsh'] = ds_out.ssh - ds.ssh.isel(Time=0) + ds_out['delLandIcePressure'] = ds_out.landIcePressure - \ + ds.landIcePressure.isel(Time=0) + return ds_out From 8af7b5d66a74bacc13c516635689708459cc850d Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 30 Nov 2023 11:06:23 -0600 Subject: [PATCH 02/30] Add decimals to time strings --- polaris/ocean/model/time.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/polaris/ocean/model/time.py b/polaris/ocean/model/time.py index 84e0e941b..e44756336 100644 --- a/polaris/ocean/model/time.py +++ b/polaris/ocean/model/time.py @@ -1,5 +1,7 @@ import time +import numpy as np + def get_time_interval_string(days=None, seconds=None): """ @@ -29,8 +31,8 @@ def get_time_interval_string(days=None, seconds=None): day_part = int(total / sec_per_day) sec_part = total - day_part * sec_per_day - + sec_decimal = sec_part - np.floor(sec_part) # https://stackoverflow.com/a/1384565/7728169 seconds_str = time.strftime('%H:%M:%S', time.gmtime(sec_part)) - time_str = f'{day_part:04d}_{seconds_str}' + time_str = f'{day_part:04d}_{seconds_str}.{int(sec_decimal * 1e3):03d}' return time_str From 4a519dc7df237472d5d1aeff9c4f606cd3ce5be0 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 30 Nov 2023 11:09:38 -0600 Subject: [PATCH 03/30] Add time integration related cfg options --- polaris/ocean/tasks/ice_shelf_2d/forward.py | 106 +++++++++++------- .../ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg | 30 ++++- .../ocean/tasks/ice_shelf_2d/ssh_forward.py | 92 ++++++++------- 3 files changed, 145 insertions(+), 83 deletions(-) diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.py b/polaris/ocean/tasks/ice_shelf_2d/forward.py index b2e4e94c0..628518c54 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.py +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.py @@ -1,7 +1,5 @@ -import time - from polaris.mesh.planar import compute_planar_hex_nx_ny -from polaris.ocean.model import OceanModelStep +from polaris.ocean.model import OceanModelStep, get_time_interval_string class Forward(OceanModelStep): @@ -13,16 +11,12 @@ class Forward(OceanModelStep): ---------- resolution : float The resolution of the task in km - - dt : float - The model time step in seconds - - btr_dt : float - The model barotropic time step in seconds """ - def __init__(self, component, resolution, mesh, init, name='forward', - subdir=None, indir=None, ntasks=None, min_tasks=None, - openmp_threads=1): + def __init__(self, component, resolution, mesh, init, + name='forward', subdir=None, indir=None, + ntasks=None, min_tasks=None, openmp_threads=1, + tidal_forcing=False, time_varying_forcing=False, + thin_film=False): """ Create a new task @@ -56,30 +50,30 @@ def __init__(self, component, resolution, mesh, init, name='forward', openmp_threads : int, optional the number of OpenMP threads the step will use """ - self.resolution = resolution super().__init__(component=component, name=name, subdir=subdir, indir=indir, ntasks=ntasks, min_tasks=min_tasks, openmp_threads=openmp_threads) + self.resolution = resolution + self.tidal_forcing = tidal_forcing + self.time_varying_forcing = time_varying_forcing + self.thin_film = thin_film + # make sure output is double precision self.add_yaml_file('polaris.ocean.config', 'output.yaml') + self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', + 'global_stats.yaml') - self.add_input_file(filename='initial_state.nc', - target='../../init/initial_state.nc') + self.add_input_file(filename='init.nc', + work_dir_target=f'{init.path}/initial_state.nc') self.add_input_file(filename='graph.info', - target='../../init/culled_graph.info') - - self.add_yaml_file('polaris.ocean.tasks.baroclinic_channel', - 'forward.yaml') + work_dir_target=f'{mesh.path}/culled_graph.info') self.add_output_file( filename='output.nc', validate_vars=['temperature', 'salinity', 'layerThickness', 'normalVelocity']) - self.dt = None - self.btr_dt = None - def compute_cell_count(self): """ Compute the approximate number of cells in the mesh, used to constrain @@ -112,25 +106,57 @@ def dynamic_model_config(self, at_setup): super().dynamic_model_config(at_setup) config = self.config - - options = dict() - # TODO config_use_tidal_forcing - # TODO config_use_time_varying_forcing + section = config['ice_shelf_2d'] # dt is proportional to resolution: default 30 seconds per km - dt_per_km = config.getfloat('ice_shelf_2d', 'dt_per_km') - dt = dt_per_km * self.resolution - # https://stackoverflow.com/a/1384565/7728169 - options['config_dt'] = \ - time.strftime('%H:%M:%S', time.gmtime(dt)) + time_integrator = section.get('time_integrator') - # btr_dt is also proportional to resolution: default 1.5 seconds per km - btr_dt_per_km = config.getfloat('ice_shelf_2d', 'btr_dt_per_km') - btr_dt = btr_dt_per_km * self.resolution - options['config_btr_dt'] = \ - time.strftime('%H:%M:%S', time.gmtime(btr_dt)) - - self.dt = dt - self.btr_dt = btr_dt + if time_integrator == 'RK4': + dt_per_km = section.getfloat('rk4_dt_per_km') + else: + dt_per_km = section.getfloat('split_dt_per_km') + dt_str = get_time_interval_string(seconds=dt_per_km * self.resolution) - self.add_model_config_options(options=options) + # btr_dt is also proportional to resolution: default 1.5 seconds per km + btr_dt_per_km = section.getfloat('btr_dt_per_km') + btr_dt_str = get_time_interval_string( + seconds=btr_dt_per_km * self.resolution) + + s_per_hour = 3600. + run_duration = section.getfloat('forward_run_duration') + run_duration_str = get_time_interval_string( + seconds=run_duration * s_per_hour) + + output_interval = section.getfloat('forward_output_interval') + output_interval_str = get_time_interval_string( + seconds=output_interval * s_per_hour) + + replacements = dict( + time_integrator=time_integrator, + dt=dt_str, + btr_dt=btr_dt_str, + run_duration=run_duration_str, + output_interval=output_interval_str, + land_ice_flux_mode='standalone', + ) + + self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', + 'forward.yaml', + template_replacements=replacements) + + if self.time_varying_forcing: + self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', + 'time_varying_forcing.yaml') + if self.tidal_forcing: + self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', + 'tidal_forcing.yaml') + if self.thin_film: + d1 = section.getfloat('y1_water_column_thickness') + replacements = dict( + thin_film_thickness=f'{d1}', + thin_film_ramp_hmin=f'{d1}', + thin_film_ramp_hmax=f'{d1 * 10.}', + ) + self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', + 'thin_film.yaml', + template_replacements=replacements) diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg index deb8e9e6b..0ec4597a9 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg +++ b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg @@ -7,6 +7,9 @@ grid_type = uniform # Number of vertical levels vert_levels = 20 +# The minimum number of vertical levels +min_vert_levels = 3 + # Depth of the bottom of the ocean bottom_depth = 1050.0 @@ -19,6 +22,8 @@ partial_cell_type = partial # The minimum fraction of a layer for partial cells min_pc_fraction = 0.1 +# The minimum layer thickness in m +min_layer_thickness = 0.0 # config options for 2D ice-shelf testcases [ice_shelf_2d] @@ -32,11 +37,32 @@ ly = 220 # How the land ice pressure at y Date: Thu, 30 Nov 2023 11:12:01 -0600 Subject: [PATCH 04/30] Add init step to ssh_adjustment --- polaris/ocean/tasks/ice_shelf_2d/__init__.py | 4 +- polaris/ocean/tasks/ice_shelf_2d/init.py | 4 +- .../tasks/ice_shelf_2d/ssh_adjustment.py | 93 ++++++++++--------- 3 files changed, 52 insertions(+), 49 deletions(-) diff --git a/polaris/ocean/tasks/ice_shelf_2d/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/__init__.py index 2ff8c1aeb..850ff0c13 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/__init__.py @@ -48,7 +48,7 @@ def add_ice_shelf_2d_tasks(component): name = f'ssh_adjust_{iteration - 1}' ssh_adjust = SshAdjustment( component=component, resolution=resolution, indir=resdir, - name=name, forward=ssh_forward) + name=name, init=shared_steps['init'], forward=ssh_forward) ssh_adjust.set_shared_config(config, link=config_filename) shared_steps[name] = ssh_adjust name = f'ssh_forward_{iteration}' @@ -63,7 +63,7 @@ def add_ice_shelf_2d_tasks(component): name = f'ssh_adjust_{iteration - 1}' ssh_adjust = SshAdjustment( component=component, resolution=resolution, indir=resdir, - name=name, forward=ssh_forward) + name=name, init=shared_steps['init'], forward=ssh_forward) ssh_adjust.set_shared_config(config, link=config_filename) shared_steps[name] = ssh_adjust diff --git a/polaris/ocean/tasks/ice_shelf_2d/init.py b/polaris/ocean/tasks/ice_shelf_2d/init.py index 383a91eac..4637e1bf4 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/init.py +++ b/polaris/ocean/tasks/ice_shelf_2d/init.py @@ -94,7 +94,7 @@ def run(self): x_cell = ds.xCell y_cell = ds.yCell - ds['bottomDepth'] = bottom_depth * xr.ones_like(ds.x_cell) + ds['bottomDepth'] = bottom_depth * xr.ones_like(ds.xCell) # Column thickness is a piecewise linear function column_thickness = xr.where( @@ -155,7 +155,7 @@ def run(self): ds.attrs['ny'] = ny ds.attrs['dc'] = dc - write_netcdf(ds, 'initial_state.nc') + write_netcdf(ds, 'output.nc') # Generate the tidal forcing dataset whether it is used or not ds_forcing = xr.Dataset() diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py b/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py index e013858bf..3b1c9b1f7 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py +++ b/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py @@ -11,7 +11,7 @@ class SshAdjustment(Step): A step for iteratively adjusting the pressure from the weight of the ice shelf to match the sea-surface height as part of ice-shelf 2D test cases """ - def __init__(self, component, resolution, forward, indir=None, + def __init__(self, component, resolution, init, forward, indir=None, name='ssh_adjust', tidal_forcing=False): """ Create the step @@ -33,10 +33,12 @@ def __init__(self, component, resolution, forward, indir=None, super().__init__(component=component, name=name, indir=indir) + self.add_input_file(filename='final.nc', + work_dir_target=f'{forward.path}/output.nc') self.add_input_file(filename='init.nc', - target=f'{forward.path}/output.nc') - self.add_input_file(filename='init_ssh.nc', - target=f'{forward.path}/output_ssh.nc') + work_dir_target=f'{forward.path}/init.nc') + self.add_input_file(filename='mesh.nc', + work_dir_target=f'{init.path}/initial_state.nc') self.add_output_file(filename='output.nc') # no setup() is needed @@ -48,58 +50,59 @@ def run(self): """ logger = self.logger - # TODO get from config - # config = self.config + config = self.config adjust_variable = 'landIcePressure' - in_filename = self.inputs[0] - ssh_filename = self.inputs[1] + final_filename = self.inputs[0] + init_filename = self.inputs[1] + mesh_filename = self.inputs[2] out_filename = self.outputs[0] + ds_mesh = xr.open_dataset(mesh_filename) + ds_init = xr.open_dataset(init_filename) if adjust_variable not in ['ssh', 'landIcePressure']: raise ValueError(f"Unknown variable to modify: {adjust_variable}") logger.info(" * Updating SSH or land-ice pressure") - with xr.open_dataset(in_filename) as ds: + with xr.open_dataset(final_filename) as ds_final: # keep the data set with Time for output - ds_out = ds + # and generate these time slices + ds_out = ds_final.copy() + ds_init = ds_init.isel(Time=0) + ds_final = ds_final.isel(Time=-1) - ds = ds.isel(Time=0) + on_a_sphere = ds_out.attrs['on_a_sphere'].lower() == 'yes' - on_a_sphere = ds.attrs['on_a_sphere'].lower() == 'yes' - - initSSH = ds.ssh - if 'minLevelCell' in ds: - minLevelCell = ds.minLevelCell - 1 + if 'minLevelCell' in ds_mesh: + minLevelCell = ds_final.minLevelCell - 1 else: - minLevelCell = xr.zeros_like(ds.maxLevelCell) - - with xr.open_dataset(ssh_filename) as ds_ssh: - # get the last time entry - ds_ssh = ds_ssh.isel(Time=ds_ssh.sizes['Time'] - 1) - finalSSH = ds_ssh.ssh - topDensity = ds_ssh.density.isel(nVertLevels=minLevelCell) + minLevelCell = ds_mesh.minLevelCell - 1 - mask = np.logical_and(ds.maxLevelCell > 0, - ds.modifyLandIcePressureMask == 1) + init_ssh = ds_init.ssh + final_ssh = ds_final.ssh + top_density = ds_final.density.isel(nVertLevels=minLevelCell) - deltaSSH = mask * (finalSSH - initSSH) + y3 = config.getfloat('ice_shelf_2d', 'y3') * 1e3 + modify_mask = xr.where(ds_mesh.yCell < y3, 1, 0) + mask = np.logical_and(ds_final.maxLevelCell > 0, + modify_mask == 1) + delta_ssh = mask * (final_ssh - init_ssh) # then, modify the SSH or land-ice pressure if adjust_variable == 'ssh': - ssh = finalSSH.expand_dims(dim='Time', axis=0) - ds_out['ssh'] = ssh + final_ssh = final_ssh.expand_dims(dim='Time', axis=0) + ds_out['ssh'] = final_ssh # also update the landIceDraft variable, which will be used to # compensate for the SSH due to land-ice pressure when # computing sea-surface tilt - ds_out['landIceDraft'] = ssh + ds_out['landIceDraft'] = final_ssh # we also need to stretch layerThickness to be compatible with # the new SSH - stretch = ((finalSSH + ds.bottomDepth) / - (initSSH + ds.bottomDepth)) + stretch = ((final_ssh + ds_mesh.bottomDepth) / + (init_ssh + ds_mesh.bottomDepth)) ds_out['layerThickness'] = ds_out.layerThickness * stretch - landIcePressure = ds.landIcePressure.values + land_ice_pressure = ds_out.landIcePressure.values else: # Moving the SSH up or down by deltaSSH would change the # land-ice pressure by density(SSH)*g*deltaSSH. If deltaSSH is @@ -108,26 +111,26 @@ def run(self): # land-ice pressure is too large, the sign of the second term # makes sense. gravity = constants['SHR_CONST_G'] - deltaLandIcePressure = topDensity * gravity * deltaSSH + delta_land_ice_pressure = top_density * gravity * delta_ssh - landIcePressure = np.maximum( - 0.0, ds.landIcePressure + deltaLandIcePressure) + land_ice_pressure = np.maximum( + 0.0, ds_final.landIcePressure + delta_land_ice_pressure) ds_out['landIcePressure'] = \ - landIcePressure.expand_dims(dim='Time', axis=0) + land_ice_pressure.expand_dims(dim='Time', axis=0) - finalSSH = initSSH + final_ssh = init_ssh write_netcdf(ds_out, out_filename) # Write the largest change in SSH and its lon/lat to a file with open('maxDeltaSSH.log', 'w') as log_file: - mask = landIcePressure > 0. - i_cell = np.abs(deltaSSH.where(mask)).argmax().values + mask = land_ice_pressure > 0. + i_cell = np.abs(delta_ssh.where(mask)).argmax().values - ds_cell = ds.isel(nCells=i_cell) - deltaSSHMax = deltaSSH.isel(nCells=i_cell).values + ds_cell = ds_final.isel(nCells=i_cell) + delta_ssh_max = delta_ssh.isel(nCells=i_cell).values if on_a_sphere: coords = (f'lon/lat: ' @@ -137,12 +140,12 @@ def run(self): coords = (f'x/y: {1e-3 * ds_cell.xCell.values:f} ' f'{1e-3 * ds_cell.yCell.values:f}') string = (f'deltaSSHMax: ' - f'{deltaSSHMax:g}, {coords}') + f'{delta_ssh_max:g}, {coords}') logger.info(f' {string}') log_file.write(f'{string}\n') - string = (f'ssh: {finalSSH.isel(nCells=i_cell).values:g}, ' - f'landIcePressure: ' - f'{landIcePressure.isel(nCells=i_cell).values:g}') + string = (f'ssh: {final_ssh.isel(nCells=i_cell).values:g}, ' + f'land_ice_pressure: ' + f'{land_ice_pressure.isel(nCells=i_cell).values:g}') logger.info(f' {string}') log_file.write(f'{string}\n') From 1e2c10eac28c6d7cc9c486bd7ff0c479ec960401 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 30 Nov 2023 11:13:05 -0600 Subject: [PATCH 05/30] Split up yaml file --- polaris/ocean/tasks/ice_shelf_2d/forward.yaml | 49 ++++++------------- .../tasks/ice_shelf_2d/global_stats.yaml | 8 +++ 2 files changed, 24 insertions(+), 33 deletions(-) create mode 100644 polaris/ocean/tasks/ice_shelf_2d/global_stats.yaml diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.yaml b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml index 8e4b10da6..3931f18e4 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.yaml +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml @@ -1,42 +1,34 @@ omega: io: - config_write_output_on_startup: true + config_write_output_on_startup: false hmix_del2: config_use_mom_del2: true config_mom_del2: 10.0 bottom_drag: config_implicit_bottom_drag_type: constant config_implicit_constant_bottom_drag_coeff: 1.0e-3 - wetting_drying: - config_use_wetting_drying: true - config_zero_drying_velocity: true - config_zero_drying_velocity_ramp: true - config_check_ssh_consistency: false - config_drying_min_cell_height: {} - config_zero_drying_velocity_ramp_hmin: {} - config_zero_drying_velocity_ramp_hmax: {} time_management: - config_run_duration: 0000-01-00_00:00:00 + config_run_duration: {{ run_duration }} + time_integration: + config_dt: {{ dt }} + config_time_integrator: {{ time_integrator }} + split_explicit_ts: + config_btr_dt: {{ btr_dt }} eos: config_eos_type: jm pressure_gradient: config_pressure_gradient_type: Jacobian_from_TS - tidal_forcing: - config_use_tidal_forcing_tau: 10000 - config_tidal_forcing_model: monochromatic - config_tidal_forcing_type: thickness_source - config_tidal_forcing_monochromatic_amp: 1.0 - config_tidal_forcing_monochromatic_period: 10.0 time_varying_forcing: config_time_varying_land_ice_forcing_start_time: 0001-01-01_00:00:00 config_time_varying_land_ice_forcing_reference_time: 0001-01-01_00:00:00 config_time_varying_land_ice_forcing_cycle_start: none config_time_varying_land_ice_forcing_cycle_duration: 0002-00-00_00:00:00 config_time_varying_land_ice_forcing_interval: 0001-00-00_00:00:00 - AM_globalStats: - config_AM_globalStats_enable: true - config_AM_globalStats_compute_on_startup: true - config_AM_globalStats_write_on_startup: true + land_ice_fluxes: + config_land_ice_flux_mode: {{ land_ice_flux_mode }} + frazil_ice: + config_use_frazil_ice_formation: true + config_frazil_maximum_depth: 2000.0 streams: mesh: filename_template: init.nc @@ -44,18 +36,10 @@ omega: filename_template: init.nc restart: output_interval: 0005_00:00:00 - forcing_data: - type: input - filename_template: forcing_data.nc - input_interval: initial_only - contents: - - tidalInputMask - land_ice_forcing: - filename_template: land_ice_forcing.nc output: type: output filename_template: output.nc - output_interval: 0000_01:00:00 + output_interval: {{ output_interval }} clobber_mode: truncate contents: - tracers @@ -63,12 +47,13 @@ omega: - daysSinceStartOfSim - normalVelocity - layerThickness - - wettingVelocityFactor - zMid + - minLevelCell + - maxLevelCell - ssh + - density - velocityX - velocityY - - tidalInputMask - atmosphericPressure - landIcePressure - landIceDraft @@ -79,5 +64,3 @@ omega: - landIceFrictionVelocity - landIceInterfaceTracers - landIceBoundaryLayerTracers - globalStatsOutput: - output_interval: 0000_00:10:00 diff --git a/polaris/ocean/tasks/ice_shelf_2d/global_stats.yaml b/polaris/ocean/tasks/ice_shelf_2d/global_stats.yaml new file mode 100644 index 000000000..e60d97ae1 --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/global_stats.yaml @@ -0,0 +1,8 @@ +omega: + AM_globalStats: + config_AM_globalStats_enable: true + config_AM_globalStats_compute_on_startup: true + config_AM_globalStats_write_on_startup: true + streams: + globalStatsOutput: + output_interval: 0000_00:10:00 From 7b240167e701adbdde8768f3e5ef6e609869b013 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Fri, 1 Dec 2023 15:57:26 -0600 Subject: [PATCH 06/30] Fixup ssh forward, adjustment steps --- polaris/ocean/tasks/ice_shelf_2d/__init__.py | 6 +- polaris/ocean/tasks/ice_shelf_2d/forward.py | 26 +-- .../ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg | 34 ++-- .../tasks/ice_shelf_2d/ssh_adjustment.py | 171 +++++++++--------- .../ocean/tasks/ice_shelf_2d/ssh_forward.py | 17 +- 5 files changed, 111 insertions(+), 143 deletions(-) diff --git a/polaris/ocean/tasks/ice_shelf_2d/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/__init__.py index 850ff0c13..86e6911bc 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/__init__.py @@ -39,8 +39,7 @@ def add_ice_shelf_2d_tasks(component): name = f'ssh_forward_{iteration}' ssh_forward = SshForward( component=component, resolution=resolution, indir=resdir, - mesh=init, init=shared_steps['init'], - name=name) + mesh=init, init=shared_steps['init'], name=name) ssh_forward.set_shared_config(config, link=config_filename) shared_steps[name] = ssh_forward @@ -54,8 +53,7 @@ def add_ice_shelf_2d_tasks(component): name = f'ssh_forward_{iteration}' ssh_forward = SshForward( component=component, resolution=resolution, indir=resdir, - mesh=init, init=ssh_adjust, - name=name) + mesh=init, init=ssh_adjust, name=name) ssh_forward.set_shared_config(config, link=config_filename) shared_steps[name] = ssh_forward diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.py b/polaris/ocean/tasks/ice_shelf_2d/forward.py index 628518c54..62ccfe2b9 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.py +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.py @@ -14,9 +14,7 @@ class Forward(OceanModelStep): """ def __init__(self, component, resolution, mesh, init, name='forward', subdir=None, indir=None, - ntasks=None, min_tasks=None, openmp_threads=1, - tidal_forcing=False, time_varying_forcing=False, - thin_film=False): + ntasks=None, min_tasks=None, openmp_threads=1): """ Create a new task @@ -55,9 +53,6 @@ def __init__(self, component, resolution, mesh, init, openmp_threads=openmp_threads) self.resolution = resolution - self.tidal_forcing = tidal_forcing - self.time_varying_forcing = time_varying_forcing - self.thin_film = thin_film # make sure output is double precision self.add_yaml_file('polaris.ocean.config', 'output.yaml') @@ -65,7 +60,7 @@ def __init__(self, component, resolution, mesh, init, 'global_stats.yaml') self.add_input_file(filename='init.nc', - work_dir_target=f'{init.path}/initial_state.nc') + work_dir_target=f'{init.path}/output.nc') self.add_input_file(filename='graph.info', work_dir_target=f'{mesh.path}/culled_graph.info') @@ -143,20 +138,3 @@ def dynamic_model_config(self, at_setup): self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', 'forward.yaml', template_replacements=replacements) - - if self.time_varying_forcing: - self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', - 'time_varying_forcing.yaml') - if self.tidal_forcing: - self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', - 'tidal_forcing.yaml') - if self.thin_film: - d1 = section.getfloat('y1_water_column_thickness') - replacements = dict( - thin_film_thickness=f'{d1}', - thin_film_ramp_hmin=f'{d1}', - thin_film_ramp_hmax=f'{d1 * 10.}', - ) - self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', - 'thin_film.yaml', - template_replacements=replacements) diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg index 0ec4597a9..07bdb294b 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg +++ b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg @@ -47,7 +47,7 @@ forward_output_interval = 0.2 ssh_adjust_run_duration = 1.0 # Whether to adjust land ice pressure or SSH -ssh_adjust_variable = +ssh_adjust_variable = land_ice_pressure # Run duration of the forward step in hours forward_run_duration = 0.2 @@ -64,11 +64,17 @@ split_dt_per_km = 5 # Time step in seconds as a function of resolution btr_dt_per_km = 0.5 -# Vertical thickness of ocean sub-ice cavity at GL -y1_water_column_thickness = 1.0e-2 +# Temperature of the surface in the northern half of the domain +temperature = 1.0 -# Vertical thickness of water column thickness at y2 -y2_water_column_thickness = 850.0 +# Salinity of the water in the entire domain +surface_salinity = 34.5 + +# Salinity of the water in the entire domain +bottom_salinity = 34.7 + +# Coriolis parameter +coriolis_parameter = 0. # GL location in y in km y1 = 30.0 @@ -79,17 +85,19 @@ y2 = 90.0 # ice shelf front location in y in km y3 = 90.0 -# Temperature of the surface in the northern half of the domain -temperature = 1.0 +[ice_shelf_2d_default] -# Salinity of the water in the entire domain -surface_salinity = 34.5 +# Vertical thickness of ocean sub-ice cavity at GL +y1_water_column_thickness = 10.0 -# Salinity of the water in the entire domain -bottom_salinity = 34.7 +# Vertical thickness of water column thickness at y2 +y2_water_column_thickness = 1050.0 -# Coriolis parameter -coriolis_parameter = 0. +# Output interval for the forward step in hours +forward_output_interval = 0.2 + +# Run duration of the forward step in hours +forward_run_duration = 0.2 # config options for ice_shelf_2d time-varying land-ice forcing [ice_shelf_2d_forcing] diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py b/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py index 3b1c9b1f7..b0e6f9cb5 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py +++ b/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py @@ -12,7 +12,7 @@ class SshAdjustment(Step): shelf to match the sea-surface height as part of ice-shelf 2D test cases """ def __init__(self, component, resolution, init, forward, indir=None, - name='ssh_adjust', tidal_forcing=False): + name='ssh_adjust'): """ Create the step @@ -26,8 +26,6 @@ def __init__(self, component, resolution, init, forward, indir=None, iteration : int, optional the iteration number - - tidal_forcing : bool, optional """ self.resolution = resolution @@ -38,7 +36,7 @@ def __init__(self, component, resolution, init, forward, indir=None, self.add_input_file(filename='init.nc', work_dir_target=f'{forward.path}/init.nc') self.add_input_file(filename='mesh.nc', - work_dir_target=f'{init.path}/initial_state.nc') + work_dir_target=f'{init.path}/culled_mesh.nc') self.add_output_file(filename='output.nc') # no setup() is needed @@ -58,95 +56,94 @@ def run(self): out_filename = self.outputs[0] ds_mesh = xr.open_dataset(mesh_filename) ds_init = xr.open_dataset(init_filename) + ds_final = xr.open_dataset(final_filename) + ds_out = ds_init.copy() if adjust_variable not in ['ssh', 'landIcePressure']: raise ValueError(f"Unknown variable to modify: {adjust_variable}") logger.info(" * Updating SSH or land-ice pressure") - with xr.open_dataset(final_filename) as ds_final: - - # keep the data set with Time for output - # and generate these time slices - ds_out = ds_final.copy() - ds_init = ds_init.isel(Time=0) - ds_final = ds_final.isel(Time=-1) - - on_a_sphere = ds_out.attrs['on_a_sphere'].lower() == 'yes' - - if 'minLevelCell' in ds_mesh: - minLevelCell = ds_final.minLevelCell - 1 - else: - minLevelCell = ds_mesh.minLevelCell - 1 - - init_ssh = ds_init.ssh - final_ssh = ds_final.ssh - top_density = ds_final.density.isel(nVertLevels=minLevelCell) - - y3 = config.getfloat('ice_shelf_2d', 'y3') * 1e3 - modify_mask = xr.where(ds_mesh.yCell < y3, 1, 0) - mask = np.logical_and(ds_final.maxLevelCell > 0, - modify_mask == 1) - delta_ssh = mask * (final_ssh - init_ssh) - - # then, modify the SSH or land-ice pressure - if adjust_variable == 'ssh': - final_ssh = final_ssh.expand_dims(dim='Time', axis=0) - ds_out['ssh'] = final_ssh - # also update the landIceDraft variable, which will be used to - # compensate for the SSH due to land-ice pressure when - # computing sea-surface tilt - ds_out['landIceDraft'] = final_ssh - # we also need to stretch layerThickness to be compatible with - # the new SSH - stretch = ((final_ssh + ds_mesh.bottomDepth) / - (init_ssh + ds_mesh.bottomDepth)) - ds_out['layerThickness'] = ds_out.layerThickness * stretch - land_ice_pressure = ds_out.landIcePressure.values + # keep the data set with Time for output + # and generate these time slices + ds_init = ds_init.isel(Time=0) + ds_final = ds_final.isel(Time=-1) + + on_a_sphere = ds_out.attrs['on_a_sphere'].lower() == 'yes' + + if 'minLevelCell' in ds_final: + minLevelCell = ds_final.minLevelCell - 1 + else: + minLevelCell = ds_mesh.minLevelCell - 1 + + init_ssh = ds_init.ssh + final_ssh = ds_final.ssh + top_density = ds_final.density.isel(nVertLevels=minLevelCell) + + y3 = config.getfloat('ice_shelf_2d', 'y3') * 1e3 + mask = np.logical_and(ds_final.maxLevelCell > 0, + ds_mesh.yCell < y3).astype(float) + delta_ssh = mask * (final_ssh - init_ssh) + + # then, modify the SSH or land-ice pressure + if adjust_variable == 'ssh': + final_ssh = final_ssh.expand_dims(dim='Time', axis=0) + ds_out['ssh'] = final_ssh + # also update the landIceDraft variable, which will be used to + # compensate for the SSH due to land-ice pressure when + # computing sea-surface tilt + ds_out['landIceDraft'] = final_ssh + # we also need to stretch layerThickness to be compatible with + # the new SSH + stretch = ((final_ssh + ds_mesh.bottomDepth) / + (init_ssh + ds_mesh.bottomDepth)) + ds_out['layerThickness'] = ds_out.layerThickness * stretch + land_ice_pressure = ds_out.landIcePressure.values + else: + # Moving the SSH up or down by deltaSSH would change the + # land-ice pressure by density(SSH)*g*deltaSSH. If deltaSSH is + # positive (moving up), it means the land-ice pressure is too + # small and if deltaSSH is negative (moving down), it means + # land-ice pressure is too large, the sign of the second term + # makes sense. + gravity = constants['SHR_CONST_G'] + delta_land_ice_pressure = top_density * gravity * delta_ssh + + land_ice_pressure = np.maximum( + 0.0, ds_final.landIcePressure + delta_land_ice_pressure) + + ds_out['landIcePressure'] = \ + land_ice_pressure.expand_dims(dim='Time', axis=0) + + final_ssh = init_ssh + + write_netcdf(ds_out, out_filename) + + # Write the largest change in SSH and its lon/lat to a file + with open('maxDeltaSSH.log', 'w') as log_file: + + mask = land_ice_pressure > 0. + i_cell = np.abs(delta_ssh.where(mask)).argmax().values + + ds_cell = ds_final.isel(nCells=i_cell) + ds_mesh = ds_mesh.isel(nCells=i_cell) + delta_ssh_max = delta_ssh.isel(nCells=i_cell).values + + if on_a_sphere: + coords = (f'lon/lat: ' + f'{np.rad2deg(ds_cell.lonCell.values):f} ' + f'{np.rad2deg(ds_cell.latCell.values):f}') else: - # Moving the SSH up or down by deltaSSH would change the - # land-ice pressure by density(SSH)*g*deltaSSH. If deltaSSH is - # positive (moving up), it means the land-ice pressure is too - # small and if deltaSSH is negative (moving down), it means - # land-ice pressure is too large, the sign of the second term - # makes sense. - gravity = constants['SHR_CONST_G'] - delta_land_ice_pressure = top_density * gravity * delta_ssh - - land_ice_pressure = np.maximum( - 0.0, ds_final.landIcePressure + delta_land_ice_pressure) - - ds_out['landIcePressure'] = \ - land_ice_pressure.expand_dims(dim='Time', axis=0) - - final_ssh = init_ssh - - write_netcdf(ds_out, out_filename) - - # Write the largest change in SSH and its lon/lat to a file - with open('maxDeltaSSH.log', 'w') as log_file: - - mask = land_ice_pressure > 0. - i_cell = np.abs(delta_ssh.where(mask)).argmax().values - - ds_cell = ds_final.isel(nCells=i_cell) - delta_ssh_max = delta_ssh.isel(nCells=i_cell).values - - if on_a_sphere: - coords = (f'lon/lat: ' - f'{np.rad2deg(ds_cell.lonCell.values):f} ' - f'{np.rad2deg(ds_cell.latCell.values):f}') - else: - coords = (f'x/y: {1e-3 * ds_cell.xCell.values:f} ' - f'{1e-3 * ds_cell.yCell.values:f}') - string = (f'deltaSSHMax: ' - f'{delta_ssh_max:g}, {coords}') - logger.info(f' {string}') - log_file.write(f'{string}\n') - string = (f'ssh: {final_ssh.isel(nCells=i_cell).values:g}, ' - f'land_ice_pressure: ' - f'{land_ice_pressure.isel(nCells=i_cell).values:g}') - logger.info(f' {string}') - log_file.write(f'{string}\n') + coords = (f'x/y: {1e-3 * ds_mesh.xCell.values:f} ' + f'{1e-3 * ds_mesh.yCell.values:f}') + string = (f'deltaSSHMax: ' + f'{delta_ssh_max:g}, {coords}') + logger.info(f' {string}') + log_file.write(f'{string}\n') + string = (f'ssh: {final_ssh.isel(nCells=i_cell).values:g}, ' + f'land_ice_pressure: ' + f'{land_ice_pressure.isel(nCells=i_cell).values:g}') + logger.info(f' {string}') + log_file.write(f'{string}\n') logger.info(" - Complete\n") diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py index ff0ab4a07..aab05937d 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py +++ b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py @@ -21,8 +21,7 @@ class SshForward(OceanModelStep): def __init__(self, component, resolution, mesh, init, name='ssh_forward', subdir=None, iteration=1, indir=None, - ntasks=None, min_tasks=None, openmp_threads=1, - thin_film=False): + ntasks=None, min_tasks=None, openmp_threads=1): """ Create a new task @@ -64,13 +63,12 @@ def __init__(self, component, resolution, mesh, init, openmp_threads=openmp_threads) self.resolution = resolution - self.thin_film = thin_film # make sure output is double precision self.add_yaml_file('polaris.ocean.config', 'output.yaml') self.add_input_file(filename='init.nc', - work_dir_target=f'{init.path}/initial_state.nc') + work_dir_target=f'{init.path}/output.nc') self.add_input_file(filename='graph.info', work_dir_target=f'{mesh.path}/culled_graph.info') @@ -153,14 +151,3 @@ def dynamic_model_config(self, at_setup): self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', 'forward.yaml', template_replacements=replacements) - - if self.thin_film: - d1 = section.getfloat('y1_water_column_thickness') - replacements = dict( - thin_film_thickness=f'{d1}', - thin_film_ramp_hmin=f'{d1}', - thin_film_ramp_hmax=f'{d1 * 10.}', - ) - self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', - 'thin_film.yaml', - template_replacements=replacements) From 4c3c66ca364fce1b87ee3ef5a479bcabada236c6 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Fri, 1 Dec 2023 16:04:50 -0600 Subject: [PATCH 07/30] fixup ice_shelf_2d viz step --- polaris/ocean/tasks/ice_shelf_2d/viz.py | 215 +++++++++++++----------- 1 file changed, 118 insertions(+), 97 deletions(-) diff --git a/polaris/ocean/tasks/ice_shelf_2d/viz.py b/polaris/ocean/tasks/ice_shelf_2d/viz.py index 2d1e4f357..81a46e7aa 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/viz.py +++ b/polaris/ocean/tasks/ice_shelf_2d/viz.py @@ -26,16 +26,19 @@ def __init__(self, component, indir, mesh, init): super().__init__(component=component, name='viz', indir=indir) self.add_input_file( filename='mesh.nc', - target=f'{mesh.path}/culled_mesh.nc') + work_dir_target=f'{mesh.path}/culled_mesh.nc') self.add_input_file( filename='init.nc', - target=f'{init.path}/init.nc') + work_dir_target=f'{init.path}/init.nc') self.add_input_file( filename='adjusted_init.nc', target='../forward/init.nc') self.add_input_file( filename='output.nc', target='../forward/output.nc') + self.add_input_file( + filename='land_ice_fluxes.nc', + target='../forward/land_ice_fluxes.nc') def run(self): """ @@ -43,7 +46,9 @@ def run(self): """ ds_mesh = xr.load_dataset('mesh.nc') ds_init = xr.load_dataset('init.nc') + ds_adj_init = xr.load_dataset('adjusted_init.nc') ds = xr.load_dataset('output.nc') + ds_ice = xr.load_dataset('land_ice_fluxes.nc') x_mid = ds_mesh.xCell.median() y_min = ds_mesh.yCell.min() @@ -51,10 +56,6 @@ def run(self): x = xr.DataArray(data=[x_mid, x_mid], dims=('nPoints',)) y = xr.DataArray(data=[y_min, y_max], dims=('nPoints',)) - # TODO add derived variable delSsh, delLandIcePressure to ds - # for tidx=0, ds.isel(Time=tidx) - ds_init.isel(Time=0) - # for tidx>0, ds.isel(Time=tidx) - ds.isel(Time=0) - # TODO add derived variable columnThickness to ds_init and ds # Plot the time series of max velocity plt.figure(figsize=[12, 6], dpi=100) umax = np.amax(ds.velocityX[:, :, 0].values, axis=1) @@ -69,10 +70,12 @@ def run(self): plt.savefig('velocity_max_t.png', dpi=200) plt.close() - vmin_del_ssh = np.min(ds.delSsh.values) - vmax_del_ssh = np.max(ds.delSsh.values) - vmin_del_p = np.min(ds.delLandIcePressure.values) - vmax_del_p = np.max(ds.delLandIcePressure.values) + ds_horiz = self._process_ds(ds_init, ds_ice, ds_adj_init, + ds_init.bottomDepth, + time_index=0) + vmin_del_ssh = np.min(ds_horiz.delSsh.values) + vmax_del_ssh = np.max(ds_horiz.delSsh.values) + vmax_del_p = np.amax(ds_horiz.delLandIcePressure.values) vmin_temp = np.min(ds.temperature.values) vmax_temp = np.max(ds.temperature.values) vmin_salt = np.min(ds.salinity.values) @@ -80,115 +83,133 @@ def run(self): vmax_uv = max(np.amax(ds.velocityX.values), np.amax(ds.velocityY.values)) - tidx = 0 # Plot the initial time + time_index = 0 # Plot the initial time ds_transect = compute_transect( x=x, y=y, ds_horiz_mesh=ds_mesh, - layer_thickness=ds_init.layerThickness.isel(Time=tidx), - bottom_depth=ds_mesh.bottomDepth, - min_level_cell=ds_mesh.minLevelCell - 1, - max_level_cell=ds_mesh.maxLevelCell - 1, + layer_thickness=ds_init.layerThickness.isel(Time=time_index), + bottom_depth=ds_init.bottomDepth, + min_level_cell=ds_init.minLevelCell - 1, + max_level_cell=ds_init.maxLevelCell - 1, spherical=False) plot_transect(ds_transect, - mpas_field=ds_init.temperature.isel(Time=tidx), + mpas_field=ds_init.temperature.isel(Time=time_index), out_filename='temperature_section_init.png', title='temperature', - interface_color='grey', vmin=vmin_temp, vmax=vmax_temp, colorbar_label=r'$^{\circ}$C', cmap='cmo.thermal') plot_transect(ds_transect, - mpas_field=ds_init.salinity.isel(Time=tidx), + mpas_field=ds_init.salinity.isel(Time=time_index), out_filename='salinity_section_init.png', title='salinity', - interface_color='grey', vmin=vmin_salt, vmax=vmax_salt, colorbar_label=r'PSU', cmap='cmo.haline') # Plot water column thickness horizontal ds_init cell_mask = ds_init.maxLevelCell >= 1 - ds_horiz = self._process_ds(ds_init, ds_init, ds_mesh.bottomDepth, - time_index=0) - plot_horiz_field(ds_init, ds_mesh, 'columnThickness', - 'H_horiz_init.png', t_index=0, + plot_horiz_field(ds_horiz, ds_mesh, 'columnThickness', + 'H_horiz_init.png', t_index=None, + cell_mask=cell_mask) + + time_index = -1 # Plot the final state + ds_horiz = self._process_ds(ds, ds_ice, ds_init, + ds_init.bottomDepth, + time_index=time_index) + vmin_del_ssh = np.min(ds_horiz.delSsh.values) + vmax_del_ssh = np.max(ds_horiz.delSsh.values) + vmax_del_p = np.amax(ds_horiz.delLandIcePressure.values) + # Plot water column thickness horizontal + plot_horiz_field(ds_horiz, ds_mesh, 'columnThickness', + f'H_horiz_t{time_index}.png', t_index=None, cell_mask=cell_mask) - # Plot land ice pressure horizontal ds_init - for tidx in [0, -1]: - ds_horiz = self._process_ds(ds, ds_init, ds_mesh.bottomDepth, - time_index=tidx) - day = ds.daysSinceStartOfSim.isel(Time=tidx) - # Plot water column thickness horizontal - plot_horiz_field(ds_horiz, ds_mesh, 'columnThickness', - f'H_horiz_day{int(day)}.png', t_index=tidx, - cell_mask=cell_mask) + plot_horiz_field(ds_horiz, ds_mesh, 'landIceFreshwaterFlux', + f'melt_horiz_t{time_index}.png', t_index=None, + cell_mask=cell_mask) + if 'wettingVelocityFactor' in ds_horiz.keys(): plot_horiz_field(ds_horiz, ds_mesh, 'wettingVelocityFactor', - f'wet_horiz_day{int(day)}.png', t_index=tidx, - z_index=0, cell_mask=cell_mask, vmin=0, vmax=1, - cmap='cmo.ice') - # Plot difference in ssh - plot_horiz_field(ds_horiz, ds_mesh, 'delSsh', - f'del_ssh_horiz_day{int(day)}.png', t_index=tidx, - cell_mask=cell_mask, - vmin=vmin_del_ssh, vmax=vmax_del_ssh) - - # Plot difference in land ice pressure - plot_horiz_field(ds_horiz, ds_mesh, 'delLandIcePressure', - f'del_land_ice_pressure_horiz_day{int(day)}.png', - t_index=tidx, cell_mask=cell_mask, - vmin=vmin_del_p, vmax=vmax_del_p) - - # Plot transects - ds_transect = compute_transect( - x=x, y=y, ds_horiz_mesh=ds_mesh, - layer_thickness=ds.layerThickness.isel(Time=tidx), - bottom_depth=ds_mesh.bottomDepth, - min_level_cell=ds_mesh.minLevelCell - 1, - max_level_cell=ds_mesh.maxLevelCell - 1, - spherical=False) - - plot_transect(ds_transect, - mpas_field=ds.velocityX.isel(Time=tidx), - out_filename=f'u_section_day{int(day)}.png', - title='x-velocity', - interface_color='grey', - vmin=-vmax_uv, vmax=vmax_uv, - colorbar_label=r'm/s', cmap='cmo.balance') - - plot_transect(ds_transect, - mpas_field=ds.velocityY.isel(Time=tidx), - out_filename=f'v_section_day{int(day)}.png', - title='x-velocity', - interface_color='grey', - vmin=-vmax_uv, vmax=vmax_uv, - colorbar_label=r'm/s', cmap='cmo.balance') - - plot_transect( - ds_transect, - mpas_field=ds.temperature.isel(Time=tidx), - out_filename=f'temperature_section_day{int(day)}.png', - title='temperature', - interface_color='grey', - vmin=vmin_temp, vmax=vmax_temp, - colorbar_label=r'$^{\circ}$C', cmap='cmo.thermal') - - plot_transect(ds_transect, - mpas_field=ds.salinity.isel(Time=tidx), - out_filename=f'salinity_section_day{int(day)}.png', - title='salinity', - interface_color='grey', - vmin=vmin_salt, vmax=vmax_salt, - colorbar_label=r'PSU', cmap='cmo.haline') + f'wet_horiz_t{time_index}.png', t_index=None, + z_index=None, cell_mask=cell_mask, + vmin=0, vmax=1, cmap='cmo.ice') + # Plot difference in ssh + plot_horiz_field(ds_horiz, ds_mesh, 'delSsh', + f'del_ssh_horiz_t{time_index}.png', t_index=None, + cell_mask=cell_mask, + vmin=vmin_del_ssh, vmax=vmax_del_ssh) + + # Plot difference in land ice pressure + plot_horiz_field(ds_horiz, ds_mesh, 'delLandIcePressure', + f'del_land_ice_pressure_horiz_t{time_index}.png', + t_index=None, cell_mask=cell_mask, + vmin=-vmax_del_p, vmax=vmax_del_p, + cmap='cmo.balance') + + # Plot transects + ds_transect = compute_transect( + x=x, y=y, ds_horiz_mesh=ds_mesh, + layer_thickness=ds.layerThickness.isel(Time=time_index), + bottom_depth=ds_init.bottomDepth, + min_level_cell=ds_init.minLevelCell - 1, + max_level_cell=ds_init.maxLevelCell - 1, + spherical=False) + + plot_horiz_field(ds, ds_mesh, 'velocityX', + f'u_surf_horiz_t{time_index}.png', t_index=time_index, + z_index=0, cell_mask=cell_mask, + vmin=-vmax_uv, vmax=vmax_uv, + cmap_title=r'm/s', cmap='cmo.balance') + plot_horiz_field(ds, ds_mesh, 'velocityX', + f'u_bot_horiz_t{time_index}.png', t_index=time_index, + z_index=-1, cell_mask=cell_mask, + vmin=-vmax_uv, vmax=vmax_uv, + cmap_title=r'm/s', cmap='cmo.balance') + plot_horiz_field(ds, ds_mesh, 'velocityY', + f'v_surf_horiz_t{time_index}.png', t_index=time_index, + z_index=0, cell_mask=cell_mask, + vmin=-vmax_uv, vmax=vmax_uv, + cmap_title=r'm/s', cmap='cmo.balance') + plot_horiz_field(ds, ds_mesh, 'velocityY', + f'v_bot_horiz_t{time_index}.png', t_index=time_index, + z_index=-1, cell_mask=cell_mask, + vmin=-vmax_uv, vmax=vmax_uv, + cmap_title=r'm/s', cmap='cmo.balance') + plot_transect(ds_transect, + mpas_field=ds.velocityX.isel(Time=time_index), + out_filename=f'u_section_t{time_index}.png', + title='x-velocity', + vmin=-vmax_uv, vmax=vmax_uv, + colorbar_label=r'm/s', cmap='cmo.balance') + + plot_transect(ds_transect, + mpas_field=ds.velocityY.isel(Time=time_index), + out_filename=f'v_section_t{time_index}.png', + title='y-velocity', + vmin=-vmax_uv, vmax=vmax_uv, + colorbar_label=r'm/s', cmap='cmo.balance') + + plot_transect( + ds_transect, + mpas_field=ds.temperature.isel(Time=time_index), + out_filename=f'temperature_section_t{time_index}.png', + title='temperature', + vmin=vmin_temp, vmax=vmax_temp, + colorbar_label=r'$^{\circ}$C', cmap='cmo.thermal') + + plot_transect(ds_transect, + mpas_field=ds.salinity.isel(Time=time_index), + out_filename=f'salinity_section_t{time_index}.png', + title='salinity', + vmin=vmin_salt, vmax=vmax_salt, + colorbar_label=r'PSU', cmap='cmo.haline') @staticmethod - def _process_ds(ds, ds_init, bottom_depth, time_index): + def _process_ds(ds, ds_ice, ds_init, bottom_depth, time_index): ds_out = ds.isel(Time=time_index, nVertLevels=0) ds_out['columnThickness'] = ds_out.ssh + bottom_depth - if time_index == 0: - ds_out['delSsh'] = ds_out.ssh - ds_init.ssh.isel(Time=0) - ds_out['delLandIcePressure'] = ds_out.landIcePressure - \ - ds_init.landIcePressure.isel(Time=0) - else: - ds_out['delSsh'] = ds_out.ssh - ds.ssh.isel(Time=0) - ds_out['delLandIcePressure'] = ds_out.landIcePressure - \ - ds.landIcePressure.isel(Time=0) + ds_out['landIceFreshwaterFlux'] = \ + ds_ice.landIceFreshwaterFlux.isel(Time=time_index) + ds_out['delSsh'] = ds_out.ssh - ds_init.ssh.isel(Time=0) + ds_out['delLandIcePressure'] = \ + ds_ice.landIcePressure.isel(Time=time_index) - \ + ds_init.landIcePressure.isel(Time=0) return ds_out From 05980289ac6938195ba33da77b393af09a20107d Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Wed, 10 Jan 2024 12:42:31 -0600 Subject: [PATCH 08/30] Add a few more config options to single layer yaml --- polaris/ocean/config/single_layer.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/polaris/ocean/config/single_layer.yaml b/polaris/ocean/config/single_layer.yaml index 4a0594a18..ec77549ff 100644 --- a/polaris/ocean/config/single_layer.yaml +++ b/polaris/ocean/config/single_layer.yaml @@ -5,8 +5,10 @@ omega: config_vert_coord_movement: impermeable_interfaces config_ALE_thickness_proportionality: weights_only debug: + config_compute_active_tracer_budgets: false config_disable_thick_vadv: true config_disable_vel_vadv: true + config_disable_vel_vmix: true config_disable_tr_all_tend: true pressure_gradient: config_pressure_gradient_type: ssh_gradient From 6a27297446aaaa76d9d89b58d3280835beaccc16 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 7 Dec 2023 14:57:22 -0600 Subject: [PATCH 09/30] Use cfg options for namelist options --- polaris/ocean/tasks/ice_shelf_2d/forward.py | 1 + .../ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.py b/polaris/ocean/tasks/ice_shelf_2d/forward.py index 62ccfe2b9..20b7265ad 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.py +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.py @@ -117,6 +117,7 @@ def dynamic_model_config(self, at_setup): btr_dt_str = get_time_interval_string( seconds=btr_dt_per_km * self.resolution) + section = config['ice_shelf_2d_default'] s_per_hour = 3600. run_duration = section.getfloat('forward_run_duration') run_duration_str = get_time_interval_string( diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg index 07bdb294b..049738b4a 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg +++ b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg @@ -40,18 +40,12 @@ y0_land_ice_height_above_floatation = 0. # Output interval for the ssh adjustment phase in hours ssh_adjust_output_interval = 1.0 -# Output interval for the forward step in hours -forward_output_interval = 0.2 - # Run duration of each ssh adjustment phase in hours ssh_adjust_run_duration = 1.0 # Whether to adjust land ice pressure or SSH ssh_adjust_variable = land_ice_pressure -# Run duration of the forward step in hours -forward_run_duration = 0.2 - # Time integration scheme time_integrator = split_explicit @@ -85,14 +79,14 @@ y2 = 90.0 # ice shelf front location in y in km y3 = 90.0 -[ice_shelf_2d_default] - # Vertical thickness of ocean sub-ice cavity at GL y1_water_column_thickness = 10.0 # Vertical thickness of water column thickness at y2 y2_water_column_thickness = 1050.0 +[ice_shelf_2d_default] + # Output interval for the forward step in hours forward_output_interval = 0.2 @@ -108,3 +102,9 @@ dates = 0001-01-01_00:00:00, 0002-01-01_00:00:00, 0003-01-01_00:00:00 # the amount by which the initial landIcePressure and landIceDraft are scaled # at each date scales = 1.0, 2.0, 2.0 + +# Output interval for the forward step in hours +forward_output_interval = 24. + +# Run duration of the forward step in hours +forward_run_duration = 1440. From 6339c8e9823c49e04b46d6746346c6ff9947f40c Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 7 Dec 2023 17:02:22 -0600 Subject: [PATCH 10/30] Move ssh adjustment to new class --- polaris/ocean/tasks/ice_shelf_2d/__init__.py | 45 +------------ .../tasks/ice_shelf_2d/default/__init__.py | 22 +++---- polaris/ocean/tasks/ice_shelf_2d/ice_shelf.py | 63 +++++++++++++++++++ .../tasks/ice_shelf_2d/ssh_adjustment.py | 3 +- .../ocean/tasks/ice_shelf_2d/ssh_forward.py | 4 +- 5 files changed, 80 insertions(+), 57 deletions(-) create mode 100644 polaris/ocean/tasks/ice_shelf_2d/ice_shelf.py diff --git a/polaris/ocean/tasks/ice_shelf_2d/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/__init__.py index 86e6911bc..5da9d81af 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/__init__.py @@ -1,12 +1,7 @@ -from typing import Dict - -from polaris import Step from polaris.config import PolarisConfigParser from polaris.ocean.resolution import resolution_to_subdir from polaris.ocean.tasks.ice_shelf_2d.default import Default from polaris.ocean.tasks.ice_shelf_2d.init import Init -from polaris.ocean.tasks.ice_shelf_2d.ssh_adjustment import SshAdjustment -from polaris.ocean.tasks.ice_shelf_2d.ssh_forward import SshForward def add_ice_shelf_2d_tasks(component): @@ -25,48 +20,12 @@ def add_ice_shelf_2d_tasks(component): config_filename = 'ice_shelf_2d.cfg' config = PolarisConfigParser(filepath=f'{resdir}/{config_filename}') config.add_from_package('polaris.ocean.tasks.ice_shelf_2d', - 'ice_shelf_2d.cfg') - - shared_steps: Dict[str, Step] = dict() + config_filename) init = Init(component=component, resolution=resolution, indir=resdir) init.set_shared_config(config, link=config_filename) - shared_steps['init'] = init - - num_iterations = 10 - - iteration = 0 - name = f'ssh_forward_{iteration}' - ssh_forward = SshForward( - component=component, resolution=resolution, indir=resdir, - mesh=init, init=shared_steps['init'], name=name) - ssh_forward.set_shared_config(config, link=config_filename) - shared_steps[name] = ssh_forward - - for iteration in range(1, num_iterations): - name = f'ssh_adjust_{iteration - 1}' - ssh_adjust = SshAdjustment( - component=component, resolution=resolution, indir=resdir, - name=name, init=shared_steps['init'], forward=ssh_forward) - ssh_adjust.set_shared_config(config, link=config_filename) - shared_steps[name] = ssh_adjust - name = f'ssh_forward_{iteration}' - ssh_forward = SshForward( - component=component, resolution=resolution, indir=resdir, - mesh=init, init=ssh_adjust, name=name) - ssh_forward.set_shared_config(config, link=config_filename) - shared_steps[name] = ssh_forward - - iteration = num_iterations - name = f'ssh_adjust_{iteration - 1}' - ssh_adjust = SshAdjustment( - component=component, resolution=resolution, indir=resdir, - name=name, init=shared_steps['init'], forward=ssh_forward) - ssh_adjust.set_shared_config(config, link=config_filename) - shared_steps[name] = ssh_adjust default = Default(component=component, resolution=resolution, - indir=resdir, mesh=init, init=ssh_adjust, - shared_steps=shared_steps) + indir=resdir, init=init, config=config) default.set_shared_config(config, link=config_filename) component.add_task(default) diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index e283bd5ec..dbb825507 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -1,15 +1,15 @@ -from polaris import Task from polaris.ocean.tasks.ice_shelf_2d.forward import Forward +from polaris.ocean.tasks.ice_shelf_2d.ice_shelf import IceShelfTask from polaris.ocean.tasks.ice_shelf_2d.viz import Viz -class Default(Task): +class Default(IceShelfTask): """ The default ice shelf 2d test case simply creates the mesh and initial condition, then performs a short forward run. """ - def __init__(self, component, resolution, indir, shared_steps, mesh, init): + def __init__(self, component, resolution, indir, init, config): """ Create the test case @@ -23,19 +23,19 @@ def __init__(self, component, resolution, indir, shared_steps, mesh, init): indir : str The directory the task is in, to which ``name`` will be appended - - shared_steps : dict of dict of polaris.Steps - The shared steps to include as symlinks """ - super().__init__(component=component, name='default', indir=indir) + super().__init__(component=component, resolution=resolution, + name='default', indir=indir) - for name, shared_step in shared_steps.items(): - self.add_step(shared_step, symlink=name) + self.add_step(init, symlink='init') + last_adjust_step = self._setup_ssh_adjustment_steps( + init=init, config=config, config_filename='ice_shelf_2d.cfg') self.add_step( Forward(component=component, indir=self.subdir, ntasks=None, min_tasks=None, openmp_threads=1, resolution=resolution, - mesh=mesh, init=init)) + mesh=init, init=last_adjust_step)) self.add_step( - Viz(component=component, indir=self.subdir, mesh=mesh, init=init)) + Viz(component=component, indir=self.subdir, mesh=init, + init=last_adjust_step)) diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf.py b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf.py new file mode 100644 index 000000000..232f89560 --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf.py @@ -0,0 +1,63 @@ +from typing import Dict + +from polaris import Step, Task +from polaris.ocean.tasks.ice_shelf_2d.ssh_adjustment import SshAdjustment +from polaris.ocean.tasks.ice_shelf_2d.ssh_forward import SshForward + + +class IceShelfTask(Task): + + """ + shared_steps : dict of dict of polaris.Steps + The shared steps to include as symlinks + """ + def __init__(self, component, resolution, indir, name): + super().__init__(component=component, name=name, indir=indir) + self.indir = indir + self.component = component + self.resolution = resolution + + def _setup_ssh_adjustment_steps(self, init, config, config_filename): + + resolution = self.resolution + component = self.component + indir = self.indir + + # TODO config + num_iterations = 10 + shared_steps: Dict[str, Step] = dict() + + iteration = 0 + name = f'ssh_forward_{iteration}' + ssh_forward = SshForward( + component=component, resolution=resolution, indir=indir, + mesh=init, init=init, name=name) + ssh_forward.set_shared_config(config, link=config_filename) + shared_steps[name] = ssh_forward + + for iteration in range(1, num_iterations): + name = f'ssh_adjust_{iteration - 1}' + ssh_adjust = SshAdjustment( + component=component, resolution=resolution, indir=indir, + name=name, init=init, forward=ssh_forward) + ssh_adjust.set_shared_config(config, link=config_filename) + shared_steps[name] = ssh_adjust + name = f'ssh_forward_{iteration}' + ssh_forward = SshForward( + component=component, resolution=resolution, indir=indir, + mesh=init, init=ssh_adjust, name=name) + ssh_forward.set_shared_config(config, link=config_filename) + shared_steps[name] = ssh_forward + + iteration = num_iterations + name = f'ssh_adjust_{iteration - 1}' + ssh_adjust = SshAdjustment( + component=component, resolution=resolution, indir=indir, + name=name, init=init, forward=ssh_forward) + ssh_adjust.set_shared_config(config, link=config_filename) + shared_steps[name] = ssh_adjust + self.init_step = ssh_adjust + + for name, shared_step in shared_steps.items(): + self.add_step(shared_step, symlink=name) + return ssh_adjust diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py b/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py index b0e6f9cb5..b01acba44 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py +++ b/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py @@ -29,7 +29,8 @@ def __init__(self, component, resolution, init, forward, indir=None, """ self.resolution = resolution - super().__init__(component=component, name=name, indir=indir) + super().__init__(component=component, name=name, + indir=f'{indir}/ssh_adjustment') self.add_input_file(filename='final.nc', work_dir_target=f'{forward.path}/output.nc') diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py index aab05937d..d98ffd0cd 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py +++ b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py @@ -59,8 +59,8 @@ def __init__(self, component, resolution, mesh, init, the number of OpenMP threads the step will use """ super().__init__(component=component, name=name, subdir=subdir, - indir=indir, ntasks=ntasks, min_tasks=min_tasks, - openmp_threads=openmp_threads) + indir=f'{indir}/ssh_adjustment', ntasks=ntasks, + min_tasks=min_tasks, openmp_threads=openmp_threads) self.resolution = resolution From 1ee0987b4614eb0bd0d5c34c9fbc81a5231a62bd Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Fri, 8 Dec 2023 09:53:18 -0600 Subject: [PATCH 11/30] Move files to ice shelf module --- .../ice_shelf.py => ice_shelf/__init__.py} | 22 +++++---- .../ssh_adjustment.py | 0 .../ice_shelf_2d => ice_shelf}/ssh_forward.py | 23 +++++---- polaris/ocean/ice_shelf/ssh_forward.yaml | 48 +++++++++++++++++++ .../tasks/ice_shelf_2d/default/__init__.py | 6 ++- .../ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg | 36 ++++++++++---- polaris/ocean/tasks/ice_shelf_2d/init.py | 2 +- .../ocean/tasks/ice_shelf_2d/ssh_forward.yaml | 13 +++++ 8 files changed, 121 insertions(+), 29 deletions(-) rename polaris/ocean/{tasks/ice_shelf_2d/ice_shelf.py => ice_shelf/__init__.py} (72%) rename polaris/ocean/{tasks/ice_shelf_2d => ice_shelf}/ssh_adjustment.py (100%) rename polaris/ocean/{tasks/ice_shelf_2d => ice_shelf}/ssh_forward.py (87%) create mode 100644 polaris/ocean/ice_shelf/ssh_forward.yaml create mode 100644 polaris/ocean/tasks/ice_shelf_2d/ssh_forward.yaml diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf.py b/polaris/ocean/ice_shelf/__init__.py similarity index 72% rename from polaris/ocean/tasks/ice_shelf_2d/ice_shelf.py rename to polaris/ocean/ice_shelf/__init__.py index 232f89560..1bd5fa7ac 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf.py +++ b/polaris/ocean/ice_shelf/__init__.py @@ -1,8 +1,8 @@ from typing import Dict from polaris import Step, Task -from polaris.ocean.tasks.ice_shelf_2d.ssh_adjustment import SshAdjustment -from polaris.ocean.tasks.ice_shelf_2d.ssh_forward import SshForward +from polaris.ocean.ice_shelf.ssh_adjustment import SshAdjustment +from polaris.ocean.ice_shelf.ssh_forward import SshForward class IceShelfTask(Task): @@ -17,21 +17,24 @@ def __init__(self, component, resolution, indir, name): self.component = component self.resolution = resolution - def _setup_ssh_adjustment_steps(self, init, config, config_filename): + def _setup_ssh_adjustment_steps(self, init, config, config_filename, + package=None, + yaml_filename='ssh_forward.yaml', + yaml_replacements=None): resolution = self.resolution component = self.component indir = self.indir - # TODO config - num_iterations = 10 + num_iterations = config.getint('ssh_adjustment', 'iterations') shared_steps: Dict[str, Step] = dict() iteration = 0 name = f'ssh_forward_{iteration}' ssh_forward = SshForward( component=component, resolution=resolution, indir=indir, - mesh=init, init=init, name=name) + mesh=init, init=init, name=name, package=package, + yaml_filename=yaml_filename, yaml_replacements=yaml_replacements) ssh_forward.set_shared_config(config, link=config_filename) shared_steps[name] = ssh_forward @@ -45,7 +48,9 @@ def _setup_ssh_adjustment_steps(self, init, config, config_filename): name = f'ssh_forward_{iteration}' ssh_forward = SshForward( component=component, resolution=resolution, indir=indir, - mesh=init, init=ssh_adjust, name=name) + mesh=init, init=ssh_adjust, name=name, package=package, + yaml_filename=yaml_filename, + yaml_replacements=yaml_replacements) ssh_forward.set_shared_config(config, link=config_filename) shared_steps[name] = ssh_forward @@ -56,8 +61,7 @@ def _setup_ssh_adjustment_steps(self, init, config, config_filename): name=name, init=init, forward=ssh_forward) ssh_adjust.set_shared_config(config, link=config_filename) shared_steps[name] = ssh_adjust - self.init_step = ssh_adjust for name, shared_step in shared_steps.items(): - self.add_step(shared_step, symlink=name) + self.add_step(shared_step, symlink=f'ssh_adjustment/{name}') return ssh_adjust diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py b/polaris/ocean/ice_shelf/ssh_adjustment.py similarity index 100% rename from polaris/ocean/tasks/ice_shelf_2d/ssh_adjustment.py rename to polaris/ocean/ice_shelf/ssh_adjustment.py diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py b/polaris/ocean/ice_shelf/ssh_forward.py similarity index 87% rename from polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py rename to polaris/ocean/ice_shelf/ssh_forward.py index d98ffd0cd..e02c6036e 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py +++ b/polaris/ocean/ice_shelf/ssh_forward.py @@ -20,7 +20,8 @@ class SshForward(OceanModelStep): """ def __init__(self, component, resolution, mesh, init, name='ssh_forward', subdir=None, - iteration=1, indir=None, + package=None, yaml_filename='ssh_forward.yaml', + yaml_replacements=None, iteration=1, indir=None, ntasks=None, min_tasks=None, openmp_threads=1): """ Create a new task @@ -63,6 +64,9 @@ def __init__(self, component, resolution, mesh, init, min_tasks=min_tasks, openmp_threads=openmp_threads) self.resolution = resolution + self.package = package + self.yaml_filename = yaml_filename + self.yaml_replacements = yaml_replacements # make sure output is double precision self.add_yaml_file('polaris.ocean.config', 'output.yaml') @@ -114,7 +118,7 @@ def dynamic_model_config(self, at_setup): if not at_setup and vert_levels == 1: self.add_yaml_file('polaris.ocean.config', 'single_layer.yaml') - section = config['ice_shelf_2d'] + section = config['ssh_adjustment'] # dt is proportional to resolution: default 30 seconds per km time_integrator = section.get('time_integrator') @@ -131,23 +135,26 @@ def dynamic_model_config(self, at_setup): seconds=btr_dt_per_km * self.resolution) s_per_hour = 3600. - run_duration = section.getfloat('ssh_adjust_run_duration') + run_duration = section.getfloat('run_duration') run_duration_str = get_time_interval_string( seconds=run_duration * s_per_hour) - output_interval = section.getfloat('ssh_adjust_output_interval') + output_interval = section.getfloat('output_interval') output_interval_str = get_time_interval_string( seconds=output_interval * s_per_hour) replacements = dict( - time_integrator=time_integrator, dt=dt_str, btr_dt=btr_dt_str, + time_integrator=time_integrator, run_duration=run_duration_str, output_interval=output_interval_str, - land_ice_flux_mode='pressure_only', ) - self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', - 'forward.yaml', + self.add_yaml_file('polaris.ocean.ice_shelf', + 'ssh_forward.yaml', template_replacements=replacements) + if self.package is not None: + self.add_yaml_file(package=self.package, + yaml=self.yaml_filename, + template_replacements=self.yaml_replacements) diff --git a/polaris/ocean/ice_shelf/ssh_forward.yaml b/polaris/ocean/ice_shelf/ssh_forward.yaml new file mode 100644 index 000000000..d23eac233 --- /dev/null +++ b/polaris/ocean/ice_shelf/ssh_forward.yaml @@ -0,0 +1,48 @@ +omega: + io: + config_write_output_on_startup: true + time_management: + config_do_restart: false + config_run_duration: {{ run_duration }} + time_integration: + config_dt: {{ dt }} + split_explicit_ts: + config_btr_dt: {{ btr_dt }} + land_ice_fluxes: + config_land_ice_flux_mode: pressure_only + debug: + config_check_ssh_consistency: false + streams: + mesh: + filename_template: init.nc + input: + filename_template: init.nc + output: + type: output + filename_template: output.nc + output_interval: {{ output_interval }} + clobber_mode: truncate + contents: + - mesh + - tracers + - xtime + - daysSinceStartOfSim + - normalVelocity + - layerThickness + - zMid + - minLevelCell + - maxLevelCell + - ssh + - density + - velocityX + - velocityY + - atmosphericPressure + - landIceInterfaceTracers + - landIceBoundaryLayerTracers + - landIcePressure + - landIceDraft + - landIceFraction + - landIceMask + - landIceFrictionVelocity + - landIceFreshwaterFlux + - landIceHeatFlux diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index dbb825507..ee86c0f32 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -1,5 +1,5 @@ +from polaris.ocean.ice_shelf import IceShelfTask from polaris.ocean.tasks.ice_shelf_2d.forward import Forward -from polaris.ocean.tasks.ice_shelf_2d.ice_shelf import IceShelfTask from polaris.ocean.tasks.ice_shelf_2d.viz import Viz @@ -28,8 +28,10 @@ def __init__(self, component, resolution, indir, init, config): name='default', indir=indir) self.add_step(init, symlink='init') + last_adjust_step = self._setup_ssh_adjustment_steps( - init=init, config=config, config_filename='ice_shelf_2d.cfg') + init=init, config=config, config_filename='ice_shelf_2d.cfg', + package='polaris.ocean.tasks.ice_shelf_2d') self.add_step( Forward(component=component, indir=self.subdir, ntasks=None, diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg index 049738b4a..43d070e5d 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg +++ b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg @@ -25,6 +25,33 @@ min_pc_fraction = 0.1 # The minimum layer thickness in m min_layer_thickness = 0.0 +# Options related to ssh adjustment steps +[ssh_adjustment] + +# Number of ssh adjustment iterations +iterations = 10 + +# Output interval for the ssh adjustment phase in hours +output_interval = 1.0 + +# Run duration of each ssh adjustment phase in hours +run_duration = 1.0 + +# Whether to adjust land ice pressure or SSH +adjust_variable = land_ice_pressure + +# Time integration scheme +time_integrator = split_explicit + +# Time step in seconds as a function of resolution +rk4_dt_per_km = 5 + +# Time step in seconds as a function of resolution +split_dt_per_km = 5 + +# Time step in seconds as a function of resolution +btr_dt_per_km = 0.5 + # config options for 2D ice-shelf testcases [ice_shelf_2d] @@ -37,15 +64,6 @@ ly = 220 # How the land ice pressure at y Date: Fri, 8 Dec 2023 13:40:57 -0600 Subject: [PATCH 12/30] Fixup ssh_adjustment steps --- polaris/ocean/ice_shelf/__init__.py | 81 ++++++++++--------- polaris/ocean/ice_shelf/ssh_adjustment.py | 8 +- .../tasks/ice_shelf_2d/default/__init__.py | 2 +- 3 files changed, 48 insertions(+), 43 deletions(-) diff --git a/polaris/ocean/ice_shelf/__init__.py b/polaris/ocean/ice_shelf/__init__.py index 1bd5fa7ac..3c201d2c6 100644 --- a/polaris/ocean/ice_shelf/__init__.py +++ b/polaris/ocean/ice_shelf/__init__.py @@ -17,51 +17,56 @@ def __init__(self, component, resolution, indir, name): self.component = component self.resolution = resolution - def _setup_ssh_adjustment_steps(self, init, config, config_filename, - package=None, - yaml_filename='ssh_forward.yaml', - yaml_replacements=None): + def setup_ssh_adjustment_steps(self, init, config, config_filename, + package=None, + yaml_filename='ssh_forward.yaml', + yaml_replacements=None): resolution = self.resolution component = self.component indir = self.indir num_iterations = config.getint('ssh_adjustment', 'iterations') - shared_steps: Dict[str, Step] = dict() - iteration = 0 - name = f'ssh_forward_{iteration}' - ssh_forward = SshForward( - component=component, resolution=resolution, indir=indir, - mesh=init, init=init, name=name, package=package, - yaml_filename=yaml_filename, yaml_replacements=yaml_replacements) - ssh_forward.set_shared_config(config, link=config_filename) - shared_steps[name] = ssh_forward - - for iteration in range(1, num_iterations): - name = f'ssh_adjust_{iteration - 1}' - ssh_adjust = SshAdjustment( - component=component, resolution=resolution, indir=indir, - name=name, init=init, forward=ssh_forward) - ssh_adjust.set_shared_config(config, link=config_filename) - shared_steps[name] = ssh_adjust + # for the first iteration, ssh_forward step is run from the initial + # state + current_init = init + for iteration in range(num_iterations): name = f'ssh_forward_{iteration}' - ssh_forward = SshForward( - component=component, resolution=resolution, indir=indir, - mesh=init, init=ssh_adjust, name=name, package=package, - yaml_filename=yaml_filename, - yaml_replacements=yaml_replacements) - ssh_forward.set_shared_config(config, link=config_filename) - shared_steps[name] = ssh_forward + subdir = f'{indir}/ssh_adjustment/{name}' + if subdir in component.steps: + shared_step = component.steps[subdir] + else: + ssh_forward = SshForward( + component=component, resolution=resolution, indir=indir, + mesh=init, init=current_init, name=name, package=package, + yaml_filename=yaml_filename, + yaml_replacements=yaml_replacements) + ssh_forward.set_shared_config(config, link=config_filename) + shared_step = ssh_forward + if indir == self.subdir: + symlink = None + else: + symlink = f'ssh_adjustment/{name}' + self.add_step(shared_step, symlink=symlink) - iteration = num_iterations - name = f'ssh_adjust_{iteration - 1}' - ssh_adjust = SshAdjustment( - component=component, resolution=resolution, indir=indir, - name=name, init=init, forward=ssh_forward) - ssh_adjust.set_shared_config(config, link=config_filename) - shared_steps[name] = ssh_adjust + name = f'ssh_adjust_{iteration}' + subdir = f'{indir}/ssh_adjustment/{name}' + if subdir in component.steps: + shared_step = component.steps[subdir] + else: + ssh_adjust = SshAdjustment( + component=component, resolution=resolution, indir=indir, + name=name, init=init, forward=ssh_forward) + ssh_adjust.set_shared_config(config, link=config_filename) + shared_step = ssh_adjust + if indir == self.subdir: + symlink = None + else: + symlink = f'ssh_adjustment/{name}' + self.add_step(shared_step, symlink=symlink) + # for the next iteration, ssh_forward is run from the adjusted + # initial state (the output of ssh_adustment) + current_init = shared_step - for name, shared_step in shared_steps.items(): - self.add_step(shared_step, symlink=f'ssh_adjustment/{name}') - return ssh_adjust + return shared_step diff --git a/polaris/ocean/ice_shelf/ssh_adjustment.py b/polaris/ocean/ice_shelf/ssh_adjustment.py index b01acba44..69394dcd8 100644 --- a/polaris/ocean/ice_shelf/ssh_adjustment.py +++ b/polaris/ocean/ice_shelf/ssh_adjustment.py @@ -51,10 +51,10 @@ def run(self): logger = self.logger config = self.config adjust_variable = 'landIcePressure' - final_filename = self.inputs[0] - init_filename = self.inputs[1] - mesh_filename = self.inputs[2] - out_filename = self.outputs[0] + mesh_filename = 'mesh.nc' + init_filename = 'init.nc' + final_filename = 'final.nc' + out_filename = 'output.nc' ds_mesh = xr.open_dataset(mesh_filename) ds_init = xr.open_dataset(init_filename) ds_final = xr.open_dataset(final_filename) diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index ee86c0f32..e0a7fce4b 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -29,7 +29,7 @@ def __init__(self, component, resolution, indir, init, config): self.add_step(init, symlink='init') - last_adjust_step = self._setup_ssh_adjustment_steps( + last_adjust_step = self.setup_ssh_adjustment_steps( init=init, config=config, config_filename='ice_shelf_2d.cfg', package='polaris.ocean.tasks.ice_shelf_2d') From 618eb9105eef2aac3e8e73bdfc41904d4551563a Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Fri, 8 Dec 2023 13:44:28 -0600 Subject: [PATCH 13/30] Add restart test --- polaris/ocean/tasks/ice_shelf_2d/__init__.py | 12 ++++ .../tasks/ice_shelf_2d/default/__init__.py | 59 ++++++++++++++++--- polaris/ocean/tasks/ice_shelf_2d/forward.py | 52 ++++++++++++---- polaris/ocean/tasks/ice_shelf_2d/forward.yaml | 37 +++++++++++- .../tasks/ice_shelf_2d/global_stats.yaml | 2 +- .../ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg | 19 +++--- 6 files changed, 147 insertions(+), 34 deletions(-) diff --git a/polaris/ocean/tasks/ice_shelf_2d/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/__init__.py index 5da9d81af..af8227815 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/__init__.py @@ -29,3 +29,15 @@ def add_ice_shelf_2d_tasks(component): indir=resdir, init=init, config=config) default.set_shared_config(config, link=config_filename) component.add_task(default) + + default = Default(component=component, resolution=resolution, + indir=resdir, init=init, config=config, + include_viz=True) + default.set_shared_config(config, link=config_filename) + component.add_task(default) + + default = Default(component=component, resolution=resolution, + indir=resdir, init=init, config=config, + include_restart=True) + default.set_shared_config(config, link=config_filename) + component.add_task(default) diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index e0a7fce4b..5d124e17c 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -7,9 +7,15 @@ class Default(IceShelfTask): """ The default ice shelf 2d test case simply creates the mesh and initial condition, then performs a short forward run. + + Attributes + ---------- + include_viz : bool + Include Viz step """ - def __init__(self, component, resolution, indir, init, config): + def __init__(self, component, resolution, indir, init, config, + include_viz=False, include_restart=False): """ Create the test case @@ -23,9 +29,20 @@ def __init__(self, component, resolution, indir, init, config): indir : str The directory the task is in, to which ``name`` will be appended + + include_viz : bool + Include VizMap and Viz steps for each resolution """ + name = 'default' + subdir = f'{indir}/default' + if include_viz: + name = f'{name}_with_viz' + subdir = f'{subdir}/with_viz' + if include_restart: + name = f'{name}_with_restart' + subdir = f'{subdir}/with_restart' super().__init__(component=component, resolution=resolution, - name='default', indir=indir) + name=name, indir=indir) self.add_step(init, symlink='init') @@ -33,11 +50,35 @@ def __init__(self, component, resolution, indir, init, config): init=init, config=config, config_filename='ice_shelf_2d.cfg', package='polaris.ocean.tasks.ice_shelf_2d') - self.add_step( - Forward(component=component, indir=self.subdir, ntasks=None, - min_tasks=None, openmp_threads=1, resolution=resolution, - mesh=init, init=last_adjust_step)) + forward_path = f'{indir}/default/forward' + if forward_path in component.steps: + forward_step = component.steps[forward_path] + symlink = 'forward' + else: + forward_step = Forward(component=component, + indir=f'{indir}/default', + ntasks=None, min_tasks=None, + openmp_threads=1, resolution=resolution, + mesh=init, init=last_adjust_step) + symlink = None + self.add_step(forward_step, symlink=symlink) + + if include_restart: + restart_path = f'{indir}/default/restart' + if restart_path in component.steps: + restart_step = component.steps[restart_path] + symlink = 'restart' + else: + restart_step = Forward(component=component, + indir=f'{indir}/default', + ntasks=None, min_tasks=None, + openmp_threads=1, resolution=resolution, + mesh=init, init=last_adjust_step, + do_restart=True) + symlink = None + self.add_step(restart_step, symlink=symlink) - self.add_step( - Viz(component=component, indir=self.subdir, mesh=init, - init=last_adjust_step)) + if include_viz: + self.add_step( + Viz(component=component, indir=subdir, mesh=init, + init=last_adjust_step)) diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.py b/polaris/ocean/tasks/ice_shelf_2d/forward.py index 20b7265ad..b23feb2e4 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.py +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.py @@ -1,3 +1,7 @@ +from math import floor + +from mpas_tools.cime.constants import constants + from polaris.mesh.planar import compute_planar_hex_nx_ny from polaris.ocean.model import OceanModelStep, get_time_interval_string @@ -14,7 +18,8 @@ class Forward(OceanModelStep): """ def __init__(self, component, resolution, mesh, init, name='forward', subdir=None, indir=None, - ntasks=None, min_tasks=None, openmp_threads=1): + ntasks=None, min_tasks=None, openmp_threads=1, + do_restart=False): """ Create a new task @@ -48,21 +53,25 @@ def __init__(self, component, resolution, mesh, init, openmp_threads : int, optional the number of OpenMP threads the step will use """ + if do_restart: + name = 'restart' super().__init__(component=component, name=name, subdir=subdir, indir=indir, ntasks=ntasks, min_tasks=min_tasks, openmp_threads=openmp_threads) self.resolution = resolution + self.do_restart = do_restart # make sure output is double precision self.add_yaml_file('polaris.ocean.config', 'output.yaml') - self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', - 'global_stats.yaml') self.add_input_file(filename='init.nc', work_dir_target=f'{init.path}/output.nc') self.add_input_file(filename='graph.info', work_dir_target=f'{mesh.path}/culled_graph.info') + if do_restart: + self.add_input_file(filename='restarts', + target='../forward/restarts') self.add_output_file( filename='output.nc', @@ -110,7 +119,8 @@ def dynamic_model_config(self, at_setup): dt_per_km = section.getfloat('rk4_dt_per_km') else: dt_per_km = section.getfloat('split_dt_per_km') - dt_str = get_time_interval_string(seconds=dt_per_km * self.resolution) + dt = dt_per_km * self.resolution + dt_str = get_time_interval_string(seconds=dt) # btr_dt is also proportional to resolution: default 1.5 seconds per km btr_dt_per_km = section.getfloat('btr_dt_per_km') @@ -118,16 +128,34 @@ def dynamic_model_config(self, at_setup): seconds=btr_dt_per_km * self.resolution) section = config['ice_shelf_2d_default'] - s_per_hour = 3600. run_duration = section.getfloat('forward_run_duration') - run_duration_str = get_time_interval_string( - seconds=run_duration * s_per_hour) + do_restart_str = 'false' + if self.do_restart: + do_restart_str = 'true' + + # For all forward runs we ensure that the time step is a factor of the + # output interval because we might want to use the forward step output + # for a restart test + output_interval = dt * floor(run_duration / (2. * dt)) + if self.do_restart: + run_duration_str = get_time_interval_string( + seconds=output_interval) + output_interval_str = get_time_interval_string( + seconds=output_interval) + else: + run_duration_str = get_time_interval_string( + seconds=output_interval * 2.) + output_interval_str = get_time_interval_string( + seconds=output_interval * 2.) - output_interval = section.getfloat('forward_output_interval') - output_interval_str = get_time_interval_string( - seconds=output_interval * s_per_hour) + start_time = '0001-01-01_00:00:00' + if self.do_restart: + start_time = f"{start_time.split('_')[0]}_" \ + f"{run_duration_str.split('_')[1]}" replacements = dict( + do_restart=do_restart_str, + start_time=start_time, time_integrator=time_integrator, dt=dt_str, btr_dt=btr_dt_str, @@ -135,7 +163,9 @@ def dynamic_model_config(self, at_setup): output_interval=output_interval_str, land_ice_flux_mode='standalone', ) - self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', 'forward.yaml', template_replacements=replacements) + self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', + 'global_stats.yaml', + template_replacements=replacements) diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.yaml b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml index 3931f18e4..f1fc1d55d 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.yaml +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml @@ -8,7 +8,9 @@ omega: config_implicit_bottom_drag_type: constant config_implicit_constant_bottom_drag_coeff: 1.0e-3 time_management: + config_do_restart: {{ do_restart }} config_run_duration: {{ run_duration }} + config_start_time: {{ start_time }} time_integration: config_dt: {{ dt }} config_time_integrator: {{ time_integrator }} @@ -35,13 +37,14 @@ omega: input: filename_template: init.nc restart: - output_interval: 0005_00:00:00 + output_interval: 0000_00:00:01 output: type: output filename_template: output.nc output_interval: {{ output_interval }} clobber_mode: truncate contents: + - mesh - tracers - xtime - daysSinceStartOfSim @@ -55,12 +58,42 @@ omega: - velocityX - velocityY - atmosphericPressure + land_ice_fluxes: + type: output + precision: double + filename_template: land_ice_fluxes.nc + output_interval: {{ output_interval }} + clobber_mode: truncate + contents: + - ssh - landIcePressure - landIceDraft - landIceFraction - landIceMask + - landIceFrictionVelocity + - topDrag + - topDragMagnitude - landIceFreshwaterFlux - landIceHeatFlux - - landIceFrictionVelocity + - heatFluxToLandIce - landIceInterfaceTracers - landIceBoundaryLayerTracers + - landIceTracerTransferVelocities + - effectiveDensityInLandIce + - accumulatedLandIceMass + - accumulatedLandIceHeat + frazil: + type: output + precision: double + filename_template: frazil.nc + output_interval: {{ output_interval }} + clobber_mode: truncate + contents: + - accumulatedFrazilIceMass + - accumulatedFrazilIceSalinity + - seaIceEnergy + - frazilLayerThicknessTendency + - frazilTemperatureTendency + - frazilSalinityTendency + - frazilSurfacePressure + - accumulatedLandIceFrazilMass diff --git a/polaris/ocean/tasks/ice_shelf_2d/global_stats.yaml b/polaris/ocean/tasks/ice_shelf_2d/global_stats.yaml index e60d97ae1..ac264c22a 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/global_stats.yaml +++ b/polaris/ocean/tasks/ice_shelf_2d/global_stats.yaml @@ -5,4 +5,4 @@ omega: config_AM_globalStats_write_on_startup: true streams: globalStatsOutput: - output_interval: 0000_00:10:00 + output_interval: {{ output_interval }} diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg index 43d070e5d..2b09e8c78 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg +++ b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg @@ -44,13 +44,13 @@ adjust_variable = land_ice_pressure time_integrator = split_explicit # Time step in seconds as a function of resolution -rk4_dt_per_km = 5 +rk4_dt_per_km = 6 # Time step in seconds as a function of resolution -split_dt_per_km = 5 +split_dt_per_km = 6 # Time step in seconds as a function of resolution -btr_dt_per_km = 0.5 +btr_dt_per_km = 1 # config options for 2D ice-shelf testcases [ice_shelf_2d] @@ -68,13 +68,13 @@ y0_land_ice_height_above_floatation = 0. time_integrator = split_explicit # Time step in seconds as a function of resolution -rk4_dt_per_km = 5 +rk4_dt_per_km = 6 # Time step in seconds as a function of resolution -split_dt_per_km = 5 +split_dt_per_km = 6 # Time step in seconds as a function of resolution -btr_dt_per_km = 0.5 +btr_dt_per_km = 1 # Temperature of the surface in the northern half of the domain temperature = 1.0 @@ -105,11 +105,8 @@ y2_water_column_thickness = 1050.0 [ice_shelf_2d_default] -# Output interval for the forward step in hours -forward_output_interval = 0.2 - -# Run duration of the forward step in hours -forward_run_duration = 0.2 +# Run duration of the forward step in minutes +forward_run_duration = 10.0 # config options for ice_shelf_2d time-varying land-ice forcing [ice_shelf_2d_forcing] From 3b6d24a708e2b7bd07f7609a2ae4745aff556e0e Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Fri, 8 Dec 2023 14:32:30 -0600 Subject: [PATCH 14/30] Fixup directory structure --- polaris/ocean/ice_shelf/__init__.py | 10 ++++++---- polaris/ocean/tasks/ice_shelf_2d/default/__init__.py | 9 ++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/polaris/ocean/ice_shelf/__init__.py b/polaris/ocean/ice_shelf/__init__.py index 3c201d2c6..c21078d5e 100644 --- a/polaris/ocean/ice_shelf/__init__.py +++ b/polaris/ocean/ice_shelf/__init__.py @@ -11,9 +11,11 @@ class IceShelfTask(Task): shared_steps : dict of dict of polaris.Steps The shared steps to include as symlinks """ - def __init__(self, component, resolution, indir, name): - super().__init__(component=component, name=name, indir=indir) - self.indir = indir + def __init__(self, component, resolution, name, subdir, sshdir=None): + if sshdir is None: + sshdir = subdir + super().__init__(component=component, name=name, subdir=subdir) + self.sshdir = sshdir self.component = component self.resolution = resolution @@ -24,7 +26,7 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, resolution = self.resolution component = self.component - indir = self.indir + indir = self.sshdir num_iterations = config.getint('ssh_adjustment', 'iterations') diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index 5d124e17c..ed8aca3e1 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -41,8 +41,9 @@ def __init__(self, component, resolution, indir, init, config, if include_restart: name = f'{name}_with_restart' subdir = f'{subdir}/with_restart' + # Put the ssh adjustment steps in indir rather than subdir super().__init__(component=component, resolution=resolution, - name=name, indir=indir) + name=name, subdir=subdir, sshdir=indir) self.add_step(init, symlink='init') @@ -60,22 +61,24 @@ def __init__(self, component, resolution, indir, init, config, ntasks=None, min_tasks=None, openmp_threads=1, resolution=resolution, mesh=init, init=last_adjust_step) + forward_step.set_shared_config(config, link='ice_shelf_2d.cfg') symlink = None self.add_step(forward_step, symlink=symlink) if include_restart: - restart_path = f'{indir}/default/restart' + restart_path = f'{indir}/default/with_restart/restart' if restart_path in component.steps: restart_step = component.steps[restart_path] symlink = 'restart' else: restart_step = Forward(component=component, - indir=f'{indir}/default', + indir=f'{indir}/default/with_restart', ntasks=None, min_tasks=None, openmp_threads=1, resolution=resolution, mesh=init, init=last_adjust_step, do_restart=True) symlink = None + restart_step.set_shared_config(config, link='ice_shelf_2d.cfg') self.add_step(restart_step, symlink=symlink) if include_viz: From 86c8f639a3de5a76a11c16aeb8baa5f2631d3056 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Fri, 8 Dec 2023 17:33:37 -0600 Subject: [PATCH 15/30] Add validate step --- .../tasks/ice_shelf_2d/default/__init__.py | 4 + polaris/ocean/tasks/ice_shelf_2d/validate.py | 91 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 polaris/ocean/tasks/ice_shelf_2d/validate.py diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index ed8aca3e1..e4d9b20cc 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -1,5 +1,6 @@ from polaris.ocean.ice_shelf import IceShelfTask from polaris.ocean.tasks.ice_shelf_2d.forward import Forward +from polaris.ocean.tasks.ice_shelf_2d.validate import Validate from polaris.ocean.tasks.ice_shelf_2d.viz import Viz @@ -80,6 +81,9 @@ def __init__(self, component, resolution, indir, init, config, symlink = None restart_step.set_shared_config(config, link='ice_shelf_2d.cfg') self.add_step(restart_step, symlink=symlink) + self.add_step(Validate(component=component, + step_subdirs=['forward', 'restart'], + indir=f'{indir}/default/with_restart')) if include_viz: self.add_step( diff --git a/polaris/ocean/tasks/ice_shelf_2d/validate.py b/polaris/ocean/tasks/ice_shelf_2d/validate.py new file mode 100644 index 000000000..249347a07 --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/validate.py @@ -0,0 +1,91 @@ +from polaris import Step +from polaris.validate import compare_variables + + +class Validate(Step): + """ + A step for comparing outputs between steps in a ice shelf 2d run + + Attributes + ---------- + step_subdirs : list of str + The number of processors used in each run + """ + def __init__(self, component, step_subdirs, indir): + """ + Create the step + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + step_subdirs : list of str + The number of processors used in each run + + indir : str + the directory the step is in, to which ``name`` will be appended + """ + super().__init__(component=component, name='validate', indir=indir) + + self.step_subdirs = step_subdirs + + for subdir in step_subdirs: + self.add_input_file(filename=f'output_{subdir}.nc', + target=f'../{subdir}/output.nc') + self.add_input_file(filename=f'land_ice_fluxes_{subdir}.nc', + target=f'../{subdir}/land_ice_fluxes.nc') + self.add_input_file(filename=f'frazil_{subdir}.nc', + target=f'../{subdir}/frazil.nc') + + def run(self): + """ + Compare ``temperature``, ``salinity``, ``layerThickness`` and + ``normalVelocity`` in the outputs of two previous steps with each other + """ + super().run() + step_subdirs = self.step_subdirs + output_variables = ['temperature', 'salinity', 'layerThickness', + 'normalVelocity'] + land_ice_variables = ['ssh', 'landIcePressure', 'landIceDraft', + 'landIceFraction', + 'landIceMask', 'landIceFrictionVelocity', + 'topDrag', 'topDragMagnitude', + 'landIceFreshwaterFlux', 'landIceHeatFlux', + 'heatFluxToLandIce', + 'landIceBoundaryLayerTemperature', + 'landIceBoundaryLayerSalinity', + 'landIceHeatTransferVelocity', + 'landIceSaltTransferVelocity', + 'landIceInterfaceTemperature', + 'landIceInterfaceSalinity', + 'accumulatedLandIceMass', + 'accumulatedLandIceHeat'] + frazil_variables = ['accumulatedFrazilIceMass', + 'accumulatedFrazilIceSalinity', + 'seaIceEnergy', 'frazilLayerThicknessTendency', + 'frazilTemperatureTendency', + 'frazilSalinityTendency', + 'frazilSurfacePressure', + 'accumulatedLandIceFrazilMass'] + output_pass = compare_variables(variables=output_variables, + filename1=self.inputs[0], + filename2=self.inputs[3], + logger=self.logger) + land_ice_pass = compare_variables(variables=land_ice_variables, + filename1=self.inputs[1], + filename2=self.inputs[4], + logger=self.logger) + frazil_pass = compare_variables(variables=frazil_variables, + filename1=self.inputs[2], + filename2=self.inputs[5], + logger=self.logger) + if not output_pass: + raise ValueError(f'Validation failed comparing outputs between ' + f'{step_subdirs[0]} and {step_subdirs[3]}.') + if not land_ice_pass: + raise ValueError(f'Validation failed comparing outputs between ' + f'{step_subdirs[1]} and {step_subdirs[4]}.') + if not frazil_pass: + raise ValueError(f'Validation failed comparing outputs between ' + f'{step_subdirs[2]} and {step_subdirs[5]}.') From 7e340e1c8b7649991f70be197fa74c639e402efe Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Fri, 5 Jan 2024 13:56:18 -0600 Subject: [PATCH 16/30] Move ssh_adjustment config options --- polaris/ocean/ice_shelf/ssh_adjustment.cfg | 29 ++++++++++++++ polaris/ocean/ice_shelf/ssh_adjustment.py | 10 +++-- polaris/ocean/tasks/ice_shelf_2d/__init__.py | 2 + .../ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg | 38 ++----------------- polaris/ocean/tasks/ice_shelf_2d/init.py | 6 +++ 5 files changed, 47 insertions(+), 38 deletions(-) create mode 100644 polaris/ocean/ice_shelf/ssh_adjustment.cfg diff --git a/polaris/ocean/ice_shelf/ssh_adjustment.cfg b/polaris/ocean/ice_shelf/ssh_adjustment.cfg new file mode 100644 index 000000000..359e05e94 --- /dev/null +++ b/polaris/ocean/ice_shelf/ssh_adjustment.cfg @@ -0,0 +1,29 @@ +# Options related to ssh adjustment steps +[ssh_adjustment] + +# Number of ssh adjustment iterations +iterations = 10 + +# Output interval for the ssh adjustment phase in hours +output_interval = 1.0 + +# Run duration of each ssh adjustment phase in hours +run_duration = 1.0 + +# Variable in init.nc that determines where to adjust SSH +mask_variable = adjustSSHMask + +# Whether to adjust land ice pressure or SSH +adjust_variable = landIcePressure + +# Time integration scheme +time_integrator = split_explicit + +# Time step in seconds as a function of resolution +rk4_dt_per_km = 6 + +# Time step in seconds as a function of resolution +split_dt_per_km = 6 + +# Time step in seconds as a function of resolution +btr_dt_per_km = 1 diff --git a/polaris/ocean/ice_shelf/ssh_adjustment.py b/polaris/ocean/ice_shelf/ssh_adjustment.py index 69394dcd8..7eb4a45f8 100644 --- a/polaris/ocean/ice_shelf/ssh_adjustment.py +++ b/polaris/ocean/ice_shelf/ssh_adjustment.py @@ -50,7 +50,8 @@ def run(self): """ logger = self.logger config = self.config - adjust_variable = 'landIcePressure' + adjust_variable = config.get('ssh_adjustment', 'adjust_variable') + mask_variable = config.get('ssh_adjustment', 'mask_variable') mesh_filename = 'mesh.nc' init_filename = 'init.nc' final_filename = 'final.nc' @@ -62,6 +63,9 @@ def run(self): if adjust_variable not in ['ssh', 'landIcePressure']: raise ValueError(f"Unknown variable to modify: {adjust_variable}") + if mask_variable not in ds_init.keys(): + raise ValueError(f"Mask variable {mask_variable} is not contained " + f"in {init_filename}") logger.info(" * Updating SSH or land-ice pressure") @@ -81,9 +85,7 @@ def run(self): final_ssh = ds_final.ssh top_density = ds_final.density.isel(nVertLevels=minLevelCell) - y3 = config.getfloat('ice_shelf_2d', 'y3') * 1e3 - mask = np.logical_and(ds_final.maxLevelCell > 0, - ds_mesh.yCell < y3).astype(float) + mask = ds_init[mask_variable] delta_ssh = mask * (final_ssh - init_ssh) # then, modify the SSH or land-ice pressure diff --git a/polaris/ocean/tasks/ice_shelf_2d/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/__init__.py index af8227815..00095c289 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/__init__.py @@ -19,6 +19,8 @@ def add_ice_shelf_2d_tasks(component): config_filename = 'ice_shelf_2d.cfg' config = PolarisConfigParser(filepath=f'{resdir}/{config_filename}') + config.add_from_package('polaris.ocean.ice_shelf', + 'ssh_adjustment.cfg') config.add_from_package('polaris.ocean.tasks.ice_shelf_2d', config_filename) diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg index 2b09e8c78..e4a863485 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg +++ b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg @@ -28,21 +28,6 @@ min_layer_thickness = 0.0 # Options related to ssh adjustment steps [ssh_adjustment] -# Number of ssh adjustment iterations -iterations = 10 - -# Output interval for the ssh adjustment phase in hours -output_interval = 1.0 - -# Run duration of each ssh adjustment phase in hours -run_duration = 1.0 - -# Whether to adjust land ice pressure or SSH -adjust_variable = land_ice_pressure - -# Time integration scheme -time_integrator = split_explicit - # Time step in seconds as a function of resolution rk4_dt_per_km = 6 @@ -92,34 +77,19 @@ coriolis_parameter = 0. y1 = 30.0 # ice shelf inflection point in y in km -y2 = 90.0 +y2 = 60.0 # ice shelf front location in y in km -y3 = 90.0 +y3 = 75.0 # Vertical thickness of ocean sub-ice cavity at GL y1_water_column_thickness = 10.0 # Vertical thickness of water column thickness at y2 -y2_water_column_thickness = 1050.0 +y2_water_column_thickness = 850.0 +# Options specific to the ice_shelf_2d/default case [ice_shelf_2d_default] # Run duration of the forward step in minutes forward_run_duration = 10.0 - -# config options for ice_shelf_2d time-varying land-ice forcing -[ice_shelf_2d_forcing] - -# the forcing dates -dates = 0001-01-01_00:00:00, 0002-01-01_00:00:00, 0003-01-01_00:00:00 - -# the amount by which the initial landIcePressure and landIceDraft are scaled -# at each date -scales = 1.0, 2.0, 2.0 - -# Output interval for the forward step in hours -forward_output_interval = 24. - -# Run duration of the forward step in hours -forward_run_duration = 1440. diff --git a/polaris/ocean/tasks/ice_shelf_2d/init.py b/polaris/ocean/tasks/ice_shelf_2d/init.py index c79f4961e..ba8901552 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/init.py +++ b/polaris/ocean/tasks/ice_shelf_2d/init.py @@ -139,6 +139,12 @@ def run(self): normal_velocity = normal_velocity.transpose('nEdges', 'nVertLevels') normal_velocity = normal_velocity.expand_dims(dim='Time', axis=0) + mask_variable = config.get('ssh_adjustment', 'mask_variable') + mask = xr.zeros_like(ds_mesh.yCell) + mask = np.logical_and(ds.maxLevelCell > 0, + ds_mesh.yCell < y3).astype(float) + ds[mask_variable] = mask + ds['normalVelocity'] = normal_velocity ds['fCell'] = coriolis_parameter * xr.ones_like(x_cell) ds['fEdge'] = coriolis_parameter * xr.ones_like(ds_mesh.xEdge) From 5234c23302452968140931dd8940293e508c0a29 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 9 Jan 2024 10:00:01 -0600 Subject: [PATCH 17/30] Add other coord type tests --- polaris/ocean/tasks/ice_shelf_2d/__init__.py | 64 +++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/polaris/ocean/tasks/ice_shelf_2d/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/__init__.py index 00095c289..e3d0a86da 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/__init__.py @@ -11,35 +11,39 @@ def add_ice_shelf_2d_tasks(component): component : polaris.ocean.Ocean the ocean component that the tasks will be added to """ - # TODO add vertical coordinate - # TODO add restart test for resolution in [5., 2.]: resdir = resolution_to_subdir(resolution) - resdir = f'planar/ice_shelf_2d/{resdir}' - - config_filename = 'ice_shelf_2d.cfg' - config = PolarisConfigParser(filepath=f'{resdir}/{config_filename}') - config.add_from_package('polaris.ocean.ice_shelf', - 'ssh_adjustment.cfg') - config.add_from_package('polaris.ocean.tasks.ice_shelf_2d', - config_filename) - - init = Init(component=component, resolution=resolution, indir=resdir) - init.set_shared_config(config, link=config_filename) - - default = Default(component=component, resolution=resolution, - indir=resdir, init=init, config=config) - default.set_shared_config(config, link=config_filename) - component.add_task(default) - - default = Default(component=component, resolution=resolution, - indir=resdir, init=init, config=config, - include_viz=True) - default.set_shared_config(config, link=config_filename) - component.add_task(default) - - default = Default(component=component, resolution=resolution, - indir=resdir, init=init, config=config, - include_restart=True) - default.set_shared_config(config, link=config_filename) - component.add_task(default) + for coord_type in ['z-star', 'z-level']: + basedir = f'planar/ice_shelf_2d/{resdir}/{coord_type}' + + config_filename = 'ice_shelf_2d.cfg' + config = PolarisConfigParser( + filepath=f'{basedir}/{config_filename}') + config.add_from_package('polaris.ocean.ice_shelf', + 'ssh_adjustment.cfg') + config.add_from_package('polaris.ocean.tasks.ice_shelf_2d', + config_filename) + config.set('vertical_grid', 'coord_type', coord_type) + + init = Init(component=component, resolution=resolution, + indir=basedir) + init.set_shared_config(config, link=config_filename) + + default = Default(component=component, resolution=resolution, + indir=basedir, init=init, config=config) + default.set_shared_config(config, link=config_filename) + component.add_task(default) + + default = Default(component=component, resolution=resolution, + indir=basedir, init=init, config=config, + include_viz=True) + default.set_shared_config(config, link=config_filename) + component.add_task(default) + + default = Default(component=component, resolution=resolution, + indir=basedir, init=init, config=config, + include_restart=True) + default.set_shared_config(config, link=config_filename) + component.add_task(default) + # TODO add tidal_forcing tests + # for coord_type in ['z-star', 'z-level', 'single_layer']: From 0f2aa3dfa07b82b31431976af587b142b5813bb4 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 9 Jan 2024 15:05:47 -0600 Subject: [PATCH 18/30] Add tidal forcing test cases --- polaris/ocean/tasks/ice_shelf_2d/__init__.py | 34 +++++++++++- .../tasks/ice_shelf_2d/default/__init__.py | 51 ++++++++++++------ polaris/ocean/tasks/ice_shelf_2d/forward.py | 43 ++++++++++----- polaris/ocean/tasks/ice_shelf_2d/forward.yaml | 1 + .../ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg | 54 ++++++++++++------- .../tasks/ice_shelf_2d/tidal_forcing.yaml | 12 +++++ 6 files changed, 146 insertions(+), 49 deletions(-) create mode 100644 polaris/ocean/tasks/ice_shelf_2d/tidal_forcing.yaml diff --git a/polaris/ocean/tasks/ice_shelf_2d/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/__init__.py index e3d0a86da..b0df9675d 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/__init__.py @@ -45,5 +45,35 @@ def add_ice_shelf_2d_tasks(component): include_restart=True) default.set_shared_config(config, link=config_filename) component.add_task(default) - # TODO add tidal_forcing tests - # for coord_type in ['z-star', 'z-level', 'single_layer']: + + default = Default(component=component, resolution=resolution, + indir=basedir, init=init, config=config, + include_tides=True) + default.set_shared_config(config, link=config_filename) + component.add_task(default) + + # The only test case that makes sense with the single_layer coordinate + # type is the one with barotropic tidal_forcing + for coord_type in ['single_layer']: + basedir = f'planar/ice_shelf_2d/{resdir}/{coord_type}' + + config_filename = 'ice_shelf_2d.cfg' + config = PolarisConfigParser( + filepath=f'{basedir}/{config_filename}') + config.add_from_package('polaris.ocean.ice_shelf', + 'ssh_adjustment.cfg') + config.add_from_package('polaris.ocean.tasks.ice_shelf_2d', + config_filename) + config.set('vertical_grid', 'coord_type', 'z-level') + config.set('vertical_grid', 'vert_levels', '1') + config.set('vertical_grid', 'partial_cell_type', 'None') + + init = Init(component=component, resolution=resolution, + indir=basedir) + init.set_shared_config(config, link=config_filename) + + default = Default(component=component, resolution=resolution, + indir=basedir, init=init, config=config, + include_tides=True) + default.set_shared_config(config, link=config_filename) + component.add_task(default) diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index e4d9b20cc..403b0cbfc 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -16,7 +16,8 @@ class Default(IceShelfTask): """ def __init__(self, component, resolution, indir, init, config, - include_viz=False, include_restart=False): + include_viz=False, include_restart=False, + include_tides=False): """ Create the test case @@ -29,22 +30,38 @@ def __init__(self, component, resolution, indir, init, config, The resolution of the test case in km indir : str - The directory the task is in, to which ``name`` will be appended + The directory the task is in, to which the test case name will be + added include_viz : bool Include VizMap and Viz steps for each resolution + + include_tides: bool + Include tidal forcing in the forward step """ - name = 'default' - subdir = f'{indir}/default' + if include_tides and include_restart: + raise ValueError('Restart test is not compatible with tidal ' + 'forcing') + if include_tides: + base_name = 'default_tidal_forcing' + else: + base_name = 'default' + + test_name = base_name + test_subdir = f'{indir}/{base_name}' if include_viz: - name = f'{name}_with_viz' - subdir = f'{subdir}/with_viz' + test_name = f'{base_name}_with_viz' + test_subdir = f'{indir}/{base_name}/with_viz' if include_restart: - name = f'{name}_with_restart' - subdir = f'{subdir}/with_restart' + test_name = f'{base_name}_with_restart' + test_subdir = f'{indir}/{base_name}/with_restart' + + ssh_dir = indir + forward_dir = f'{indir}/{base_name}' + # Put the ssh adjustment steps in indir rather than subdir super().__init__(component=component, resolution=resolution, - name=name, subdir=subdir, sshdir=indir) + name=test_name, subdir=test_subdir, sshdir=ssh_dir) self.add_step(init, symlink='init') @@ -52,40 +69,42 @@ def __init__(self, component, resolution, indir, init, config, init=init, config=config, config_filename='ice_shelf_2d.cfg', package='polaris.ocean.tasks.ice_shelf_2d') - forward_path = f'{indir}/default/forward' + forward_path = f'{forward_dir}/forward' if forward_path in component.steps: forward_step = component.steps[forward_path] symlink = 'forward' else: forward_step = Forward(component=component, - indir=f'{indir}/default', + indir=forward_dir, ntasks=None, min_tasks=None, openmp_threads=1, resolution=resolution, - mesh=init, init=last_adjust_step) + mesh=init, init=last_adjust_step, + tidal_forcing=include_tides) forward_step.set_shared_config(config, link='ice_shelf_2d.cfg') symlink = None self.add_step(forward_step, symlink=symlink) if include_restart: - restart_path = f'{indir}/default/with_restart/restart' + restart_path = f'{test_subdir}/restart' if restart_path in component.steps: restart_step = component.steps[restart_path] symlink = 'restart' else: restart_step = Forward(component=component, - indir=f'{indir}/default/with_restart', + indir=test_subdir, ntasks=None, min_tasks=None, openmp_threads=1, resolution=resolution, mesh=init, init=last_adjust_step, do_restart=True) symlink = None restart_step.set_shared_config(config, link='ice_shelf_2d.cfg') + restart_step.add_dependency(forward_step, forward_step.name) self.add_step(restart_step, symlink=symlink) self.add_step(Validate(component=component, step_subdirs=['forward', 'restart'], - indir=f'{indir}/default/with_restart')) + indir=test_subdir)) if include_viz: self.add_step( - Viz(component=component, indir=subdir, mesh=init, + Viz(component=component, indir=test_subdir, mesh=init, init=last_adjust_step)) diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.py b/polaris/ocean/tasks/ice_shelf_2d/forward.py index b23feb2e4..7ec7eea37 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.py +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.py @@ -19,7 +19,7 @@ class Forward(OceanModelStep): def __init__(self, component, resolution, mesh, init, name='forward', subdir=None, indir=None, ntasks=None, min_tasks=None, openmp_threads=1, - do_restart=False): + do_restart=False, tidal_forcing=False): """ Create a new task @@ -61,6 +61,7 @@ def __init__(self, component, resolution, mesh, init, self.resolution = resolution self.do_restart = do_restart + self.tidal_forcing = tidal_forcing # make sure output is double precision self.add_yaml_file('polaris.ocean.config', 'output.yaml') @@ -69,9 +70,6 @@ def __init__(self, component, resolution, mesh, init, work_dir_target=f'{init.path}/output.nc') self.add_input_file(filename='graph.info', work_dir_target=f'{mesh.path}/culled_graph.info') - if do_restart: - self.add_input_file(filename='restarts', - target='../forward/restarts') self.add_output_file( filename='output.nc', @@ -110,11 +108,17 @@ def dynamic_model_config(self, at_setup): super().dynamic_model_config(at_setup) config = self.config - section = config['ice_shelf_2d'] - - # dt is proportional to resolution: default 30 seconds per km + if self.tidal_forcing: + section = config['ice_shelf_2d_default_tidal_forcing'] + run_duration = section.getfloat('forward_run_duration') + run_duration = run_duration * constants['SHR_CONST_CDAY'] + else: + section = config['ice_shelf_2d_default'] + run_duration = section.getfloat('forward_run_duration') + run_duration = run_duration * 60. time_integrator = section.get('time_integrator') + # dt is proportional to resolution: default 30 seconds per km if time_integrator == 'RK4': dt_per_km = section.getfloat('rk4_dt_per_km') else: @@ -127,8 +131,6 @@ def dynamic_model_config(self, at_setup): btr_dt_str = get_time_interval_string( seconds=btr_dt_per_km * self.resolution) - section = config['ice_shelf_2d_default'] - run_duration = section.getfloat('forward_run_duration') do_restart_str = 'false' if self.do_restart: do_restart_str = 'true' @@ -153,6 +155,11 @@ def dynamic_model_config(self, at_setup): start_time = f"{start_time.split('_')[0]}_" \ f"{run_duration_str.split('_')[1]}" + if self.tidal_forcing: + land_ice_flux_mode = 'pressure_only' + else: + land_ice_flux_mode = 'standalone' + replacements = dict( do_restart=do_restart_str, start_time=start_time, @@ -161,11 +168,21 @@ def dynamic_model_config(self, at_setup): btr_dt=btr_dt_str, run_duration=run_duration_str, output_interval=output_interval_str, - land_ice_flux_mode='standalone', + land_ice_flux_mode=land_ice_flux_mode, ) self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', 'forward.yaml', template_replacements=replacements) - self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', - 'global_stats.yaml', - template_replacements=replacements) + # We only do this at set-up so that the user may change the + # tidal forcing parameters + if self.tidal_forcing and at_setup: + self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', + 'tidal_forcing.yaml') + if not self.tidal_forcing: + self.add_yaml_file('polaris.ocean.tasks.ice_shelf_2d', + 'global_stats.yaml', + template_replacements=replacements) + + vert_levels = config.getfloat('vertical_grid', 'vert_levels') + if vert_levels == 1: + self.add_yaml_file('polaris.ocean.config', 'single_layer.yaml') diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.yaml b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml index f1fc1d55d..dbd5f67bf 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.yaml +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml @@ -38,6 +38,7 @@ omega: filename_template: init.nc restart: output_interval: 0000_00:00:01 + filename_template: ../forward/restarts/restart.$Y-$M-$D_$h.$m.$s.nc output: type: output filename_template: output.nc diff --git a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg index e4a863485..c0bd04fe1 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg +++ b/polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg @@ -11,7 +11,7 @@ vert_levels = 20 min_vert_levels = 3 # Depth of the bottom of the ocean -bottom_depth = 1050.0 +bottom_depth = 2000.0 # The type of vertical coordinate (e.g. z-level, z-star) coord_type = z-star @@ -29,13 +29,13 @@ min_layer_thickness = 0.0 [ssh_adjustment] # Time step in seconds as a function of resolution -rk4_dt_per_km = 6 +rk4_dt_per_km = 10 # Time step in seconds as a function of resolution -split_dt_per_km = 6 +split_dt_per_km = 10 # Time step in seconds as a function of resolution -btr_dt_per_km = 1 +btr_dt_per_km = 2.5 # config options for 2D ice-shelf testcases [ice_shelf_2d] @@ -49,18 +49,6 @@ ly = 220 # How the land ice pressure at y Date: Wed, 20 Dec 2023 14:57:33 -0600 Subject: [PATCH 19/30] Update docs for ice_shelf_2d and ice_shelf framework --- docs/developers_guide/ocean/api.md | 45 +++ docs/developers_guide/ocean/framework.md | 19 + .../ocean/tasks/ice_shelf_2d.md | 88 +++++ docs/developers_guide/ocean/tasks/index.md | 1 + docs/users_guide/ocean/framework/ice_shelf.md | 55 +++ docs/users_guide/ocean/framework/index.md | 1 + docs/users_guide/ocean/tasks/ice_shelf_2d.md | 356 ++++++++++++++++++ .../ocean/tasks/images/ice_shelf_2d.png | Bin 0 -> 99151 bytes docs/users_guide/ocean/tasks/index.md | 1 + 9 files changed, 566 insertions(+) create mode 100644 docs/developers_guide/ocean/tasks/ice_shelf_2d.md create mode 100644 docs/users_guide/ocean/framework/ice_shelf.md create mode 100644 docs/users_guide/ocean/tasks/ice_shelf_2d.md create mode 100644 docs/users_guide/ocean/tasks/images/ice_shelf_2d.png diff --git a/docs/developers_guide/ocean/api.md b/docs/developers_guide/ocean/api.md index 4da44adb0..4e2fea465 100644 --- a/docs/developers_guide/ocean/api.md +++ b/docs/developers_guide/ocean/api.md @@ -107,6 +107,32 @@ viz.Viz.run ``` +### ice_shelf_2d + +```{eval-rst} +.. currentmodule:: polaris.ocean.tasks.ice_shelf_2d + +.. autosummary:: + :toctree: generated/ + + add_ice_shelf_2d_tasks + + default.Default + + forward.Forward + forward.Forward.compute_cell_count + forward.Forward.dynamic_model_config + + init.Init + init.Init.run + + validate.Validate + validate.Validate.run + + viz.Viz + viz.Viz.run +``` + ### inertial_gravity_wave ```{eval-rst} @@ -283,6 +309,25 @@ SphericalConvergenceForward.compute_cell_count ``` +### Ice Shelf + +```{eval-rst} +.. currentmodule:: polaris.ocean.ice_shelf + +.. autosummary:: + :toctree: generated/ + + IceShelfTask + IceShelfTask.setup_ssh_adjustment_steps + + SshAdjustment + SshAdjustment.run + + SshForward + SshForward.compute_cell_count + SshForward.dynamic_model_config +``` + ### Ocean Model ```{eval-rst} diff --git a/docs/developers_guide/ocean/framework.md b/docs/developers_guide/ocean/framework.md index 736c03c61..666ef7a66 100644 --- a/docs/developers_guide/ocean/framework.md +++ b/docs/developers_guide/ocean/framework.md @@ -383,6 +383,25 @@ rather needs to be computed. The default behavior is to read the requested variable (the value associate the `'name'` key) at the time index closest to the evaluation time specified by the `convergence_eval_time` config option. +(dev-ocean-framework-ice-shelf)= + +## Ice Shelf Tasks + +The `polaris.ocean.ice_shelf` module provides support for ice shelf tasks. + +The {py:class}`polaris.ocean.ice_shelf.IceShelf` class can serve as a parent +class for ice shelf tests, such as +{py:class}`polaris.ocean.tasks.ice_shelf_2d.IceShelf2d`. + +The {py:meth}`polaris.ocean.ice_shelf.IceShelf.setup_ssh_adjustment_steps()` +sets up `ssh_forward` and `ssh_adjustment` steps from the classes +{py:class}`polaris.ocean.ice_shelf.ssh_forward.SshForward` +{py:class}`polaris.ocean.ice_shelf.ssh_adjustment.SshAdjustment`. +The `ssh_adjustment` section of the config file sets the parameters for these +steps, as described in {ref}`ocean-ssh-adjustment`. It returns the last +`ssh_adjustment` step, which is typically used as the +initial state for subsequent forward steps. + (dev-ocean-framework-vertical)= ## Vertical coordinate diff --git a/docs/developers_guide/ocean/tasks/ice_shelf_2d.md b/docs/developers_guide/ocean/tasks/ice_shelf_2d.md new file mode 100644 index 000000000..899fc934d --- /dev/null +++ b/docs/developers_guide/ocean/tasks/ice_shelf_2d.md @@ -0,0 +1,88 @@ +(dev-ocean-ice-shelf-2d)= + +# ice_shelf_2d + +The `ice_shelf_2d` test group +(`polaris.ocean.tasks.ice_shelf_2d.IceShelf2d`) +implements a very simplified ice-shelf cavity that is invariant in the x +direction (see {ref}`ocean-ice-shelf-2d`). Here, we describe the shared +framework for this test group and the 2 test cases. + +(dev-ocean-ice-shelf-2d-framework)= + +## framework + +The shared config options for the `ice_shelf_2d` test group +are described in {ref}`ocean-ice-shelf-2d` in the User's Guide. + +Additionally, the test group has shared `ssh_forward.yaml` and `forward.yaml` +files with common namelist options and streams related to the `ssh_adjustment` +and `forward` steps, respectively. + +The test case class is inherited from +{py:class}`polaris.ocean.ice_shelf.IceShelfTask` and +its {py:class}`polaris.ocean.ice_shelf.ssh_forward.SshForward` and +{py:class}`polaris.ocean.ice_shelf.ssh_adjustment.SshAdjustment` step classes +are used to set up one of each step for each iteration given by the config +option `ssh_adjustment:iterations`. + +### init + +The class :py:class:`polaris.ocean.tasks.ice_shelf_2d.init.Init` +defines a step for setting up the initial state for each test case. + +First, a mesh appropriate for the resolution is generated using +{py:func}`mpas_tools.planar_hex.make_planar_hex_mesh()`. Then, the mesh is +culled to remove periodicity in the y direction. A vertical grid is generated, +with 20 layers of 100-m thickness each by default. Then, the 1D grid is either +"squashed" down so the sea-surface height corresponds to the location of the +ice-ocean interface (ice draft) using a z-star {ref}`dev-ocean-framework-vertical` +or top layers are removed where there is an ice shelf using a z-level +coordinate. Finally, the initial salinity profile is computed along with +uniform temperature and zero initial velocity. + +### forward + +The class {py:class}`compass.ocean.tests.ice_shelf_2d.forward.Forward` +defines a step for running MPAS-Ocean from the initial condition produced in +the `init` step. For MPAS-Ocean, PIO namelist options are modified and a +graph partition is generated as part of `runtime_setup()`. Next, the ocean +model is run. + +### validate + +The class {py:class}`polaris.ocean.tasks.ice_shelf_2d.validate.Validate` +defines a step for validating outputs in two step directories against one +another. This step ensures that `temperature`, `salinity`, `layerThickness` +and `normalVelocity` are identical in `output.nc` files in the two steps. +It also checks a number of land ice variables and frazil variables stored in +`land_ice_fluxes.nc` and `frazil.nc`, respectively. This step is used by the +restart test (see {ref}`dev-ocean-ice-shelf-2d-restart`). + +### viz + +The class {py:class}`polaris.ocean.tasks.ice_shelf_2d.viz.Viz` uses the planar +visualization capabilities provided by +{py:func}`polaris.ocean.viz.compute_transect()`. + +(dev-ocean-ice-shelf-2d-default)= + +## default + +The {py:class}`polaris.ocean.tasks.ice_shelf_2d.default.Default` test case +config options are described in {ref}`ocean-ice-shelf-2d-default`. + +The test creates and mesh and initial condition, performs 15 iterations of +SSH adjustment to make sure the SSH is as close as possible to being in +dynamic balance with the land-ice pressure. Then, it performs a 10-minute + forward simulation. If a baseline is provided, a large number of variables +(both prognostic and related to land-ice fluxes) are checked to make sure +they match the baseline. + +The restart test, `ocean/planar/ice_shelf_2d/$RES/$COORD_TYPE/default/with_restart` +is just a variant of the default test that has two additional steps, a `forward` +step and a `validate` step. + +The tidal forcing test, `ocean/planar/ice_shelf_2d/$RES/$COORD_TYPE/default_tidal_forcing` +is a variant of the `default` test that has tidal forcing, is run for longer +(0.1 days), and uses the RK4 time integration scheme by default. diff --git a/docs/developers_guide/ocean/tasks/index.md b/docs/developers_guide/ocean/tasks/index.md index 1f7e7effb..bba11ef80 100644 --- a/docs/developers_guide/ocean/tasks/index.md +++ b/docs/developers_guide/ocean/tasks/index.md @@ -10,6 +10,7 @@ correlated_tracers_2d cosine_bell geostrophic divergent_2d +ice_shelf_2d inertial_gravity_wave internal_wave manufactured_solution diff --git a/docs/users_guide/ocean/framework/ice_shelf.md b/docs/users_guide/ocean/framework/ice_shelf.md new file mode 100644 index 000000000..b0ed31993 --- /dev/null +++ b/docs/users_guide/ocean/framework/ice_shelf.md @@ -0,0 +1,55 @@ +(ocean-ice-shelf)= + +# Ice shelf + +The framework defines an `IceShelfTask` class that provides methods common +to tasks that feature ice shelf cavities. At present, the only method included +sets up ssh adjustment steps, described in ocean-ssh-adjustment. + +(ocean-ssh-adjustment)= + +## SSH adjustment steps + +For tasks that feature ice shelf cavities, a series of forward simulations is +run in order to iteratively bring the SSH in equilibrium with the land ice +pressure. These steps are typically shared between all tasksthat also share +initial conditions and namelist options that influence the dynamics. Only the +output from the final SSH adjustment step is used as the initial state for the +forward step(s) of the task. + +### config options + +The following config options are used by SSH adjustment steps. The defaults are +shown here, but may be changed in a task's local config file. + +```cfg +# Options related to ssh adjustment steps +[ssh_adjustment] + +# Number of ssh adjustment iterations +iterations = 10 + +# Output interval for the ssh adjustment phase in hours +output_interval = 1.0 + +# Run duration of each ssh adjustment phase in hours +run_duration = 1.0 + +# Variable in init.nc that determines where to adjust SSH +mask_variable = adjustSSHMask + +# Whether to adjust land ice pressure or SSH +adjust_variable = landIcePressure + +# Time integration scheme +time_integrator = split_explicit + +# Time step in seconds as a function of resolution +rk4_dt_per_km = 6 + +# Time step in seconds as a function of resolution +split_dt_per_km = 6 + +# Time step in seconds as a function of resolution +btr_dt_per_km = 1 +``` diff --git a/docs/users_guide/ocean/framework/index.md b/docs/users_guide/ocean/framework/index.md index f7bd59bc2..09f9e2a06 100644 --- a/docs/users_guide/ocean/framework/index.md +++ b/docs/users_guide/ocean/framework/index.md @@ -9,4 +9,5 @@ across tasks. :titlesonly: true vertical +ice_shelf ``` diff --git a/docs/users_guide/ocean/tasks/ice_shelf_2d.md b/docs/users_guide/ocean/tasks/ice_shelf_2d.md new file mode 100644 index 000000000..6126b1254 --- /dev/null +++ b/docs/users_guide/ocean/tasks/ice_shelf_2d.md @@ -0,0 +1,356 @@ +(ocean-ice-shelf-2d)= + +# ice shelf 2d + +## description + +The ``ice_shelf_2d`` tasks describe a series of very simplified ice-shelf test +cases where topography and initial conditions only vary in the y direction. +The Coriolis parameter `f` is zero. This makes the test case quasi-two- +dimensional, with negligible variability in x. + +Polaris includes three test cases, a default case, a restart case, and the +default case with visualization. Each case includes an `init` step to set up +the mesh and initial condition, a series of `ssh_adjustment` steps (see +{ref}`ocean-ssh-adjustment`), and a forward step. Some cases include additional +steps, described below. + +All test cases include a relatively strenuous, iterative process to +dynamically adjust `landIcePressure` or `ssh` to be compatible with one +another in the `ssh_adjustment` steps. In this test case, we perform 10 +iterations of adjustment, enough that changes in pressure should be quite +small compared to those in the first iteration. Reducing the number of +iterations will make the test case run more quickly at the risk of +having longer-lived transients at the beginning of the simulation. + +```cfg +# Options related to ssh adjustment steps +[ssh_adjustment] + +# Number of ssh adjustment iterations +iterations = 10 + +# Output interval for the ssh adjustment phase in hours +output_interval = 1.0 + +# Run duration of each ssh adjustment phase in hours +run_duration = 1.0 + +# Whether to adjust land ice pressure or SSH +adjust_variable = land_ice_pressure + +# Time integration scheme +time_integrator = split_explicit + +# Time step in seconds as a function of resolution +rk4_dt_per_km = 10 + +# Time step in seconds as a function of resolution +split_dt_per_km = 10 + +# Time step in seconds as a function of resolution +btr_dt_per_km = 2.5 +``` + +If a baseline run of the test case was provided for comparison, we perform +validation of both the prognostic variables (layer thickness, velocity, +temperature and salinity) and a large number of variables associated with +freshwater and heat fluxes under ice shelves. + +Frazil-ice formation is not included in the `ssh_adjustment` steps but is +included in the `forward` step of this test case. + +## mesh + +The test case currently supports only 5-km horizontal resolution. The x +direction is periodic and only 10 cells wide, whereas the y direction has +solid boundaries and is 44 cells long. These dimensions are set in the config +file by `lx` and `ly`. + +## vertical grid + +The conceptual overlying ice shelf is described by a piecewise linear function. +The config options `y1`, `y2`, and `y3` dictate the inflection points in the +piecewise function and `y1_water_column_thickness` and +`y2_water_column_thickness` dictate the water column thickness at those +locations. The water column thickness at `y3` is always equal to the bottom +depth, indicating the ice shelf front location. By default, the ice shelf +depresses the sea surface height by as much as +1040 m (leaving a 10-m water column) for the first 30 km in y. Over the next +30 km, it rises to 200 m, then fairly abruptly to zero over the next 15 km, +where it remains for the second half of the domain. The ice shelf occupies +these first 75 km of the domain: fluxes from ice-shelf melting are only applied +in this region. + +```{image} images/ice_shelf_2d.png +:width: 500 px +:align: center +``` + +The geometry does not represent a particularly realistic ice-shelf cavity but +it is a quick and useful test of the parameterization of land-ice melt fluxes +and of frazil formation below ice shelves. + +Two vertical coordinates, `z-star` and `z-level`, are available. In each case, there are 20 vertical levels given by the config option `vert_levels`. In the open ocean, each level is 50 m thick. + +```cfg +# Options related to the vertical grid +[vertical_grid] + +# the type of vertical grid +grid_type = uniform + +# Number of vertical levels +vert_levels = 20 + +# The minimum number of vertical levels +min_vert_levels = 3 + +# Depth of the bottom of the ocean +bottom_depth = 2000.0 + +# The type of vertical coordinate (e.g. z-level, z-star) +coord_type = z-star + +# Whether to use "partial" or "full", or "None" to not alter the topography +partial_cell_type = partial + +# The minimum fraction of a layer for partial cells +min_pc_fraction = 0.1 + +# The minimum layer thickness in m +min_layer_thickness = 0.0 +``` + +## initial conditions + +The initial temperature for the whole domain is constant (1 degree Celsius), +while salinity varies linearly with depth from 34.5 PSU at the sea surface +to 34.7 PSU at the sea floor, which is at a constant at 2000 m depth. These +initial conditions can be modified with config options `temperature`, +`surface_salinity`, and `bottom_salinity` + +## forcing + +N/A + +## time step and run duration + +The time step is determined by the config options `dt_per_km` so that the time +step is proportional to the resolution. By default, a 10 km-resolution test +has a time step of 5 min. Run duration will be specified for each test case. +Run duration will be discussed for individual test cases. + +## config options + +```cfg +# config options for 2D ice-shelf testcases +[ice_shelf_2d] + +# width of domain in km +lx = 50 + +# length of domain in km +ly = 220 + +# How the land ice pressure at ySD);eF#hr>#iET%lqbKm>k<+`rDe>c=W zD*Uy`*CY~2m~!mrQzVkmAc^#4&{wPBFD9>ji{T#?pTlN8MxG8nerLVxNqT2}F1UI6 zxH+Hu-q+sC+u74YUgn^T-2U&Ke0(l=tIEo{|N90RPcKK=@8=f(fiGEe;h4EMiL~Qy z{C|Z<6IGM6f<&VH{L^Xwqz^p-KFP*aT8lH3=5=~URXdu${z6Xe`=`b$R;)Pr=+zHt zQc}N#+ZcW;6Zlu)<}bH@Q+45|%&)I+-emYqe(*b?&71B^$>%*fb~Nq8mGwRiyoJ|Y zI)}OxC{ao4=u8X8ybz9hV{huuaY_NHjlEaS6_~Aq+rgiviM_Sff?r9b$(JSwG1DD&DBUoy$GcI3Rq5CL_@dr~bk=A~mhuDp`Oly1^Mvr`7k}j} z?Pt?Z*@AucIeJr_bIgOm8)}|2GSE=Gyjpm#r3So9n@Ns_4}~lF58OV=NoEIUO)lO! zf8*QQmGGkL!w`Iry`o3=<4qCInsbtrdVE?Winn{eI`F?->%T7hU%xKhL0;*(XV=xI z-<%J4|GOlIQzz^A`p?4;^787_e!5&(BavuVF43TlAFcO{-_yk}mwc=D~vp9dFrp3oiCQA5`n(>5qmK zn7NhQ*e35lrfANTHR3)vJ2}1F5cc`0mCx(L0TX4z?f#i1B}%ruT2s!rXPDa5)Kq=D zT*&NXWxp6(s;U}p2p05(LRprfRPI#A^Tw*S9>2b1*lhXX)UeRSeyzp3lT&jO6|xl* z<*r4FiYEKF6!C@XC6Hf7Bps z?@&%I`RF^yQMqFlo=#3?{Vdm_@`})rrgUCwmS&7= zewhTt>#4BvfTR|Wa&_Id=0A%?uYHCMRGA&QYiX^8D_%wzd(RWkP;Mfwdd5sDSg!ZG zD_b3&gs*U|3Iii5o9PYWa#Q*0ul-RVceeY#y;Sc}wfH_lb_k64m9eRH z3^`yV{hAkidgu@NhBx7Cr>8v^o(h|nxS7NCX?3#I>O$hdA4O)m1BR1TCYyA0#SI^i zW#@Pe#XCB1>yK_K(oLUl&d`;ISA!Q= zwzh2WBQsVWRa5QFbGvS;dnNOExO3Lm##s=e$}g$0D~l9*z3QaBd4qAvqA#aMTO`F7 zKVFkR`}eOO9{1mHtr~`NVXUt))#|X4N8Y9x?4MuBsZH`}rex3;X2SS>Cizw!wd_Ab zq%BMKFU(HW{qy@qrpQal8Z;#SBAYK$CA%e}Y`W7`E$5%&PWw3P!WMnCm|k1o4*NY@ zB3L04aQ;0x@}*RCL_QlZJb^{QVbA!qUhTuJ>(?9j)QJy8jWT&^LDL`9IWKo5+dQ>g zV1_N2mUMYkIIva5?%(B zUQ=`Pcx%6&;>l3XK#iEWNyPSx|9q?0aN9H3WWKXIkd+s&5#&2sC4+%>M03etWGX(K z5;2IE+im;#@2zO!C3W3(vC?&V%01DtA)~rHIB|Dg-#zO1_Q@si@dnx1*$I_4{_n0d zdq?WL_j+^jg;-*+S-MJOV_>SXr;U(tt5wP7Zl-{3`VY9qLx*35+1{pQV?!=1YQX(1tBUpXXX z(yWx|USh5}`}$^VuWcxA&{ZS$Ej)2R6uql_2VA#&s~{>Az`-0w&$31`>DJ=H9Q|U* zyT7mS@X|?vV-Eu6y82RNgWs(U7>qG(S7lByZBy6ERz93$6c)Rrw*quUd_(~ zzT-`1oS~ z@qSp*A#i*(k;=Vy8p9U39bg^u!Jj`)j(7P*uIJ_#P6Zi!dVMog0oP@8AtTPFb6~b! zxi6{AdpNe2MZ+Xu;;0nH4&k=8w!ypN3r-CXg1SRydIA=vnBFrze#7^o^pArBKR#2R zptui0g;P{AD^0+dD+ixI#GkB+^jDoEG%3$+d8)fO&!_r!pv)`P* zsRuviE?>fRI`xUids?|L?%5dcn9?!edQ)@O$$7rrW9F~RLZwcU)?y_6ptEEJVt=sSj!m@He zwOdDF^W!l7qF%mUs%A(PSA###E#uVg>&hred7r2CB7i$o9FQMnpjoC63ij*`>+1-o zFoQl0@yT`G5sQ+4on7-iN?ti1_S8FS20Xd5b0t`ja#=}W#U$0d+{gKb=Ilu2;w&pK zJ3Y&FUS5w@4bSJE;+mE0TZ=9xFszv(3Fd3Fr;6Vk$7mfY1K%Fb`H$e-T@w%W@14r` z8~igurZ(5Ss6D_|M@J_Vj@Ixst@*Q~uIHcsQKPRmemanU!>@7|JCIp%obCDe-PQTw zuti4`a0JhTalMafa&C81nbr^P-#_N=?yeScw5Ja|aN?zvLb1JJ?4Z87@zTDOXAxUV zof*%2IFVYU11_!i{}59v8Nc!cJg&X5@dWs|eaQ{t^MVn@ zV}$n}dgaXNKD^aB`ug?E)SAT!zv{%x>s9YhE<&1DqsX+1f_0L;dnB@Up2y{We7a$n zF*mUT7{yjYf|MWa5 zX4)juL#J{&;3S@s+LTaLYFizO$qE;u-q2e3yefMRF9um|b9$vRc%HVkO~nP?C~<;Fr=&}}Z-!3Zwa@(up74z!QK63f(92Dk z8GuAzmxfvN%SX;Fo!wOQCO*QUMd-|A-hw;a11YH&nE#pqu=zBhrakx$d(59%`4vMD z&C@!$(0fbaCdv!g9J z_vVFWEM&>b7_~Wrb%|D8J}r7&a5@=q`0caT!?!pY7!S(8zofuKFn%n0T!TZJ(thIg zutOzp;f?P2XL`y<6mJ0%kXZMU4hhIFJDl!Df~Tn%z(d#z(b+2w-rFRhsHhk)tkxF~ zX?q=J{TLtV-MiuGNaHWzd)DG|av{*F(yJF|E$E)bZ6;;oC0&Ib4vAF2Fa$p=iRi4l zpvl)qxqod4{n4}X(~;RJR-AmJx;*Tj=@wa=q%|8Q1|an^pt(VE$m{A`j166wjVU`P z_&DvE zM(^2n;M~tpf3~{`6IW=m#0*HbYVs6qz?vOs>f-8JXZ5W%bNAVMbrywo6!Z|}8mKQ5 ztNcE`{p$gp-uUO?2szjG+%5D2;CFh;bcD}~gb5?Qrj-jS1&xL!6ehxvJRaFf>fl}!I>HQXZ0V97F@_|8E{xdY= z)JahlFT050)g;yDpFj0`x0^f0^v*oke&^8Ff`b&8diDMFTqmX$$MZsiWZFNwzL{0&3A^U1zLGS#&aXGX5n{;0 z=IT|e+uResUGURQf4dz|GAWIPpM>+_K{0XHl2qU5Y|x0l2}d)aKT?@SUT{^ad%ki;KmBd$L%a*^nRl`YKF$>%3Uz(^LT1P{ zm}|_jtHUCL!10OOLrp=U8R@I+#v{=xvLbwX1{|y%q=t16+M>u)ZRZO=r7fJ2Ivrak zhhG@p{$Q`)iiet8t$j}V_E4GVg2j0+k8&{d{r3^al$d4*$@VK|_2QJA=~|N8eK@Vi$CYx9p>x7#jAI@VcOS@On_}d@b=6^rE>BFzYm-!f!(6cPR18iUi z)T+T#Ikxq&mLBnt$~u6D>;QL&v2GYj`i3o*ZYL+?R)h;rM%d2(z@KGS)5wa(I)ycF z3m)X&m+>~nn)mWb_e<4@~e0nFqY-zdIpPg$^ z8BS`ROP^B9G?a=iSbB9A@#+L5C?^sW_E1h4DUugll%zd>!ViS!eZ3ffX(efL|Kivh z_%`)V|7_&J%cwxe)MU+(kUrzyUx=4gZyD|&h3ilCi}MdcMAfCOo0!W#ZPFUO{O!alwLrzSuWYhe zH;R1aw8OE!nb^|r@#Ep^CGfsm<|nhX>P0;(Z}|SKC5~YZ+yKU7R&%mP-u&aiCLL07vj-3LUOnra~9QB$Pm2$TD3L0cf)v= zE@l{{5e+QHeeOfFwS%Kgq0Nq!X5}}2>Jot{~8CdAok z;t5{s<1fx2ct*SDSwHlEzQcIvI)0R-8b{pI$^BcrAx6jc-u&*N?Y11kHl#M-ql&%f zFI4m6uI&hvqMYh-JTcqmVY&R-RN}L*AM1vGesc33+NlVBXE=x0{|SKHs_owY^E-u> zkp1m8vzgwd`!$rn2kq^JH7kN$ijpUU>_RF_RnDY40XjW%p4z`9imWpic5w6NHv4?c7fETAxK58EFf=B(=2K{=H;u{N<4-KVJTJhfHJ)OQ zx!;zdPLX@W-vK^_)ijlztu)@mgEb4M=D3X*P5g&cE{v|h?I9#Bd!XoS1RFd=y*k~O zqr=>|P=OWmblCy9%s^b%^#6hy%EX`4>Pr!UM* zJS_eaz8j)}AKLku%UC*K|nM3=XGnr>zKqrp?{pV?!z3Tb#hzpgzHzB2t%cR0?$HX52QmGpPCkf9?i`!MYIaU zW11YG+P9_MGaQL3AJglkdMsUc<5zgysNs|#8Nj0;ch2i2Z@!Z4n`kL`w#iL|0T7_F zvz2X8Kt=t&CExiU++X+RgVAiuBJ*wg2tynxC-$ah42qNKwS&|e!m>3YaRp5*A741T z17IsMMKAR>Y`Nu?vw0F<)`yLRNt*wpO=gnW3k)&krU45_I>)7&f72H+rMg#TRtJZEIn^RMQ(+%IG z@qCuHoDqFfj)g`>b7FW1J6_?mBQaf>l2t^G8#&~+=QVb|1 zj^jB`-`cIsrdfn(JP9PW9&dc$^-*y>TI=+-nA+>*O7(Y_7DNp-iR6fg?O0y+O0At3SC&UKwT}J*>WM=L{=MIZdNJj-wn*e>*bol z2F|G(5FRq^8U9Hj$83MB>fpYw+a9orF6N{xFR%kHaDpNr0kDpel;+Y|F^pDLL~^Y? zLqsEUz0!Qoo&Zj{!18+$1kx@NxyG4Ve)P?FFZF@)(X!w}Go?fIQL`mQNu=TQ)*bw*HvT}p^t#bu;$PYZ9EnkCVg0cCbeq>`5o23qS2y147TMmEH z;mvNjx^yq3UgX}+ou|DMvE<#i!DXZ3O*Cg8Xy78+2!3-ZPgOaGLmy${bM zE^pkD$Fkf4UyUn%8mbw@u3g3hILSS`t8@9k>06^E`nO!)ty^dPg?4GW5j>M3fE1Y2 zQfSu?xx37{Sq)uJ_|JcPFU1q7ni`E-{9p_!nR_-R@Ey-P`qr#n@*O|p_KTE{1#rF~ z9xkooVkWGZ>u115EJS9k8{zV=cgy}k7*_c{^q9Ht0MFYWIcHK`^+(LI57HyPC-X!+ zjd1C;*BSOXI!jK{kJwp%#1eoUk)XKDM$2y6D!54v;&@0b@*2ZppDP;I3-yapQ!2Fu z5p@q?FR#hbvN~DIf1?n8J2?Rx3O{L*xIIljw~Vu{`Hgih*+t3`w2O^7I=n-BjjR>z zl4q!`0dVK=qw%|8isGvz6?r~aiyvhWGG^4_l9Pv(C6p|Hf~dmgoe2d##SsdR`i29L zr2{y)r3x9IEp3X(m6Y~{Zi9LZ%EY44%*3IQyEhxaaGKWlDUxoH0Z4z<`oJiP+-Cd~ za(Lg)fA8sjcK52L;-rx24qopJBko+m z7=+4|_6E8}(Hrj^*abQ8OQ>Yxkgf#qY3!o~q>hOP&L6?d{GX)<9REqmU?bvQke-gQ zSl%E15^`i$%``y$-q70K?f~@%4`qy=SRQnZ4K0mESU zfBZQ3XN3cHFK;D59;pIpYD;w(UxMiiRU`5kh{t##Vmj9PpzPz{Ynff?j8D%+9RWV@ zG}728ss9Qen2eMo@LrjxP4YXB_(6%?utI42Ex-Z}Ak+fMM9S0kYWsm&aA~n1J+VB* z%ccqqF1v(nj9BDF({YinyU9zV#>*Qd=-msXEA1Ue$@} zLs1fXd-IBrwB;h&SP9IJtVU5VkF!2!4Joj^Ej`%&aP?{9cE8Vo%XY3WWsp<#`F#YD zG}^K9N^RH+T5}zZ9E5iPUfOo>!WpE_f=6|2YXRoF>5yt!bwsz_JR}6nJ6@GK7U=o~ zsr}KJzkX2%)CF=t&8N(O)TKCOk&h<3K#;{yY|+SoFMkeXdv0gQ0t0BEF?BixU^FX$ z997=^y3q@=q>Ri=hsa@oLybk~uB_QMph;Q=%G`l2sG{z!fif1)*kN%kW10hG-M&MizQ?A_~ElYgZ>}INr4r6 zj54F2D~5bIL^uRIFA?o=p;P8XTj8bdC8k+`sm!X{c_V&jup^z}}lG(w-YKcBC zkZiN;PXRyG?B7vnU*BE|uuk*i{QUfmAkjocSCb+-08qvLGNh4-gkL$T5VGN?^NM~z zyK4% zr`LZr`-fQeD3>sb##W>$2gpa$kLk5NEbcA3aCe1h<&C8bM)})XKOPC65 zftL<+I^-3|N?c>bFsEie6k+sA&he|lLGmQ0?ROeM+MqW6 zs6}-+9v;d0Ly|wqsS1)CWaBBcvF!B!N(vgkk7hqF=?!G9Pp(c+-+5F##K)s-FqT}C z1@Jza#CZ9U0yC@OVz9sxg%mma<3-$Dh7C{z(j{;H47t8C+{SMDLx*{#|3%u3o^l&O zuF%wV&?tqf3MMTF?w(vDsrl)5g!A=E;ygE?)zj_)Qm1ouowJuaxFUTZj%ipyv69Zi zwgiClZ1-PHaw?M;t}zvsJ-?eUGmQwtl;YV=CLMmHdxO~I8W70DcN96&sVPtcAK(r*8ql@}fBb9QVi#$0i{2p~)u+~TB!I)c zw!bHBnAq*|UFqts-zj@o$y9#)n_MHL_4%*)#hGdY;LUP|9s{A3*pmBZU&(Q1u5NlYq)#{dGYDh_kV@an=j$-);pc-%83Z892>KRdRJD{OK zcvA!9V4T{-<6Wj!7Y`%bN>%|D(EH`iYq``36x^u1|7{m;!2G)nVK336qSgr+AYe*T zK>H&JuKxrr2>`)u>i!?EE`CKyGj*W|GG|hrWOUZ=eG3zQ8J~dVE(BxCn~8_pQbPHJ zh;6Epy9K&@hg1y+NxIfNmRFmxsr}+S5Ak&`q(_0T@$jnp^Uog~lc^^92jBfV)&4ap zw`a_dQD_)fdp&eqwRG9;0e0L8LJF+`B-Jq&2LaK*V4Q6O!o+8ugroSS1FADFBsrR0 zp4AJ1!`V4Gbr|ri{CcR{v_gY-fGnXBloJR|BWxJVtFvto0uWyRb3Q4}(E>nMB$>z$ zM!6-Bk0?%bG%Y#lzPcH+2|#&j8MtQRDy3Uy$r}tK>y%%*{ib4RtH(cr+{!anX3B5-$O+;SZd57TllS$EuW9<{`iw2#iKR!CBcpK3S(T>*Z6^D9Wg8VIs6+V$Qe_ zz;P?fNmE2Y2ZYN$16nY$q`*=n0vR1wws%AfX{O0jz$FrdbL(j;ojb6K$R_f*C?t#6 zlIN0p2Y_?~9eod@IHbtuM zSt&hLIk=3-xzmIt^pox&K2P!LeXP?`>Z!-@YyXKt1By)sY0PaomwSIAE>^Xzb}L|; zxw*v(N58|$q)7>AKoxM^G;+{HiTyPHWd)`Tl}5guXQRk9%8XBuP^=WTeMOYSXzAf9SK zuW}2GHTm?;Nf}}X?^C2~+lmFj-l+m^cGyv?LW;u4Ytc;k>+)D)72kc=>Y+#UU^0bs zSv*)vw({elYs{~z5SU5ZY9Ur@x+%LQ%gZpzb*-|9;nnK`*y7PO&k&X+2-Oaw-6Z*R zr$`45xk|DVOC!I#bvPo{0c4h-L7F^5+=Bzgz3C1|ta^3t#9(za`m3r~;?P`7!T6-z$OTUN0J4d+ zfqA~F!sg9+5i4@C_i`6J#S_SoC|Jr}eE-k1%(sEkgg245uZ?bgoN2(*2@vG@q7INH zXG}~R1W)#KoM+~BDm1>B2{N7TcCRDXAi)cHiEqxgf%3%1TkaV#BDfN%*I zdysX?-2688xmf2=07#i+K2JzS<*KFv3_qR-w-b&y2tr&^GCpS;3 z^Eg+QU=by-lm0@d*Ugpg|AYsR4}^44Cp<$#`0IAGDszYG>)}8B zW#7qIHW;=3KjaI&>~%_d0_|=rObV&lMMmA|l4rlMbBdL=yp7f^R=9c+N)8A)ND60M zR4Jtk9{K;`0wJUA*vV>bovfK@P?e&10RuM(W<7R`nwBM4wj8y=wO2GpjZ_}j6kib) z9{D&W2(oU7$tC%AI#XyGeqC~NVFT7;<-PBO*wM+pZ{RdWdIs-$cCJ8&c{v3+p(b5K ztQ*z2Cm_Fc@&Ect`+`xEpr{}8gAzGTwOtnM2cnRwPZjXxRv&ww(#UAtDAZbSy&>=i zV*Nisb%J(F6mx1J3%ghTItx2V5n(IxibwIAB@1dLEf|1)HZ~gl`q_r zOdLO_|J2TmcEhiP1?Jc;?4@}9T_9eY=_%g%z||A|;PlH4WJ)L`Ilg0^O+AP%2L3KQ z8r^q`J0MuRM>i3O>+s@o6s&S>{HS7BF5|m0lR3#@2vNZS_dHF6>5=V5nJ0h@Xq_?u zOpkD6O*>r$35iqKvrR_(m7?vf)24_J`~{jF2o0mj54PVIlw)Z{Bk06eiFjJusyO*` zj?|^Pvz7rs)guRA`JJ&f4b^q)wXOYbfTM)+&rfiyu@EV4P>R z5O8tR#p}E! zXJsikcce3$(UVMG_!m!#S$Q?n@b$&ix>mszK3(6FX?A0r)xvWVA{1r&5%Ga z?pX2zk%N`5+Hi?TlQP6@KWmJtY^G&rC)KpK(I~ec*p+v2)P^{g_ls3!qfr<*{)zW+XP*Bq3nzw)dKEE|p z7NEx~Sl7jTF+J5JLEQU8AhF98LRsxoJ@HGxpbW8@A=7_d+m{%?yQ%a zwJgC=1Xi58?tE<6uXgKQqaB@xh^Hx;Rp;Gvbh8z_gH$ex+oECCZ^-a!`c2NH@a(Uj z@F_g^;`$`9^9^=%9w9zx^7Y`_%@00QpInZS76wbmrXz8K_u-tMNLE#(oKiV+?CZ@j z#DM`jSK=xKyoP1kBDf18fr-f*iT3>c2JH3{ zi=!ZTF|&)QJRWWP_;q-L40^e6-%}?J2oH`#8w}IeXk3CcI(&xdK*X0W;lX1BAYvO|KC}}KhfzAj$lz3=+o+mcl0d{1>DDisv;<#Lvp!h1k zarIoY;fMLWfe3rEje%bfM*Q;qvj}+*can3;2i9IW6p@msb+2D%Msp#S`k7#RN!F3X z?$`m&nqjcNdP<$=C7=l&rQ)$Zr!!24^RQ$}x1J+(F{TQq8Gf`qGYAuK-d=0EetY|X zg`Rttrbboza=PMV3k$vT-(|TB&X9dbkOz?_O$gf_s5fNXs$HQ|XQBJ2z~??ejibiT z1!nuNa5gWj&N6JvI^0+jF6L z#c&e@Uoz=SnE7vlGhn}*IBLJxBY|!pZ)@E{iODYo?@?}yGT7VKyvNq4i`NxTz$S!! zy>NmD6y9r_C7@-(Zy4{79X$~Ms{($={3$39;}4}b{b{PJ4$T)lP~S!aiu41ZKhSSv zra76uVG!VrIsi&7teC&9*;oi|FZKg3Z)0CnW}-*J-J6=KNU>Q~8j zob;3QMx(QItU;Qt0^eTbGnyFIY764CiK?02Zcx7=NPxW^>p*&$?2VvV>imGDH$c8h zyxBjt$;-j+6JP>hKMks^-1T#sq%<`%vD&S(t@r+9hi}o44?D)XjQUpKQ1zu6RiRMR z$Ns6V`T5kv$*kZ{5w<4){5$f;cSLr?D-D|~t%TQ6hV2c=&d?>H9e~bkr-6`FZS!W= zzQ@os^#7O}@U9{?B}-eED*)Z#M!V!Q%j1V+VHsn-dgIZ2}g;3t>oLLW0d&N&*8Gv9S_z=KR>3}~MNpFu$5qeUYXbP4f1nXM zQ@kNj+f8|XLo5*>pFJ@#F%%m1n+a{dppE1MQa~tdL@n7m5rzADiYvAKrlD`=Ui#TK zm2)vGuPLHK;SEO+2J-N_{NU&}$~`_tnR_CmB)a{@C|RJ9;X0_i zJ$`B4Md#Z3dSo5Q@le!bYk}_^oO3m#n%jV(Xb@n*XJ=euOF_0^LHpf^&e33 zTZxE68<}fWmS-OWaj__}f%>PPGxd`}=>PYXwcbo?zulB+kTL^h6`04}?)7pfEKgfm z8Q>uOt4-FEdRn6Q7#;7gz4TPspRs#ok|M!kH|+qe=Wm3V5~Wjv?w&zis_ZThOdszD zkc3`Sj1or36J5J!OhA+v07@0xKnG}ga?ckfmFGN9=6Lon7dzQV&d@;Ot|AfWmDixGS0M$on0br;C8htJZU9p3(UZk-;MWeze^JRHSlCArzI|nj8fsQP}%5|Pm z%zdN~?K!`lZNRkWrUM!XYv+fGa#TLo z+}f#A2*^DE_xzX8?i5=SB{_`PfKAYN3@qdz;5tE3!&{d`p!AMnAmjvXH80hHQ>ucK zg~mZsEX}7s7=L5iP+LX~u@CDS0-vT`wn=RF+NN`d0@^RY=|d46h>Zc6Y@|pgM*{|` zS^fE$teoFOH)@&7fL==ME``><=EvpbNzEO3eP9&vOd;{1$wA`5u$4}&41ntu>G>$rRii=lqa;BhB3_;o* z06m|84ktVXxX{p@^4P}!$Qb8JH*;>-kGAEL^+j&1u`yU$dO$)E!~N5WG&OLl)x|~( z>DYB6<&O1h3BZkZuC^$qvYwUhz86|rdklayV0^qH8{KOFYR#G{Xx)i>&gwcI3Igkl zPnMZ2~gdysKe-I=~hHoPhrdfwwJj9go-|U5f}0xL7p+*e|-cxX7LR=eJ6B2)oXd z!HGeF3G9`skr$9xf8fRTmiNP1SaSBMN#8YMBti>UEl8Y|{ZoJdEg40c3rGgA!YJ3s zibh9N9IHo`-bC4J5%&XlTlWMZ;PHqMWi0(6I#tW=X~jD=uDalno+M(;ut~J`p~s7< zS_6;{9tFva1-%O7aq$N)p2MnC9Kb~$xWjnphM+(5P6aXhIy#)6egrrIsz+{cF&k78Q^K-MP!VQwRn%@Ut8 zOF>~S!4whVODAV#XYaLiD>z&<^M$s7(O5M7L)7)q%e5A|K^vcA=$+hwO(kgYNXO#u zFK_Q^0a+yw7X-uMQTge4C0M%3+whefpgg3pGYq=Eo^H@e!Wahe5gTjpAo=D1PtAS> zFm@4G@+7-Y3z@hg;7T0-`R&_)(Tq6kwC#q-02d-Cp)L4&YaqoNrFOVu8q0NATa^d$eiR(1g0pvACr z<UF4k)$|q3P z1Vejeyl2>47tvfK9UTUb>-EpKM`0y)!*;lPy%6(L=vWn4j6(am*t`X{6~QGidC-fR zWY=aggOX}!5&vY=F7SdV|)hj-)l~H|IPZ8^ZQJ-drC)C9kA&FQ9YJS2N$dK{yu9<8EN4 z%isvMi@c<_w_k0B4!6{J1?N*VL5nZ66e!i}@YW1EzXuYg0h9+N4J=%5IF6>BGNB*d z6u4?UU4W+H2IzSKC>#i`RG{D;B7*>8_Oycz)+dwC&JEz~Zf6k>jjaMI z>L@abMq2XKq^p6dDFd#b22e_X9THu>i4_IVVVtR8)7L)b;|^Nr-_dr_=)JHl9o>KDb@ym_+R8tB3APY#;zIMD~Zz`x-Siuf||GEa5KRIYuK~K{CR-|_f&bm~j z7WBakgz;y>xFF3@4g+D3uB|wze{LLn|64FOvF}w=x{hVla~$SCj15>1G$(W+c~&)| zw3%T2KKI|msR!5v#DTsxwev4-)MJ+;baR%rQJbKHSm7de??b1U=R`GsQ5`l9eI0fY zX9Cl_>&PE3p=mD~B`&Cgh-jET2>Z&tv?2I=t%XqoOa=!5NYxDq0ov6eF&ONp0jc)v z>><9ZnWi zo*&hb_`IPycHj5@SERrj5OB$qy|e3&9j3aEvJ{Jj2lqbE6M!qZjX?llV}CAD?ofNV z2+Be_S&!W7c8D)xr(WdH83|<62;lsL*49>{6-tvE-)Q&d;aSkoxFC~kqU2s$nl&20Vp3aWaLrzLu!ac5EH1r{<~yN~|r9wiFR zGk9Q^cnu^aiJ&<-`!tmFC;<_I#6U>PaD;}WbE*t)_sUq0kJ~W~_NlyjF1{g}U#;i| zKH`*9_qXVVG2ft=tm;ENIUl;mx}lj3N&jRFxx9ZQH4C15qu=!g-a7+AJcyv3fuXq8 zT8h|5O`&~`h+aPQyhu3Kd4eo~_e?hQHYiW#q0nlC{yZ-{t#Ap~*YnuM1$|Dp$Pj%r zqX3w8U{`~ZOCH>;B7-q|c@3YJ%8v(vS{d^#v`dhw-+vmalxxl9ula)1WUts;&d9zp zT;qBF#+xM(hW5-UBcmwPP|dWNV9WS)muD(;4{AaqgEyYNZfK(`8%l8H(8Ct?p=J-7 z5c&qo+boCZj)#kN z7ZU)jE4DY=1;D7btFY*_gLf_9636@7hAV7=3`3|PF}WXQ5zsdfPwoKk0qi3hSJ}I( zqO^OP(&)PVEMsHVO$Ry{9yAqbh8$NcxpwaL`h-D3nUHxLByh%iXN4y|PN^=#eDpi_ z(%0>DHT}w0)Gq*9?dbazgGY44_eNqfy93)gq$)pOM%4Y$$)Xuem?$BT|DyCl9z==4 zxs!UR=y6tuVCkC}7mMOoZRJNW-Ud5=DW^KZoW9;HI^9?f=dI+ZxpXFhUWJ#K^`xhe ziT>$+5OVf1B@*3e4*M&LQo-P zLB)Y`y&d3T=f_N|YjN@mv^`O&GS%~(Ey=HYPcAO>@inl)kOTLy3)4GPixEv~56?c8 zbMJgK|M`Vh1Ilg4ME(F;RwcDQgt9>Tw3b4H87TPF&L_qAbJhGrj33CH7r~Gr2e4sc zRuKs3aEuXV-15+a03C9S5FC#J!+YGG_~{eMfb#T)^@Cjzf@mmgikKj8{df$~QfCFN zp@@$$0~&Gu5Ne2^CL3+CXwQNnL8#nC017&2*hJNaPwCoS%3qvwgcHaT{#(@3n%~hF zT4G$hAD}UO9~TnC(Bv6ntN}9T0H>50d$&N60R4J~z*V@sO~lit6n&TQ{%^hq1(E3l zwA$cs2h@RL{)9<3wrPW!s22#@p^d4Ze_pROj{_u1y9XsGM{aD7N0mCfkJxMiwZKj( z@J)6wXruXK7H0ZVq3+%<%lLfG25?L0 z9JYs9I{^?-;|k8d7`lJ=#&G}#601#>{=oPpI(7qcv#EXD=ZE<7$ex|i!{JX#eZgFu zy&zWdytCn&Z6aucvYvUHKpXO}8};9$l?$!=?iPxB^cp>2v=l0cy`d+sBp^gJrTY38 zBp)4AdAnDiZf$jMveI3>-+ekT2sBNm)n1o}F+84$0c>&PZQcNrA=9*FGxl$Xw_|FJ z#5S2?ytZP1H8x*M(0PXI5g{&*&$MK2a2;yM|A`>4wC7xo6&AkX>3t{sFXGZH?d5@AD1W*~!khx>AvMiS?PCj_dm7a9G+w9Bocg%1d=(;j|*2>60!@DVJLCs-;uvi0<7YJ zyVOyY-AhksdVr#`_`|)EOSGQy>x0pBm!GnNt$WJD^ep7gjH=S7MoV+4R|JT!zTm4Q zk~x>l+vDN*L8_HvNzS$XR{m`sr!$Q@!UFZwj0qDIp6P6HzCa z9R{>|G8xIkuVWUsA@hc+$sOfek+PgytOSaNWNgKlWNOq z@ib?By>I1Ntk0XeHb>Od%6-_~cZMJ6w2?5Tm3nG}LG0M_>o)l~PP(`J>ukD?iv@Gcj>YQ7`9JRqP zC^`j(SE*`YdLwI)p7c41!Z}bSN0EU9$uRt=OE3r8en6zdHY9*@7W(38xnB>O=D_hX zrHmgUyd-M0)(!&oH@P|8k>E!UD-u4YA*#=Cyo_44MD-FJsLx4)x)=4UEj6L$HU3Bd ziDY4lNhViq5D?HMuCDw(5Dqb7t$X^wd#sQ5b01?>%#_UGlat)BA#CK&1UFl)GfqV_ z?QPT(%E3YIOZ-16a38S(KP|)h`u_v#=SWEa+4TFB!&&4*0ypvtjy6I0Ed^41pamCH z8oIX8Fj&(Jdg0%9typ0I#dh4ytsDO8i4$69-(WFacAyhL4Wa@o0q zbr&>^($5JkmN|pUIsV79hZs=XINsElehKaPw_&nRW_h{l(`y?Vk>`XtgWqETkTV<# zkO|Hk$uN|H-eEhKp6Jf(+zN;gs)3LUQ3ApVieftiXfgC7bW_%UL#=(b8$c1GZ08U< zlt6iBzqm_BBXK1;2zAuD?%F|-H^u^cn4t?oCigNKn#V2%^Pde z>O#Fn!6|o&Kx*aXuA5e@Vr<<-gckTdE{-axo*$Gg0^YUQe*#)G)R1cois}3Zc(PEu?qk05aQDl#FBur4L9t3o0CU7|T^Sf)@sML8fByR`>LS5bDG1Uvx0kH!L5`7wQ zBg0tWVuQdR-yI;vmH@Ao0_cYZ;10{8^Qr2bJ5k?OwGP<#LX=WBc7F}9 za#5)KzkBqiO+RA^&aVDx2*ALjI4n}B%?kjkAsjN zlCtMlj|Ln%g_$?@0eS%r=XN17G8t+|oDzp11di#-13nk%(DCwVOQd{{(bnxyf2`o) zhMW_jF~+nk6v$FPCyiVRR&UP~KvDI`eQtKFGymIm^l#ch*>t)A4XFEj9dlq4x6wi= z94)|<$3_15UlHYEAbb-D42|=^AY^aI+_W_-m(fUDi1S3U~Z@*Oy}u1drgef zMh{gq&|2^*YSe>ib^yr>Z8ot@g?1uPYIE5=C@Z_Q~TvKXH)hp9#!HLS+ zekD*8zDm&us+v*sYY6nl;TGUK{&%2L(0&5P%!1wZO-^ZqIP}|rX!Uv$q@}LiTxfM0 z1`P9+uyXe;L~1*HyWA6#xzHve8)*x0I*zzRRDc5$EEKTMgt0L0iGyQ5bpu0vynkw; z@&!OhWJv81TQo22VY?IF=U5bXK&XT-yKcN*Pr23_%Wi8HiP~Y>wX^e zu}435O{^{eYNgp9SP=JxY3G&cm<%?Pqs3c;rEE=0r)^!bU8#TLiD?SKX*@VO2GR`% z;yG4f3scA(ad0RWj(FqzZV-Gfa+uJ6z#zrrz$D86VOs1PTY9a{vKspoDR?(ZDwd&e zqjg9XLKo~1&dNb02d)py*0YETK$i)QdKW?loT(WAI<@uB$$)bccvxw;J9s7%EWis)gv`A+X3wHuM+*5lIj++-oj6@2ZS9^boEYN=_r<#>xTV7ZoRWMjRz{MM z6o=zA#pvvU83f5l`rt@p5Zep_A4T^WN5mAzX8_nI0a%agOtB~nI@p}&VYZ`N@{I5{ zxqGdj_~2*IBJyllIDrw#A~KTNMa8+Ey0dL!i#4;QdpcxX8973s3>Y!O6e5dfjI&%L`0tTmZ}>?^`1Km~VM6a)TXOiVxm2}d0iY3+}D z{TG1Ib_9e2hDc9MtO60Fj;6w2<)~dQ{|!6dCY?L7FV&|ChE-I(et6b%2k#)@sm=aV z?c=DB&S%VyISl&)zJzKR2@3eqnDp z7cKK93=L?0RV?d_i_n9~#wn^4nj2s^8FUOAq8#T*2Xu%A=3zslWP#>21w|99`_G%ErtTn)0Zu+g+#IGzv-~T z`oWyl#r)~1Hbv{V|A)2rfQm9(y9UeHidhMY3I>p*f&>GS45)>HAQ^!c1r$V3Kyt8K zK}AJN5J8E8WJ<{(sYOMQprn$lB3W|Ivrhrt-^~2$pP99$R)6Z`)dPMPN?GAV2UAN~x@mgBTE>p^qm|n-AIz zJ=g_dK4C&RIn^hnJ5K74sXXa`j9jPtkNG=J`+1R)m#Xf+V7l^vx{mH=!WS_|%{vJB zl7-TOj|!B_GT3ng6C?Gw-p_;!Z;szgH86}gOq%Lpya%5Ez z(A=khkEMv!G6tHNED<4Ai3W~`iU~r3RHlsH9~$cEMxq2%>ih(7EjJOTF&!es0!L2~ zpLN)%HDMCw8mVh}@{xT4VPTe--w#(odo(?C!jcq28Q?_}9NUjSJY{HTNQ^nj7#Q)^ zB%T%*)q2RBXl&Bc5~nUpqA9>tOz@(XzDBxWLX}|*(}Fw>qVFI88}a&12%LH>6h@mdo!v307eu!^GPI}7A&t~a?ix@fa$M5 z#zIebVfw$A@VYIeq+FJLr1}Xn3ff}QLJXoRp3`qp!02$DKwmxG*|u>;>=L7Ugyk~- z@o>jK;_L|PO4KT(xyXOQN6ey_C8Lhcb9hTL*<1_`EB=C6sUl$IgE0>)*P{y{Ov^d^ z;3#~z{7G#GvO}9d?a*fNqP8oPOu5I65{E>DT9@fbB8(tvE7C2H;u#XB1W5M`iJ*Xx zX)u_!%1k0=d!%b3OQHoH1fL;HN&u5bOgKrGM#!)$D6={z)&+$zO9(l*jnqs_jSfae z>sN)0OA7JN%-s#_{$HW8gZ_i{nuv*H5LTT5GgW`WMw5~G=l~u4U9fpII0F;18_%B% zDZ|{6LOH=~Vl`r0q=5fb+Pv3bSOxB$QHxN%JYn{^^9$+(8q{VCL@Mc;r|gpH|A7aK z#8)&u@AeY*dAPE0W@JK6k*P#q#t*@;Hv!aUI^ZBvp}7Bntnw##3uzcZm90TSp`~Ic_qUhhM zz_?n%ma~Ouh8nm5RmT%p5|hdF=!E0zvGY-EhMWypk&M@A-8w;DE};^M!}#ee86*x; zj$oDP;#c`36iY@Q>7ckigzFN-^BMQOtz>3^qgahoTR#{akZih{3k+aS7Xw8ne!5lT*(hzAKo*{B8N_JnzVQhTNbgj(WMiY_%z!ib@M&g78kB5B z7&LoAx)j6&vep>C&r!axVd^(bTu(pT@qm@8#qL>0C{%H$iHarHeP2Vc4ct47fih5O zq*{n1MFvq5?I#TV^LCQXGNzMJ zNWQ};w^G?YwxmH!N>vXnjg2`QXStUaxSq>UitP5{w8FQ{5AA03cNt#1^~t z$4Y$&tM;<0Wrt$GIZktM!$tLP$Hr!rx9k;*D7uHr`hU55&$vWUBoC{lEejfd23(g2 z(s}an0{sZJDPJP7vVmBHlSoET92B?lgw9E84e>ztobb^y!Kg^^0VBzJ(gaHyCeJXO zs0pc_F=WM&7+LcD6geGCeneN62{dST98}DTb2fY%mm-toEJ%HHxQv8K-n zyT91OBjh*ne(Rl{VhmXZj@R0EbBkY6A|kcl5Wz4Fbs_*4sPEJ)O|kj|P^gv!zbYYh`q;}3&7?&+^X5~dS52Vb6JTZ8I6-(`k4gu4OY~u5 zK$8cV2{HVy8b<@dSn_M51B`^LLJv;C^S_F77%k4zkFZ@dy^#q;#M6)XjYah}--ztl z>9n7C%Y!AyytJAuaQTmiG(3=rYMIrT$)p{bldr(W_TbU37U_6M+O7cRS8Ex(7zAl!f-(XxIuLByLpL`WhAlrgE8%xoAjqcEVp)VlTst`93O zj*oGb3(O9{**;MUFq(w>YB99>PZHx!PN7t~dc$;#)QdnQtlB1#OcE={AwEmw$wzRw zjyG=HRPd^$A&UrlF=9*OA>tg88-8-m3#vsobGtpA)nuHPYGSf)V(0Mekdak}!5b-I zcW@Z6RxyeN>=GXzPrZ{RL-cIP-wZs+00Y!Zx}?=45-aTF&qVb_%u*m5QlM=D?m!$w zDkV$cqWyz3D5R$&wi_3*28$rO_an03-aPISC?206h3ufBJj(!5XS*Q#0*pKHJ%W=i zn&3hOSSvFJHFR}5oNFTEE>BIhr#A=E>D~3*GiPeGDnRUCMta%_w0|dQOvf**#pr;4 zeM7pLL^KX|+3Qe34thbC(t>sJnG}O~F>mt+$yo`c2!)M|wi-9Zs|;A$$;K!crkYS_D{Y*YO4>UIm!UGyy|ND@$x(2|h?FJ|dYS*cbX9L!$jBx`Od5 zV~)c^7!pa{*PEcBtrX=OJJSrPYMVkj3u^o(O`+B@^=BeDA|y z&VnyVal8v>#zpybW{0lgTrkLeqbRl!H_=5uFaxMiE^Qz@k^d!R0$j{r_ZcVq;XuDtWQu@J!S<{o(S22VmoSQy%ae-Oibb?LG59JEu={(sdr*>>S`Hr@F z%lweP?zk3&Y&pz&bl2NOi;f=BVomyXPrHUi(XXI;j*5^C{*hW^b1Z+HUJx(~kpO=NzmY#&rrzzzcS?_#>g+$i7k>}eqIThg=&6yUM;l5O{x#2=`DGSU6Ly_d z3CC%y8#InTeUvk5`Yc6=A1D&_NxWSyb& zDU^NdY3DTy>laZt7M;0^b5Z0dmeAEO-uqANkgQIs9IZb*GhvKd2#?c!YXXp~ps+Be zr~1Hw1L?>uhHo@4zR@gCy>`K~3w~gb9t}Q43fwDk>5JTtA3yGSdXnRNh#~1YOfB&b zW?`(W`4EjxSa)P1O5?H_F1cLsNQCQ$auH<57%%OM3t#o<;5 zchgJcfVJ-n;72vo*eBSueY@^-{zrDM)SCy?nCJvfgW1i?D~}{6I9Ll+lh=zEF~5FY zJnb*3hPqGLlS}%Xo&%z9b%L>Hcyjxn6;W!OGLET3U3DEjli;`7Aq%UHH}t5C(~U+Y zQ}<&6Ut$Fzug}=lztKFJqDUiuUhmj{8jY1awBdsVQ$IG&tcdSlvZfdcqdiL))p<4B z1iAhk$FR`c`|;YbXOA+KN==7)((Bo?h+DT7vazzZ0v{NIo;c~!*KM0+ozG=D3|9h6 zEss(Y3OBu46)z9aCw<$tZLI9= z;EhvlZEa&_*R{mL^ks;-r$%!N5|lqCt~%nR2|_t+SKey1QN%uI~-jsF9xT z55%uR($>MlEGR0f0CpFPt~1hpu-&jrVCT+LIPWZ`cH`d~B2Yk3ay~!gmW>1Hd}1tC zq5cZGIkh%YG~<)Cnv=7Azz>%9QBcCuLM2Qq_h*vM_e*op^NJWPF=-%k-t`x*ftQXE z6%%9Q;)+8}8(J=xb*IQks5cYm3|u0PGmHD=C9PV6fJ>hcymC1kRBD0wrdKnYEdFO~ z$IAj=KJMS!LLz58wunifYk3K zEz=-tCptLg8#j{OR&H)zeZ}D&+PApc+H=%WlqkO&WF3ZLZgf_MVcFaN*0Qs+8)|&C zI;2kSgX{r39AV2PO`jvZJnvgcrzYR_af;nl+GL3NIlI1@hKE84Fka8TPA|(P5HX-S z$C6KH_5{8cU>GGMk3E2y zZB#TXH#Z#%rUSM76TC0INMl|;z8X(|r`21<#Li-Is3S7G%gyzE^e6}uw5ca|V)L;0vyj*qs+QoApsu?xg<{$hrtHYoyk-@D=T6}jc>nM1hpcepEz!b!S zVF35Ot1kfLskEnIsHYw0kUio-uSLCf?b_AOU@4qo>BECXa2C6sfhDH<(+_Hk4xI5* zhC!ZCwD!^9W7(XGAg*? zou!~62GDJ^A=b2)g?lDnsR}Wvh)SDSo+~l{xU&OBpR`cOpwB#h9G3DaQFCWHL}3-t z8p66@(MasZIc14vHG7Mm?KvVKARuYm6NIzTZp7-o1-59T+cdpeX)7}V>RJ7Gwu`um4XJ#-mMYl4C>PeLuKs*J)7JU-$wYL(q1$B0Xrp zOY&rGict|~(Tf9T)uYwOP2+GQjUgX@|6No>gAATFtZkcTyIgS2M=MA)IzrdGW}5iz(KhoiGfLx=Fx)q}3jihOEq??uPDx zrnqO%)+b%5QbE8`#dEfTZ?6f+D|`I+0KPo}lYM^D4#~OFSb+$zUdwa8ph`#1iB`z^ z94~+L=r>P7wY$q@rIIT~qNIhQo5*PzY$^7SCNG3WQfkrjeTrL~SjWBCKXIVuhW5#) zJXU8PwG=~D;m8lwO_@1jM2$+xh={Px+ye%RqNDhza*_i;T%d*2bf}sszLLG|_1FWsR#vB@%_SWOQ&^2o{ zTPK+^k~Jo#v1ZaOe*9IIL=J7ajLjsv{77$a?-+(%kdiVLy6q6WhZ=wV`lUd3zZXmn zvUTM+aI3FLwcz-%r_1Bb^qx%?&)9z8)aQ5^r?E>w-3p3}E5T0$1LizQ1JJdT+Flv^ z8~}n!d#1cuOF}{dW{4fBD(B98@q}~BoH%vLi>IljMMz96q1M%D zu)Sgo7#tn5N&I{Fs-x!k0vkT%;7so@?AoEmzyDZefAMtcLvRR`9FGiTf$oxsx!^o6 z!L4BYAG&jUjX2VWQ~c5+o=6Dq*j6^t)0#Ok8*-e+ zk~`GLDr=tfbaT*9*MtSID>TUhW!Mk-KW@huZm>8Zl%xT>V(JJUIFL z#4?fD^+m~uwe_}1GO0LKm5bqD1KBetPYMlJhx8*!_-QXN>LCx!A!7!WxZRO25Jf(>H|)Rco%a#NVaYng2=yk=&Fo3U7-v<<9erI-SKNayLF|qWD1xk9De78 zeLy*?DjvHy>&#uXlReL8O^hf#8mMq85o-!En@OC-X{o$al!BE&E`NJxT6I>dv3tnl z9~3#f#qMryCU7gGv3Oj+*IdP6gUIt>gV?b}ix$yBf%c`omlz-FJezX04z*D5moG|W z7ZL6K?VMQJbJKu^4-sj+Xg{UeWm9Z6(XTw>f7kKBLVn%2I8ATaEXiPO0 z?~R&q(xfP9A=s9a!kAPbn59a9!ZHfcv<)GnI@#~R^ZXX_Lc#N3TXQVkp4JI16VdKVVlpgyX%>liU+ycYbm83-4rJbmRJlWURmuTIg zJ{zrRp%D@Mr=RaV&FyGBSqWx>I=)la91zw$DmjJTsC4AAnQo8^$m*PO8 zlM8>;B%76?P57;k6ISJkbadDkRw}LpR{S}RU^s^R9|V#8)UF#K(koP7k-BehoT=ZC zLG0xzIVU$A9BxFfx zhoN#LIgFVa4&C6TE?v5`I`!I1fHT1et8z%`47^~3YO^Hs20fg5dKL$2W~93#g$v2Z z=xyA4Ll<&M@Y6b4q8S4yc zwtbc@nT=+yTh?97?z&u^7)h1|=HVPVH-r_r3m3IlMCbfOE{!e_KqwM*#tGD9BHa#WD}{B|jR} zPE@&+p84)}eZ7Y6eOL|AMP^(9(_IBpqj&hD>Ir0mPtL}f#%s~TV8_jLna}KP6-_7Q z=YC!PdfIYl=>-dy3o>(1&Q`DKBqyhc2Z3HQf145%HPd`AyXW4g+>}VS@!nPIGT&)# zPn|NF7!z$wqX^ztU~W5bAKpB-)jXa`6x^EX%Ql!5+EXacJeWVQ>Y!!w{fCxY z9E^)kTOBe}&EcbyA1lwaC5TQ)D`_QYcZ9M`-6i(oc*eE&u-1b$JtJ;5jCdWGm2; zdj$ksoz7A&aB+gt{cd-ll${EC!E(&X4}$y?Kr)1dO$F*Bx)431w5A3x_E4NHKlh;N z_?Xi3ZWn_$_O>#aRp(QyO7KU_kIlmJQ=_O}BT$!-GJF87qY0|8p`6S_xq9#Jjc&o{ z>Dr*&tAz3_wm#Fr6bV7E3TJOeaNav?ck;xYccu(cyL~r>w^eVMA^5O-U(XLwamI5S zgQbhns?WkJs&}z_z*0v6X-xroS8wFcAhc@&qM|A&dI~;&K6~cOLsHcCd_Ob^MGy(3 zH@fRPR~O?7AjohTYnYx2Cr|{WrK))kH#<$KbbkAs+GaCI>S% z5CQV)t$hl6wzKlxnZ41~%O@ngWKp)F{|JYy?ZoxvjT>4xSkaY!UPGX4C#u8`Zzl|PWVB^ zts6r(Z_ViDDM6duE}}o;>KHek|4$D6$SB-81orJ?aCseCXz{`o==upp zrw6<0bV%e3RY;g|#}z*D zywg|m{rT*#1<=*o;oFCn3^f^A8GD$DSmUXpqAuHh)A2Iww0Dr|8q2*LV!BWS&JAwo zSG8iJzqq!fsZof#pyplCstBhIv^iL7zwrst|CUn>Uf!?|i?zO5>jH{vjs|Oi&dY6V>bt6& z#;@!ybhYv3YsnwqwsWqiiP)l@zhK@6{0@BKLRw7B&dvKR zM55LFe5{|&ynsu(=F3)1^#ll#2t-1I*!_1t8%h=_T~mcrmSTISQEF6jN7s9?jLpyH z9tZd9OO(A>C1dS{57(cV+r=C+lR_rlZsEK+ zYnvBlnbSxMP*_;FR;?H3y_<KrvySQjhkOPRAW z6w1W2t!l58#lN#5Zx7mL`Jpd|@S2LbX$DEa09L*Z7X7K$pt4FU+c^`K+%FsRJbgH$ zHoE=j!$CPY%y`3`%_Zy_V&z%I#W^?gBFzN7<6+Y?|_N--@N zj@@etX9lC}Ooe453ZtmjXh}yk2yd`-7;C}NK@sz%R4#{y z^hH+u)1Zk^tvJU|=WdX+ia=Sn<9U%$UojKP4dfFR5Uh6ycHHvsoBE4LqQ5`3210>X z9EN>uf`m_6CZ4Kg3E^k_qX2QW!)?6(Q@`V`pd+L3m{(W%5ze!A-8#~-xn4%1l*hKw zea3BW8QhdXx4Zst1M1qcVMmcy%RcGUa`YooSBfYs!F8JYU`{@5f@ar|DZ9^6;b95e~jyI>uBU5|F<2ivNI zSZB>^%qb+G9HKjSWE47=MVR1fUXghZ#3ejvxCx0noV>cURVfSA-%ul32fx&HNR8f2 z&CTerooFmN3?P4WsXX>JZ^O*LW;Z5aryhcG2ns6DtR_aTkxF_;cf`;6l3v}lX$gQ$ zV~*-z#pS=n=2ojf5qfb#^1YB~v8)c`ZNa+#al5t(xECBh!9LEAof>(0_w6-BIZ^>V z#(e#KYycosi8ng66H+j08Wr@j|JTc?k^j(KZV{f`TO5BrMO3fy#M3=pd)EQi;1dd$ zlvd~+up&9JR5Go1;yKZA07bRu_Qv)Tw8P|;_J7%A>*C8d>q<(>D+2?A-+3^r+H1c3 zD$9tH3=$*MbzyLH(8dU`B2X58m_JOn0xC2z2g#wj=7Uhvb|EbRM6JR<=YoHZO<@Er zT${@Xin@62tR}_}rvpGpui++0d;59|1 zTYWSy8Z87(&6!1uZuhHL0*)e>&ZQN(A|yn~-CmD8)0Auafxn#HsPCad-;k8uTm?w6 z^*Fy?;mnz2m%7BuXV=*@1_Ui>!r_Y91U>dq)ogPX$i1z{8CHi4{k;(OT~5I^(G+I+e&q0BR^%^~ck^Mb z_+8>5#sEhqMk|+CMwic_vy|_D@Q-mKItKsw8`|v_oM~@##sJK7oi|3G*PKuH@T& z)6%UT#iI8N6h%==g1Ez+|(USAc}HId-3W?T)A}k4`-sf6)E7 zWmFsMTXu5b$z&=TOM>F&v@qE)!z6l2y3&E!9h+MCn)HCXl?pMppCPJ{ap|I+a>@WM zN((PHE-$cAXseNVkSMy$-+6%GcXVNcsH(|(_XYphTV+xyVnVbg>A*A{Yn60-=tVx zLVtgoF^sne`I)IYjFLQGl+cc}fl`Ao*$D>h|@(T$?AMSt)$~p9{Du)i;{Mpzz2(Jh= zBDA9YkZm1j+B9O&sAt0PGO3zDYgary2gd_7EtmDk%k_w!H-9nLVJFiYzCGI*4bqQ!Roq zk+7XgmMk*m*>=hN{Cq;zguY{$f-S!arc#uF2oGT3&ICgi=}@!rP%+|^2t+}xU6M$D zy&pf`N+o}XRLpqy(HIBxld_AS+fZ&gdj48HFOfi-O+Gm+^8z z^e(6C%WiNBChnPI=$MM?X_vvJgaJJ5z&^2XcmSsAC{XdIJkrp%;k91AYYN7Na53Z8 zE+eCFqq3}6Q2`9y{I;8$z%{05?i@o;Jc$1j5*AiO8(D#f znP@*~;%?V!QG^HFnTHlpXwVO-hirM;0EV1MowTwgAwGTp{U()`Zqpq{*ueBNSWap$ zkFB7V;*1`J7t>Iidj$pxQ$gO^`a0~cHU_B(8=wSH`T|N$87?b5%)QU3w^GV(E4*2jHS6RnR7LmU?D1@ z-+)n_b&yP0}Y@Aoc?2a=dCjP^PwI|pSeH3_)$&Qc< zxgu7*Kjk+|+n0w~<6kY$wa|=?$}cD=MK{Vr1*R!HT>Z1f=J&TB7I7ntxR?Zk4=lPF zwq)_C5Fu8~Spe79Ww$&+Y$RP8T1Z^ZsjuU^J`>a{5tBsi`@z9tUGlc2*!9J|u2xWg zUGwhb^s_g+N@HW~R?HFW&hPK*SJ?LY!tm-VkXgWOJDiXl2^j=l>lJv#5AWYc09}Wi z{VjatRz+e{v1uRV<2w$1q#P4^OiWn?_m(#l;O`$xh&Zq%3a3w3^fuiV4VT+W+a=wOCq-Q%^3IjE;jC353{uZADzdg(&xXL_gCv&J@Fc@ScaiNWx-4%eDw zD3sbCU(#iYv$Yv-@BIs9bAWk5U8NM+LLLqv1@M`sKo3aY2F=`$zH0PCDSz|@BqKYy+DPTt9mJ;ij(;07{tGOYPEp zEr;IDFz&IhZzLI$7(*i3loj~&>8r#fHp_ZscZsv{TpxWZzt)N@H~aSb5GqqO07`)Ef4<2aSIKej!v+;~Sazrpwf)eA=dlxMslddB z8nL11aad|UJI2DvPlAoOD%=|e4tXtysCu2?HI-k$B@ioe~63lc2kqh8+J!mi0$dcK>F`sd%W!O zOb46rVN_$05GG?Q(>@qnTd(LG8M5ZrA1gO~>G_enIjU~~^-k5s2on^3m!^H9Md^ql z4vyv@bo9Iix!t!hRScg_|~(x z*yU04xcQZIec?zEDQ+~1Cx@B860#&|3slRXgVRXVf`EV@*^^Li_&$;W9WG^~E8V4+ z@$G`cFz;czZ(v;OoPWoxa0Hq%IZ`^*hdcLvIw0SXTH214p~%nDbmZMTR<5(TVGc2S z88rnpfqJ`q@+?ufB!Z@5f z>IG2@AC)klcqq-4lJfH`vDM0PJgf@+G5{B_Lp!L10zUc62-&9;OO=<3<8C}dLP8XD zjSOFL8s+g{FbowvJg7;=|`iBUL`C~dDM$;R_TzEpxOPqoeU&(>Loh%Q1CsR--*-h<9XxBQG@?w3#qtk^86irST{E)RkDDtF{LsXH>z&VYdk!J`n|y*sW}qBu~Nj*L$& z#eA1)m-}Lr10m3ekkaS*UbE}AvIytasc}02pO-;^90o~?31lKo*@ws>tOX*=wRWE|!NJ%c!&iLM^cqIKqR+XDZrf%(Z?K(7L)pcR+$y-^48g|Dn-=5OI?u@VYC-ejmV;eQO6jJK05c>()agm6IV!uS+L| z+0;`xCLey)x}iBSfy{|Y?&pUblr@$uXF40Q^{c9?hT;}<5&7O^Dr(Bzb^X&*4bzq* z=_u&i5r)!+;Z<@9gPiwqPV-Ar-hqH29G+0t`E_5jxK_f{*@$|3mmTCeD$Uj?+lLxa z`y6s-!jB99U+ldRLdv zpFfu&=D*0Qm2eokf>(%t@BoKeX~@hN`CO7)0T1sG$<{_0RQQEo~}#>9gseJV$m(oi*^;a@oLb z^3zhg>n_;c6CC%5`t{kQM*p0xZK~vr-0ibh;c%EnXv)_%psA1??mB zi*X}|Oa@t&$d4~yx>Ommh|d`-5ncc!eva@;8M}4@uzdsj@SHkqN*Fp!Y{oPC0T489 zG^&=*%-r3d}A?i@a&7%A0XLJC%ve z6%~8d{a^%7U}`lAf7T8HiLfq?W501ZZJ3x=0?$2&XFfVmWrWr=5X7w@bqv^8aAIQO zS_X0V9dfA(gde#ILtt!6E(*y(Ff-LUcUcdhm<+IM-ax%Qttbh9-nFK{*-EAa!m;x^ z(9P_=Ry3;>J4pl7u5IMuK%q!Qf|FzG9#yQa-d6Xv1Y=uHb?E_fkiE+JHElo?h}tSF zG_)0?AWGnj`2nn2!}V_UF4Im# zwurLiChA;RL2xqpLl7)H)LD^KN;1(9am z_lrtY3fQtIUDhQ;b02K{!JAK=BfMRONif zqi^57sW6BSGXcYi6c&v@De_k|Iu8XJWJDpvR}x1aLKH{rgceQV_#g}YUL~1#AAI}N zb@Fp++4bJ6t_CFTba(c3Y_R}VP#?fycLcJEBTH)HbPv{{R3e4(AZ9X|0xtljXi<%_ zcVMjo7NG(71;~L)u_tUOnziM`DL!W13$NfRpQE4KVa`PS7L>L&T6V(V%I6^TH zYv3^;laX)*QpUz1@QZZmm;wF^CqX^NUjK92=N}Okmpz-fxVS_*I~d3?XaIfecSA7s zJ`J6(x3-ouaOO5By}rUgn0G<4{v(;-ukqfC-+a1>&`}qlqM3H@bi@{KW$= zRxMrn6&3;E=w5u%jzKymGd*EBh*FjGDqzAT?D{Uco1(1?$L2Zc{{*mRIP2}cWIu_E z50=UVlFdzz0lz#m%PAG8FqFn$fyjo|B()87$12vZ6dFH8)9I(_0_`LQM-n$x5dOiYyWsU!rYG_(KQlo!|b`sDjuMoD+qQ4V*n$Z_&2DgkA3;Vw)c}ynrHS$ZD?3y+L1t#%dxat2s0v zWv4&KR$Dyj?f87Z#V-Z}4MUBjhV%5$&MEPeI7zan%>#7(rsx=HEPwyF=hlZY=m!`h z-cG%J-ofr(ps-$#Jv?az#Kl$d_yCOTvU%{cMR;?6U5NZL_YPoqjaL5e-ycPZ;WK_p z12BgMVTPdR-9{ZF8Z$^aysBwaV0l0?N*Z!+#Ac5pz`$JPXfO+iE6Xwwq{Nb*0bJHd^o1|>NIRC!O{<|v`UPHh0P+2zV z=~xkt3kY$%OM_8DBuY72C}170gBmgi4rr;Ie^iGO-rbUzmEY}9u>AFDO=oS|SsF>* z*N4xo`H-jTg8901P+6O`t6ETo62Ty1P>T{zU5dFrgxh6d#xXi6XVTKrAQ4QUr?(4w zb~b3t-{B<;3~|{cf~7L_V`yp!=+3`m$E)(7I)gXg^nE~3qGwBo{C41 zs>raj)XxC*wmN8&U{hoJQ_C=8t?h!`2IHUqWd&Ue_AGy*XOw*{UoUt!6%+cglc^CR zDH;DeL1pBDFdj1agENXUc_J}S!m{+*eIq19A*v)~PAJ<|+c`=53!M)yl>i&L%k3gx z|ICW8cP66Q{Cojxe-uPDp5ucZ8bi;cgD|71g!G&2GKtU@fp3h`N_d4HI0Dcw8LWY> zIZ=Pz+O;9TR^CqBClC>hC7}WRnvl47`GxUeZ`Wl_9!HNJZG~pM0wI)qM;m$~23#g3 zZv56H(Ht{aVC!aihZZn4_8E>H(UCg6qzj?Y}F2l6>gx>)U1_gJ}dzLA*)H z5w>p*yO%}u6Vf1!d5a-HX#>aS&J&4v!L_0(Ry`UWw`riEzUm4W(2GQPKv*=5JP!ZB z{ntW1-!3H;5Js4;WM^-)Y@NK@P~v4Hf8xX)1f}`Wm5&e=4bFfn{8=g43>62h= z1Ob0#8KGN{PPxX-a*wI*sp(J=C>e%=E^T$cu3%EzaE?&e~%%D{gy)DYl%di|EtWK^35PTdmkZslCZ=>Gh zpWcjns6>VlCvYpMt>^p9D^cdKfjdCeN3uSV3qW_#*5?P&75NT8BwMJn6K~z#lMWj( zT}g5EC@m|q1_VJ>3H6BW5PHYn*2_CNe@^uMVidpU|5s_NWuzJ^Bw0F{mWfTNFU5oo z7^h$|v2e9c8>1`?&@nIKl*7+JOgG3t*W*WGA+|0*=J!xMG00RIblUAuGxWZ%*?kr( zoeY&g3**G4B@6)u$r{HiMQ88tUda>B`$8`svzk~=*uJi49`#(Nv)ys{jj>|MlR4m^ zM=F=!anTd>VNuw0oYMA*i~!|o$C1()z0B$+32eb*o?NFvk@BLNpZ}?9J0#dDQkyj&{HFfk^svv-Xr|BXSsS@Qo&=cqO+JGSj$bLfa66r9Cu?lc=MfLHDGZz#A z=}ffV>B@)`o^fXVzpuZV^+%!f%xg?Z>yvTvoy_{-gDf5f7^EFr?(uonDIJ41nhF^_ z`6A0nARzjC3`<>YPIj8vSc?##)REL6s;ak9>#MvI?^522ns%YlnO=%34l+Xd>M&o7xGq;K4K`!E>|Sc;>Tf(2l-C2JO0HC z1&hG44_j08Hw5_2_^?qZ-D0Qg=lKv)7Bn_$k;8`%55Og$1JE>TBi$woT~ty7FyTfM zE<+p8k)OnGFZ`BVGV@%6NOw(M^9c8xy2Lf=GMGjh%aI{i$jPD9*fl&xkZX8*pr$w5ovWIzmGfj2$TrF^RRbp+5(Vezc^<& zjo&gY)_)mtPa)Myu%u24JLsrkfa`)&D=udAc(=_-bq;U98!6_K~dqb9uhV4-Lr)_N6|gCg2g^;W>(g zVc8ad=qH5#fuRTB<$cXV6N{PS#Akk^DFzl!b2ioG#(rG$Ui+u%^z!f0cjshOK_)_e zE6m?$3R>xZ9dz3exO>k$W|&$>l%*dWZUkhci2SP=m^cE=H5;{7XCA3H361>d(LI;S zAiR4rjf@u$kg+L$*L;dAMMDZhgY?F)NoIJ84V=nDd`G1ZDAw0i4yw+a{YrWE>Cu7A zU(v^u#lByIbQ%*w+7PNhQGv>jAjdFI!(fZ@4{M}SM9!Z80aJ6BN=s&P7;y(5oY^^jOP+K#0Qru9-d$tydHZCIU}X=>S!p_Zun?!jgu@L(yjTkyqbA zm~|L}d*;nhM10TxT*faipL^ZogMo|MqC;y6IMhCsK+8njB0B$>2tML&g)_WT2P28w z1(h^8s4A_UB8tRhoxW{5Ih`f(&c!7?cuv&s`g3`1PrlHjMW@Hdw!tuMIvnZ>0trFg zCFwMlNQOm-Y=~3jPuD7D(B2)M7Ew0!oSek41-vLArWr?H|Rh&0oisLatH~vglrXY0Jycqm339q_QpwBui+hY+mFJ^h)~`R@^JNE zAx=)0(N)D@e1u<8lDu`S+%b4KN1*d9VJh1eu69?FiF>GHYHRs1l3k5{$;ccEriBy-e(tp)cO;WCrZW1P^WEj2ade%X9i#s) zyFB*=2W5DnHNP;G09+to=zto({XL5EA;P#M0Oj-^avwRKs3|Xj5u=6PseZ%q=IM)x z3rQP%96Pmb%@TBDCKe0pKAC%il5~#!lS4Y-FqLt} zibwuiysD|$G^T;Ee%q(JO8eEuW>yi!wN%X)UjWb`3B@y}w@Iv5uskk7^^*Xcc`ucR!S<_&voy3hG5hFJ_pL%-C9#_9<+ zIEo&_H(##eBKBX0PCQdz!2E&5bhuv>Y<@Ddv#NBM7ZRj!ko!YPGv#}fpqhYw?KBRB zxs3Lic9e933Tj`K>MNpbpBH5e_1>&aCx35p(d-S}-1c3lN%-w=QG%k!vbhKmtYz)m zwc?H=7SK`rvy9yT&6ruiX;SOjme}O)U5utxNW|Y~6p0GQh3vLiEOg`#3<%JR9EZd` z2EDEd#C*erZs<9mgsomwn-^y6j78Z9+es`O_jW$M~hiA zHMs79)_NKN>?H#85Va#6h^;TJij6lC6xHrR>vUqVuu84={eG- zW&N=qKxpPi>}S4;dj~M2iQ1f(idT$9Ar&82r2a;!`o3M?|8&gQPUfWejetLkI!}Hw zzZ~1K;I3@qe=kF+0^||~s-A^Z4#LAjKd)`@W_46O$BbPfCUf793@+NTg?WKW_wg`V z>J;ouZ%t49`qlQ|6Qd~IVLlLp7q|P=pN}3&7wZ32bVYYgDE!^YZA11e(P@nfa&jTW z&Ln7ko>ErwVWIW!d@76UjiNPAY-V4Rx~unJO?S>3=F`g-U7pKT;GXx9zuf4o+QB{eK2i5Yur#OArpu@e*p<_u?#ka7PnVKwb& zdyU&weRb(2+%5V44n@dSF=xekgb>H;?fAP_WCiebN6sF(L+RNgW}jb{&1JD{=E4L4 z3V5psm?wEppFMuryMM2ag~sE#cui5Or%#B;tdApT40nwSYkkf6_{;j zDmjxZQS}2){cP|CaW4XM;NJD-fDk%6yoUw5wc=`B+M>k~o-IeG03|47+o*1uxq|C4 zUi6nrd|NYQ`>f|C+MeY+x}P&%p_YcuR~!f9sgxjyo&s42jdCS0C1 znC;c^Z2@c4Y&A*H9DCr=JXCUaeHsH9P9JBn3J?2{iBYE=Ql6ylmMaCJG6)si3EH;C z_D{fq{^X{8n44kDx%g?@k+2le;V#QeJ`B(kFsgq7qNpU>UITQ7iWs_JklqsT%hR_; z5Lgjy463Ws&qNO4%!VG6;81g+YfiVO3o+>`mGjZ#*md|P*qA?E*!EE$Ay)R;|)FeE2VD(}IaQ6@}0>2jK`QW{k9=hGpOp2ni|uH;wz9 z?^KKzXe%cFtylm0{E-wc7};HcmMx>O9~-X>r>!eU+kn=S#vx!dP~JPRXNzN{Q^|zk zO8xNga3QF!!R#m@)(il>i-P!rO)tf^V+>$pLzn{w+!L?XNscoJjRcAW$NaHC7)~XB z1!6y;3=UN;X4B%ltjTPT1S=<3T%U84fSL20JYEgJ7XO|-U)!v`BUyz3^Oz05VlDz$ zn4w-jMtq3On3$1Z`L=y+;V@39Du>pr4)5QjDn1ONPoT|bNbg7hU?Qm?S`+|R3_w4s zP?P&GicwL(+y=8XuYZ86+%;^EK7(*@gyF>a10C~qSYBtyKr{^3$%F*vP1~V)VE~Y^ zh8LV#d*;iZ%mE2+AE@~7LZ~Vi&%tJ;*o(ci=1M>`|L}m+=%-K13S@Py_Ma17!m8)0dAly#;^Pl-nu>Io{y?6&6_|}7HKHfWFU3>+2XPBfthzEf9 zXlv#Pf#QrG2I?C!i&WO_GVKtn`Q|WmHDAc?MoU-GlmxUm22_&*jbJ}ebIU%SKt(8I zEiBJ{Y6SD0ub^1bf-Hk@RHR;|PgMo+7-444Rj@>>!#>)*`Lly*Kn8tIhfATALq!cO zM?>3ZU93Cuqye81v1l8?n-Tt)UM2WiW#Gx;SW@l|U^&I1FN5A0fD~~-@OJbm9@cz{ z_SY03coZlFU?(f#4zhFe#>K17v~=nCA^U;hyEBt5j-&A4M-4 z-HRSmQ2bF5B4*(c0E9+7MNSUGXFwBEx?VhFPJ}3Njt}AVi%a9zpaDEy zd8>n&E2A=)W)Q09Hs7|75UuByKc*+*fEgU}ajhYo9cESuET`|-dmH`JPAUYV%H3}il@LV|!d>6s=SSe0+Kx8e4;xj~qSzwTA%k59dMC8XD+ z8>=mWF^O7G51n~PEH?JgIk?b4UMSFDO5RXqT>AVChb=qVv0kVj4K5a2#J44GEDT z=2U1zH7y_|l~HTl+|tqk?Z+b?jE3ltlX}iocRd8l|;>WH7` zv^NGEA;xtPr38q*M~_zG^X|>M_&+wcs9&gVDqw=S=|9eW{1#zu_T@!e!&l$})@a8L^L4xUqcd(hmKx}eYSd-q-lvhiBX-fTLACVHYmGh&PmViFDb{mC@i{AXTuA?4!tm*}{lKc# zkg$ZZgZ0V06ljgBbv1x+q;dQW861>K=C)-fUaU32rraovClaEEKR8%uoqi$QeI>wA z?Xs@oYJ?bklvs{ncD!WKzV|z?=WM=(nlGq;Xh|{YCYW#ME#>GTGP`}`UNimD)YNY|0aNt#)x z(qTWV>ZMTrVRlnWhN!F9O!d)Y$oALyF43u)mxzh+KUn6}zxVdMR0%k2mm^I^y;`%4k{39wRM5%r)XVe|-H#@pN_Rk0SHrrm}CytNX z%B+3uc}|1b&f3!h#n*s%b~nftL| z$BXAigEK1BmAV)foBzGgC^sdUI~QSBh9a7@4B{5wBffNm zc}AylF`-HoDr8+8hD?aB|AVx*4vTXC`bNjvU}B+ew+e0tDHS_tu^2!JK?DggFi^oj z92!Mz!2v`?!2oGckuD1f36)Y15CtizK|uO^))4OJyytqa_nhPP{NeN4d&A6qe`Bpr zEn<5E>eZm3=sR*C0@45(L#h9rpyk$rmgL2<(!l}6X9Kf%KnKmiObZ>bovV8eTEmG&So*8jaT=)-arl$ zTm_`mhb3`I#BpRNus%}TNqIkrc+-Lx3`&R)6d?qSR@O)9sdJ;Sf*S# z%H0zkdB}~y>5zUns!kojcGm4b#aXzpFHuq+(aRK91JVt~s8L?!k_=$-K%5I63eK48 zpQ-ld+2_Pv`{$((BM2`L;gx8-R`1)>zR$a-HrqIJ*YAUUjdXZ0ct#+4SxL*J%4U;N zC+<@-3@na^BV1McB?k6p&z3#&grXm)jm05e0G6DZWv>KNK;DvlruIA=AB<-lKXM#G zncaR8b?Xj1w^djc_!2dUc+Nu&S!zFnEJM&gPns*uWL>@-@<>DBsBV*F+2LWFg2Bl0 z5RiZv?jyv(4Ni$N$$edB=$9X5MgKw-UVsEG5f%!|e0b{X+9U5msQn%RCrp-{^)Czl z$}1CQEmg!{-I#<^w=;woj$Ca&%Ou_cPX1OW%XTxPcfvq>GVy_|#3Hm-_IGdI z{1H`c3w}H^SL$w>3~ZRoBarHIi%Fq`6bM(;j=Yw6lOEXG&b@GQht!Vj?ma$12-E4wZRC%v zva#K2-m>pMM~~Pg`U!XM9#J?3WqV#dq?h5S_}~2jF0evQaVs-UG?hHv3lRdus=-K9 zx?Lo~WCp^?H>m^Xg3o1y<-9d*&5=EAb?fu6O7z7AHJ;Dg!WaCJ@Dt>$QGA{k^Yf9_ z&}O97%Hei;E+q5^vLFo}ko@IEpHduBm_f*M+U9!$8Kf{GWZ1igz;L6{*a==s_aO3? zcavj4X#-JF841U83fNrJA#Rf4_-sn%BPG(jVk4?^eu0Cdu5CX?(nS^V()}P7B#r>I>Qv)wy3{&~>lj}Fh}U*ycS_>A9#5iPx{9ExkL_xCcA zXe}nG8;YJqExlN*HQ@V-u!}<|Hv)VCbu19%g{7w}UogwPCnFm4DYqry$Bm|4=D(tnCHWX~tR7N2>2sEI$P5R;1 zr2y>B&^mDZKqz3S*P9AkR12Q&?%%akyCBqMg-69p<#@93u)lJ6Xms47&7Ozmm`Ac5 z3KL>}WY|0$VwAE>((ksnJ00wXT{}sRa^jST^M!GVVsnlm)X+85 zQ1%ldcXnTDB{m(bG*;QyQ(;I5QQfmymhmD^Rk8}P30Ep_V}U&n4;T!JWH1aih?$?o zvFM#N#>>y&KZr=itSnMHlNO28A4lw7XQTx|*!loM$a++J7*TmoJGY`2E9yBZI+uy;G}^n`=tl+lkxt(-jaWCMwV{2~hlHrOxKMieSx> zA6&a|=U+a+Ji`jxK1`{Wv{V=Z{up1SmDnB@@b^(qim&C#Olr-Mp|d&7w-x<$v1mw|NZJ^z~Vt;^Ur~glc1>9tDWGYAm&U3S(CEdq=?tkj3|IReT zRxm;IM~3e7Ki_i~&i{W&Z4Oh=oMbOg&;9)=n*^&i(K@*>Sq6)pdYXZpT12ou#7nJ( zuxe53?GGP5kWTe=2WfhyEZ8gt{E?hvq^qS~2)Bkd&^%~(9+A(QGabAueGU9~{^9*d zuRMgd173(*R8&)}3hr1BCP|2i3KeAq?}`aGvTfNU>YxPrA;-k0Xa6#Jw&~LBKN=MQ zQ=Rbbiv@58$X_G{Ar(@Q6Tt%3X^BOb+W_O;>6S%u+KCwxC6;w#K3DgI4I zYiny1MbsI@JIV7D|Q1$xl7}>s8p4c_32@5VmllV;n<~ zpvu@pl}^>_v;_1&c?QS{8G@x5%?{4o=2#4|QN?R}4;1P-fZ@UZ!^I>JA~cwaqUb4O z r+wTL%@+Kb!-H0PW4CXea>ibmkdFOt#&crmiqMcw*!r?_!kdz;rdTxX}`%mWc8 zH-w5XUdU0k0f{VhgttmM)*xcRg|F-So7B`=Xe9H|s_v|fx(sA|3q-PKF=1?sYuH#u zpfzQLUEE|jTW5`>l;-;U!od}qY)L|f9UdR$8(A#T z{$QndHruRox6+V?f)VRFP%h6w@r3e>SJPbOi=2^2 z=1326@~_MKMXFjy9zEN@R&ViQbBCkx7_Gd#`Ln=^jG>5GvHBm+p9AIAQs0+qY~lMf zblCAev>p+8u`a!9WuNb9{VI8d&Gp$XC!PBf4tM;~@Hf1ADq@2WLwgf-e!?}MH?Xfv z_n!6rqg;5Fmp~+YNj_nlyFqXbwu=nm$t0z5yl*;ak1zNh@@}?fm$8Gv*oq39(Jcd? zVLx%1i(5y;419{4(6ym)R_5%Hpu&EN=Mhcgo0p%53%E$IUkz1>FL{Ea(NSoc_DW94 zEIHz7CS_bE%T|vG7ue@K_=A@q)#u5rmYs}a{L9d&z%{51HA~#0w~5zYh!3|X18wEx zeCUAxDR?Gy;ld9uwyR`o)$1es%bO0pEF11C?(d;x*v;qNtR~FQ>&DLNw*XpjpY3n- zU4_4CoO!mpfa#Dri*a_YCq1S>!Sh^`8z#X!))a@Wf_s`h^yxfJZm8W7m%81eCfq#m zdUn7_LKF&1OKs29MK{0pJ(#cYhuLEcgncT$S5Z~nzJ9%lSr={$x#;i|kl3S#eybQ_ za}Gc+X-gTS6oiL)xZ?=Ogb)%~VqI}m2*M&BA|-5G#69p3AdL)$JH+r`_Q?7;VjPjE z%g!Z(gQ^*H{UoWc^&z;9PUe^|C$5mL0c92$53MmWI|>>sm6QXquI&JKgbxb(iVB_7 zyQ(P7ANJMt9a{qM^qN=qkJ7*>U_xsB1`9p@hov7ch&Z*ZE*ML^gL;)m$ z079U5Bv7L)14Jb17zq84pE5>S?~+9$`$A_?1tE`2mYwQvCj z{hHTXfD1*01u=g6_iwZ;DanLGhLBqH^li0$iiFmk-Ki;Nyg2;M5zX+ZOD7g)-8BAY zW18#Ufo#xK4|9jJ-foyc5womff4htqL^2|*>KxDhZZg^aF*;YR<~o@IvMElsOJLUe z*feTtYikD;;VR9tPsDr%<0BJO2TmL!Xzu}RXPgYV;0=>1zD}p&;qA{T zd-lI20#~G=BjiEM;;r!{wSg6r!vvt4Q{R1dff7AD4_-jiQ?Jz6D3{x4)~i> zUtf#}@LGfD@vt-ur`|p2@w`T z=TP5Ml1rZ5OI}>Mq6X9lMTZRX-rV!347IeoKh)+1>Vxn)mG}^)wmWoJFVW9U@*jm>RPqa&RYtH6h2GOZCb9f zV{3P1ocw$6>Co-(;CKMd!qG@!BEGiVOH_{|70wWa^P=PB5L>J~`9n=FYzMU}{yT2k zsHmu1MqHhWZ3?Pe3Q76}8Kw~>tV5{$3z~CM>MlNh6e6nU;9Jh0KflhlABi2hU@BLE z`XYLjbckf}#nBmzvvN|Ak4#5NQ}vP6yW(sn2?)dpRsZ-A-@K$kJd0#E>w}>BiaH;> zIjL@2L`?Cx#3T{dR;)7KQNjq+C-e(#rHoyiMx0r9S?yR_4L zadUOH*JJB5KCM>_HV&!o)vpxbu5@A7jnTIegm82Lkj~oQ{4{?s<#7cTr1nA+D ztH3rFXY==Yoz z6_T(2LGfC(t=?LF$Dm@EVH$|Ot@jLyV1)4(Hh^~ee+c6@8ecW>b#s?=>bZW*q?@{; z`id$gNrRx3kd`i9U~_I@e9+l#AB>myKa;GIl*sz=Sy91Fb4VV_hwy0Q;NQ2D)g_G< zH#RN)6k}d~Od%XYadw#*AXEuC_lr(mgHZUhy@9?>_rD;#o<`*1RElXI*)dSKZco%o z-u8VqZ=KFbZil+?0Hk6ghCuf$aFRlwU1^4J_fwNu+l*l{j!ks|ew4YAc%J}JwLqDsm4i_=P?^$w~8 zvS#l05kQC=d9Oh$df8B{8ZK`up5GW&B#gYRvABjO1w)n;;jUGQl4r(00Hek)L59QL zwb47pgytPS_Jq!WHM@qwH2WG6m+%peL;7X7|2Kdc!28$UxY3!Cv*2NXu4Fs;%(b;W zf^6)-9#LQ$NoBflaamG%1jH8vXfQSpnUMh!*WxzxO4aJRAW8r+@*tG~&1>K`q0m3* zr0el&$*PDe=P@)XhcOrt10J5{$>G%4V(icx1-_C-1ssC*2eU$U0I;&eXwyTOd>H@Z zj+tza=m_0`P52B6Io-!!MU5TYhS8rTc5G^D8k+FIuVm!YWCU@WAD$Bh9>R3g5Ou&~ zwNUcP9Mhy2@Sj;bxZx;KD|AFvjeH0IrJBs)@Cq4NRfTf72DoiYG>e?P0Q zM}Wl3wSRm_i~%-w^fd}0!Gq!W9X?*5z2Jehf6=0&KDN#mURw=oVicPnk~o)tHVudX zw#Z~biti;IA^vZEM?>vBK;AbqqlY2V7Yce4t}NPI8=0BZvu~Qu*wON&(o%d+rP%uV zzXX#NKK&RvI2l=6NEN_88@`Sr^X=vKbLQzk8clvN9+m&ESyFZ%U{cbpjxOg^ORmtT#Cp4@LHH!zL_j$c<;{X5e( z13}yS4+ark;q<>bYPZD&Ud$c%cQtH%Y?bP}sto=($h$iTZK0uPkWVMBF)wtLelk&~^!jFU~(_7g|-Llt) z4o$}7PXqsc1g@yA^{XMOme?gS1jsouGgCiIB0$@Y;$6+mQEW& zU#hCMa7Q>WEChM`{h-rY^)l$VyKOvW=MPfq3$AGX9yxgy@4+i#-dD+57jA{ovAs== zvTR*2Zgxv|WDLm#ppEC1HunXy-D8}0dRQD)I$xbpa%AQ`ang{mR0#crBZw&PsLoD`LWpE7;uRALe28*&7FEE?_(buZ|u;sxA0CZ!%L?U)I@o z!~;Sa0jv4V? zjqx}?{5L~=1w*5NhXRe{yq4`O>;qWkC9z$^+(rM!d6S;FD?M!pz@|Tn1+UCEVMi;T z!1USsS#0i<2QwSeqTNPDG=(fe;(vs4eOG3n;Ej+-c11FK2h(#9zM6MHdB$sJQ?9A( zH2#s#X%yv%Y7}S`y~GT%wzbrW&@GkBZD|AMwqeMg&)}_D#&dS7DPTqwJfhmd9+Lzg480O&EljwKX#k(x`T#{@K)Jc95bTN zk7<0fJ@a?iPq~-(*xbJS_%iPyhLv%j$$QZjc(&&6AKT;ZuSoaS8FnZp?^|}@u`DBI zpiJ<9rWhC9HVEpc1p&Kym$>@hUk`gf&Cq&kZ`22e0|4Ko^F#0qV_-1L$V{Zm1>&k# zo`_gWlv^FPw5>uXsckJ*Sn&brnZvcS0KiyZ&!Q9ER;{?2w#A>yco6RdajL|}6x&9#Tu1H;j5;H-GPbb^h)?N04mbq1PS zI2A3`+WPqPm}_@SZQeVF8hzZjo}zK_}ff;Y(uzk?KvC$s)@`7@gy}Q8OS=YY|)P_1Q$

S_B|$3S&niI z{aa8A0mNT4OY2JSm%IOY^^C9b#Z-T!SfPMRYJN4oJwhtQKj>X+<NQ2kWC}X@r(R>%hZU)Yg4tJ^&dwOU+lC4&8^2{X9D0 zcNF5)HxcB7*?TjBz2`OV{B}ptTeRu$pAWAQJo3t-F*AG>3itf(yREo%@IQpiG*{Xo z`N>>cZ=3M#*Q{5;F+$MHu(guWEJb^YkwR4?Z!(x)`X}YmrN91SNm%*V!VV-Clvvp2 zb63=S*dy2DmFl)=VgB8vY4ZjQFE6BMI4G$`dg~`GIOeffI#J2&lZ3`Z<6_xPMI430 zoXBsQ63qDOMezVAN|iZ>R|Wackc$v+lh(zma5-b%X!~U7E=K&+g?2Q~FPbV5hk}@s z$lm&Gu2wrP7W6Bq&RR6gpOi0pCidpZ@4h&kB457sr<5W=PbY^$1JDDrI7QjeW(-txhDLmhulj`2)21vU@Z>Z2QPv~W@znxtHvHIGWe#v{vCEe8OaQUD$yv% zq%w*mMd9k%N#g-Cllq4vw~6#48_V9e6DVi`hRfMN$mMLsv*A}o&nZO`Wi;!QPK_Bj zj{xx2&}c63?Wm7P%QzPSE)C!&n+4Dn5s`4-rlh+>Tw--*g)JNB(tdQg=EE!OuhnI5u{zC8oGxBG~beb0-i&w*=P4aaOmMsN*La*2x-{ zMG|d5%q}I4($pPPpfpnsN^wf@qx28dLhDN3fiE>^biN0tUv0zJlQ+X*DoU%c9RX;QvQ|o)6 zDA6H9{h_i%HYvb+4d4kSff(sK(G*cGJKaKI`b?a_6Pf3{(nL2M9~EVzHAb)%QKHm>}e)*J64rjRweD z^aPsP_E!PV;M#hTA#--k=v?!JiZyVO{LesfYeQfK6sJmQw8%P?n3Cx*AR7xkJBKpG zb4fNvPXi^09sUUg6<#PC%Xp#tSlu@q;wM$kw}_wsN%Mq{6^W72Z9rU6Zqc8w{h)}j zl#-X{3Sg1o*-^3tB~Z!r_p~A)#3arP@@_NCZiRQgh!XX%HC-=ovBZyA`b6(sR9BXi z&_}~B5OT2SxI8H!Af3WbC8QSThdva;TF?!=GgyK2&xR$RnwOYnzW$C)?%}2T$vy^i zDfb(}%<{AU24YJ;oF9K($C;;K^NQKlauTS)NwZuAqxG)5oj!$7JJ_`fNy9d$eR%7x zr>#` z9~P3Q4bS*m*3pGzx<%F>vBPX@?2!S4(U*VQW2<4>I&duQiGhs z{-|&%&aGQD7RFdxDxpJx;i4A~Bs~1xI)ahxg;{(AvU8?So$(OW$H7JywP6fQ;+2-h zZw$mUErv3X*bP?B_zi!uX!GZPnzV9LX^@CG)7iRSag&(qRN(P64}i{Aq^pAH1UfBzX6?BU`_?U+c5!SIc0{ zl*g~x2O=x|IjF+s`}AkLz@&J|;9%Vh(T%MsP$=Tht?L5aueVU<3d%vs&%U#K|7C9L zc!vYXmH%IdKpT}3r$F<~agHO%BsDE91T_Wa3PWqPy;k!cWwP!)AhaV@zJyD46jsd37^TzI$Y;w-q&YbU*uAGzIeKqkEGxZb0lxSsBH^BwP8s%D4SIz zK+SbU<`&RN@wP9}3R8NeGKaVi+=VgiUxPujthL287XItkDT3UAn|&mm8}v)DQSB%n zu%-SFhf9Z;#%M)O$?X1wltly-?WnZ`m+B8%Pse_P*OSjjs$9O(>Y!X{1e2>u+?8=S zBqlUviiv`}LvqiPhiBehOHWUqS`R(xTEj=brMo;m1k`NYN^^2p})!Ta``xfhQ?!7y?HpXlOyx}#OE{~hkn$qT^MdMCRXkgyQXTz|OPue^sDmGr0IA=>OooOh z*)#zWekwxC!{uzBr1bI7fIT@bF5DC`uzl?!}K%f`W+#%$36@kzLIImj^ zD@OrRW`DMRzi@!Rs^Td*+s=+(!W$@sSoEr@s?VV&nvd1~u0N`qmjN5(F7ui99w)2a zUQYMluI(RZ{`#Jy-2q+ZE^t?ff`)3%Kta!I;=#aC=bs(2dK~5@>m4tj@h)Q0h2s1+ z)4*BYK`Zpa4?G;`YP651F~+-kw`x5D*?Q>z_O7D@_I=F1`sFoSDwIh5L0o z?coWog;K4#qx`GK-ZPYEE)!9`C-${ z&ZD+t_MY;?J0d<2ht++2+FFRNX*7I8At#$d&eDV6E;xQTcj{m_j1qTi+XoNgCQ_$h zwmr$9DYUc;GGwYCW8RKDIXRPWJu2UGa-)6Mc^@MRHGG?gsO zMP=@XA5x1s1;P4J!KLy9nDYP*nj%p6dj#WUD=8nL{o?qwmT2ofijJ-qkJ}|;zFNJ= zea)!t!zu61kADCDeSgsuS%hkUCsm_mftU9HMN<$`6*zw2=js5TXn-PF$>vr%x`qXk zd>9>7L?ZVxw1ZJ5um6fEJlT(#Ltw4Qktv}QTm!b?#H}0bJ%Hl%5q!ctgzLqbI|5#r z2%1){)wgMlh)AlbF?o|`&I~}qgX@|Y7(5}9jb2WxFSP<`%uyw| zuvdk~#5pn)4?@WQ1VJwntY;hb^6SfX%-iZ-nPJV%an7{nI?YhQSv&}Ul||0ltRYdg zcz*L!oDaRvA=;q%4_KK+=ogqQuq^IOx+OsnN7@{Dc_JO^0?KS-2A!zNM1FXr@mu{^ zH%ksJ>V{? z9&OaUt4+bUASYsXWVxQF?FYrO0&DIi&Hm>CzpS}^x8misz#x4eFou7<@=8^r>2qY7 zwTz^sLtF;B(!-5k;O8slok9>gMfuc27nTq9=}Huhe`tfKK_%1o*_*4iGDxj?O4Fzjfy$ZgHT_Y@ zv4pkh3%V`@Y%faCS1+4ewdaJvn&p~LLMlT<{X?v^tToaGF4oD*CM-zNmw%z4)qJk> zl};JP5>EQ2Y#=Fi!qX3ZxXh6boU}oh_(t+JwEvV*jBGn;vAs?X8sb9hQGGnX<@xE0 z*Ug)zz5Qn|b4LOw5Sy{b80IBugauOzZ+QCiemY)gS_lYgykrCLozdGGmO%xL)aDnB zJE;prCwv$Yfpp{{KY9%rqR=`QfstBiS*P8LD~IEU%DgzfYzxJl7tk;ezw zkKu|fw@!c$x3Hv9`z*4Xb>ZgF#L@d4*^dqrauKn!-5P2DChC0_IjT!fJM?u1(0xZT zB`B!6Fa!ou*SF-QZP!>TQk6T&@x{L$uEK<}AR7EC-OKamm z<_5sCSa!%8{N{F|rja}S7!~|s+!3q0A3c0X?$2v;WeQD%Kox#OOvgjgwcwZe3NQ$uVcQJ>2LD7vQ54}vEM%#K>#OLNSvZxX6_2kp+zLD>p+2MgWL+0WOJpeS zgkeQ|Cr)S`yxrHUk?)Y$cJeKbSNOe8+yAYK8;bZz)<>{~G)djN*BJh2VjB^#k&xz# z)(&;o2Q<-47Flrdj;};@Lts+?CoZyWp$gEaRwF#Q>#*zFcq|=a8gX<*CX_FoKv)j3 zj?5r%j)zO4!?sjAHt|=PP*z|eWLItIR1U(uOQuX5j5^Ur^a>ERK@}E>gQ(6}(oG4~ zg!o_|=^`#Q53=!KZ1Z3hPlm zaJ}DM??*7~Ip_cpKwcDI+M1&(=)Jpp#v6AHxldU*7(NcA82u(Jk?rKGQuZy)GD8&8 z#b~+yg`LkCsodNsjSCA_#eH63)V?vDs zHWH1Z@66wVF>O>x*P!iRdPtk$sK~}y9cIonGt$t^wkeGH+_cE8<8*P?n?Q%_swbxW zwu$$#z27NqX(xCR#esYLXS%8l4J;eeoTtA1dmck-4V?v zY|^PEpv+T3hbEjtw`MMl815!3kbeIG*-8!`Y0!O(eAJF-00JvYg^@cj(*?2=9nek=GbP=)Z=be+dNXPg(foIhs#4K9htG@mgN* zpghZNHf`Vix0*#RzzwR`D61frvH502D3h}LI{OD|NBYf8><*qd}O+UVK6 z_~bypnimm~Iy;$vsJPKiCYAjR{_&bGiD$hii`t#!n}poeUN+h4p%oIk)mu?J78edi z*WP~g<#v48v=?e{q56iFEl-*ntho=ElmE+C z%zI1}+ha=lL+I|Y`t}!FN+g1gB<@O!A6I!LT-4YjGN|6Jsvbqk-T1Qdp=Wb8+by-Z zU(F*`{hddr;~E}|^O!7)UsYj4 zJbs3kIJ5gQatMGI8*!MaZ#LD(2gr!6e%apb&=<;)!fxSQs?X*-QP-srHaz+T`J6lo z)AlsFeTbJb<4etyBz0s`&TjqZfRpXnNIUs$yPC{R)eVI%4}D>!tv<|LQ=gShi456I zL_;;RpqO(J5DG_kpu)LxI@JGN#L=JaEjc6Ek+G5B=^lr7_^Fi5?E5ovhQTNw_`Te$ z6}dI2P;N3k!#w=fyBBBdd20Bi^yi`aEA6_c>a2BthKV*^jM!de^U5oTQhV@6M}@R>svGB6gH+&ljI{ zUR=$1@Lhd<3OOjHoq%rp_;hyrDZWiuiuP~*^hk&0y{*c;-t+lORB3?(6QFuYhyxp5Fu z(}l*;QVCmL*AcNgwV1tYe$ym}_J&IuxBiG7I9FFyIaw&t0b@YUNE)1kvr2`NxkKe+ zpo7(R+n}A^r^A&_8V6UFj;*}1&9Et=ZD<5B!(t}U&KBg{qv{y|>PK%gFyenaQ2JgJuJoe;q=ZQ1h^~IuHA=GCeJz6R7z%zxBwhQTGGIBWTY4F1hG#(8ob*U0)t3NjcQpW{taz)_)M z@}7IoX3Hc56geeI>2UihU2za({~T6HU>7Rz=+gq&(t_XrAu395I0bWUy{zntM5vpY;&$jJBghzC4RjWUO@0U%0L_Hq1zt3C4; zM|5b^j=Pjv9RJTPTI6r_`Ww{@o5B=P=uI3pl?lu8b`<4!QMMbfE)iD2>>ckHN5+R} zcaDze{&l+8+96c4sS&;2t^d5d=6mxOi0Raa(lLfj%@C`Arg#mwj*QvqMF%+#@$@Zb zrC%tP=C(f$SHFDPev&!1*TP7&7^z%WQQnyV?UF}x*T&fZFlj<7y#flQUs|4F>op*ehj%Q-R_M!`bX`S%mcz;IE$iv##P$In{DPsXDiLlzd zb@Q+FH-^7ie*AQa_Y~~!qe(LL841B0fi@0Se%@MSWcsLn0rntpQxMFg(StYvi!ExoL&3m*pUI@Rxv8_(=`T_t4N=r zU_QmMtpFZ^;4Oz1%V>ArTj0 z*6^BeRNkXJ4%#E%Wh2T+5_5mF9y~o%9;1s^<;;!=50>dG)ofw4mruPVm`-n2Fx}?g zkB`F#xetc30@XPx7ttb+DVYE*W>BqQ!KvA5}M8v;_D zZap*N_CIfH3>6i+7*u~w++Y-Y1}#uW{tm!#fizPvD=Q-LSlLsTxANsiPmlKr zRE}D8G--PL_9DIn75TeEL*N(}H`43a_ym{ZAyU3(W(vqiHR`MG^*IzUOK2q9s?)64 zT6`%Q*kI5KbzQs1dHr#kssGJ6`!sRy8QZea&a>%h{F{&dXBFDX{RgTqyz@F85%wXk zSbSmh@9B+W`x7LyQ;)QmYNxCA`9{m%y{#i6ROd8=axxq_17Cm!Q5-}F6;jzEekA{_ zi;u4jlAJ#G`&u_E5sy)98+?*TAKN`1R_5owgmF4d>$;S;462*H%Dhf@HyNG$B$Z#q zQhstDoq89S(g@%1ls;nY;qc`-hlx5gQ0et0QRnLT;j_M&c8za1Gxm%FQlF5Z5y zYUSW+ZgVp$v z>;Svw9cgNRe%Cs>Blt(5poVar!Zt%YCdPN{u}rBQnb5Fd9nPf?-nrB`y0?tG#WV5m zk$S^lbF%HSWt|_4Hs{>9e>df8B2b9+>l7x3iwTWl2)_Yxh>1$+3*CgcFZ@mZN$*}E z+i^1lchm#h=jQzuLy|c$Hs%Rp6bif1YX3f=@ilR|`~B-qy`AKJoMFMcK<8&IoluHs zC&C-rk{|RJX_anX`LslS>PYsA$6;1)J`hfL*wgm@CdV-9lomhmZ z(2_s>zylG$2|#7NnEv}4HLZUnJKkNh_+(6usgN6Q1oyL@N*2;;!p`R#0}61~??&8D z{P*iJzu>P>LFrlhHcda|d(^~+Xn{7Il6|3WRtvFeE{qz9Pl7*=@5$asdJ2w*^$(f9 zT^z=*ov=VX=m0l_+uhL=0hXb&%~p~5FYUEA7@}HBv(o^~N{B1C39YL-VYsfwHlgA5 ze7BBjX>MctTnYSeR1$-CnPLqTHr79GA7HZnUbp3x^mp|XpQbl)VVRk7^D`b2v=vc3 z@?V~mbUqU^nh4j&88Dw9*;9FWM=dw?|8=?~VO-_j#T8T1(-PK|dSXkf;w|t#Jwy2; z&frj7@v59jzIp+&uPu*XT63cH*(qiNAM4}Mm}}e7k@lRJC;X9Wg31440ukZMGz*0@ zUGV6K#d#&cTbry?ix+N;!^V{?+jCrIR>yO5tDJ^O>Nt^$++AbVE;%WXIw+!L+<$>t z1oN*v^t@-aIW4#Ehu6tDs^x)S5>HAP#y_8D_e(S7?kKUbbQi&?Q?CKZhWl|XkB9xR&L3X1=5P$=h z6wQI7@h(st-1HDiS7<)L7gb{rvUAr+c5d!>+X=~A4TY5Z!v7ov>HzWbfJTV0(sGY! z?Z^?|-j$s$|Jc1cnw9ChPyvZL9`;Tg8ZF@{oB_xNBL3$XXTj1sV&E~)I_iQaC8`^@p#>WE{#XaO}1DQpPEWoa4MKJROa+7|C;cWuwn>`9fSRT*6^op#=9{xToQ z-Xd#=U^of3IzWdpLn1AUg$M*57*l-e?df~(RN8`4er4IIy!*0%bq8~a9zGiN+ho?{ z2fR1_1a(A^af*ExxNat^<bf7gi5BAOb+d*PM^+hH*VQ-|K%M~24 zmiLn#5r|S-hmkZ6{oTeatvx|Ix8BLyH1m0|R%jRw;?kh1-Z*<(`~Jr06;ikgRLX?V z2>?!XlIpQ=6 z0lrSgZU7=`p$l^x9=`R`jy^Qg{E}8;YNvxw@AkRonxPlUtyisZ>+lrS;U=pY3I*cJ z@o@$BPU|UF7}))1zmuYDpXQFnvG>--6g=AN>f5{ZGyA|k&F3(efAbwNQe--hazLQ6US^Ug;DT&`FUtQ4hOjH;$_-+h` zq3_U_*j?C+AMG`c8Qt8KR#kZ~@56IAFS>Zo2FPY`Uxrp`5P#}RiA(EE$+yFZX z!zJ5Xv+EzX-tqtRe7u*UeAzd>^NBKhTbMX3^V=T0^8K?PZQkb4u(! zT55J=v#`)*oWpzmoWmI!{wjiUzrUX2`0YKgMnYyy_Mz_3yW6GhPrEY@fnSu+4ZoXm zf!|{-RXD$2;w~fRiHJoc{dkO)>pQ)sz-&$r7I1x<&GJbOI_WmAe+JbjRW(HoE&`3^ zYRG8@eSuS?M!DEJ;HW@An7c;+5Ie%*ocZfmQm1av zw%6R%4QEmJOQ7;p53aPEf?*qv!v;D4M?IjC6f2_4+2&L7`0CHSr%SLG3u>=6fFSX) zRsj<~;-3W?9NsT@h(?ucQpFfW%oO)m;rQHg@I1Uq84C=va7B4vKt1j2M4S2ynLF|F z+QF5*Lu&8)`zXF?2`2KfTqXtB86Qdc?=8OapoZfXevdiyjBZsDIDk<(=u!ca?Oe=K zUlgmds?Ia&(99)Hi3b`=6<(A|uyyy|S1DUHblGww|L0{c@7;Uoc7o;VZbxPlryiLu%%@$lFysQcLuT2@a zu9`n!plSYt4SeLw;%NKtIJEV)gJ*C)SJusHOJJx_Q^roe7o)k?yPrEWI*V0%HaHDq z4n3HvNQLGN=Kjh#A8M~Eh_FO+j_P1nvStuWzkbeqjpg)azMf0z>rzET-Fy6ICaa*% z;{$!E6W2Z#u5P&*=q6D4ees7$=gf@L{=F@fj(6iD(P=xR2)zH|e_Zu<6HiKW+Rrg- zqf6cfX+GNga?U)Be|f*epRfvg{XkFPPbht6y{AtncxRYL)HR#>G(H8l4((X&beU;E ze|DeP;Y<(lXJYy|JDzuc;>*mI2|#mYhAsJW-t562dtRZxXFGj%h;l_(tNvk{Wu6W3 z&=ScgUYL)IDq6bCU9n6^7JbqWRX!(;h0-~{;_$|)m6wwzga7qLRaRbbtzA=I;JF~+jwm14ZI8ILQUgKu9(lSY}*G2+6 zZGniJ=*5B6*-V!>QTo_&8=qVl=^;k@D%ONx6U2jH1l1CjbcEjJh1s8>lB!P?a+8w5D9#Q);_}t`D12 zJXX>dTig6!5T*9)yWpCNB5Cob`3iy`Z&2y)vZs1%v3W4u(pn82I+YRcct8(>*==7NLWasKJ5ZZ%36JHxbt>T z+dw|VPR51VL;4|x1-%f*{!TO}`6&(#5Ynq~Q0@hF8y}AN6q`P_a+KcOriqb7Zfm>) zbhtNUj}FDOVqNe;XjxELNjS-x3;}az=<{nlTx^uMlO|U2ER}}cE8j23J*};#=HcPs^;*%V z?u<+td)C|T6H8X)G%Qgc)v3H;$P&gV8{cq#SH%tFA|TdFpQSy0s``y8*r{7siSjha+k`GR{d$g}Y@;3K4UX7ARoLTbBi++EKQAYDb zPwo^y@S?}Yd@-fpfZf?^eI0>=njlp3poTo!*pF0zUyw-f0&fu1n2&IZlN}c?BX1TPT z2DeKo7h#48I}mVitm7#J@B8=fUw1ST`leDikvV?!^5(x}?|uUKKcKt5Af(!&95rVF zq;tf&BclNc9V9^$;jI#KFN2*XI2b-yx6hgA!8;9d7>uyM_PkrtK8gQz+XOkR`4IaUn1lbqG@>!N6y*UW_w8NOf zOk;4-EfGmqYGBP=X5v}izNT#u7@H+Xbn^N1Hd z1ZGi~(keHhmET5|cF%H3)J=0y7H(SXpp&s>NW5dj_z%i`&Y z{x#RQkb2CC{s&TaCc?*yy3XT>q14(!#IS^xY%o9eI6>5C$(7W8C)-ew%mzjIw^OS$ z(;K%63keA}Cr+Gk8=LrEEpV!D5sP{dv89Ef zwcI(RiNWd#O^Y={;J~&HAvkFs^)Z}plFshNmU&N)mCN>NU5js>S|9xgk9&5zsmv$y zW5P}0%9Fl}oRqAzyB#mT6R_&|L6`<5Ki=+_XiRxP_qXREvcG>mvqKyPozgrl6KV@e z1OcX3LzCPQ+-Qjr9AsCaa2G@n>;3%A3rkb+TGGHZ@K^EVFrma|bKNK?-L-fWzW~5K z=m&~$3x{=Fi;M9;me{bTKDu79EP3}f-6nD4$H5)v1FEk@R;J8d7ci$Ix1vg)`v7$D zlmD#w7yWo)H=9!Wji4-YW0JNKZ(>`CtB^w3!U>WvmYbEH`F;&j@SDey+I9ekK8zBW z1n)4B94$7*(&c>nX22)b($d0H=Et>KFg7klU<(9CTklC1SeG9aFnVmV_s*^uz$X_D z&ypG=e;?sJ%yWA5mRk~LXn&U0KBlwp<4vnbUcMIK8 zS}RtM<{rBzSzLAUYh9w#xXJ}VD)O_OY*o|_`M-(vIS=0)@6Yb*?QQg1e`8N8j|d4dx$Dc zUmg6?iuvg^ILtVHs0fWdd`RY7%S-5}Z$OaF%o1Y^H=*I*ShP#JixaFOn1vVlR3u(6 zJw>S;I5DEjtjeb~?b;B)af?>3qvhXIKpqLdo})uI-*YeTdQ#wKp_ z%@J4yq3fQQiH_->US0t*jj-aMlP%rajG;ceH!JmkhC-8f`FE5e$|z}c4#*Ao^l%`wBP+Sz1Wc zK*?rSH*17*_?IsRrAY7Qtq*aD%!ij&NC#EFa4B3}hWjq|pY(*+-fmw?G-Pj?BvzIy z5<>|DeH3az)nSiPt&WQHOO5A&N{b)(+Fx zA>kLZ_J7rLa-i5~w76`;xPjQcEl_7Yw-*1QQd(4qh zOTofXM^OQn&Q2o_&D8Pl^2sez&A8 zgGqlzbF8Tbq~-!0p(+Ol@?%`a${Z+{{lvFFj$I|ss921z1h}J!hWW5QESn+MzGtT8 zPtDxox0=FDSO3l{NLb)zk{EGz%3D>Rt-4KSp0;;g%|6(i9X6k|N3x-(66rImx;jy+J3~L8JppTF60kB}y@$Z|l=Yf>$ z1+WRFn+DBy2j_s8Gc7QomY7w5161C5pjWluwM(<=r+0`$**641m_`159@s%v{j&77SI%3pcH8_I3n5m*0>uZY= zLUc-Ib9!c`%Y5I4LueLm$Wd(LSEp8V@mJny#Bpfslz2OE5a|JrLt;99Jsj{lfOe2o zZp`9Low|3W-DxYskB{S-xue*+BTla!!>yS2R<(?eYpR`*AUUV-#($m^LBaeiNY_a_ z-iP=#70y)E)Vj~>N(&NoR3$b(pvRN%!)o`>o9Xy)?*e&Gta`UBhZzMuiias>xd0eC5Mv-B4By zGf3_8S~;1RfF%bt^*FtX@^=eerGiWnr{7X63zg{9@QwZ>gnGpXh*x;uT^#V(WB zLoOgR(pAp?X~h?)>8`@b3URZFkS=4-nisfK{#|;@?@ydLZ@qPfDNP1r=|A)>0*NU$$(MA07iKTXl3L`sr>!$G~EKmO0vu<;EuHBeD4QERB8t~O7 zU(J|1d^9t*P?Nv@B&>QwA@HI{bb<|3PWG+PXv+#$zuBwa9nNw1 zE7-K5(rs+zhNsf<9}T8e*Z2ujU!Te7ND~|fansaVC)p8$y}u*Jg_h}L=x4o1x(|b$G6&Bh zU|s*;fx!itXQrojoAsV~b*5SgEA5LwW$E81ZM741_crT#+WL**>NJdQW^Xo{ym?UW z#CS<)escnWmAHN=DvTLqbdGzc)z>N5!2h|pldTxa;-g|l76#XE)aSLBE|a|KHFAYv zk|=*=_I@eH0T2x#9{^T+ev-QedNVQ`u(Zy#N*>Hf81;X(_vK+VukX9dm|3gpG@7fa8o@Yen~8Y_}IpnY#i=1@ox=cQ5 zcBAkcyssq3J>fTi2lm(uJmu%Mz=^|&vWNSQ$@)h;JYw^L8LIsq%vWX3i(hjbN_KWB zY#96)9G!E+$<^@gzWD~v3lXxbKt%Ew->+W4l)8HLsB^~1)_;K)YL?z;z|_HBCr%uSi3ebCMUTfoJ=5{cAbKTAC;0x&nd=C-yKxAFt}zHP zSZ*CN`GJ;Q-mMa-vhbrrR5Q^2BFcA_k3($q=-GD3r0=ORl~DN?`0r|CNN57XwdcQ3 zVyIT&KhCvz=|Ts}5)>+R4lcc5=(_9f9qF9c-C=&?@22Xf#>vkWs-9YtxBZVgPaU1_ zzQecg&D7tVXFLT$2Ye` z$ngkZAgH(nV+4|)scjz?zEHn0@+BYMj8=}zV%FSjmk-fQ zLsxeL{LOD?W4Ln2ZWO5H#CyLw(tIk^HvpEo-PK7rEy-I;)mqq?yzl>9KFI%x`N)pnDLekP6 zk=-q6?@J0rdQ@HYj!o*gGB4XpgHz<}4&3m6z?PHp^qk9|I<_A=;80-)g4PxO?uL?` z8o`ZGiTl{2{(XyId+oQAF-&zC65F1Zkbfq`^i7~Z(H|%rJVfI2oK%KPT+z(U<#&)F zGS#b2?e2QHv0~nm&U}v@rw{P7hP4jK4e9c8Whlad}Jg3R+!cn2PXmR|0l!A{q_%hNaY*9(K46v&gU$g$pM^WV&w z)dI*lVgWt=dj#&DMGXD&0W)g0g}XOnOq==fOJktPH~;q+FO00(*7M!Adx7+DVxh5N z(8Qvz+}U0>oTDnE6FbuXMRPw;*nDCNUMm?z0ko1DT0UG=eS17m+-cRfyZiG$PpQuP z)+#F3<6-E!$HKS7&A&_R&v*voC*S(wO-!9wq%?~+_T=5yH62=EK98qX;__zGWM_l$ zYUQn5yi>QNcByc+u2I2f_)mes7!$|+>`2-`NdI*baw?sZmIi&+GD)g09(0{rGdw^0 zyWiWi(TGVKd_siMC1NujLl0B@{I#_>mr@MbONbSkH3uuwhu_CL|1dakL~z~%uS0yi zZwym2&h*_I=VY&M|NMPzI7{Zpzk*7hH1Bbe-vkpP&TTI^BH7igCeec}jfa&eLp0#r zX)IJ&@2)K~6KPg%!YaM6Z+WwF&RENV-LNsN27tyNDw{$d9pYo)r;_zr1iQDgWXs%@ zwogr}g)VQ7m8!h0P{w=8^+{IWkF9=!`JxGjWdAzdH~z&Gyhyh4pHzc03Loaq#Emol z#{H9s;T^NI))!tq^TZS3Q&pYb(hjG! zhMJ_=R=4dfZoaq0Q8cW2YBi7d%h3Ai6HPA;+(SG2-vvJm3f0NKmcyjPq1@UF>gOyt)odtl)N6`2*55hfJ4%BJp)zwo5Lrq3fN@_IrM~24=o?dgzhqxbZiq0TeW% z_H0W;Ot`dEc0aD}tNh!y>vUx*4~tnT*qCDEB2_-T8jsp*Kc|7C%*c!!H>dKWZHinX zXSMkZRzC9K7u+ba;?DO;zc%$m^6lG3kQmBTB^c>m=HITU+tsmVLQ%BkM$^E|7mj!; z3wivi(=#{w%+^#`+Qg^dVStx2{&OX*UWCQv+lu1*80;v=re_6Lu2hPC$Nji6P5AQR z_qN>v0zR{I_~hno?Hhl0akjF$t8MCrj(0k5E9d{|Fs;5xuQDE}NFxnNf-c3!g!cHy zcRFW2#VXEazL<(oFwE=krNr5q&zUZ|aeTkRZ&Dz5v1IJLd>1gfC`jq9JCLl&!eDWv zj^&JEt%}9U9j?QD_lDGXc$ExOoA_oPE5K{r9IR^9juKNgWF~28XqHz%p!~*nY}zFEGxP{~i&>Ji4(Ag)wDM zN>i9=7eKQ&#(D6zVREXSEYo*_Q$kpE^RuZXqH;yy-@N6bC3pB5ysuVzFsXgg@7Wxf zBm$sf7&bw)i8_sYqxIK;-XgVUdS%j<$o-$L*SPVuGVNUXvif$0U3BQGJ*RN-(#19Q z1{V5O=`;BHSD{$r{rB)NZcTMxj^r>G#(Q?;!!-uiUt6~%+M$vsN?ABm5J|tQXjY$U zP@ z!J{)UQYm(EQf~xXdb#(JlpKGi@@we( zVzt>Ue`Cjx%wNCG)0B1e40lkw51j}Tbi`Q?rp;${Ro+|j>6yu=JE-Ehvgf|!*vpr* zlaH56GX+Ct^mTHOKGZI5U{k~p`Z>R)8R}oDyJ_pC#acbbhaEk%a|T!V%)Z7aw{IDm z!)-6Pc@0xXIiLT21xj)GW4oAb8@={wpt%UXb_kl)8oTFm}PpGpsp2k%W)3(8iiexisRG_igC^<$0AdbNpe zx8?Zc!|_2wiE>Q* zRC~MBn})7m`6`#-g}3JioA-%ui7d|ax~Mwyi+2Y?6)V&6^zY{P6rT^fP*W1_OhYwe zHgF9^QFqRIp9{CRJ*`-L(w3y?PwnqB9ToM?wx62vE${L9-fnM3(3Cj7%1WdBjXr!n zFE@@%sZmzqG7yeW?Y3W?m;H0y8H_7ypMAuDAwQ`lP#pxgR{$W^=Iz14NxN&q>kQ&2 zXz^I8;6k;U`YmhO$=M4>Zavb!IHZtn_1mO|F(vhb(hLRz!7kQjTiPhe|quoRw;PmOW)- z_bx|!kariK9BaH|N@FRzd{h-?~Rs8QqvPl$>I`D5(LDuYzxkcbrpPz3A>?Q z7H(I5W==N8Q=v+OanWh*HBRfz(?bs3Kh#_qY+BPdTz+)=wYId!fTDg6!ZY|>k!>qn zrV%{SAvyZ)#$5ne%#W|~Opn(O8fo2i74)0nWEm3Xu)vpnD{FOn)m2V$f1z(~w^vr_ z`i}3jnin?CRqgBI*K;#}9V3Mr)FdYvP~D!%87rnfnsXx?Se8fIynDRzE#38S*& ziPUD1q2i|jnQMGhE(#t=Gw+j_{u85t+fEl&@MJL8xx3&r?3yh<1i`d!?Lar}-b4jI zxgw2)n<6dJ@(UH@R1H%%ySX^aS#my?Ua)6eoJeM~^@3TyW~Z^a2+VW*g1!ZL-ed)% z{@1Z=t;#SU-GRJz?RfJ$%-Ox_jeG6PTl`LJSm&lTGOhY=$@8Cm$3?1Ovxch3=(yZsszc@Z?esQ{krY=jDTm4 zA2MefMDF2PoZFG1TA@Fs1!S70K83UGM<=##&+#`sAz^{*MeOT5nL&+2x!Hdb`;+7E z1}ia^-6JmA((#$Q%*>t%A5-7md#UwadYZ@yYgX_yB z)!AQ+*7oECgnWqpxLG%Ga@$|aejnoJF(38_G599(ugneu1%8kBRMH7e5c!)`LAdxP1Jd z@bH}H!Gx0>Qt_5tP0OB7U*PS3gERc4=RN`ck=D_0(Ng<-H^oBD^A8UKD%ZT{&DPDYF8mmtl1F34-} z{%kOTscfEQp1(1#8<8~+A#LKBayKqzSxl)$)}W2Ev&xM*F9&1y zS6w@{ImwElk&UEz5t=&CuhXdspU*xU%Zs+dLO+)Sn)tnZ0o4i;AhQ^4!w~ zY;xT_PHgs>rF~_jZ%9B+=!G*9=c(Uz{)z9tDjnnao)68>4qW8Q=D8m@zi47x-T4PO ze<@A#`hCb46UPVlO;BJs{`FG%RHSFGt=S0InKf7bQdLw;8kOi!`m(8GWb41-{?Y55 zk|!DM0;$^$TRT*k$7b~@s03Q}BptAxx8KWIp_%#F_xT?QY=-A}^@)tO)1?L<+K!~b zx3#_Yq~6st?N(EsW*PdtR%Bq(b?j@3)`OW0&VwVfzD!SC@-k312;VWGTl%EOpF5tB z$^5Zu6@ycu6Lo{$#PK3&_~K0Rr3nb6aIU5ub7vZ-_8e+4qea zVSqjN^UFrfnQP0)$e<-jWPy7UiP^0O&}R~aWdfRPnvkYI4mPBS;R%c01o#kEz#D~R zRs3Nr3w?`FTxHbWR~&};uWCh#gErU@{t)d0@P3>M2iWMc-H0O%1weI_HrH<5ihx8) z7&udC_p*>v)!EA{tUmv=Q$E2jZrl99sYOX{l8Suqb=6<(JG5)~*A>NJ)IH@oXE@P? zv#kPFm0{@Uz5Wy(i;fj(oIdJplP5WMwk_0G6|jl1znUmD*t&mXE7 zLK96)a^bN>Lv=x;F(IQMck_pW3`Y$|w16WA{g%pxq3YMfTv$SGfCyAq29(k9gqGIp zD=RRDXvt(jK;r?rChR7BPcKbpGCzVH)dg~wl=H>Ztq)~RztQJxVQNK|ZgG~-?AoE; zw)>&$7w8Io>Fg*QV|DN?CozGRH8&sn%{c}J5G_#CA@;A$1|0bp6(5?#Pl^g!7`yV`1?`oW9=)Hr)tMUT~1z|G~8p*#rMPb&4{ospLga*%GmM&bcAtl06w zN0+*9z+)`M$oi9CYz$*$Ya3GSUtbQQC6-f9pkFrD=z`dD^`SvH%#d)e*WmOusTp@v zT-YsLvEU>UJOO$I>^)3k+*wzz-ge(|2U;vby73WcU>-1A8#PZxUlC0z)3n=HZbrHd zsyG(OGaS9eYA39EY?{kqa-es}X2}*w5mVs1_~KkNi&qjFBv?$pf_`Csr`_YtO4ZHQws3!s z5;Jvp{Qm50XZ*%rUsqc2OkhrH@x*H!W0`l-HRbc*B5ffz)dxeD2jkfmYl;!YJkmey zO|~@&8HRRCuCw&Ib$QXli4FIyc+U*0n0?{G1^349Xb1`qt}a(8kkhj^K0eg>UONB2+$mK^)rlqOS1GLi*6nST zW6Jf_r$i)^C)&_+qkhH|htzX(-a8IE`w2FVdoH`nfcFLBJt?JP*g=K$VCvkJKMY0( z0r#zM#G~uO;WUVEyy~YvVANe^++OJfYeNYe8Uz6H#XpuQn!> zu+6{?-Jf*gA-@S2Y@}IJXf(}Z?vp!DE3af0rk8|iy)}`?{nC{j431}jksy79e_xW| z)iZZMuy=2U(yE@l>KlacVA znp(?n7(32%6>|Uw8;ZTlyqdvp1XT%Qg@JB(=IVNSdNKCf!S*#lZ|s$?pwt5GVTmO_ z*%D))XyV+vuAVnBl}KnOlPbmqBKi?8yO#XIp4Y_Oq%d5OE7nM}ESnSvb1y)y)hKdOqudm9=5%FpKt2d*6>GP&5_j}y1?)1@v=>zV3+ zS;@i)QzdWf*W*86M8vyfNjTll1lj>6BP~eTj@K)ao1VU<$WKzjl)3QNs)*1`UWywW zrgccC65J%R;e zlR5U7suBrpk_KHMH0b_(O+5EXbjFI$XHK@F1pa{^bOAW>5b0W3MK5^LKYXQINK3e4 znE2Fk!(AGXiYhQ1#iAA>&~#xgspeOBkVqp+)2xz25bDN=Opr5` zZvI^z;5U!Iq$;=d22ZZ?N1z1VP_e8Bs8S5kf6XvWK{1n$+lY+@f2PPAT)ivzJK}+n z&i*i<7b&NX2bAi-BtV^jdPvEiNEjgLL-O|~NEim>*MD1^7xuMRUt_Vp?fnyR5z==8 zy!-d7kE!|OKktm8@PKBi|9m}HL2>%$zZh+E(c$^`YseGD|G!`I)U^5Y^*{L0tsl^6 zg5IsGI`|6Ma&e?`j?t!Y91zkTPap2l4RCu0J-h|puOfqyBlnMPxx3A}H?AbcNHM_8 zt^L(P8lNSfgT6jTGNB}F8ajkuM>$Ua6@vQQBEx;UdwPG}l0dn{k<8#(a4|=cObCM( zWbCw%eKa(h58di+CahVxEH3`H^#rVGNy^ji3(Lm41g$RVb-lmJ=tpFnjhq zI1EWt*t5-N^#&jZ_?iw+vX=vXH8}43C@m*#THKiMQ(( zOW8gKKbs8h$B;6(uY!Rd0XlxeJ#XQTkr&IqId%@n!rfEsGANvp#5w@oQV2vxcqJB) zu61dwaU<@0GVj1B-+jz7N;Oll_K&nfl#1UZfvX}VU&g1Ws(e{I!vL=psdoPy2&*+u zD>PJOj||^BgiiJz_KBJ2Qr-e2XM2qJk=(&I3cxKRAUQYZm|<+Iv~C%6FFKk|EnnV0FfiPb(&~c|87|#;GwaVsA-j6+)uHwL ziy)UT3U%0~c*rQ7S@s6}$upa0PZIE7RB|x3a+TH0b0O~>hm(=X8fixBS6n^}EspG+ zDk=$VUu?J|#l}a*&dyw~W-yb`=(QwG5`eGIj4zae}J04uDA}bSpr@EwM@#1h|wLL4lHWpoQ{czsuaIshqoFfWIuBldK_mcTXMuV>tp}BGdwT>IN?3W-64(eET>My!- z-c)q!Cz`=YYd+rK{M;-`?&op-Jl1|eE~H~A?c6!rGaT_j=kI(VTHph}?ZJE3#H*3EG8`Ak zQg@6jFv>M3f`&zwvre>onTMTiFDi0*cp~7 z5<9{JV;Hv*TIVKCUNMg*@IZCF=094i<^Aeg2UpY!(W~L3Bb{ozE~MkS z30{bz9XF7-*2BsH4v)o9ZtWtKRV>1oUN6#Nk68-^%8^8Lqw6Y!1}tXKXlSI;ebC$v zX;O)v%_TfKt)?3o*w}Ab5}{LQGK%E+sSEd)*}Sf8r^XmmxIi2l&+LfGm?C!cfYJ1L zJc)7*WX4Z@^HS7;tkvM9$q91GUkZN*sY_g`M+uOE# z{4yseMQrbL4b)nTWu2cvc?GGYT>pctcv7cE4o?DL$h!34B2T(v?j!xiq~=U+tauX8 z%w0vF3zxvGqAZ1}UmEF(pCU$CLYz`Roq8CLet#xga_~mp?fhIv-WiN3;z*vRtiTTb zm;q-}raHhGFA#EDyGRW~`8R03| zXRNOTU_s`lG#Nntdo+=SS{by0ln8Sad=UxN-@e@n3TcC0?``StgU`ot;7EmFQga8Z z2wtYZpFYs-H{}TJh5_qOUg7SHjTP28JY`P|&+wbz$|0h4Ry-+#R`d#uN&JG6RDm0j zkSPCu{s{y9y%AfrtK^AV=8bl_Q-~UQ7)v&e-?#=ffm+6InwIr|S(c!95 zCW`=QWXa`6llNJo=KTSzF5bc>atB|M^VmB*jLkoQa3{b7^lX=u9WR|YIg2Og_qx-y zabDW$%gJQ><}eJLVvP3(E~+9>5sj3ppjfvTvcduvEQ3~jftb|sv;%$J+Q_*YoAGLr z%|^(AirJGIt;Zi_V^<)35)#aCj)ZEyT<@Fig)a^ji^bmwMrsA0rH_&Bm@RoJ^^a(U z$4DnHWoxVlXk{P6FCa`s&`sJodVF7Cc<)pMdnF29jpmSgbXZt&P@+2T*HupRA?Z7@Vvp=xDASADw~p zS|{PaZjD4_VPnNG>2!B;T=+`_ZBj5E{rwFpKEPs3W1x1x`{4S<#cbFBG@2U^!+qt2 zl({itOxmIi2TM=|M$%X@vKB-k_+LkpkhMns*B@!qzF0<4ezfCpA8rx5t{-a5j}ZHx zhxBcK6#}uJ$bYx%X`4Cb$A{9^=k#+(@F~ZeS=Te>pvUm#iXUL8WWw?nZk6m%E#J zt+GFjh0^&W|EOIw6>Vzoj@?knF@bUV5^Bw@W8^19OE=WStOwxP=px5&f9!|4$`m}5 zJ?ynGcuQBkk1Z_C}2v_`h zg>d2KzVkZuR|8^{8d@j*u(zR3Mr^)ZA;g9o)OaB@Z<{>X%-%j8wY~YULORg>F7HCM+j5aG_cdE*&3z7cl}Gp_In~#c>?tDp$p2y8%aV%bf09ianpm$NaFznP643T&B((i$S>0rnvk9d zT=ClN+mS#Z8j#6q<2heL=0qcdlrbU*9=+`wi(JqC?|%G-hV;RfLRf%AVziFz_h>T# zb)7(-E076~b-zP)2#rX(X8$nS_QneM4g&u!mhIQG3maImqtX6_ ze+-NM6nK%FiL(E2j2vQNC9YCZ_wT}ja0=^|E-`_3$iJK z5H}&wqH$5@Aykz=Ufti$-_b2*R!2{!&3{qrvT3@o%z&pFztdiUd@-&{(dyU@7pNBF z+lX3hJ4;a>Yc5!vgN{pulsI(6Zit!g{ld$?d_U@-a_3=8BY&VNo-B7H8)3$V5KL1xw$2=4R3x7cWCp}!J9|nUUvELHbuoH9J7~h za^u!Jqjn<%$x5$|&bYDTbrzb*=N0IBT(EZE9pp8~fu9iq4uRM(P$#d680at5vpPj{ z4;V^Z7|uQ3m6}Q6@Cj1iiqI5z!vhINOtNCMsMF6QnhPQ3sZh{Pd zv4qtfs?Ud!NRlFp5DGOQu4HJARL1z^()mjFP;x4>(9^Pm8hnZ!HjC$UB;+`zL6qqr z6WNX#>vXcZfRhS-zcRrVq()9@83Q1F6&!CKW7{7C1X-4B7fypqAjPhclhX$-poT^$ z;88xB{=DexJFCZP&X#xXbeq0->NaRUjB<$e01!ak)-y)4>3+hUa!>+4K zmtvZ{(bNJKq_fyg9a6p%0aC}yRU&Y%`J6?Ick(Y9siUuL{sh`&`z(TrSIuF%m$ocb z`;iFlvnCEK^CQ}xVK7(VaN0)S&DlRzRDjO&5zeY2WEf%RGSt@&aWK6P*=O2pF!E2a zptu=$QaE@-Tb8IKuF<<$1+35xmBKNjEeFd1a$y|ZpPxiK(2||naek{Jwre-A=LVGW zS{4>jAim$JeH*~xvTy9S7W_m&uafDnb5l$X^b-_)`Te~V;WJ>;xfpqh@ZHqe)8qqK zQ)*X~gK2(j_>d;xHe2l1$qow3j=j6Fo>1pKwE+MwgZ3)@ zp-nnVG(sNg&K|@A=*0}p3xx_$r%;29avQ}Mg>enG!{%Ts}06tm6Co)t`5sK@!Tz%w36 z3z_JVT@hkeutc1p)wB3ZJOd4kZYJvBO7b8H3#im}uXY;I+sdlFT0P^XRCSY+rcb z0sb?3pb}&S)M+88dK)naB?KQMIC$J8+#JugH!MGs{G^c9j9EO`F3S@`MXaNBMLY9G8--LKZ6tJqo_ps`DL;wJS8acE9u?Mc0yc2kM zN52f^1Q_>@KEpAarj#$D6@=zCq0+dfK8IJ?Af2EOij&lUZYr%8l5O0rkLcf}w30K{ z9!M@)sks!@q(T1oa9rxgvC1^jM|cZD9C22J4GKMm+P&z7@QZ|?Dv29_)a*$I%rEBy zgf{T0_JGz8zUD2tzAqCnApJ3gV~k;E^^P9!d0eBxSM|4Td20kMqw-cC1x6PdS^J}|?u<7ouM~`C#6j~y4Bmh(fR!@)6iOoI+ zKsBoq?Y%dB01V3Ag*^bS-3RcwW7jU9!gfVmXw;Zlk2*SGATKXZ3>_6?J%D+822&ft z!hKM^zG!Y=fD~UG7`Hht`FHh+KoiVo;5zO@&TH=53eYhp`Yr%qCz(CQ^z%j7X)Q@W z9WNonWCL&9JR?gK;z5QL+97**LWZ3;)InUT8uM4S626)80HHSQ@a3i}EbEb9rjbpr zG4AeH*(Z?r7{QxrN3fim4@f>zvT~A=rHQbOxrACVsavlV4YImEtk(W~N#OSP5jO1e zHPG9w1`^}Y$bA+bNs9oCh?B=(SJ~yumk*-bnguq=>~gpZWQ=UENNwM79|mo+2C?!_ z+u=XA-#~gq6y%=9{`c~ut_gmfHFkOj->?LBKscp45y{Y^QP|{~mq`o^TU4c2Z~<$a zvt)^GlM{B%4$zx?NDD*yJ4z)V>;|@*yOEL6CG?EB0BG3_S}#0SaPMqfFfbWs*D)sG zbFr~2?>T?aZu4f=UcGu%cBvwAVIRzpCb|0bZ0&~+QgMCXGtXC0Fub4-)$JviGP!JO!NXy9 z&3UIPCoRq9f(wl9_f!-SJ~&@&k-I}sm+--NxD3e*eU~4Nsn3Z}9Rcp#lNgMX%>~yd z>;7Bp>E<5&wF~b9FSEF94CP1Re6=s=)@0Au*85sey%RQwrLBlPmM>qua_UOh-!*;S z(bsR@tfv;EZ2r20@^3vcLFYP%qL3#RhF8f3)PFH9ws-IVkYAV@?c9K~ZPHYcQc_le z*J&-bi-Gps4nSLMzY5VA)v#!ZS!#ej-wI%#_kU7LqX-?r31XeDe=svkRbO8}X0X{> z>~<$qQ@Zf5G3ox-Uqrg5-r&@!Q|a)M%0oyFRGm}-d}<+M*Mhks$WyqSejxEL0QPVM zI7~hA^iS~nc?@7Rxl=Wga$xjW+*l8Ybw)$AAg>XGN)QD~mDoM2$d?l+(jFAxS_m~2 zBV@({l(V8Poh}LF$mAV{cjS+_g=hix%5D;~%(H2J{U2L};z2y)hX6`K(W4jAc;th{ zdPw{-EXTDgqmzzO{};219|5k{JsU;z4hP|f8k~8<6b_;izfD8*(IHL+27d`uM>$zK zP`+>h50AN98ob>1xB6YYc%pAV8eJ#)`zow4wP#XrZ@YLeRFrojAnwK6hlOGsm@&uDz8<;GdRiCUlop|4SzC-FpYpk=6NUcfO4w#qV|DlB zBTRZM0;M2EvI;zb+eUKL0x3o+h?riZO=LSJst4r;BrT^(EGr-Fqwn;}DdkGR*ix_)cn%T%Z4&Anw z20Np9T6N~IbeO+E-0^F>Q;^}7fwN@y*%*l@OjGw)U)Qx*Po&G>_sy>%c5fon_IyG% zFsH*%H8hc_|G;`tBx&--5tfV61^CcuPUc(E9hZhW0fPz-}8AA>+Js?9SB<*ExKn-hj zAp0QVs(o}UxkcVwCIEak@}|l(GaTN!TNF6iIb$jV<;3}iRPDQWs`{+;xUv#TTzfUK z4h10RG$x^=RsGDZ0RPth*~m{)whm;59l#O~oYO~ZT>NeYq|U!KXPuC!1B2BFIlQo+ zBn1!6E`qAF;x|{=Ud`)V4-20aU{yuNNKSK_uqgL?O$`^0$x|m^_?)+N-N2miJQtoD zb8=O%N6J>>ewF~*AK;hUX%qOcZ6P<(=op|qUgq8-K>~}AIN9_kdZ*XjnnaogiM!cr zq7pNkpBEQ2b|T9Z*LZ^C_C7N18+rvtHZE|u9&4ptaAYz{=!Tw{sIC3rLRCL~D;!(z zffd>T~T^BM(>TgJ7Ih|GXJ-@a$5jnmHlnLjkC`h(CdXu?UCtJrdlKASR60;ibdQTDF5w2DBc{~8S27v< zVK=YxSJ46NxDS9xcFQaB%^L~qsA;XIK9fLz-nXX}DB}*=9C*?TH%!t$#g02(kq++= zO@R3cIn^XoXr5Mq{xcwwoMt0*9S@}q13G*yIwLq|1rx%)Pb#C3LJGP?f$FTSXML7t z#02G=Jn#%`0q41VGJs}dgu!Pk$cGn;vw^LG8h5ME2%Ty(eFyB}?qg=CV&W<*F%hTu z^N8z_ZUP2+_hDtVsg=UUkb`OP--X0mxg zbrYiUdQdK0j1vTV6+U@p74AdrIo&AI1w};io7?Bn)!W#NtFEl8RSEka@Umsa2&#mO zaiDrzn#JQuG_-S_K?P=4%qI3e`1Tc2QK)hsX+Jf92Wl}JMv+Cx{{9x?8$x&24xf|m zF1o;lG#}qdOq2-B2!V!J8glkxqoMX*{?d`%rGqZ4iy3m!@S05+bO3i~g}A}^L>ODj zN{t|KNT`$ptYwl>g+STZ416+p&;?Kc5sVvc0#Vz|U$gfZPJw=JeT$7&m-k#?RGoc^ zKuMBih2Qi@t{KuB3y<&S%xxOOnylVkPP0}NKfl)g0%EG-R6sv zM}4;jyR;-J;9ybi=ZvNcrQu$NUR6;B*_m2Wz z(X~X@*6$wt>A4xsBsSR-i+)Bg#J<>=G%r9wZ!(aJP^s4C#cB zvOeYOM?O6%#pZRPA`xPPfjKz?)ve5@!+|HddNupHZ(;%h`NQkx5gtLQGp`^TQCko( zb^DrXAQnfJy4^wG?~ad-_9H5opG?Pjy9f|NXvWy0^Xzy_N0+p&Iti6C-I*E5)tOj5;Ep}BeP5{^qyg5bY zc25d&+DE>tfFmMx{sb8-^V_~Ma0UV0bi#+_S(Pfz%aC0sHpoezlYHp#0`s$e)u(~ZkYt^j6 z0)ZS?RTO@4|0X%6xM{kyMwfGao^_D$bk#oPnMdFKbSirCiX8VC3RAo340y|U z?(3YK9O``)k$I7Ud;b_82@wQ>f~yE^I(suNJ2rthFvO%I;lmg!B^DIUA`z2AJw`|9 zAW*jD<>n-HHg;oAW=`hDIX7glx$w=4eALZ9ujf&kG{G2H+InF0s<@uLn=z_3mJx#Z zw2v^`$+Ng0V|{PP%@UNq!z1UK2y-DQVG@mOKdA6DZF3v0waD|uJyf7k`6PaJUDZ{2 z{otb`yP4@;1Cyr7%NYozp`AT%es{j205JY%H>2d$(IqZ+X7hTK;A1mX$1GGBj5*8K ztWl;$zFi8S4FbQ$Kk=Rsa~RhAuFW1Y`q&dwMeEq8e@zTfwUeuvl?EX@N(ij z7ZenT=bX(oeZd=@e@}}|v0TMN8^zhCBYJ0z!z5r?S~3jLQVOIA?}e=D-Yx2eias9h z!+fI|M=(24%nn+6pO)m8R}h198Sy9sL+=1KGa>qPt@YWg8>$-kjihpiv6o^*W2#@^ z)~JiQ>KK#FfMOubd*^fe*)@@1;+hQfO^aTZfl8)uur~o%ZD80zm8@MJ8TJU?bcRUe znS6*LiynnfH9!2GJaVyf2*8CM6MJ0ItSM&WLli-Owp>bBbL7#q3^@%AjkKWC8BgxT zF6^sr&0PX%w%E$|uU|j8nOvoT%AgR=+L0RHaN_p>Kl%i6qw7-R)uRczq^cPFU zW+b+)9s?lww&Hf+6=%~)ZcIdHs?SKmT`g2yU?#7e9vx4l!sb1tf!XP80e^w8^Z-Rs zVNp8)Le4I@p5!K-dT}sjAI)ks_8{v=!iK~hNyJt8bG6jtC#EMVSrcHl;`yUAWek;b zm^9)#F~P|xHg^~eLT85*1q-vkdS~XVpynrwzTm;N1KWe~fcwis7Lj>Fpt-?l@gU>_ z(ag~?p8Og$Fz%pc_x@Go1dEElU8`qXuzd^n*ud1bl|S*7!`n+vZBB76xpL1`HbBIn zGP0V=7_Wl9Drx{??x!f(TtroPzuG9$$A6^G-P5of1q!nMVUWp;^Nk%&#SEF1}*eh5TWL#3M68E~d-nIS!XZ8&o|K8YXPu z=G_(hz?D1_Yo5Ju2N4PYrLIAmraodyh!+jM<-2*Y?&WJ0d><1F_%ddm#&~|1cn_t* zi(4i&q@q5xcY24jLVWdplWijBr9-LWYO5|#BHnGihbBKrM&UMLn+USVDVDz{*J^i-5NnLf6B1C^)IO#MQHIqiN z-5@ZKC!r&~WCv`V=7xYyTu!;(`f#%40d@C@1r7T=>cUyn9q!E-joZTx#$Xuw*jk{d zias1wRqabsQc_Y!^XQBO928Ne?i$0>PrkVrm1DYBq9&J;uvl|TsoqRNsnJiDJ9up< ziW_qY#0z3K$3A^Z1lq>+=P~4RQrW^x5p|fVG#jb zF%;~wgn@N3M{EtTVY-!{A5#IOM#9C7MCc109qG;35(SEctWt?Rf+>WX& z*#G%#t3z+nS|nL{olha33{2#G{d#l}9_N{E_zY639s20H3Ft#d64Rww#8<-EZ4WHn z>XfIGTr^k_-wQqygyPhB)X&#?rI3&kZp~N=4_Hq>>UMi05I|fk@^oo^-LgE6SXoCIA48TkM#EgLVr=XwuqPcG?g`@ zWIc$|%3UW`PD*M_3Exf{gDSvA6P7$(eC{shdx#;$I_&J|fCRaL2!^QE>rd=%1DEgz zl8^+8RV!D95h}pO1zTIpZY+r|0n|l^Su(fESDlG;@sUI|5Pp&37QlOC2K{KyZ6W5e z&dma~@*!kMZAaGa+E}66QIkBN3rBOQ1b9~rgA(FgdrdonjiikT0mSwH=bwfrRcJcU z2L1%F@*a(y;!@U^S)McPXUCLo;$jEfK_pgQ=K7*KCTD3O=oH3flBd6df+()MciwHu zu@=)oU#^nfw9y@xWBLaP|3{n^K0*1D^ppf@IlR==MqM}e00=1ojBAqXR9#)&{d$Gp zF*M~^9a%&mJs^_acaI_klA{JlFVp<%?zlFzCLSXb4csu<023$;K(b2kF{8n%Jb=hY z&5>U>HMNhBkbeprMN?^OMAQNa-_fEI>KcV_HYjGT%!yD~rMzPGM%j5g?X@>FF)#nM z+?C9sE=@7!X1muS7D4I#Hq zC_z#k?4h`MI;I!Xb;VZEmW22@R+GCZKmC?OzF6V$4 z5rU3ON?IMx$Og<#4x#PEWFe6>#>9jq<~s|~E&7;_M>A91x!U42{6G`l%AM%E(4oL+zh0f(iR5 zEKJDww%y)=-qv8?$F+!eZ)llN*oVZ$*+qM_MyGb6qjM7ge~1Djql6App*Sc(>mXmW9)Lk)pKPmso>MwgGU)u0Zi--nq37)a*1y z8K{E_TR%rjOX~>WESH(*5i0PpQm+DJx|i2djy`V<=8eo9gqxeE@X9*dLPh%Obm7E1 zyVq7GS{nr(cs%Nj4g)Ky?c-1gCzl3EnEBA z<$7 z_g=8Bp-tW!ozGOH=#nTKltT>P1$JG2mcY*v~I2P8`PXw`|_O0mN^E z3vQuYup2rFk7bZ4+}vcM?km;s*gj|Wq7D3j11v#Bxq+F%^on*m$LOX!MVsD-S6&Vd zpFi|-VC*4_>PNpWKe(zzt>F#t-2v2d@4z$D(9**E5~$k5Q9=*IugR_)Ca9OO^L7Qx z$$t9u2?KH}DV-zXvm|RSuigJ_HSn%uHr%68FfzD_p@ten?#k*L8P&L@cJ1E1dk<0> z@sM#%dk`Rm(HBZZTRI8f6dKh=vB5au4w{O`^tg5T%=N(46jn{g&G>035 zWJy?@r-xo$1p6qY!gT2r5VUt5IB;Nru-=V^hK4lXX7mDA?zxqy62iCZwvhqzMfVe~U<#7F}3njJRcPlH-h5kOQe>R)!)PiyhcM&z#m~0;u0; z^6|keUH({kSLAg%z`^RfD;LD$qG{{39dNUpt%kPph-F-Sd>!^>E`nJnmagXf3g6f3 zbGWYpHI&8sllV|*N^H4*1_lOPNBX~;SXjKr1GMS;>J@VD-f{%dW1C$QLLNR801o{I zAo&#lBEpf8k&<{zg{Wn2S!GU@Y=1eoSFo(*4f4^aK)hVRIKm6>dP`RVruSNG)N zQeuZZ8`HttYn?(=D39B+a#yb+Z;j^JvXFe>yVQ zNZwt|>|REGLok{>0y#@v%m|<4r{l2^BN!88)_}}&CZM_zhwbE*TOB7czhiQT+N&b9AU5JoniZ^_$3?-t)9IHCo*MgI_!C2wQ~LK1;~l Date: Wed, 10 Jan 2024 16:31:02 -0600 Subject: [PATCH 20/30] fixup docs not related to ice_shelf_2d --- docs/developers_guide/ocean/tasks/correlated_tracers_2d.md | 4 ++-- docs/developers_guide/ocean/tasks/divergent_2d.md | 2 +- docs/developers_guide/ocean/tasks/geostrophic.md | 2 +- docs/developers_guide/ocean/tasks/nondivergent_2d.md | 2 +- docs/developers_guide/ocean/tasks/rotation_2d.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/developers_guide/ocean/tasks/correlated_tracers_2d.md b/docs/developers_guide/ocean/tasks/correlated_tracers_2d.md index a004480bc..26d1cff6a 100644 --- a/docs/developers_guide/ocean/tasks/correlated_tracers_2d.md +++ b/docs/developers_guide/ocean/tasks/correlated_tracers_2d.md @@ -13,14 +13,14 @@ of convergence. ## framework The config options for the `correlated_tracers_2d` test is described in -{ref}`ocean-correlated_tracers-2d` in the User's Guide. +{ref}`ocean-correlated-tracers-2d` in the User's Guide. Additionally, the test uses a `forward.yaml` file with a few common model config options related to drag and default horizontal and vertical momentum and tracer diffusion, as well as defining `mesh`, `input`, `restart`, and `output` streams. This file has Jinja templating that is used to update model config options based on Polaris config options, see -{ref}`dev-ocean-spherical-convergence`. +{ref}`dev-ocean-convergence`. ### base_mesh diff --git a/docs/developers_guide/ocean/tasks/divergent_2d.md b/docs/developers_guide/ocean/tasks/divergent_2d.md index c99a01371..b00faeb72 100644 --- a/docs/developers_guide/ocean/tasks/divergent_2d.md +++ b/docs/developers_guide/ocean/tasks/divergent_2d.md @@ -20,7 +20,7 @@ model config options related to drag and default horizontal and vertical momentum and tracer diffusion, as well as defining `mesh`, `input`, `restart`, and `output` streams. This file has Jinja templating that is used to update model config options based on Polaris config options, see -{ref}`dev-ocean-spherical-convergence`. +{ref}`dev-ocean-convergence`. ### base_mesh diff --git a/docs/developers_guide/ocean/tasks/geostrophic.md b/docs/developers_guide/ocean/tasks/geostrophic.md index e6bc8b0f3..d9087987e 100644 --- a/docs/developers_guide/ocean/tasks/geostrophic.md +++ b/docs/developers_guide/ocean/tasks/geostrophic.md @@ -35,7 +35,7 @@ in {ref}`ocean-geostrophic-init` in the User's Guide. The class {py:class}`polaris.ocean.tasks.geostrophic.forward.Forward` descends from {py:class}`polaris.ocean.convergence.spherical.SphericalConvergenceForward`, and defines a step for running MPAS-Ocean from an initial condition produced in -an `init` step. See {ref}`dev-ocean-spherical-convergence` for some relevant +an `init` step. See {ref}`dev-ocean-convergence` for some relevant discussion of the parent class. The time step is determined from the resolution based on the `dt_per_km` config option in the `[spherical_convergences]` section. Other model config options are taken from `forward.yaml`. diff --git a/docs/developers_guide/ocean/tasks/nondivergent_2d.md b/docs/developers_guide/ocean/tasks/nondivergent_2d.md index ca46ec97f..607e51db1 100644 --- a/docs/developers_guide/ocean/tasks/nondivergent_2d.md +++ b/docs/developers_guide/ocean/tasks/nondivergent_2d.md @@ -20,7 +20,7 @@ model config options related to drag and default horizontal and vertical momentum and tracer diffusion, as well as defining `mesh`, `input`, `restart`, and `output` streams. This file has Jinja templating that is used to update model config options based on Polaris config options, see -{ref}`dev-ocean-spherical-convergence`. +{ref}`dev-ocean-convergence`. ### base_mesh diff --git a/docs/developers_guide/ocean/tasks/rotation_2d.md b/docs/developers_guide/ocean/tasks/rotation_2d.md index ec93863c1..3ebc2908a 100644 --- a/docs/developers_guide/ocean/tasks/rotation_2d.md +++ b/docs/developers_guide/ocean/tasks/rotation_2d.md @@ -20,7 +20,7 @@ model config options related to drag and default horizontal and vertical momentum and tracer diffusion, as well as defining `mesh`, `input`, `restart`, and `output` streams. This file has Jinja templating that is used to update model config options based on Polaris config options, see -{ref}`dev-ocean-spherical-convergence`. +{ref}`dev-ocean-convergence`. ### base_mesh From fc51036958182b902cde5b4c6b203d45b217455e Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 11 Jan 2024 17:16:19 -0600 Subject: [PATCH 21/30] Fixup docstrings --- polaris/ocean/ice_shelf/__init__.py | 70 +++++++++++++++++-- polaris/ocean/ice_shelf/ssh_adjustment.py | 23 +++--- polaris/ocean/ice_shelf/ssh_forward.py | 32 +++++++-- .../tasks/ice_shelf_2d/default/__init__.py | 15 ++-- polaris/ocean/tasks/ice_shelf_2d/forward.py | 14 +++- polaris/ocean/tasks/ice_shelf_2d/validate.py | 7 +- polaris/ocean/tasks/ice_shelf_2d/viz.py | 14 +++- 7 files changed, 144 insertions(+), 31 deletions(-) diff --git a/polaris/ocean/ice_shelf/__init__.py b/polaris/ocean/ice_shelf/__init__.py index c21078d5e..cf27169b6 100644 --- a/polaris/ocean/ice_shelf/__init__.py +++ b/polaris/ocean/ice_shelf/__init__.py @@ -8,10 +8,41 @@ class IceShelfTask(Task): """ - shared_steps : dict of dict of polaris.Steps - The shared steps to include as symlinks + A class for tasks with domains containing ice shelves + + Attributes + ---------- + sshdir : string + The directory to put the ssh_adjustment steps in + + component : polaris.ocean.Ocean + The ocean component that this task belongs to + + resolution : float + The resolution of the test case in km """ def __init__(self, component, resolution, name, subdir, sshdir=None): + """ + Construct ice shelf task + + Parameters + ---------- + component : polaris.ocean.Ocean + The ocean component that this task belongs to + + resolution : float + The resolution of the test case in km + + name : string + The name of the step + + subdir : string + The subdirectory for the step + + sshdir : string, optional + The directory to put the ssh_adjustment steps in. If None, + defaults to subdir. + """ if sshdir is None: sshdir = subdir super().__init__(component=component, name=name, subdir=subdir) @@ -24,6 +55,37 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, yaml_filename='ssh_forward.yaml', yaml_replacements=None): + """ + Setup ssh_forward and ssh_adjustment steps for all iterations + + Parameters + ---------- + init : polaris.Step + the step that produced the initial condition + + config : polaris.config.PolarisConfigParser + The configuration for this task + + config_filename : str + the configuration filename + + package : Package + The package name or module object that contains ``namelist`` + from which ssh_forward steps will derive their configuration + + yaml_filename : str, optional + the yaml filename used for ssh_forward steps + + yaml_replacements : Dict, optional + key, string combinations for templated replacements in the yaml + file + + Returns + ------- + shared_step : polaris.Step + the final ssh_adjustment step that produces the input to the next + forward step + """ resolution = self.resolution component = self.component indir = self.sshdir @@ -58,8 +120,8 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, shared_step = component.steps[subdir] else: ssh_adjust = SshAdjustment( - component=component, resolution=resolution, indir=indir, - name=name, init=init, forward=ssh_forward) + component=component, indir=indir, name=name, init=init, + forward=ssh_forward) ssh_adjust.set_shared_config(config, link=config_filename) shared_step = ssh_adjust if indir == self.subdir: diff --git a/polaris/ocean/ice_shelf/ssh_adjustment.py b/polaris/ocean/ice_shelf/ssh_adjustment.py index 7eb4a45f8..27c5bd594 100644 --- a/polaris/ocean/ice_shelf/ssh_adjustment.py +++ b/polaris/ocean/ice_shelf/ssh_adjustment.py @@ -11,24 +11,28 @@ class SshAdjustment(Step): A step for iteratively adjusting the pressure from the weight of the ice shelf to match the sea-surface height as part of ice-shelf 2D test cases """ - def __init__(self, component, resolution, init, forward, indir=None, + def __init__(self, component, init, forward, indir=None, name='ssh_adjust'): """ Create the step Parameters ---------- - resolution : float - The resolution of the test case in m + component : polaris.ocean.Ocean + The ocean component that this task belongs to - coord_type: str - The coordinate type (e.g., 'z-star', 'single_layer', etc.) + init : polaris.Step + the step that produced the initial condition - iteration : int, optional - the iteration number - """ - self.resolution = resolution + forward: polaris.Step + the step that produced the state which will be adjusted + + indir : str, optional + the directory the step is in, to which ``name`` will be appended + name : str, optional + the name of this step + """ super().__init__(component=component, name=name, indir=f'{indir}/ssh_adjustment') @@ -46,7 +50,6 @@ def run(self): """ Adjust the sea surface height or land-ice pressure to be dynamically consistent with one another. - """ logger = self.logger config = self.config diff --git a/polaris/ocean/ice_shelf/ssh_forward.py b/polaris/ocean/ice_shelf/ssh_forward.py index e02c6036e..cd32d5a1c 100644 --- a/polaris/ocean/ice_shelf/ssh_forward.py +++ b/polaris/ocean/ice_shelf/ssh_forward.py @@ -12,11 +12,15 @@ class SshForward(OceanModelStep): resolution : float The resolution of the task in km - dt : float - The model time step in seconds + package : Package + The package name or module object that contains ``namelist`` - btr_dt : float - The model barotropic time step in seconds + yaml_filename : str, optional + the yaml filename used for ssh_forward steps + + yaml_replacements : Dict, optional + key, string combinations for templated replacements in the yaml + file """ def __init__(self, component, resolution, mesh, init, name='ssh_forward', subdir=None, @@ -37,16 +41,32 @@ def __init__(self, component, resolution, mesh, init, name : str the name of the task + mesh: polaris.Step + The step used to produce the mesh + + init: polaris.Step + The step used to produce the initial condition + subdir : str, optional the subdirectory for the step. If neither this nor ``indir`` are provided, the directory is the ``name`` - indir : str, optional - the directory the step is in, to which ``name`` will be appended + package : str, optional + where ssh_forward steps will derive their configuration + + yaml_filename : str, optional + the yaml filename used for ssh_forward steps + + yaml_replacements : Dict, optional + key, string combinations for templated replacements in the yaml + file iteration : int, optional the iteration number + indir : str, optional + the directory the step is in, to which ``name`` will be appended + ntasks : int, optional the number of tasks the step would ideally use. If fewer tasks are available on the system, the step will run on all available diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index 403b0cbfc..235ff2c86 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -7,12 +7,11 @@ class Default(IceShelfTask): """ The default ice shelf 2d test case simply creates the mesh and - initial condition, then performs a short forward run. + initial condition, then performs a short forward run. Optionally, + tidal forcing can be added or visualization and restart steps. Attributes ---------- - include_viz : bool - Include Viz step """ def __init__(self, component, resolution, indir, init, config, @@ -33,10 +32,16 @@ def __init__(self, component, resolution, indir, init, config, The directory the task is in, to which the test case name will be added - include_viz : bool + config : polaris.config.PolarisConfigParser + The configuration for this task + + include_viz : bool, optional Include VizMap and Viz steps for each resolution - include_tides: bool + include_restart : bool, optional + Include restart and validation steps to test restart capabilities + + include_tides : bool, optional Include tidal forcing in the forward step """ if include_tides and include_restart: diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.py b/polaris/ocean/tasks/ice_shelf_2d/forward.py index 7ec7eea37..9d384fa08 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.py +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.py @@ -31,7 +31,13 @@ def __init__(self, component, resolution, mesh, init, resolution : km The resolution of the task in km - name : str + mesh: polaris.Step + The step used to produce the mesh + + init: polaris.Step + The step used to produce the initial condition + + name : str, optional the name of the task subdir : str, optional @@ -52,6 +58,12 @@ def __init__(self, component, resolution, mesh, init, openmp_threads : int, optional the number of OpenMP threads the step will use + + do_restart : bool, optional + if true, the step is restart from another forward run + + tidal_forcing : bool, optional + if true, apply tidal forcing in the forward step """ if do_restart: name = 'restart' diff --git a/polaris/ocean/tasks/ice_shelf_2d/validate.py b/polaris/ocean/tasks/ice_shelf_2d/validate.py index 249347a07..b8c17ab40 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/validate.py +++ b/polaris/ocean/tasks/ice_shelf_2d/validate.py @@ -9,7 +9,7 @@ class Validate(Step): Attributes ---------- step_subdirs : list of str - The number of processors used in each run + The steps to be compared (full run and restart run) """ def __init__(self, component, step_subdirs, indir): """ @@ -21,10 +21,11 @@ def __init__(self, component, step_subdirs, indir): The component the step belongs to step_subdirs : list of str - The number of processors used in each run + The steps to be compared (full run and restart run) indir : str - the directory the step is in, to which ``name`` will be appended + the directory the step is in, to which ``validate`` will be + appended """ super().__init__(component=component, name='validate', indir=indir) diff --git a/polaris/ocean/tasks/ice_shelf_2d/viz.py b/polaris/ocean/tasks/ice_shelf_2d/viz.py index 81a46e7aa..84d334935 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/viz.py +++ b/polaris/ocean/tasks/ice_shelf_2d/viz.py @@ -20,8 +20,18 @@ def __init__(self, component, indir, mesh, init): Parameters ---------- - test_case : compass.TestCase - The test case this step belongs to + component : polaris.Component + The component the step belongs to + + indir : str, optional + the directory the step is in, to which ``viz`` will be appended + + mesh: polaris.Step + The step used to produce the mesh + + init: polaris.Step + The step used to produce the initial condition + """ super().__init__(component=component, name='viz', indir=indir) self.add_input_file( From 1117673b3bbe0ac16466a264f2834f2492e4f5cb Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Fri, 12 Jan 2024 12:16:39 -0600 Subject: [PATCH 22/30] Add ice shelf tests to suites --- polaris/ocean/suites/nightly.txt | 2 ++ polaris/ocean/suites/pr.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/polaris/ocean/suites/nightly.txt b/polaris/ocean/suites/nightly.txt index 320dade71..725b504e1 100644 --- a/polaris/ocean/suites/nightly.txt +++ b/polaris/ocean/suites/nightly.txt @@ -1,5 +1,7 @@ ocean/planar/baroclinic_channel/10km/threads ocean/planar/baroclinic_channel/10km/decomp ocean/planar/baroclinic_channel/10km/restart +ocean/planar/ice_shelf_2d/5km/z-star/default/with_restart +ocean/planar/ice_shelf_2d/5km/z-level/default/with_restart ocean/planar/inertial_gravity_wave # ocean/planar/manufactured_solution diff --git a/polaris/ocean/suites/pr.txt b/polaris/ocean/suites/pr.txt index 0b1a93cf7..3d63ada67 100644 --- a/polaris/ocean/suites/pr.txt +++ b/polaris/ocean/suites/pr.txt @@ -1,6 +1,8 @@ ocean/planar/baroclinic_channel/10km/threads ocean/planar/baroclinic_channel/10km/decomp ocean/planar/baroclinic_channel/10km/restart +ocean/planar/ice_shelf_2d/5km/z-star/default/with_restart +ocean/planar/ice_shelf_2d/5km/z-level/default/with_restart ocean/planar/inertial_gravity_wave ocean/planar/internal_wave/standard/default ocean/planar/internal_wave/vlr/default From 88e3009ceda13eae7cd34865ce246e2b8d3f1dbe Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 16 Jan 2024 17:02:43 -0600 Subject: [PATCH 23/30] Change number of cells (to match compass) and levels ... (to keep z-level case from crashing) --- docs/users_guide/ocean/tasks/ice_shelf_2d.md | 8 +++++--- polaris/ocean/tasks/ice_shelf_2d/ice_shelf_2d.cfg | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/users_guide/ocean/tasks/ice_shelf_2d.md b/docs/users_guide/ocean/tasks/ice_shelf_2d.md index 6126b1254..aed8c890f 100644 --- a/docs/users_guide/ocean/tasks/ice_shelf_2d.md +++ b/docs/users_guide/ocean/tasks/ice_shelf_2d.md @@ -91,7 +91,9 @@ The geometry does not represent a particularly realistic ice-shelf cavity but it is a quick and useful test of the parameterization of land-ice melt fluxes and of frazil formation below ice shelves. -Two vertical coordinates, `z-star` and `z-level`, are available. In each case, there are 20 vertical levels given by the config option `vert_levels`. In the open ocean, each level is 50 m thick. +Two vertical coordinates, `z-star` and `z-level`, are available. In each case, +there are 50 vertical levels given by the config option `vert_levels`. In the +open ocean, each level is 40 m thick. ```cfg # Options related to the vertical grid @@ -101,7 +103,7 @@ Two vertical coordinates, `z-star` and `z-level`, are available. In each case, t grid_type = uniform # Number of vertical levels -vert_levels = 20 +vert_levels = 50 # The minimum number of vertical levels min_vert_levels = 3 @@ -151,7 +153,7 @@ Run duration will be discussed for individual test cases. lx = 50 # length of domain in km -ly = 220 +ly = 190 # How the land ice pressure at y Date: Wed, 17 Jan 2024 09:17:54 -0600 Subject: [PATCH 24/30] Add ssh forward class as argument to setup_ssh_ ... --- polaris/ocean/ice_shelf/__init__.py | 13 +++++---- polaris/ocean/ice_shelf/ssh_forward.py | 23 ++++++--------- .../tasks/ice_shelf_2d/default/__init__.py | 2 ++ .../ocean/tasks/ice_shelf_2d/ssh_forward.py | 28 +++++++++++++++++++ 4 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py diff --git a/polaris/ocean/ice_shelf/__init__.py b/polaris/ocean/ice_shelf/__init__.py index cf27169b6..a93d8a3d2 100644 --- a/polaris/ocean/ice_shelf/__init__.py +++ b/polaris/ocean/ice_shelf/__init__.py @@ -51,7 +51,7 @@ def __init__(self, component, resolution, name, subdir, sshdir=None): self.resolution = resolution def setup_ssh_adjustment_steps(self, init, config, config_filename, - package=None, + ForwardStep, package=None, yaml_filename='ssh_forward.yaml', yaml_replacements=None): @@ -69,6 +69,9 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, config_filename : str the configuration filename + ForwardStep : polaris.ocean_model.step + the step class used to create ssh_forward steps + package : Package The package name or module object that contains ``namelist`` from which ssh_forward steps will derive their configuration @@ -101,10 +104,10 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, if subdir in component.steps: shared_step = component.steps[subdir] else: - ssh_forward = SshForward( - component=component, resolution=resolution, indir=indir, - mesh=init, init=current_init, name=name, package=package, - yaml_filename=yaml_filename, + ssh_forward = ForwardStep( + component=component, min_resolution=resolution, + indir=indir, mesh=init, init=current_init, name=name, + package=package, yaml_filename=yaml_filename, yaml_replacements=yaml_replacements) ssh_forward.set_shared_config(config, link=config_filename) shared_step = ssh_forward diff --git a/polaris/ocean/ice_shelf/ssh_forward.py b/polaris/ocean/ice_shelf/ssh_forward.py index cd32d5a1c..c0e246e6d 100644 --- a/polaris/ocean/ice_shelf/ssh_forward.py +++ b/polaris/ocean/ice_shelf/ssh_forward.py @@ -1,16 +1,15 @@ -from polaris.mesh.planar import compute_planar_hex_nx_ny from polaris.ocean.model import OceanModelStep, get_time_interval_string class SshForward(OceanModelStep): """ - A step for performing forward ocean component runs as part of baroclinic - channel tasks. + A step for performing forward ocean component runs as part of ssh + adjustment. Attributes ---------- resolution : float - The resolution of the task in km + The minimum resolution in km used to determine the time step package : Package The package name or module object that contains ``namelist`` @@ -22,7 +21,7 @@ class SshForward(OceanModelStep): key, string combinations for templated replacements in the yaml file """ - def __init__(self, component, resolution, mesh, init, + def __init__(self, component, min_resolution, mesh, init, name='ssh_forward', subdir=None, package=None, yaml_filename='ssh_forward.yaml', yaml_replacements=None, iteration=1, indir=None, @@ -35,8 +34,8 @@ def __init__(self, component, resolution, mesh, init, component : polaris.Component The component the step belongs to - resolution : km - The resolution of the task in km + min_resolution : float + The minimum resolution in km used to determine the time step name : str the name of the task @@ -83,7 +82,7 @@ def __init__(self, component, resolution, mesh, init, indir=f'{indir}/ssh_adjustment', ntasks=ntasks, min_tasks=min_tasks, openmp_threads=openmp_threads) - self.resolution = resolution + self.resolution = min_resolution self.package = package self.yaml_filename = yaml_filename self.yaml_replacements = yaml_replacements @@ -111,12 +110,8 @@ def compute_cell_count(self): cell_count : int or None The approximate number of cells in the mesh """ - section = self.config['ice_shelf_2d'] - lx = section.getfloat('lx') - ly = section.getfloat('ly') - nx, ny = compute_planar_hex_nx_ny(lx, ly, self.resolution) - cell_count = nx * ny - return cell_count + raise ValueError('compute_cell_count method must be overridden by ' + 'spherical or planar method') def dynamic_model_config(self, at_setup): """ diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index 235ff2c86..0be9a7fed 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -1,5 +1,6 @@ from polaris.ocean.ice_shelf import IceShelfTask from polaris.ocean.tasks.ice_shelf_2d.forward import Forward +from polaris.ocean.tasks.ice_shelf_2d.ssh_forward import SshForward from polaris.ocean.tasks.ice_shelf_2d.validate import Validate from polaris.ocean.tasks.ice_shelf_2d.viz import Viz @@ -72,6 +73,7 @@ def __init__(self, component, resolution, indir, init, config, last_adjust_step = self.setup_ssh_adjustment_steps( init=init, config=config, config_filename='ice_shelf_2d.cfg', + ForwardStep=SshForward, package='polaris.ocean.tasks.ice_shelf_2d') forward_path = f'{forward_dir}/forward' diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py new file mode 100644 index 000000000..d4e4d8f2a --- /dev/null +++ b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py @@ -0,0 +1,28 @@ +from polaris.mesh.planar import compute_planar_hex_nx_ny +from polaris.ocean.ice_shelf.ssh_forward import ( + SshForward as IceShelfSshForward, +) + + +class SshForward(IceShelfSshForward): + """ + A step for performing forward ocean component runs as part of ssh + adjustment. + """ + + def compute_cell_count(self): + """ + Compute the approximate number of cells in the mesh, used to constrain + resources + + Returns + ------- + cell_count : int or None + The approximate number of cells in the mesh + """ + section = self.config['ice_shelf_2d'] + lx = section.getfloat('lx') + ly = section.getfloat('ly') + nx, ny = compute_planar_hex_nx_ny(lx, ly, self.resolution) + cell_count = nx * ny + return cell_count From c5cf2df5c54d1bb9d5795f4784bd569b97ae5209 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 18 Jan 2024 10:50:07 -0600 Subject: [PATCH 25/30] Create update_layer_thickness function --- docs/developers_guide/ocean/api.md | 5 ++ docs/developers_guide/ocean/framework.md | 5 +- polaris/ocean/vertical/__init__.py | 74 ++++++++++++++++++++---- polaris/ocean/vertical/sigma.py | 19 ++++++ polaris/ocean/vertical/zlevel.py | 20 +++++++ polaris/ocean/vertical/zstar.py | 20 +++++++ 6 files changed, 131 insertions(+), 12 deletions(-) diff --git a/docs/developers_guide/ocean/api.md b/docs/developers_guide/ocean/api.md index 4e2fea465..6a22f28f1 100644 --- a/docs/developers_guide/ocean/api.md +++ b/docs/developers_guide/ocean/api.md @@ -381,11 +381,16 @@ vertical.grid_1d.write_1d_grid vertical.partial_cells.alter_bottom_depth vertical.partial_cells.alter_ssh + vertical.sigma.init_sigma_vertical_coord + vertical.sigma.update_sigma_layer_thickness + vertical.update_layer_thickness vertical.zlevel.init_z_level_vertical_coord + vertical.zlevel.update_z_level_layer_thickness vertical.zlevel.compute_min_max_level_cell vertical.zlevel.compute_z_level_layer_thickness vertical.zlevel.compute_z_level_resting_thickness vertical.zstar.init_z_star_vertical_coord + vertical.zstar.update_z_star_layer_thickness ``` ### Visualization diff --git a/docs/developers_guide/ocean/framework.md b/docs/developers_guide/ocean/framework.md index 666ef7a66..1ecebd693 100644 --- a/docs/developers_guide/ocean/framework.md +++ b/docs/developers_guide/ocean/framework.md @@ -421,7 +421,10 @@ this section of the config file. The function `minLevelCell`, `maxLevelCell`, `cellMask`, `layerThickness`, `zMid`, and `restingThickness` variables for {ref}`ocean-z-level` and {ref}`ocean-z-star` coordinates using the `ssh` and `bottomDepth` as well -as config options from `vertical_grid`. +as config options from `vertical_grid`. The function +{py:func}`polaris.ocean.vertical.update_layer_thickness` can be used to update +`layerThickness` when either or both of `bottomDepth` and `ssh` have been +changed. (dev-ocean-rpe)= diff --git a/polaris/ocean/vertical/__init__.py b/polaris/ocean/vertical/__init__.py index 451d4a445..28cffa54e 100644 --- a/polaris/ocean/vertical/__init__.py +++ b/polaris/ocean/vertical/__init__.py @@ -1,9 +1,18 @@ -import numpy -import xarray - -from polaris.ocean.vertical.sigma import init_sigma_vertical_coord -from polaris.ocean.vertical.zlevel import init_z_level_vertical_coord -from polaris.ocean.vertical.zstar import init_z_star_vertical_coord +import numpy as np +import xarray as xr + +from polaris.ocean.vertical.sigma import ( + init_sigma_vertical_coord, + update_sigma_layer_thickness, +) +from polaris.ocean.vertical.zlevel import ( + init_z_level_vertical_coord, + update_z_level_layer_thickness, +) +from polaris.ocean.vertical.zstar import ( + init_z_star_vertical_coord, + update_z_star_layer_thickness, +) def init_vertical_coord(config, ds): @@ -101,13 +110,56 @@ def init_vertical_coord(config, ds): ds['maxLevelCell'] = ds.maxLevelCell + 1 +def update_layer_thickness(config, ds): + """ + Update the layer thicknesses in ds after the vertical coordinate has + already been initialized based on the ``bottomDepth`` and ``ssh`` + variables of the mesh data set. + + Parameters + ---------- + config : polaris.config.PolarisConfigParser + Configuration options with parameters used to construct the vertical + grid + + ds : xarray.Dataset + A data set containing ``bottomDepth`` and ``ssh`` variables used to + construct the vertical coordinate + """ + + for var in ['bottomDepth', 'ssh']: + if var not in ds: + raise ValueError(f'{var} must be added to ds before this call.') + + if 'Time' in ds.ssh.dims: + # drop it for now, we'll add it back at the end + ds['ssh'] = ds.ssh.isel(Time=0) + + coord_type = config.get('vertical_grid', 'coord_type') + + if coord_type == 'z-level': + update_z_level_layer_thickness(config, ds) + elif coord_type == 'z-star': + update_z_star_layer_thickness(config, ds) + elif coord_type == 'sigma': + update_sigma_layer_thickness(config, ds) + elif coord_type == 'haney-number': + raise ValueError('Haney Number coordinate not yet supported.') + else: + raise ValueError(f'Unknown coordinate type {coord_type}') + + # add (back) Time dimension + ds['ssh'] = ds.ssh.expand_dims(dim='Time', axis=0) + ds['layerThickness'] = ds.layerThickness.expand_dims(dim='Time', axis=0) + + def _compute_cell_mask(minLevelCell, maxLevelCell, nVertLevels): cellMask = [] for zIndex in range(nVertLevels): - mask = numpy.logical_and(zIndex >= minLevelCell, - zIndex <= maxLevelCell) + mask = np.logical_and(zIndex >= minLevelCell, + zIndex <= maxLevelCell) cellMask.append(mask) - cellMaskArray = xarray.DataArray(cellMask, dims=['nVertLevels', 'nCells']) + cellMaskArray = xr.DataArray(cellMask, dims=['nVertLevels', 'nCells']) cellMaskArray = cellMaskArray.transpose('nCells', 'nVertLevels') return cellMaskArray @@ -142,6 +194,6 @@ def _compute_zmid_from_layer_thickness(layerThickness, ssh, cellMask): z = (zTop - 0.5 * thickness).where(mask) zMid.append(z) zTop -= thickness - zMid = xarray.concat(zMid, dim='nVertLevels').transpose('Time', 'nCells', - 'nVertLevels') + zMid = xr.concat(zMid, dim='nVertLevels').transpose('Time', 'nCells', + 'nVertLevels') return zMid diff --git a/polaris/ocean/vertical/sigma.py b/polaris/ocean/vertical/sigma.py index 5e427abaf..c418bb5c1 100644 --- a/polaris/ocean/vertical/sigma.py +++ b/polaris/ocean/vertical/sigma.py @@ -74,6 +74,25 @@ def init_sigma_vertical_coord(config, ds): ds.refInterfaces, ds.ssh, ds.bottomDepth) +def update_sigma_layer_thickness(config, ds): + """ + Update the sigma vertical coordinate layer thicknesses based on the + ``bottomDepth`` and ``ssh`` variables of the mesh data set. + + Parameters + ---------- + config : polaris.config.PolarisConfigParser + Configuration options with parameters used to construct the vertical + grid + + ds : xarray.Dataset + A data set containing ``bottomDepth`` and ``ssh`` variables used to + construct the vertical coordinate + """ + ds['layerThickness'] = compute_sigma_layer_thickness( + ds.refInterfaces, ds.ssh, ds.bottomDepth) + + def compute_sigma_layer_thickness(ref_interfaces, ssh, bottom_depth, max_level=None): """ diff --git a/polaris/ocean/vertical/zlevel.py b/polaris/ocean/vertical/zlevel.py index 43d1dfe6b..ecc52535e 100644 --- a/polaris/ocean/vertical/zlevel.py +++ b/polaris/ocean/vertical/zlevel.py @@ -79,6 +79,26 @@ def init_z_level_vertical_coord(config, ds): ds.maxLevelCell) +def update_z_level_layer_thickness(config, ds): + """ + Update the z-level vertical coordinate layer thicknesses based on the + ``bottomDepth`` and ``ssh`` variables of the mesh data set. + + Parameters + ---------- + config : polaris.config.PolarisConfigParser + Configuration options with parameters used to construct the vertical + grid + + ds : xarray.Dataset + A data set containing ``bottomDepth`` and ``ssh`` variables used to + construct the vertical coordinate + """ + ds['layerThickness'] = compute_z_level_layer_thickness( + ds.refTopDepth, ds.refBottomDepth, ds.ssh, ds.bottomDepth, + ds.minLevelCell, ds.maxLevelCell) + + def compute_min_max_level_cell(refTopDepth, refBottomDepth, ssh, bottomDepth, min_vert_levels=None, min_layer_thickness=None): """ diff --git a/polaris/ocean/vertical/zstar.py b/polaris/ocean/vertical/zstar.py index e02a72257..604c3336c 100644 --- a/polaris/ocean/vertical/zstar.py +++ b/polaris/ocean/vertical/zstar.py @@ -86,6 +86,26 @@ def init_z_star_vertical_coord(config, ds): ds.maxLevelCell) +def update_z_star_layer_thickness(config, ds): + """ + Update the z-star vertical coordinate layer thicknesses based on the + ``bottomDepth`` and ``ssh`` variables of the mesh data set. + + Parameters + ---------- + config : polaris.config.PolarisConfigParser + Configuration options with parameters used to construct the vertical + grid + + ds : xarray.Dataset + A data set containing ``bottomDepth`` and ``ssh`` variables used to + construct the vertical coordinate + """ + ds['layerThickness'] = _compute_z_star_layer_thickness( + ds.restingThickness, ds.ssh, ds.bottomDepth, ds.minLevelCell, + ds.maxLevelCell) + + def _compute_z_star_layer_thickness(restingThickness, ssh, bottomDepth, minLevelCell, maxLevelCell): """ From e07da2781c958791db23f24c7133d61d9f7b7893 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 18 Jan 2024 10:51:48 -0600 Subject: [PATCH 26/30] Use update_layer_thickness in ssh_adjustment --- polaris/ocean/ice_shelf/ssh_adjustment.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/polaris/ocean/ice_shelf/ssh_adjustment.py b/polaris/ocean/ice_shelf/ssh_adjustment.py index 27c5bd594..8c5854990 100644 --- a/polaris/ocean/ice_shelf/ssh_adjustment.py +++ b/polaris/ocean/ice_shelf/ssh_adjustment.py @@ -4,6 +4,7 @@ from mpas_tools.io import write_netcdf from polaris import Step +from polaris.ocean.vertical import update_layer_thickness class SshAdjustment(Step): @@ -101,9 +102,7 @@ def run(self): ds_out['landIceDraft'] = final_ssh # we also need to stretch layerThickness to be compatible with # the new SSH - stretch = ((final_ssh + ds_mesh.bottomDepth) / - (init_ssh + ds_mesh.bottomDepth)) - ds_out['layerThickness'] = ds_out.layerThickness * stretch + ds_out = update_layer_thickness(config, ds_out) land_ice_pressure = ds_out.landIcePressure.values else: # Moving the SSH up or down by deltaSSH would change the From 0a5ef2996df00e49ceedb16537e26c35ea958a8b Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 13 Feb 2024 16:30:39 +0100 Subject: [PATCH 27/30] Update shared ice_shelf framework Generalize input filenames (instead of steps) resolution --> min_resolution --- polaris/ocean/ice_shelf/__init__.py | 41 +++++++++++++--------- polaris/ocean/ice_shelf/ssh_adjustment.cfg | 6 ++-- polaris/ocean/ice_shelf/ssh_adjustment.py | 16 ++++----- polaris/ocean/ice_shelf/ssh_forward.py | 41 +++++++++++----------- 4 files changed, 56 insertions(+), 48 deletions(-) diff --git a/polaris/ocean/ice_shelf/__init__.py b/polaris/ocean/ice_shelf/__init__.py index a93d8a3d2..859fc1cb1 100644 --- a/polaris/ocean/ice_shelf/__init__.py +++ b/polaris/ocean/ice_shelf/__init__.py @@ -18,10 +18,10 @@ class IceShelfTask(Task): component : polaris.ocean.Ocean The ocean component that this task belongs to - resolution : float + min_resolution : float The resolution of the test case in km """ - def __init__(self, component, resolution, name, subdir, sshdir=None): + def __init__(self, component, min_resolution, name, subdir, sshdir=None): """ Construct ice shelf task @@ -30,7 +30,7 @@ def __init__(self, component, resolution, name, subdir, sshdir=None): component : polaris.ocean.Ocean The ocean component that this task belongs to - resolution : float + min_resolution : float The resolution of the test case in km name : string @@ -48,9 +48,10 @@ def __init__(self, component, resolution, name, subdir, sshdir=None): super().__init__(component=component, name=name, subdir=subdir) self.sshdir = sshdir self.component = component - self.resolution = resolution + self.min_resolution = min_resolution - def setup_ssh_adjustment_steps(self, init, config, config_filename, + def setup_ssh_adjustment_steps(self, mesh_filename, graph_filename, + init_filename, config, config_filename, ForwardStep, package=None, yaml_filename='ssh_forward.yaml', yaml_replacements=None): @@ -60,16 +61,23 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, Parameters ---------- - init : polaris.Step - the step that produced the initial condition - config : polaris.config.PolarisConfigParser The configuration for this task + mesh_filename : str + the mesh filename (relative to the base work directory) + + graph_filename : str + the graph filename (relative to the base work directory) + + init_filename : str + the initial condition filename (relative to the base work + directory) + config_filename : str the configuration filename - ForwardStep : polaris.ocean_model.step + ForwardStep : polaris.ocean.ice_shelf.ssh_forward.SshForward the step class used to create ssh_forward steps package : Package @@ -89,7 +97,7 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, the final ssh_adjustment step that produces the input to the next forward step """ - resolution = self.resolution + min_resolution = self.min_resolution component = self.component indir = self.sshdir @@ -97,7 +105,7 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, # for the first iteration, ssh_forward step is run from the initial # state - current_init = init + current_init_filename = init_filename for iteration in range(num_iterations): name = f'ssh_forward_{iteration}' subdir = f'{indir}/ssh_adjustment/{name}' @@ -105,8 +113,9 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, shared_step = component.steps[subdir] else: ssh_forward = ForwardStep( - component=component, min_resolution=resolution, - indir=indir, mesh=init, init=current_init, name=name, + component=component, min_resolution=min_resolution, + indir=indir, graph_filename=graph_filename, + init_filename=current_init_filename, name=name, package=package, yaml_filename=yaml_filename, yaml_replacements=yaml_replacements) ssh_forward.set_shared_config(config, link=config_filename) @@ -123,8 +132,8 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, shared_step = component.steps[subdir] else: ssh_adjust = SshAdjustment( - component=component, indir=indir, name=name, init=init, - forward=ssh_forward) + component=component, indir=indir, name=name, + mesh_filename=mesh_filename, forward=ssh_forward) ssh_adjust.set_shared_config(config, link=config_filename) shared_step = ssh_adjust if indir == self.subdir: @@ -134,6 +143,6 @@ def setup_ssh_adjustment_steps(self, init, config, config_filename, self.add_step(shared_step, symlink=symlink) # for the next iteration, ssh_forward is run from the adjusted # initial state (the output of ssh_adustment) - current_init = shared_step + current_init_filename = f'{shared_step.path}/output.nc' return shared_step diff --git a/polaris/ocean/ice_shelf/ssh_adjustment.cfg b/polaris/ocean/ice_shelf/ssh_adjustment.cfg index 359e05e94..ef2488eb4 100644 --- a/polaris/ocean/ice_shelf/ssh_adjustment.cfg +++ b/polaris/ocean/ice_shelf/ssh_adjustment.cfg @@ -20,10 +20,10 @@ adjust_variable = landIcePressure time_integrator = split_explicit # Time step in seconds as a function of resolution -rk4_dt_per_km = 6 +rk4_dt_per_km = 1.5 # Time step in seconds as a function of resolution -split_dt_per_km = 6 +split_dt_per_km = 30 # Time step in seconds as a function of resolution -btr_dt_per_km = 1 +btr_dt_per_km = 1.5 diff --git a/polaris/ocean/ice_shelf/ssh_adjustment.py b/polaris/ocean/ice_shelf/ssh_adjustment.py index 8c5854990..6b49ef63d 100644 --- a/polaris/ocean/ice_shelf/ssh_adjustment.py +++ b/polaris/ocean/ice_shelf/ssh_adjustment.py @@ -12,7 +12,7 @@ class SshAdjustment(Step): A step for iteratively adjusting the pressure from the weight of the ice shelf to match the sea-surface height as part of ice-shelf 2D test cases """ - def __init__(self, component, init, forward, indir=None, + def __init__(self, component, mesh_filename, forward, indir=None, name='ssh_adjust'): """ Create the step @@ -22,8 +22,8 @@ def __init__(self, component, init, forward, indir=None, component : polaris.ocean.Ocean The ocean component that this task belongs to - init : polaris.Step - the step that produced the initial condition + mesh_filename : str + the mesh filename (relative to the base work directory) forward: polaris.Step the step that produced the state which will be adjusted @@ -42,7 +42,7 @@ def __init__(self, component, init, forward, indir=None, self.add_input_file(filename='init.nc', work_dir_target=f'{forward.path}/init.nc') self.add_input_file(filename='mesh.nc', - work_dir_target=f'{init.path}/culled_mesh.nc') + work_dir_target=mesh_filename) self.add_output_file(filename='output.nc') # no setup() is needed @@ -81,13 +81,13 @@ def run(self): on_a_sphere = ds_out.attrs['on_a_sphere'].lower() == 'yes' if 'minLevelCell' in ds_final: - minLevelCell = ds_final.minLevelCell - 1 + min_level_cell = ds_final.minLevelCell - 1 else: - minLevelCell = ds_mesh.minLevelCell - 1 + min_level_cell = ds_mesh.minLevelCell - 1 init_ssh = ds_init.ssh final_ssh = ds_final.ssh - top_density = ds_final.density.isel(nVertLevels=minLevelCell) + top_density = ds_final.density.isel(nVertLevels=min_level_cell) mask = ds_init[mask_variable] delta_ssh = mask * (final_ssh - init_ssh) @@ -102,7 +102,7 @@ def run(self): ds_out['landIceDraft'] = final_ssh # we also need to stretch layerThickness to be compatible with # the new SSH - ds_out = update_layer_thickness(config, ds_out) + update_layer_thickness(config, ds_out) land_ice_pressure = ds_out.landIcePressure.values else: # Moving the SSH up or down by deltaSSH would change the diff --git a/polaris/ocean/ice_shelf/ssh_forward.py b/polaris/ocean/ice_shelf/ssh_forward.py index c0e246e6d..2cad9d38b 100644 --- a/polaris/ocean/ice_shelf/ssh_forward.py +++ b/polaris/ocean/ice_shelf/ssh_forward.py @@ -8,7 +8,7 @@ class SshForward(OceanModelStep): Attributes ---------- - resolution : float + min_resolution : float The minimum resolution in km used to determine the time step package : Package @@ -21,11 +21,11 @@ class SshForward(OceanModelStep): key, string combinations for templated replacements in the yaml file """ - def __init__(self, component, min_resolution, mesh, init, - name='ssh_forward', subdir=None, + def __init__(self, component, min_resolution, init_filename, + graph_filename, name='ssh_forward', subdir=None, package=None, yaml_filename='ssh_forward.yaml', - yaml_replacements=None, iteration=1, indir=None, - ntasks=None, min_tasks=None, openmp_threads=1): + yaml_replacements=None, indir=None, ntasks=None, + min_tasks=None, openmp_threads=1): """ Create a new task @@ -37,14 +37,15 @@ def __init__(self, component, min_resolution, mesh, init, min_resolution : float The minimum resolution in km used to determine the time step - name : str - the name of the task + init_filename : str + the initial condition filename (relative to the base work + directory) - mesh: polaris.Step - The step used to produce the mesh + graph_filename : str + the graph filename (relative to the base work directory) - init: polaris.Step - The step used to produce the initial condition + name : str, optional + the name of the task subdir : str, optional the subdirectory for the step. If neither this nor ``indir`` @@ -60,9 +61,6 @@ def __init__(self, component, min_resolution, mesh, init, key, string combinations for templated replacements in the yaml file - iteration : int, optional - the iteration number - indir : str, optional the directory the step is in, to which ``name`` will be appended @@ -82,7 +80,7 @@ def __init__(self, component, min_resolution, mesh, init, indir=f'{indir}/ssh_adjustment', ntasks=ntasks, min_tasks=min_tasks, openmp_threads=openmp_threads) - self.resolution = min_resolution + self.min_resolution = min_resolution self.package = package self.yaml_filename = yaml_filename self.yaml_replacements = yaml_replacements @@ -91,9 +89,9 @@ def __init__(self, component, min_resolution, mesh, init, self.add_yaml_file('polaris.ocean.config', 'output.yaml') self.add_input_file(filename='init.nc', - work_dir_target=f'{init.path}/output.nc') + work_dir_target=init_filename) self.add_input_file(filename='graph.info', - work_dir_target=f'{mesh.path}/culled_graph.info') + work_dir_target=graph_filename) self.add_output_file( filename='output.nc', @@ -135,19 +133,20 @@ def dynamic_model_config(self, at_setup): section = config['ssh_adjustment'] - # dt is proportional to resolution: default 30 seconds per km + # dt is proportional to resolution time_integrator = section.get('time_integrator') if time_integrator == 'RK4': dt_per_km = section.getfloat('rk4_dt_per_km') else: dt_per_km = section.getfloat('split_dt_per_km') - dt_str = get_time_interval_string(seconds=dt_per_km * self.resolution) + dt_str = get_time_interval_string( + seconds=dt_per_km * self.min_resolution) - # btr_dt is also proportional to resolution: default 1.5 seconds per km + # btr_dt is also proportional to resolution btr_dt_per_km = section.getfloat('btr_dt_per_km') btr_dt_str = get_time_interval_string( - seconds=btr_dt_per_km * self.resolution) + seconds=btr_dt_per_km * self.min_resolution) s_per_hour = 3600. run_duration = section.getfloat('run_duration') From 55f8afaa05fb8bb0545c926904b15079a3bcf2a6 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 13 Feb 2024 16:33:44 +0100 Subject: [PATCH 28/30] Update ice_shelf_2d with ice_shelf framework updates --- polaris/ocean/tasks/ice_shelf_2d/default/__init__.py | 7 +++++-- polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py index 0be9a7fed..32de752e9 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py +++ b/polaris/ocean/tasks/ice_shelf_2d/default/__init__.py @@ -66,13 +66,16 @@ def __init__(self, component, resolution, indir, init, config, forward_dir = f'{indir}/{base_name}' # Put the ssh adjustment steps in indir rather than subdir - super().__init__(component=component, resolution=resolution, + super().__init__(component=component, min_resolution=resolution, name=test_name, subdir=test_subdir, sshdir=ssh_dir) self.add_step(init, symlink='init') last_adjust_step = self.setup_ssh_adjustment_steps( - init=init, config=config, config_filename='ice_shelf_2d.cfg', + mesh_filename=f'{init.path}/culled_mesh.nc', + graph_filename=f'{init.path}/culled_graph.info', + init_filename=f'{init.path}/output.nc', + config=config, config_filename='ice_shelf_2d.cfg', ForwardStep=SshForward, package='polaris.ocean.tasks.ice_shelf_2d') diff --git a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py index d4e4d8f2a..87be2ed76 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py +++ b/polaris/ocean/tasks/ice_shelf_2d/ssh_forward.py @@ -23,6 +23,6 @@ def compute_cell_count(self): section = self.config['ice_shelf_2d'] lx = section.getfloat('lx') ly = section.getfloat('ly') - nx, ny = compute_planar_hex_nx_ny(lx, ly, self.resolution) + nx, ny = compute_planar_hex_nx_ny(lx, ly, self.min_resolution) cell_count = nx * ny return cell_count From c82d60a3a7a735b0becf9f6ea6922ea36a8d3f83 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 16 Jul 2024 09:47:18 -0500 Subject: [PATCH 29/30] Fixup namelist, streams --- polaris/ocean/tasks/ice_shelf_2d/forward.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/polaris/ocean/tasks/ice_shelf_2d/forward.yaml b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml index dbd5f67bf..fb2fffe19 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/forward.yaml +++ b/polaris/ocean/tasks/ice_shelf_2d/forward.yaml @@ -5,6 +5,8 @@ omega: config_use_mom_del2: true config_mom_del2: 10.0 bottom_drag: + config_use_implicit_top_drag: true + config_implicit_top_drag_coeff: 2.5e-3 config_implicit_bottom_drag_type: constant config_implicit_constant_bottom_drag_coeff: 1.0e-3 time_management: @@ -66,6 +68,7 @@ omega: output_interval: {{ output_interval }} clobber_mode: truncate contents: + - xtime - ssh - landIcePressure - landIceDraft @@ -90,6 +93,7 @@ omega: output_interval: {{ output_interval }} clobber_mode: truncate contents: + - xtime - accumulatedFrazilIceMass - accumulatedFrazilIceSalinity - seaIceEnergy From 8364eda79d97e5cd53537911c8a56a9c7d3ac2b6 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 16 Jul 2024 09:46:11 -0500 Subject: [PATCH 30/30] Fixup restart tests --- polaris/ocean/tasks/ice_shelf_2d/validate.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/polaris/ocean/tasks/ice_shelf_2d/validate.py b/polaris/ocean/tasks/ice_shelf_2d/validate.py index b8c17ab40..c870a97f5 100644 --- a/polaris/ocean/tasks/ice_shelf_2d/validate.py +++ b/polaris/ocean/tasks/ice_shelf_2d/validate.py @@ -83,10 +83,12 @@ def run(self): logger=self.logger) if not output_pass: raise ValueError(f'Validation failed comparing outputs between ' - f'{step_subdirs[0]} and {step_subdirs[3]}.') + f'{step_subdirs[0]} and {step_subdirs[1]}.') if not land_ice_pass: - raise ValueError(f'Validation failed comparing outputs between ' - f'{step_subdirs[1]} and {step_subdirs[4]}.') + raise ValueError(f'Validation failed comparing land ice outputs ' + f'between {step_subdirs[0]} and ' + f'{step_subdirs[1]}.') if not frazil_pass: - raise ValueError(f'Validation failed comparing outputs between ' - f'{step_subdirs[2]} and {step_subdirs[5]}.') + raise ValueError(f'Validation failed comparing frazil ice outputs ' + f'between {step_subdirs[0]} and ' + f'{step_subdirs[1]}.')