diff --git a/examples/compressible_euler/mountain_hydrostatic.py b/examples/compressible_euler/schaer_mountain.py similarity index 62% rename from examples/compressible_euler/mountain_hydrostatic.py rename to examples/compressible_euler/schaer_mountain.py index 1fc113a71..c73d7aad6 100644 --- a/examples/compressible_euler/mountain_hydrostatic.py +++ b/examples/compressible_euler/schaer_mountain.py @@ -1,68 +1,71 @@ """ -The hydrostatic 1 metre high mountain test case from Melvin et al, 2010: -``An inherently mass-conserving iterative semi-implicit semi-Lagrangian -discretization of the non-hydrostatic vertical-slice equations.'', QJRMS. +The Schär mountain test case of Schär et al, 2002: +``A new terrain-following vertical coordinate formulation for atmospheric +prediction models.'', MWR. -This test describes a wave over a mountain in a hydrostatic atmosphere. +This test describes a wave over a set of idealised mountains, testing how the +discretisation handles orography. The setup used here uses the order 1 finite elements. """ - from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter + from firedrake import ( as_vector, VectorFunctionSpace, PeriodicIntervalMesh, ExtrudedMesh, - SpatialCoordinate, exp, pi, cos, Function, conditional, Mesh, Constant + SpatialCoordinate, exp, pi, cos, Function, Mesh, Constant ) from gusto import ( - Domain, IO, OutputParameters, SemiImplicitQuasiNewton, SSPRK3, DGUpwind, - TrapeziumRule, SUPGOptions, ZComponent, Perturbation, - CompressibleParameters, HydrostaticCompressibleEulerEquations, - CompressibleSolver, compressible_hydrostatic_balance, HydrostaticImbalance, - SpongeLayerParameters, MinKernel, MaxKernel, logger + Domain, CompressibleParameters, CompressibleSolver, logger, + OutputParameters, IO, SSPRK3, DGUpwind, SemiImplicitQuasiNewton, + compressible_hydrostatic_balance, SpongeLayerParameters, Exner, ZComponent, + Perturbation, SUPGOptions, TrapeziumRule, MaxKernel, MinKernel, + CompressibleEulerEquations, SubcyclingOptions, RungeKuttaFormulation ) -mountain_hydrostatic_defaults = { - 'ncolumns': 200, - 'nlayers': 120, - 'dt': 5.0, - 'tmax': 15000., - 'dumpfreq': 1500, - 'dirname': 'mountain_hydrostatic' +schaer_mountain_defaults = { + 'ncolumns': 100, + 'nlayers': 50, + 'dt': 8.0, + 'tmax': 5*60*60., # 5 hours + 'dumpfreq': 2250, # dump at end with default settings + 'dirname': 'schaer_mountain' } -def mountain_hydrostatic( - ncolumns=mountain_hydrostatic_defaults['ncolumns'], - nlayers=mountain_hydrostatic_defaults['nlayers'], - dt=mountain_hydrostatic_defaults['dt'], - tmax=mountain_hydrostatic_defaults['tmax'], - dumpfreq=mountain_hydrostatic_defaults['dumpfreq'], - dirname=mountain_hydrostatic_defaults['dirname'] +def schaer_mountain( + ncolumns=schaer_mountain_defaults['ncolumns'], + nlayers=schaer_mountain_defaults['nlayers'], + dt=schaer_mountain_defaults['dt'], + tmax=schaer_mountain_defaults['tmax'], + dumpfreq=schaer_mountain_defaults['dumpfreq'], + dirname=schaer_mountain_defaults['dirname'] ): # ------------------------------------------------------------------------ # # Parameters for test case # ------------------------------------------------------------------------ # - domain_width = 240000. # width of domain in x direction, in m - domain_height = 50000. # height of model top, in m - a = 10000. # scale width of mountain, in m - hm = 1. # height of mountain, in m - zh = 5000. # height at which mesh is no longer distorted, in m - Tsurf = 250. # temperature of surface, in K - initial_wind = 20.0 # initial horizontal wind, in m/s - sponge_depth = 20000.0 # depth of sponge layer, in m - g = 9.80665 # acceleration due to gravity, in m/s^2 - cp = 1004. # specific heat capacity at constant pressure - sponge_mu = 0.15 # parameter for strength of sponge layer, in J/kg/K + domain_width = 100000. # width of domain in x direction, in m + domain_height = 30000. # height of model top, in m + a = 5000. # scale width of mountain profile, in m + lamda = 4000. # scale width of individual mountains, in m + hm = 250. # height of mountain, in m + Tsurf = 288. # temperature of surface, in K + initial_wind = 10.0 # initial horizontal wind, in m/s + sponge_depth = 10000.0 # depth of sponge layer, in m + g = 9.810616 # acceleration due to gravity, in m/s^2 + cp = 1004.5 # specific heat capacity at constant pressure + mu_dt = 1.2 # strength of sponge layer, no units exner_surf = 1.0 # maximum value of Exner pressure at surface - max_iterations = 10 # maximum number of hydrostatic balance iterations - tolerance = 1e-7 # tolerance for hydrostatic balance iteration + max_iterations = 100 # maximum number of hydrostatic balance iterations + tolerance = 1e-12 # tolerance for hydrostatic balance iteration # ------------------------------------------------------------------------ # # Our settings for this set up # ------------------------------------------------------------------------ # + spinup_steps = 5 # Not necessary but helps balance initial conditions + alpha = 0.51 # Necessary to absorb grid scale waves element_order = 1 u_eqn_type = 'vector_invariant_form' @@ -81,9 +84,9 @@ def mountain_hydrostatic( # Describe the mountain xc = domain_width/2. x, z = SpatialCoordinate(ext_mesh) - zs = hm * a**2 / ((x - xc)**2 + a**2) + zs = hm * exp(-((x - xc)/a)**2) * (cos(pi*(x - xc)/lamda))**2 xexpr = as_vector( - [x, conditional(z < zh, z + cos(0.5 * pi * z / zh)**6 * zs, z)] + [x, z + ((domain_height - z) / domain_height) * zs] ) # Make new mesh @@ -95,64 +98,50 @@ def mountain_hydrostatic( # Equation parameters = CompressibleParameters(g=g, cp=cp) sponge = SpongeLayerParameters( - H=domain_height, z_level=domain_height-sponge_depth, mubar=sponge_mu/dt + H=domain_height, z_level=domain_height-sponge_depth, mubar=mu_dt/dt ) - eqns = HydrostaticCompressibleEulerEquations( + eqns = CompressibleEulerEquations( domain, parameters, sponge_options=sponge, u_transport_option=u_eqn_type ) # I/O output = OutputParameters( - dirname=dirname, dumpfreq=dumpfreq, dump_vtus=True, dump_nc=False + dirname=dirname, dumpfreq=dumpfreq, dump_vtus=False, dump_nc=True ) diagnostic_fields = [ - ZComponent('u'), HydrostaticImbalance(eqns), - Perturbation('theta'), Perturbation('rho') + Exner(parameters), ZComponent('u'), Perturbation('theta'), + Perturbation('rho') ] io = IO(domain, output, diagnostic_fields=diagnostic_fields) # Transport schemes + subcycling_opts = SubcyclingOptions(subcycle_by_courant=0.25) theta_opts = SUPGOptions() transported_fields = [ - TrapeziumRule(domain, "u"), - SSPRK3(domain, "rho"), - SSPRK3(domain, "theta", options=theta_opts) + TrapeziumRule(domain, "u", subcycling_options=subcycling_opts), + SSPRK3( + domain, "rho", rk_formulation=RungeKuttaFormulation.predictor, + subcycling_options=subcycling_opts + ), + SSPRK3( + domain, "theta", options=theta_opts, + subcycling_options=subcycling_opts + ) ] transport_methods = [ DGUpwind(eqns, "u"), - DGUpwind(eqns, "rho"), + DGUpwind(eqns, "rho", advective_then_flux=True), DGUpwind(eqns, "theta", ibp=theta_opts.ibp) ] # Linear solver - params = {'mat_type': 'matfree', - 'ksp_type': 'preonly', - 'pc_type': 'python', - 'pc_python_type': 'firedrake.SCPC', - # Velocity mass operator is singular in the hydrostatic case. - # So for reconstruction, we eliminate rho into u - 'pc_sc_eliminate_fields': '1, 0', - 'condensed_field': {'ksp_type': 'fgmres', - 'ksp_rtol': 1.0e-8, - 'ksp_atol': 1.0e-8, - 'ksp_max_it': 100, - 'pc_type': 'gamg', - 'pc_gamg_sym_graph': True, - 'mg_levels': {'ksp_type': 'gmres', - 'ksp_max_it': 5, - 'pc_type': 'bjacobi', - 'sub_pc_type': 'ilu'}}} - - alpha = 0.51 # off-centering parameter - linear_solver = CompressibleSolver( - eqns, alpha, solver_parameters=params, - overwrite_solver_parameters=True - ) + tau_values = {'rho': 1.0, 'theta': 1.0} + linear_solver = CompressibleSolver(eqns, alpha, tau_values=tau_values) # Time stepper stepper = SemiImplicitQuasiNewton( eqns, io, transported_fields, transport_methods, - linear_solver=linear_solver, alpha=alpha + linear_solver=linear_solver, alpha=alpha, spinup_steps=spinup_steps ) # ------------------------------------------------------------------------ # @@ -189,7 +178,8 @@ def mountain_hydrostatic( bottom_boundary = Constant(exner_surf, domain=mesh) logger.info(f'Solving hydrostatic with bottom Exner of {exner_surf}') compressible_hydrostatic_balance( - eqns, theta_b, rho_b, exner, top=False, exner_boundary=bottom_boundary + eqns, theta_b, rho_b, exner, top=False, exner_boundary=bottom_boundary, + solve_for_rho=True ) # Solve hydrostatic balance again, but now use minimum value from first @@ -243,7 +233,7 @@ def mountain_hydrostatic( theta0.assign(theta_b) rho0.assign(rho_b) - u0.project(as_vector([initial_wind, 0.0]), bcs=eqns.bcs['u']) + u0.project(as_vector([initial_wind, 0.0])) stepper.set_reference_profiles([('rho', rho_b), ('theta', theta_b)]) @@ -268,38 +258,38 @@ def mountain_hydrostatic( '--ncolumns', help="The number of columns in the vertical slice mesh.", type=int, - default=mountain_hydrostatic_defaults['ncolumns'] + default=schaer_mountain_defaults['ncolumns'] ) parser.add_argument( '--nlayers', help="The number of layers for the mesh.", type=int, - default=mountain_hydrostatic_defaults['nlayers'] + default=schaer_mountain_defaults['nlayers'] ) parser.add_argument( '--dt', help="The time step in seconds.", type=float, - default=mountain_hydrostatic_defaults['dt'] + default=schaer_mountain_defaults['dt'] ) parser.add_argument( "--tmax", help="The end time for the simulation in seconds.", type=float, - default=mountain_hydrostatic_defaults['tmax'] + default=schaer_mountain_defaults['tmax'] ) parser.add_argument( '--dumpfreq', help="The frequency at which to dump field output.", type=int, - default=mountain_hydrostatic_defaults['dumpfreq'] + default=schaer_mountain_defaults['dumpfreq'] ) parser.add_argument( '--dirname', help="The name of the directory to write to.", type=str, - default=mountain_hydrostatic_defaults['dirname'] + default=schaer_mountain_defaults['dirname'] ) args, unknown = parser.parse_known_args() - mountain_hydrostatic(**vars(args)) + schaer_mountain(**vars(args)) diff --git a/examples/compressible_euler/skamarock_klemp_hydrostatic.py b/examples/compressible_euler/skamarock_klemp_hydrostatic.py deleted file mode 100644 index f75ad9b10..000000000 --- a/examples/compressible_euler/skamarock_klemp_hydrostatic.py +++ /dev/null @@ -1,204 +0,0 @@ -""" -This example uses the hydrostatic compressible Euler equations to solve the -vertical slice gravity wave test case of Skamarock and Klemp, 1994: -``Efficiency and Accuracy of the Klemp-Wilhelmson Time-Splitting Technique'', -MWR. - -Potential temperature is transported using SUPG, and the degree 1 elements are -used. This also uses a mesh which is one cell thick in the y-direction. -""" - -from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter -from firedrake import ( - as_vector, SpatialCoordinate, PeriodicRectangleMesh, ExtrudedMesh, exp, sin, - Function, pi -) -from gusto import ( - Domain, IO, OutputParameters, SemiImplicitQuasiNewton, SSPRK3, DGUpwind, - TrapeziumRule, SUPGOptions, CourantNumber, Perturbation, - CompressibleParameters, HydrostaticCompressibleEulerEquations, - CompressibleSolver, compressible_hydrostatic_balance -) - -skamarock_klemp_hydrostatic_defaults = { - 'ncolumns': 150, - 'nlayers': 10, - 'dt': 25.0, - 'tmax': 60000., - 'dumpfreq': 1200, - 'dirname': 'skamarock_klemp_hydrostatic' -} - - -def skamarock_klemp_hydrostatic( - ncolumns=skamarock_klemp_hydrostatic_defaults['ncolumns'], - nlayers=skamarock_klemp_hydrostatic_defaults['nlayers'], - dt=skamarock_klemp_hydrostatic_defaults['dt'], - tmax=skamarock_klemp_hydrostatic_defaults['tmax'], - dumpfreq=skamarock_klemp_hydrostatic_defaults['dumpfreq'], - dirname=skamarock_klemp_hydrostatic_defaults['dirname'] -): - - # ------------------------------------------------------------------------ # - # Test case parameters - # ------------------------------------------------------------------------ # - - domain_width = 6.0e6 # Width of domain in x direction (m) - domain_length = 1.0e4 # Length of domain in y direction (m) - domain_height = 1.0e4 # Height of domain (m) - Tsurf = 300. # Temperature at surface (K) - wind_initial = 20. # Initial wind in x direction (m/s) - pert_width = 5.0e3 # Width parameter of perturbation (m) - deltaTheta = 1.0e-2 # Magnitude of theta perturbation (K) - N = 0.01 # Brunt-Vaisala frequency (1/s) - Omega = 0.5e-4 # Planetary rotation rate (1/s) - pressure_gradient_y = -1.0e-4*20 # Prescribed force in y direction (m/s^2) - - # ------------------------------------------------------------------------ # - # Our settings for this set up - # ------------------------------------------------------------------------ # - - element_order = 1 - - # ------------------------------------------------------------------------ # - # Set up model objects - # ------------------------------------------------------------------------ # - - # Domain -- 3D volume mesh - base_mesh = PeriodicRectangleMesh( - ncolumns, 1, domain_width, domain_length, quadrilateral=True - ) - mesh = ExtrudedMesh(base_mesh, nlayers, layer_height=domain_height/nlayers) - domain = Domain(mesh, dt, "RTCF", element_order) - - # Equation - parameters = CompressibleParameters(Omega=Omega) - balanced_pg = as_vector((0., pressure_gradient_y, 0.)) - eqns = HydrostaticCompressibleEulerEquations( - domain, parameters, extra_terms=[("u", balanced_pg)] - ) - - # I/O - output = OutputParameters( - dirname=dirname, dumpfreq=dumpfreq, dump_vtus=True, dump_nc=False, - dumplist=['u'], - ) - diagnostic_fields = [CourantNumber(), Perturbation('theta'), Perturbation('rho')] - io = IO(domain, output, diagnostic_fields=diagnostic_fields) - - # Transport schemes - theta_opts = SUPGOptions() - transported_fields = [ - TrapeziumRule(domain, "u"), - SSPRK3(domain, "rho"), - SSPRK3(domain, "theta", options=theta_opts) - ] - transport_methods = [ - DGUpwind(eqns, "u"), - DGUpwind(eqns, "rho"), - DGUpwind(eqns, "theta", ibp=theta_opts.ibp) - ] - - # Linear solver - linear_solver = CompressibleSolver(eqns) - - # Time stepper - stepper = SemiImplicitQuasiNewton( - eqns, io, transported_fields, transport_methods, - linear_solver=linear_solver - ) - - # ------------------------------------------------------------------------ # - # Initial conditions - # ------------------------------------------------------------------------ # - - u0 = stepper.fields("u") - rho0 = stepper.fields("rho") - theta0 = stepper.fields("theta") - - # spaces - Vt = domain.spaces("theta") - Vr = domain.spaces("DG") - - # Thermodynamic constants required for setting initial conditions - # and reference profiles - g = parameters.g - - x, _, z = SpatialCoordinate(mesh) - - # N^2 = (g/theta)dtheta/dz => dtheta/dz = theta N^2g => theta=theta_0exp(N^2gz) - thetab = Tsurf*exp(N**2*z/g) - - theta_b = Function(Vt).interpolate(thetab) - rho_b = Function(Vr) - - theta_pert = ( - deltaTheta * sin(pi*z/domain_height) - / (1 + (x - domain_width/2)**2 / pert_width**2) - ) - theta0.interpolate(theta_b + theta_pert) - - compressible_hydrostatic_balance(eqns, theta_b, rho_b, solve_for_rho=True) - - rho0.assign(rho_b) - u0.project(as_vector([wind_initial, 0.0, 0.0])) - - stepper.set_reference_profiles([('rho', rho_b), - ('theta', theta_b)]) - - # ------------------------------------------------------------------------ # - # Run - # ------------------------------------------------------------------------ # - - stepper.run(t=0, tmax=tmax) - -# ---------------------------------------------------------------------------- # -# MAIN -# ---------------------------------------------------------------------------- # - - -if __name__ == "__main__": - - parser = ArgumentParser( - description=__doc__, - formatter_class=ArgumentDefaultsHelpFormatter - ) - parser.add_argument( - '--ncolumns', - help="The number of columns in the vertical slice mesh.", - type=int, - default=skamarock_klemp_hydrostatic_defaults['ncolumns'] - ) - parser.add_argument( - '--nlayers', - help="The number of layers for the mesh.", - type=int, - default=skamarock_klemp_hydrostatic_defaults['nlayers'] - ) - parser.add_argument( - '--dt', - help="The time step in seconds.", - type=float, - default=skamarock_klemp_hydrostatic_defaults['dt'] - ) - parser.add_argument( - "--tmax", - help="The end time for the simulation in seconds.", - type=float, - default=skamarock_klemp_hydrostatic_defaults['tmax'] - ) - parser.add_argument( - '--dumpfreq', - help="The frequency at which to dump field output.", - type=int, - default=skamarock_klemp_hydrostatic_defaults['dumpfreq'] - ) - parser.add_argument( - '--dirname', - help="The name of the directory to write to.", - type=str, - default=skamarock_klemp_hydrostatic_defaults['dirname'] - ) - args, unknown = parser.parse_known_args() - - skamarock_klemp_hydrostatic(**vars(args)) diff --git a/examples/compressible_euler/skamarock_klemp_nonhydrostatic.py b/examples/compressible_euler/skamarock_klemp_nonhydrostatic.py index 057d34ade..313c21337 100644 --- a/examples/compressible_euler/skamarock_klemp_nonhydrostatic.py +++ b/examples/compressible_euler/skamarock_klemp_nonhydrostatic.py @@ -4,6 +4,10 @@ ``Efficiency and Accuracy of the Klemp-Wilhelmson Time-Splitting Technique'', MWR. +The domain is smaller than the "hydrostatic" gravity wave test, so that there +is difference between the hydrostatic and non-hydrostatic solutions. The test +can be run with and without a hydrostatic switch. + Potential temperature is transported using SUPG, and the degree 1 elements are used. """ @@ -19,19 +23,21 @@ import numpy as np from gusto import ( Domain, IO, OutputParameters, SemiImplicitQuasiNewton, SSPRK3, DGUpwind, - SUPGOptions, CourantNumber, Perturbation, Gradient, - CompressibleParameters, CompressibleEulerEquations, CompressibleSolver, - compressible_hydrostatic_balance, logger, RichardsonNumber, - RungeKuttaFormulation, SubcyclingOptions + logger, SUPGOptions, Perturbation, CompressibleParameters, + CompressibleEulerEquations, HydrostaticCompressibleEulerEquations, + compressible_hydrostatic_balance, RungeKuttaFormulation, CompressibleSolver, + SubcyclingOptions, dx, TestFunction, TrialFunction, ZComponent, + LinearVariationalProblem, LinearVariationalSolver ) skamarock_klemp_nonhydrostatic_defaults = { 'ncolumns': 150, 'nlayers': 10, 'dt': 6.0, - 'tmax': 3600., - 'dumpfreq': 300, - 'dirname': 'skamarock_klemp_nonhydrostatic' + 'tmax': 3000., + 'dumpfreq': 250, + 'dirname': 'skamarock_klemp_nonhydrostatic', + 'hydrostatic': False } @@ -41,7 +47,8 @@ def skamarock_klemp_nonhydrostatic( dt=skamarock_klemp_nonhydrostatic_defaults['dt'], tmax=skamarock_klemp_nonhydrostatic_defaults['tmax'], dumpfreq=skamarock_klemp_nonhydrostatic_defaults['dumpfreq'], - dirname=skamarock_klemp_nonhydrostatic_defaults['dirname'] + dirname=skamarock_klemp_nonhydrostatic_defaults['dirname'], + hydrostatic=skamarock_klemp_nonhydrostatic_defaults['hydrostatic'] ): # ------------------------------------------------------------------------ # @@ -61,6 +68,8 @@ def skamarock_klemp_nonhydrostatic( # ------------------------------------------------------------------------ # element_order = 1 + alpha = 0.5 + u_eqn_type = 'vector_advection_form' # ------------------------------------------------------------------------ # # Set up model objects @@ -73,18 +82,27 @@ def skamarock_klemp_nonhydrostatic( # Equation parameters = CompressibleParameters() - eqns = CompressibleEulerEquations(domain, parameters) + if hydrostatic: + eqns = HydrostaticCompressibleEulerEquations( + domain, parameters, u_transport_option=u_eqn_type + ) + else: + eqns = CompressibleEulerEquations(domain, parameters) # I/O points_x = np.linspace(0., domain_width, 100) points_z = [domain_height/2.] points = np.array([p for p in itertools.product(points_x, points_z)]) + # Adjust default directory name + if hydrostatic and dirname == skamarock_klemp_nonhydrostatic_defaults['dirname']: + dirname = f'hyd_switch_{dirname}' + # Dumping point data using legacy PointDataOutput is not supported in parallel if COMM_WORLD.size == 1: output = OutputParameters( dirname=dirname, dumpfreq=dumpfreq, pddumpfreq=dumpfreq, - dump_vtus=True, dump_nc=False, + dump_vtus=False, dump_nc=True, point_data=[('theta_perturbation', points)], ) else: @@ -94,14 +112,10 @@ def skamarock_klemp_nonhydrostatic( ) output = OutputParameters( dirname=dirname, dumpfreq=dumpfreq, pddumpfreq=dumpfreq, - dump_vtus=True, dump_nc=True, + dump_vtus=False, dump_nc=True, ) - diagnostic_fields = [ - CourantNumber(), Gradient('u'), Perturbation('theta'), - Gradient('theta_perturbation'), Perturbation('rho'), - RichardsonNumber('theta', parameters.g/Tsurf), Gradient('theta') - ] + diagnostic_fields = [Perturbation('theta'), ZComponent('u')] io = IO(domain, output, diagnostic_fields=diagnostic_fields) # Transport schemes @@ -125,12 +139,17 @@ def skamarock_klemp_nonhydrostatic( ] # Linear solver - linear_solver = CompressibleSolver(eqns) + if hydrostatic: + linear_solver = CompressibleSolver( + eqns, alpha=alpha + ) + else: + linear_solver = CompressibleSolver(eqns) # Time stepper stepper = SemiImplicitQuasiNewton( eqns, io, transported_fields, transport_methods, - linear_solver=linear_solver + linear_solver=linear_solver, alpha=alpha, num_outer=2, num_inner=2 ) # ------------------------------------------------------------------------ # @@ -160,12 +179,23 @@ def skamarock_klemp_nonhydrostatic( # Calculate hydrostatic exner compressible_hydrostatic_balance(eqns, theta_b, rho_b) + # Define initial theta theta_pert = ( deltaTheta * sin(pi*z/domain_height) / (1 + (x - domain_width/2)**2 / pert_width**2) ) theta0.interpolate(theta_b + theta_pert) - rho0.assign(rho_b) + + # find perturbed rho + gamma = TestFunction(Vr) + rho_trial = TrialFunction(Vr) + dx_qp = dx(degree=domain.max_quad_degree) + lhs = gamma * rho_trial * dx_qp + rhs = gamma * (rho_b * theta_b / theta0) * dx_qp + rho_problem = LinearVariationalProblem(lhs, rhs, rho0) + rho_solver = LinearVariationalSolver(rho_problem) + rho_solver.solve() + u0.project(as_vector([wind_initial, 0.0])) stepper.set_reference_profiles([('rho', rho_b), ('theta', theta_b)]) @@ -223,6 +253,16 @@ def skamarock_klemp_nonhydrostatic( type=str, default=skamarock_klemp_nonhydrostatic_defaults['dirname'] ) + parser.add_argument( + '--hydrostatic', + help=( + "Whether to use the hydrostatic switch to emulate the " + + "hydrostatic equations. Otherwise use the full non-hydrostatic" + + "equations." + ), + action="store_true", + default=skamarock_klemp_nonhydrostatic_defaults['hydrostatic'] + ) args, unknown = parser.parse_known_args() skamarock_klemp_nonhydrostatic(**vars(args)) diff --git a/examples/compressible_euler/test_compressible_euler_examples.py b/examples/compressible_euler/test_compressible_euler_examples.py index 384824e12..b060f2403 100644 --- a/examples/compressible_euler/test_compressible_euler_examples.py +++ b/examples/compressible_euler/test_compressible_euler_examples.py @@ -46,66 +46,42 @@ def test_dry_bryan_fritsch_parallel(): test_dry_bryan_fritsch() -# Hydrostatic equations not currently working -@pytest.mark.xfail -def test_mountain_hydrostatic(): - from mountain_hydrostatic import mountain_hydrostatic - test_name = 'mountain_hydrostatic' - mountain_hydrostatic( - ncolumns=20, - nlayers=10, - dt=5.0, - tmax=50.0, - dumpfreq=10, - dirname=make_dirname(test_name) - ) - - -# Hydrostatic equations not currently working -@pytest.mark.xfail -@pytest.mark.parallel(nprocs=4) -def test_mountain_hydrostatic_parallel(): - test_mountain_hydrostatic() - - -# Hydrostatic equations not currently working -@pytest.mark.xfail -def test_skamarock_klemp_hydrostatic(): - from skamarock_klemp_hydrostatic import skamarock_klemp_hydrostatic - test_name = 'skamarock_klemp_hydrostatic' - skamarock_klemp_hydrostatic( +def test_skamarock_klemp_nonhydrostatic(): + from skamarock_klemp_nonhydrostatic import skamarock_klemp_nonhydrostatic + test_name = 'skamarock_klemp_nonhydrostatic' + skamarock_klemp_nonhydrostatic( ncolumns=30, nlayers=5, dt=6.0, tmax=60.0, dumpfreq=10, - dirname=make_dirname(test_name) + dirname=make_dirname(test_name), + hydrostatic=False ) -# Hydrostatic equations not currently working -@pytest.mark.xfail @pytest.mark.parallel(nprocs=2) -def test_skamarock_klemp_hydrostatic_parallel(): - test_skamarock_klemp_hydrostatic() +def test_skamarock_klemp_nonhydrostatic_parallel(): + test_skamarock_klemp_nonhydrostatic() -def test_skamarock_klemp_nonhydrostatic(): +def test_hyd_switch_skamarock_klemp_nonhydrostatic(): from skamarock_klemp_nonhydrostatic import skamarock_klemp_nonhydrostatic - test_name = 'skamarock_klemp_nonhydrostatic' + test_name = 'hyd_switch_skamarock_klemp_nonhydrostatic' skamarock_klemp_nonhydrostatic( ncolumns=30, nlayers=5, dt=6.0, tmax=60.0, dumpfreq=10, - dirname=make_dirname(test_name) + dirname=make_dirname(test_name), + hydrostatic=True ) @pytest.mark.parallel(nprocs=2) -def test_skamarock_klemp_nonhydrostatic_parallel(): - test_skamarock_klemp_nonhydrostatic() +def test_hyd_switch_skamarock_klemp_nonhydrostatic_parallel(): + test_hyd_switch_skamarock_klemp_nonhydrostatic() def test_straka_bubble(): diff --git a/figures/compressible_euler/schaer_mountain_initial.png b/figures/compressible_euler/schaer_mountain_initial.png new file mode 100644 index 000000000..5e78c469a Binary files /dev/null and b/figures/compressible_euler/schaer_mountain_initial.png differ diff --git a/figures/compressible_euler/skamarock_klemp_nonhydrostatic_final.png b/figures/compressible_euler/skamarock_klemp_nonhydrostatic_final.png index c1187a1cd..e76a82582 100644 Binary files a/figures/compressible_euler/skamarock_klemp_nonhydrostatic_final.png and b/figures/compressible_euler/skamarock_klemp_nonhydrostatic_final.png differ diff --git a/gusto/core/labels.py b/gusto/core/labels.py index 15b687f65..b3706980d 100644 --- a/gusto/core/labels.py +++ b/gusto/core/labels.py @@ -113,3 +113,5 @@ def __call__(self, target, value=None): hydrostatic = DynamicsLabel("hydrostatic") incompressible = DynamicsLabel("incompressible") sponge = DynamicsLabel("sponge") +horizontal_prognostic = Label("horizontal_prognostic") +vertical_prognostic = Label("vertical_prognostic") diff --git a/gusto/equations/compressible_euler_equations.py b/gusto/equations/compressible_euler_equations.py index 73bc76d45..272631923 100644 --- a/gusto/equations/compressible_euler_equations.py +++ b/gusto/equations/compressible_euler_equations.py @@ -4,10 +4,11 @@ sin, pi, inner, dx, div, cross, FunctionSpace, FacetNormal, jump, avg, dS_v, conditional, SpatialCoordinate, split, Constant, as_vector ) -from firedrake.fml import subject, replace_subject +from firedrake.fml import subject, drop, keep from gusto.core.labels import ( time_derivative, transport, prognostic, hydrostatic, linearisation, - pressure_gradient, coriolis, gravity, sponge + pressure_gradient, coriolis, gravity, sponge, implicit, + horizontal_prognostic ) from gusto.equations.thermodynamics import exner_pressure from gusto.equations.common_forms import ( @@ -336,49 +337,62 @@ def __init__(self, domain, parameters, sponge_options=None, no_normal_flow_bc_ids=no_normal_flow_bc_ids, active_tracers=active_tracers) - # Replace - self.residual = self.residual.label_map( - lambda t: t.has_label(time_derivative), - map_if_true=lambda t: self.hydrostatic_projection(t, 'u') - ) - - # Add an extra hydrostatic term u_idx = self.field_names.index('u') u = split(self.X)[u_idx] + w = self.tests[u_idx] k = self.domain.k - self.residual += hydrostatic( - subject( - prognostic( - -inner(k, self.tests[u_idx]) * inner(k, u) * dx, "u"), - self.X - ) + u_vert = k*inner(k, u) + u_hori = u - u_vert + + # -------------------------------------------------------------------- # + # Add hydrostatic term + # -------------------------------------------------------------------- # + # Term to appear in both explicit and implicit forcings + # For the explicit forcing, this will cancel out the u^n part of the + # time derivative + self.residual += hydrostatic(subject(prognostic( + inner(k, w)*inner(k, u)/domain.dt*dx, 'u'), self.X) ) - def hydrostatic_projection(self, term, field_name): - """ - Performs the 'hydrostatic' projection. + # Term that appears only in implicit forcing + # For the implicit forcing, in combination with the term above, the + # u^{n+1} term will be cancelled out + self.residual += implicit(hydrostatic(subject(prognostic( + Constant(-1.0)*inner(k, w)*inner(k, u)/domain.dt*dx, 'u'), self.X)) + ) - Takes a term involving a vector prognostic variable and replaces the - prognostic with only its horizontal components. It also adds the - 'hydrostatic' label to that term. + # # Add Euler-Poincare term + # self.residual += hydrostatic(subject(prognostic( + # Constant(0.5)*div(w)*inner(u_hori, u)*dx, 'u'), self.X) + # ) - Args: - term (:class:`Term`): the term to perform the projection upon. - field_name (str): the prognostic field to make hydrostatic. + # -------------------------------------------------------------------- # + # Only transport horizontal wind + # -------------------------------------------------------------------- # + # Drop wind transport term, it needs replacing with a version that + # includes only the horizontal components for the transported field + self.residual = self.residual.label_map( + lambda t: t.has_label(transport) and t.get(prognostic) == 'u', + map_if_true=drop, + map_if_false=keep + ) - Returns: - :class:`LabelledForm`: the labelled form containing the new term. - """ + # u_term = prognostic( + # advection_equation_circulation_form(domain, w, u_hori, u), 'u' + # ) - f_idx = self.field_names.index(field_name) - k = self.domain.k - X = term.get(subject) - field = split(X)[f_idx] - - new_subj = field - inner(field, k) * k - # In one step: - # - set up the replace_subject routine (which returns a function) - # - call that function on the supplied `term` argument, - # to replace the subject with the new hydrostatic subject - # - add the hydrostatic label - return replace_subject(new_subj, old_idx=f_idx)(term) + # Velocity transport term -- depends on formulation + if u_transport_option == "vector_invariant_form": + u_term = prognostic(vector_invariant_form(domain, w, u_hori, u), 'u') + elif u_transport_option == "vector_advection_form": + u_term = prognostic(advection_form(w, u_hori, u), 'u') + elif u_transport_option == "circulation_form": + circ_form = prognostic( + advection_equation_circulation_form(domain, w, u_hori, u), 'u' + ) + ke_form = prognostic(kinetic_energy_form(w, u_hori, u), 'u') + u_term = ke_form + circ_form + else: + raise ValueError("Invalid u_transport_option: %s" % u_transport_option) + + self.residual += horizontal_prognostic(subject(u_term, self.X)) diff --git a/gusto/solvers/linear_solvers.py b/gusto/solvers/linear_solvers.py index ca684c6af..57775ab8e 100644 --- a/gusto/solvers/linear_solvers.py +++ b/gusto/solvers/linear_solvers.py @@ -23,8 +23,8 @@ logger, DEBUG, logging_ksp_monitor_true_residual, attach_custom_monitor ) -from gusto.core.labels import linearisation, time_derivative, hydrostatic -from gusto.equations import thermodynamics +from gusto.core.labels import linearisation, time_derivative +from gusto.equations import thermodynamics, HydrostaticCompressibleEulerEquations from gusto.recovery.recovery_kernels import AverageWeightings, AverageKernel from abc import ABCMeta, abstractmethod, abstractproperty @@ -55,7 +55,7 @@ def __init__(self, equations, alpha=0.5, tau_values=None, """ self.equations = equations self.dt = equations.domain.dt - self.alpha = alpha + self.alpha = Constant(alpha) self.tau_values = tau_values if tau_values is not None else {} if solver_parameters is not None: @@ -135,7 +135,7 @@ class CompressibleSolver(TimesteppingSolver): (3) Reconstruct theta """ - solver_parameters = {'mat_type': 'matfree', + hybrid_parameters = {'mat_type': 'matfree', 'ksp_type': 'preonly', 'pc_type': 'python', 'pc_python_type': 'firedrake.SCPC', @@ -152,8 +152,33 @@ class CompressibleSolver(TimesteppingSolver): 'pc_type': 'bjacobi', 'sub_pc_type': 'ilu'}}} - def __init__(self, equations, alpha=0.5, tau_values=None, - solver_parameters=None, overwrite_solver_parameters=False): + full_parameters = { + 'pc_type': 'fieldsplit', + 'pc_fieldsplit_type': 'schur', + 'ksp_type': 'gmres', + 'ksp_max_it': 100, + 'ksp_gmres_restart': 50, + 'pc_fieldsplit_schur_fact_type': 'FULL', + 'pc_fieldsplit_schur_precondition': 'selfp', + 'fieldsplit_0': {'ksp_type': 'preonly', + 'pc_type': 'bjacobi', + 'sub_pc_type': 'ilu'}, + 'fieldsplit_1': {'ksp_type': 'preonly', + 'pc_type': 'gamg', + 'mg_levels': {'ksp_type': 'chebyshev', + 'ksp_chebyshev_esteig': True, + 'ksp_max_it': 1, + 'pc_type': 'bjacobi', + 'sub_pc_type': 'ilu'}} + } + + solver_parameters = None + + def __init__( + self, equations, alpha=0.5, tau_values=None, + solver_parameters=None, overwrite_solver_parameters=False, + formulation='hybridized' + ): """ Args: equations (:class:`PrognosticEquation`): the model's equation. @@ -168,9 +193,20 @@ def __init__(self, equations, alpha=0.5, tau_values=None, `solver_parameters` that have been passed in. If False then update the default parameters with the `solver_parameters` passed in. Defaults to False. + formulation (str, optional): the formulation to use. Valid options + are 'hybridized' and 'full'. Defaults to 'hybridized'. """ + assert formulation in ['hybridized', 'full'], \ + f'Invalid solver formulation: {formulation}' + self.equations = equations self.quadrature_degree = equations.domain.max_quad_degree + self.formulation = formulation + + if formulation == 'hybridized': + self.solver_parameters = self.hybrid_parameters + elif formulation == 'full': + self.solver_parameters = self.full_parameters super().__init__(equations, alpha, tau_values, solver_parameters, overwrite_solver_parameters) @@ -178,43 +214,74 @@ def __init__(self, equations, alpha=0.5, tau_values=None, @timed_function("Gusto:SolverSetup") def _setup_solver(self): + # Declare constants and relaxation parameters -------------------------- equations = self.equations cp = equations.parameters.cp dt = self.dt + # Set relaxation parameters. If an alternative has not been given, set # to semi-implicit off-centering factor beta_u = dt*self.tau_values.get("u", self.alpha) beta_t = dt*self.tau_values.get("theta", self.alpha) beta_r = dt*self.tau_values.get("rho", self.alpha) + # Specify degree for some terms as estimated degree is too large ------- + dx_qp = dx(degree=(equations.domain.max_quad_degree)) + dS_v_qp = dS_v(degree=(equations.domain.max_quad_degree)) + dS_h_qp = dS_h(degree=(equations.domain.max_quad_degree)) + ds_v_qp = ds_v(degree=(equations.domain.max_quad_degree)) + ds_tb_qp = (ds_t(degree=(equations.domain.max_quad_degree)) + + ds_b(degree=(equations.domain.max_quad_degree))) + + # Split up the rhs vector (symbolically) ------------------------------- + self.xrhs = Function(self.equations.function_space) + u_in, rho_in, theta_in = split(self.xrhs)[0:3] + + # Get the function spaces ---------------------------------------------- Vu = equations.domain.spaces("HDiv") - Vu_broken = FunctionSpace(equations.domain.mesh, BrokenElement(Vu.ufl_element())) Vtheta = equations.domain.spaces("theta") Vrho = equations.domain.spaces("DG") - h_deg = Vrho.ufl_element().degree()[0] - v_deg = Vrho.ufl_element().degree()[1] - Vtrace = FunctionSpace(equations.domain.mesh, "HDiv Trace", degree=(h_deg, v_deg)) + if self.formulation == 'hybridized': + h_deg = Vrho.ufl_element().degree()[0] + v_deg = Vrho.ufl_element().degree()[1] + Vtrace = FunctionSpace(equations.domain.mesh, "HDiv Trace", degree=(h_deg, v_deg)) + Vu_broken = FunctionSpace(equations.domain.mesh, BrokenElement(Vu.ufl_element())) - # Split up the rhs vector (symbolically) - self.xrhs = Function(self.equations.function_space) - u_in, rho_in, theta_in = split(self.xrhs)[0:3] + # Build the function space for "broken" u, rho, and pressure trace + M = MixedFunctionSpace((Vu_broken, Vrho, Vtrace)) + w, phi, dl = TestFunctions(M) + u, rho, l0 = TrialFunctions(M) - # Build the function space for "broken" u, rho, and pressure trace - M = MixedFunctionSpace((Vu_broken, Vrho, Vtrace)) - w, phi, dl = TestFunctions(M) - u, rho, l0 = TrialFunctions(M) - - n = FacetNormal(equations.domain.mesh) + elif self.formulation == 'full': + # Mixed function space is just for velocity and density + M = MixedFunctionSpace((Vu, Vrho)) + w, phi = TestFunctions(M) + u, rho = TrialFunctions(M) - # Get background fields + # Get background fields ------------------------------------------------ _, rhobar, thetabar = split(equations.X_ref)[0:3] exnerbar = thermodynamics.exner_pressure(equations.parameters, rhobar, thetabar) exnerbar_rho = thermodynamics.dexner_drho(equations.parameters, rhobar, thetabar) exnerbar_theta = thermodynamics.dexner_dtheta(equations.parameters, rhobar, thetabar) - # Analytical (approximate) elimination of theta + # Set up elimination of theta ========================================= k = equations.domain.k # Upward pointing unit vector + n = FacetNormal(equations.domain.mesh) + + # Vertical projection + def V(u): + return k*inner(u, k) + + # Hydrostatic projection + h_project = lambda u: u - k*inner(u, k) + + if isinstance(self.equations, HydrostaticCompressibleEulerEquations): + u_mass = inner(w, (h_project(u) - u_in))*dx + else: + u_mass = inner(w, (u - u_in))*dx + + # Analytical (approximate) elimination of theta theta = -dot(k, u)*dot(k, grad(thetabar))*beta_t + theta_in # Only include theta' (rather than exner') in the vertical @@ -224,21 +291,6 @@ def _setup_solver(self): # for linear perturbations) exner = exnerbar_theta*theta + exnerbar_rho*rho - # Vertical projection - def V(u): - return k*inner(u, k) - - # hydrostatic projection - h_project = lambda u: u - k*inner(u, k) - - # Specify degree for some terms as estimated degree is too large - dx_qp = dx(degree=(equations.domain.max_quad_degree)) - dS_v_qp = dS_v(degree=(equations.domain.max_quad_degree)) - dS_h_qp = dS_h(degree=(equations.domain.max_quad_degree)) - ds_v_qp = ds_v(degree=(equations.domain.max_quad_degree)) - ds_tb_qp = (ds_t(degree=(equations.domain.max_quad_degree)) - + ds_b(degree=(equations.domain.max_quad_degree))) - # Add effect of density of water upon theta, using moisture reference profiles # TODO: Explore if this is the right thing to do for the linear problem if equations.active_tracers is not None: @@ -258,65 +310,92 @@ def V(u): theta_w = theta thetabar_w = thetabar - _l0 = TrialFunction(Vtrace) - _dl = TestFunction(Vtrace) - a_tr = _dl('+')*_l0('+')*(dS_v_qp + dS_h_qp) + _dl*_l0*ds_v_qp + _dl*_l0*ds_tb_qp - - def L_tr(f): - return _dl('+')*avg(f)*(dS_v_qp + dS_h_qp) + _dl*f*ds_v_qp + _dl*f*ds_tb_qp - - cg_ilu_parameters = {'ksp_type': 'cg', - 'pc_type': 'bjacobi', - 'sub_pc_type': 'ilu'} - - # Project field averages into functions on the trace space - rhobar_avg = Function(Vtrace) - exnerbar_avg = Function(Vtrace) - - rho_avg_prb = LinearVariationalProblem(a_tr, L_tr(rhobar), rhobar_avg, - constant_jacobian=True) - exner_avg_prb = LinearVariationalProblem(a_tr, L_tr(exnerbar), exnerbar_avg, - constant_jacobian=True) - - self.rho_avg_solver = LinearVariationalSolver(rho_avg_prb, - solver_parameters=cg_ilu_parameters, - options_prefix='rhobar_avg_solver') - self.exner_avg_solver = LinearVariationalSolver(exner_avg_prb, - solver_parameters=cg_ilu_parameters, - options_prefix='exnerbar_avg_solver') - - # "broken" u, rho, and trace system - # NOTE: no ds_v integrals since equations are defined on - # a periodic (or sphere) base mesh. - if any([t.has_label(hydrostatic) for t in self.equations.residual]): - u_mass = inner(w, (h_project(u) - u_in))*dx - else: - u_mass = inner(w, (u - u_in))*dx - - eqn = ( - # momentum equation - u_mass - - beta_u*cp*div(theta_w*V(w))*exnerbar*dx_qp - # following does nothing but is preserved in the comments - # to remind us why (because V(w) is purely vertical). - # + beta*cp*jump(theta_w*V(w), n=n)*exnerbar_avg('+')*dS_v_qp - + beta_u*cp*jump(theta_w*V(w), n=n)*exnerbar_avg('+')*dS_h_qp - + beta_u*cp*dot(theta_w*V(w), n)*exnerbar_avg*ds_tb_qp - - beta_u*cp*div(thetabar_w*w)*exner*dx_qp - # trace terms appearing after integrating momentum equation - + beta_u*cp*jump(thetabar_w*w, n=n)*l0('+')*(dS_v_qp + dS_h_qp) - + beta_u*cp*dot(thetabar_w*w, n)*l0*(ds_tb_qp + ds_v_qp) - # mass continuity equation - + (phi*(rho - rho_in) - beta_r*inner(grad(phi), u)*rhobar)*dx - + beta_r*jump(phi*u, n=n)*rhobar_avg('+')*(dS_v + dS_h) - # term added because u.n=0 is enforced weakly via the traces - + beta_r*phi*dot(u, n)*rhobar_avg*(ds_tb + ds_v) - # constraint equation to enforce continuity of the velocity - # through the interior facets and weakly impose the no-slip - # condition - + dl('+')*jump(u, n=n)*(dS_v + dS_h) - + dl*dot(u, n)*(ds_t + ds_b + ds_v) - ) + cg_ilu_parameters = { + 'ksp_type': 'cg', + 'pc_type': 'bjacobi', + 'sub_pc_type': 'ilu' + } + + # Hybridization formulation ============================================ + if self.formulation == 'hybridized': + _l0 = TrialFunction(Vtrace) + _dl = TestFunction(Vtrace) + a_tr = _dl('+')*_l0('+')*(dS_v_qp + dS_h_qp) + _dl*_l0*ds_v_qp + _dl*_l0*ds_tb_qp + + def L_tr(f): + return _dl('+')*avg(f)*(dS_v_qp + dS_h_qp) + _dl*f*ds_v_qp + _dl*f*ds_tb_qp + + # Project field averages into functions on the trace space + rhobar_avg = Function(Vtrace) + exnerbar_avg = Function(Vtrace) + + rho_avg_prb = LinearVariationalProblem( + a_tr, L_tr(rhobar), rhobar_avg, constant_jacobian=True + ) + exner_avg_prb = LinearVariationalProblem( + a_tr, L_tr(exnerbar), exnerbar_avg, constant_jacobian=True + ) + + self.rho_avg_solver = LinearVariationalSolver( + rho_avg_prb, solver_parameters=cg_ilu_parameters, + options_prefix='rhobar_avg_solver' + ) + self.exner_avg_solver = LinearVariationalSolver( + exner_avg_prb, solver_parameters=cg_ilu_parameters, + options_prefix='exnerbar_avg_solver' + ) + + # Function for the hybridized solutions + self.urhol0 = Function(M) + + # "broken" u, rho, and trace system + # NOTE: no ds_v integrals since equations are defined on + # a periodic (or sphere) base mesh. + + eqn = ( + # momentum equation + u_mass + - beta_u*cp*div(theta_w*V(w))*exnerbar*dx_qp + # following does nothing but is preserved in the comments + # to remind us why (because V(w) is purely vertical). + # + beta*cp*jump(theta_w*V(w), n=n)*exnerbar_avg('+')*dS_v_qp + + beta_u*cp*jump(theta_w*V(w), n=n)*exnerbar_avg('+')*dS_h_qp + + beta_u*cp*dot(theta_w*V(w), n)*exnerbar_avg*ds_tb_qp + - beta_u*cp*div(thetabar_w*w)*exner*dx_qp + # trace terms appearing after integrating momentum equation + + beta_u*cp*jump(thetabar_w*w, n=n)*l0('+')*(dS_v_qp + dS_h_qp) + + beta_u*cp*dot(thetabar_w*w, n)*l0*(ds_tb_qp + ds_v_qp) + # mass continuity equation + + (phi*(rho - rho_in) - beta_r*inner(grad(phi), u)*rhobar)*dx + + beta_r*jump(phi*u, n=n)*rhobar_avg('+')*(dS_v + dS_h) + # term added because u.n=0 is enforced weakly via the traces + + beta_r*phi*dot(u, n)*rhobar_avg*(ds_tb + ds_v) + # constraint equation to enforce continuity of the velocity + # through the interior facets and weakly impose the no-slip + # condition + + dl('+')*jump(u, n=n)*(dS_v + dS_h) + + dl*dot(u, n)*(ds_t + ds_b + ds_v) + ) + + # Full formulation ===================================================== + elif self.formulation == 'full': + # Function to store result of u-rho solve + self.urho = Function(M) + one = Constant(1.0) + eqn = ( + u_mass + - beta_u*cp*div(theta_w*V(w))*exnerbar*dx_qp + # following does nothing but is preserved in the comments + # to remind us why (because V(w) is purely vertical. + # + beta*cp*jump(theta*V(w),n)*avg(pibar)*dS_v_qp + - beta_u*cp*div(thetabar_w*w)*exner*dx_qp + - (one - beta_u)*cp*div(thetabar_w*V(w))*exner*dx_qp + + beta_u*cp*jump(thetabar_w*w, n)*avg(exner)*dS_v_qp + + (phi*(rho - rho_in) - beta_r*inner(grad(phi), u)*rhobar)*dx + + beta_r*jump(phi*u, n)*avg(rhobar)*(dS_v + dS_h) + ) + + # Add additional terms ================================================= # TODO: can we get this term using FML? # contribution of the sponge term if hasattr(self.equations, "mu"): @@ -329,68 +408,99 @@ def L_tr(f): aeqn = lhs(eqn) Leqn = rhs(eqn) - # Function for the hybridized solutions - self.urhol0 = Function(M) - - hybridized_prb = LinearVariationalProblem(aeqn, Leqn, self.urhol0, - constant_jacobian=True) - hybridized_solver = LinearVariationalSolver(hybridized_prb, - solver_parameters=self.solver_parameters, - options_prefix='ImplicitSolver') - self.hybridized_solver = hybridized_solver - - # Project broken u into the HDiv space using facet averaging. - # Weight function counting the dofs of the HDiv element: - self._weight = Function(Vu) - weight_kernel = AverageWeightings(Vu) - weight_kernel.apply(self._weight) - - # Averaging kernel - self._average_kernel = AverageKernel(Vu) - - # HDiv-conforming velocity - self.u_hdiv = Function(Vu) + # Set up the problem =================================================== + if self.formulation == 'hybridized': + hybridized_prb = LinearVariationalProblem( + aeqn, Leqn, self.urhol0, constant_jacobian=True + ) + hybridized_solver = LinearVariationalSolver( + hybridized_prb, solver_parameters=self.solver_parameters, + options_prefix='HybridImplicitSolver' + ) + self.hybridized_solver = hybridized_solver + + # Project broken u into the HDiv space using facet averaging. + # Weight function counting the dofs of the HDiv element: + self._weight = Function(Vu) + weight_kernel = AverageWeightings(Vu) + weight_kernel.apply(self._weight) + + # Averaging kernel + self._average_kernel = AverageKernel(Vu) + + # HDiv-conforming velocity + self.u_hdiv = Function(Vu) + u_hdiv = self.u_hdiv + + # Store boundary conditions for the div-conforming velocity to apply + # post-solve + self.bcs = self.equations.bcs['u'] - # Reconstruction of theta + else: + # Boundary conditions (assumes extruded mesh) + self.bcs = [ + DirichletBC(M.sub(0), 0.0, "bottom"), + DirichletBC(M.sub(0), 0.0, "top") + ] + + # Solver for u, rho + urho_problem = LinearVariationalProblem( + aeqn, Leqn, self.urho, bcs=self.bcs + ) + self.urho_solver = LinearVariationalSolver( + urho_problem, solver_parameters=self.solver_parameters, + options_prefix='ImplicitSolver' + ) + # Velocity to appear in theta reconstruction + u_hdiv = self.urho.subfunctions[0] + + # Reconstruction of theta ============================================== theta = TrialFunction(Vtheta) gamma = TestFunction(Vtheta) self.theta = Function(Vtheta) theta_eqn = gamma*(theta - theta_in - + dot(k, self.u_hdiv)*dot(k, grad(thetabar))*beta_t)*dx - - theta_problem = LinearVariationalProblem(lhs(theta_eqn), rhs(theta_eqn), self.theta, - constant_jacobian=True) - self.theta_solver = LinearVariationalSolver(theta_problem, - solver_parameters=cg_ilu_parameters, - options_prefix='thetabacksubstitution') + + dot(k, u_hdiv)*dot(k, grad(thetabar))*beta_t)*dx - # Store boundary conditions for the div-conforming velocity to apply - # post-solve - self.bcs = self.equations.bcs['u'] + theta_problem = LinearVariationalProblem( + lhs(theta_eqn), rhs(theta_eqn), self.theta, constant_jacobian=True + ) + self.theta_solver = LinearVariationalSolver( + theta_problem, solver_parameters=cg_ilu_parameters, + options_prefix='thetabacksubstitution' + ) - # Log residuals on hybridized solver - self.log_ksp_residuals(self.hybridized_solver.snes.ksp) - # Log residuals on the trace system too - python_context = self.hybridized_solver.snes.ksp.pc.getPythonContext() - attach_custom_monitor(python_context, logging_ksp_monitor_true_residual) + if self.formulation == 'hybridized': + # Log residuals on hybridized solver + self.log_ksp_residuals(self.hybridized_solver.snes.ksp) + # Log residuals on the trace system too + python_context = self.hybridized_solver.snes.ksp.pc.getPythonContext() + attach_custom_monitor(python_context, logging_ksp_monitor_true_residual) + elif self.formulation == 'full': + # Log residuals on mixed solver + self.log_ksp_residuals(self.urho_solver.snes.ksp) @timed_function("Gusto:UpdateReferenceProfiles") def update_reference_profiles(self): - with timed_region("Gusto:HybridProjectRhobar"): - logger.info('Compressible linear solver: rho average solve') - self.rho_avg_solver.solve() - with timed_region("Gusto:HybridProjectExnerbar"): - logger.info('Compressible linear solver: Exner average solve') - self.exner_avg_solver.solve() + if self.formulation == 'hydridized': + with timed_region("Gusto:HybridProjectRhobar"): + logger.info('Compressible linear solver: rho average solve') + self.rho_avg_solver.solve() + + with timed_region("Gusto:HybridProjectExnerbar"): + logger.info('Compressible linear solver: Exner average solve') + self.exner_avg_solver.solve() - # Because the left hand side of the hybridised problem depends - # on the reference profile, the Jacobian matrix should change - # when the reference profiles are updated. This call will tell - # the hybridized_solver to reassemble the Jacobian next time - # `solve` is called. - self.hybridized_solver.invalidate_jacobian() + # Because the left hand side of the hybridised problem depends + # on the reference profile, the Jacobian matrix should change + # when the reference profiles are updated. This call will tell + # the hybridized_solver to reassemble the Jacobian next time + # `solve` is called. + self.hybridized_solver.invalidate_jacobian() + + elif self.formulation == 'full': + self.urho_solver.invalidate_jacobian() @timed_function("Gusto:LinearSolve") def solve(self, xrhs, dy): @@ -405,23 +515,30 @@ def solve(self, xrhs, dy): """ self.xrhs.assign(xrhs) - # Solve the hybridized system - logger.info('Compressible linear solver: hybridized solve') - self.hybridized_solver.solve() + # MIXED SOLVE ========================================================== + if self.formulation == 'hybridized': + # Solve the hybridized system + logger.info('Compressible linear solver: hybridized solve') + self.hybridized_solver.solve() + + broken_u, rho1, _ = self.urhol0.subfunctions + u1 = self.u_hdiv - broken_u, rho1, _ = self.urhol0.subfunctions - u1 = self.u_hdiv + # Project broken_u into the HDiv space + u1.assign(0.0) - # Project broken_u into the HDiv space - u1.assign(0.0) + with timed_region("Gusto:HybridProjectHDiv"): + logger.info('Compressible linear solver: restore continuity') + self._average_kernel.apply(u1, self._weight, broken_u) - with timed_region("Gusto:HybridProjectHDiv"): - logger.info('Compressible linear solver: restore continuity') - self._average_kernel.apply(u1, self._weight, broken_u) + # Reapply bcs to ensure they are satisfied + for bc in self.bcs: + bc.apply(u1) - # Reapply bcs to ensure they are satisfied - for bc in self.bcs: - bc.apply(u1) + elif self.formulation == 'full': + logger.info('Compressible linear solver: mixed solve') + self.urho_solver.solve() + u1, rho1 = self.urho.subfunctions # Copy back into u and rho cpts of dy u, rho, theta = dy.subfunctions[0:3] @@ -811,9 +928,10 @@ def __init__(self, equation, alpha, reference_dependent=True): lambda t: Term(t.get(linearisation).form, t.labels), drop) + self.alpha = Constant(alpha) dt = equation.domain.dt W = equation.function_space - beta = dt*alpha + beta = dt*self.alpha # Split up the rhs vector (symbolically) self.xrhs = Function(W) diff --git a/gusto/solvers/parameters.py b/gusto/solvers/parameters.py index a7f61edb0..b43703455 100644 --- a/gusto/solvers/parameters.py +++ b/gusto/solvers/parameters.py @@ -4,7 +4,7 @@ """ from gusto.core.function_spaces import is_cg -__all__ = ['mass_parameters'] +__all__ = ['mass_parameters', 'hydrostatic_parameters'] def mass_parameters(V, spaces=None, ignore_vertical=True): @@ -97,3 +97,28 @@ def mass_parameters(V, spaces=None, ignore_vertical=True): } return parameters + + +hydrostatic_parameters = { + 'mat_type': 'matfree', + 'ksp_type': 'preonly', + 'pc_type': 'python', + 'pc_python_type': 'firedrake.SCPC', + # Velocity mass operator is singular in the hydrostatic case. + # So for reconstruction, we eliminate rho into u + 'pc_sc_eliminate_fields': '1, 0', + 'condensed_field': { + 'ksp_type': 'fgmres', + 'ksp_rtol': 1.0e-8, + 'ksp_atol': 1.0e-8, + 'ksp_max_it': 100, + 'pc_type': 'gamg', + 'pc_gamg_sym_graph': True, + 'mg_levels': { + 'ksp_type': 'gmres', + 'ksp_max_it': 5, + 'pc_type': 'bjacobi', + 'sub_pc_type': 'ilu' + } + } +} \ No newline at end of file diff --git a/gusto/spatial_methods/spatial_methods.py b/gusto/spatial_methods/spatial_methods.py index 6ad214dcd..bc0c9a1e1 100644 --- a/gusto/spatial_methods/spatial_methods.py +++ b/gusto/spatial_methods/spatial_methods.py @@ -3,9 +3,11 @@ spatial discretisation of some term. """ -from firedrake import split +from firedrake import split, dot from firedrake.fml import Term, keep, drop -from gusto.core.labels import prognostic +from gusto.core.labels import ( + prognostic, horizontal_prognostic, vertical_prognostic +) __all__ = ['SpatialMethod'] @@ -47,6 +49,15 @@ def __init__(self, equation, variable, term_label): assert num_terms == 1, f'Unable to find {term_label.label} term ' \ + f'for {variable}. {num_terms} found' + # If specified, replace field with only horizontal or vertical part + if self.original_form.terms[0].has_label(horizontal_prognostic): + k = self.equation.domain.k + self.field = self.field - k*dot(k, self.field) + + if self.original_form.terms[0].has_label(vertical_prognostic): + k = self.equation.domain.k + self.field = k*dot(k, self.field) + def replace_form(self, equation): """ Replaces the form for the term in the equation with the form for the diff --git a/gusto/time_discretisation/time_discretisation.py b/gusto/time_discretisation/time_discretisation.py index d8e07bf86..93c439ed3 100644 --- a/gusto/time_discretisation/time_discretisation.py +++ b/gusto/time_discretisation/time_discretisation.py @@ -419,11 +419,12 @@ def update_subcycling(self): # Cap number of subcycles if max_subcycles is not None: + if self.ncycles >= max_subcycles: + logger.warning( + 'Adaptive subcycling: capping number of subcycles at ' + f'{max_subcycles}' + ) self.ncycles = min(self.ncycles, max_subcycles) - logger.warning( - 'Adaptive subcycling: capping number of subcycles at ' - f'{max_subcycles}' - ) logger.debug(f'Performing {self.ncycles} subcycles') self.dt.assign(self.original_dt/self.ncycles) diff --git a/gusto/timestepping/semi_implicit_quasi_newton.py b/gusto/timestepping/semi_implicit_quasi_newton.py index 19142dbaa..145c61298 100644 --- a/gusto/timestepping/semi_implicit_quasi_newton.py +++ b/gusto/timestepping/semi_implicit_quasi_newton.py @@ -5,15 +5,16 @@ from firedrake import ( Function, Constant, TrialFunctions, DirichletBC, div, assemble, - LinearVariationalProblem, LinearVariationalSolver + LinearVariationalProblem, LinearVariationalSolver, dot ) from firedrake.fml import drop, replace_subject -from firedrake.__future__ import interpolate +import numpy as np from pyop2.profiling import timed_stage from gusto.core import TimeLevelFields, StateFields -from gusto.core.labels import (transport, diffusion, time_derivative, - linearisation, prognostic, hydrostatic, - physics_label, sponge, incompressible) +from gusto.core.labels import ( + transport, diffusion, time_derivative, linearisation, prognostic, + hydrostatic, physics_label, sponge, incompressible, implicit +) from gusto.solvers import LinearTimesteppingSolver, mass_parameters from gusto.core.logging import logger, DEBUG, logging_ksp_monitor_true_residual from gusto.time_discretisation.time_discretisation import ExplicitTimeDiscretisation @@ -39,7 +40,8 @@ def __init__(self, equation_set, io, transport_schemes, spatial_methods, slow_physics_schemes=None, fast_physics_schemes=None, alpha=Constant(0.5), off_centred_u=False, num_outer=2, num_inner=2, accelerator=False, - predictor=None, reference_update_freq=None): + predictor=None, reference_update_freq=None, + spinup_steps=0): """ Args: equation_set (:class:`PrognosticEquationSet`): the prognostic @@ -106,15 +108,24 @@ def __init__(self, equation_set, io, transport_schemes, spatial_methods, time step. Setting it to None turns off the update, and reference profiles will remain at their initial values. Defaults to None. + spinup_steps (int, optional): the number of steps to run the model + in "spin-up" mode, where the alpha parameter is set to 1.0. + Defaults to 0, which corresponds to no spin-up. """ self.num_outer = num_outer self.num_inner = num_inner - self.alpha = alpha + self.alpha = Constant(alpha) self.predictor = predictor self.accelerator = accelerator + + # Options relating to reference profiles and spin-up + self._alpha_original = Constant(alpha) self.reference_update_freq = reference_update_freq self.to_update_ref_profile = False + self.spinup_steps = spinup_steps + self.spinup_begun = False + self.spinup_done = False # Flag for if we have simultaneous transport self.simult = False @@ -353,13 +364,49 @@ def update_reference_profiles(self): if float(self.t) + self.reference_update_freq > self.last_ref_update_time: self.equation.X_ref.assign(self.x.n(self.field_name)) self.last_ref_update_time = float(self.t) - if hasattr(self.linear_solver, 'update_reference_profiles'): - self.linear_solver.update_reference_profiles() + self.linear_solver.update_reference_profiles() elif self.to_update_ref_profile: - if hasattr(self.linear_solver, 'update_reference_profiles'): - self.linear_solver.update_reference_profiles() - self.to_update_ref_profile = False + self.linear_solver.update_reference_profiles() + self.to_update_ref_profile = False + + def start_spinup(self): + """ + Initialises the spin-up period, so that the scheme is implicit by + setting the off-centering parameter alpha to be 1. + """ + logger.debug('Starting spin-up period') + # Update alpha + self.alpha.assign(1.0) + self.linear_solver.alpha.assign(1.0) + # We need to tell solvers that they may need rebuilding + self.linear_solver.update_reference_profiles() + self.forcing.solvers['explicit'].invalidate_jacobian() + self.forcing.solvers['implicit'].invalidate_jacobian() + # This only needs doing once, so update the flag + self.spinup_begun = True + + def finish_spinup(self): + """ + Finishes the spin-up period, returning the off-centering parameter + to its original value. + """ + logger.debug('Finishing spin-up period') + # Update alpha + self.alpha.assign(self._alpha_original) + self.linear_solver.alpha.assign(self._alpha_original) + # We need to tell solvers that they may need rebuilding + self.linear_solver.update_reference_profiles() + self.forcing.solvers['explicit'].invalidate_jacobian() + self.forcing.solvers['implicit'].invalidate_jacobian() + # This only needs doing once, so update the flag + self.spinup_done = True + + def log_w(self, field, time_stage): + k = self.equation.domain.k + w = Function(self.equation.domain.spaces('theta')) + w.interpolate(dot(k, field)) + logger.info(f'{time_stage} -- w: min {np.min(w.dat.data)}, max {np.max(w.dat.data)}') def timestep(self): """Defines the timestep""" @@ -376,6 +423,13 @@ def timestep(self): # Update reference profiles -------------------------------------------- self.update_reference_profiles() + # Are we in spin-up period? -------------------------------------------- + # Note: steps numbered from 1 onwards + if self.step < self.spinup_steps + 1 and not self.spinup_begun: + self.start_spinup() + elif self.step >= self.spinup_steps + 1 and not self.spinup_done: + self.finish_spinup() + # Slow physics --------------------------------------------------------- x_after_slow(self.field_name).assign(xn(self.field_name)) if len(self.slow_physics_schemes) > 0: @@ -389,6 +443,7 @@ def timestep(self): logger.info('Semi-implicit Quasi Newton: Explicit forcing') # Put explicit forcing into xstar self.forcing.apply(x_after_slow, xn, xstar(self.field_name), "explicit") + self.log_w(xstar('u'), 'Explicit forcing') # set xp here so that variables that are not transported have # the correct values @@ -402,6 +457,7 @@ def timestep(self): self.io.log_courant(self.fields, 'transporting_velocity', message=f'transporting velocity, outer iteration {outer}') self.transport_fields(outer, xstar, xp) + self.log_w(xp('u'), 'Transport') # Fast physics ----------------------------------------------------- x_after_fast(self.field_name).assign(xp(self.field_name)) @@ -423,6 +479,7 @@ def timestep(self): if (inner > 0 and self.accelerator): # Zero implicit forcing to accelerate solver convergence self.forcing.zero_forcing_terms(self.equation, xp, xrhs, self.transported_fields) + self.log_w(xrhs.subfunctions[0], 'Implicit forcing') xrhs -= xnp1(self.field_name) xrhs += xrhs_phys @@ -434,6 +491,7 @@ def timestep(self): xnp1X = xnp1(self.field_name) xnp1X += dy + self.log_w(xnp1('u'), 'After solver') # Update xnp1 values for active tracers not included in the linear solve self.copy_active_tracers(x_after_fast, xnp1) @@ -503,7 +561,7 @@ def __init__(self, equation, alpha): """ self.field_name = equation.field_name - implicit_terms = [incompressible, sponge] + implicit_terms = [incompressible, sponge, implicit] dt = equation.domain.dt W = equation.function_space @@ -526,7 +584,7 @@ def __init__(self, equation, alpha): map_if_false=drop) # the explicit forms are multiplied by (1-alpha) and moved to the rhs - L_explicit = -(1-alpha)*dt*residual.label_map( + L_explicit = -(Constant(1)-alpha)*dt*residual.label_map( lambda t: any(t.has_label(time_derivative, hydrostatic, *implicit_terms, return_tuple=True)), diff --git a/plotting/compressible_euler/plot_schaer_mountain.py b/plotting/compressible_euler/plot_schaer_mountain.py new file mode 100644 index 000000000..6b78b27b2 --- /dev/null +++ b/plotting/compressible_euler/plot_schaer_mountain.py @@ -0,0 +1,198 @@ +""" +Plots the Schär mountain test case. + +This plots: +(a) w @ t = 5 hr, (b) theta perturbation @ t = 5 hr +""" +from os.path import abspath, dirname +import matplotlib.pyplot as plt +from netCDF4 import Dataset +import numpy as np +import pandas as pd +from tomplot import ( + set_tomplot_style, tomplot_cmap, plot_contoured_field, + add_colorbar_ax, tomplot_field_title, tomplot_contours, + extract_gusto_coords, extract_gusto_field, reshape_gusto_data +) + +test = 'schaer_alpha_0p51' + +# ---------------------------------------------------------------------------- # +# Directory for results and plots +# ---------------------------------------------------------------------------- # +# When copying this example these paths need editing, which will usually involve +# removing the abspath part to set directory paths relative to this file +results_file_name = f'{abspath(dirname(__file__))}/../../results/{test}/field_output.nc' +plot_stem = f'{abspath(dirname(__file__))}/../../figures/compressible_euler/{test}' + +# ---------------------------------------------------------------------------- # +# Final plot details +# ---------------------------------------------------------------------------- # +final_field_names = ['u_z', 'theta_perturbation', 'u_z', 'theta_perturbation'] +final_colour_schemes = ['PiYG', 'RdBu_r', 'PiYG', 'RdBu_r'] +final_field_labels = [ + r'$w$ (m s$^{-1}$)', r'$\Delta\theta$ (K)', + r'$w$ (m s$^{-1}$)', r'$\Delta\theta$ (K)' +] +final_contours = [ + np.linspace(-1.0, 1.0, 21), np.linspace(-1.0, 1.0, 21), + np.linspace(-1.0, 1.0, 21), np.linspace(-1.0, 1.0, 21) +] + +# ---------------------------------------------------------------------------- # +# Initial plot details +# ---------------------------------------------------------------------------- # +initial_field_names = ['Exner', 'theta'] +initial_colour_schemes = ['PuBu', 'Reds'] +initial_field_labels = [r'$\Pi$', r'$\theta$ (K)'] + +# ---------------------------------------------------------------------------- # +# General options +# ---------------------------------------------------------------------------- # +contour_method = 'contour' # Need to use this method to show mountains! +xlims = [0., 100.] +ylims = [0., 30.] + +# Things that are likely the same for all plots -------------------------------- +set_tomplot_style() +data_file = Dataset(results_file_name, 'r') + +# ---------------------------------------------------------------------------- # +# INITIAL PLOTTING +# ---------------------------------------------------------------------------- # + +fig, axarray = plt.subplots(1, 2, figsize=(18, 6), sharex='all', sharey='all') +time_idx = 0 + +for i, (ax, field_name, colour_scheme, field_label) in \ + enumerate(zip( + axarray.flatten(), initial_field_names, initial_colour_schemes, + initial_field_labels + )): + + # Data extraction ---------------------------------------------------------- + field_data = extract_gusto_field(data_file, field_name, time_idx=time_idx) + coords_X, coords_Y = extract_gusto_coords(data_file, field_name) + field_data, coords_X, coords_Y = \ + reshape_gusto_data(field_data, coords_X, coords_Y) + time = data_file['time'][time_idx] + + contours = tomplot_contours(field_data) + cmap, lines = tomplot_cmap(contours, colour_scheme) + + # Plot data ---------------------------------------------------------------- + cf, _ = plot_contoured_field( + ax, coords_X, coords_Y, field_data, contour_method, contours, + cmap=cmap, line_contours=lines + ) + + add_colorbar_ax(ax, cf, field_label, location='bottom') + tomplot_field_title(ax, None, minmax=True, field_data=field_data) + + # Labels ------------------------------------------------------------------- + if i == 0: + ax.set_ylabel(r'$z$ (km)', labelpad=-20) + ax.set_ylim(ylims) + ax.set_yticks(ylims) + ax.set_yticklabels(ylims) + + ax.set_xlabel(r'$x$ (km)', labelpad=-10) + ax.set_xlim(xlims) + ax.set_xticks(xlims) + ax.set_xticklabels(xlims) + +# Save figure ------------------------------------------------------------------ +fig.suptitle(f't = {time:.1f} s') +fig.subplots_adjust(wspace=0.25) +plot_name = f'{plot_stem}_initial.png' +print(f'Saving figure to {plot_name}') +fig.savefig(plot_name, bbox_inches='tight') +plt.close() + +# ---------------------------------------------------------------------------- # +# FINAL PLOTTING +# ---------------------------------------------------------------------------- # +xlims_zoom = [30., 70.] +ylims_zoom = [0., 12.] + +fig, axarray = plt.subplots(2, 2, figsize=(18, 12), sharex='row', sharey='row') +time_idx = 1 + +for i, (ax, field_name, colour_scheme, field_label, contours) in \ + enumerate(zip( + axarray.flatten(), final_field_names, final_colour_schemes, + final_field_labels, final_contours + )): + + # Data extraction ---------------------------------------------------------- + field_data = extract_gusto_field(data_file, field_name, time_idx=time_idx) + coords_X, coords_Y = extract_gusto_coords(data_file, field_name) + time = data_file['time'][time_idx] + + # # Filter data for panels that are zoomed in mountain region + # if i in [2, 3]: + # data_dict = { + # 'X': coords_X, + # 'Y': coords_Y, + # 'field': field_data + # } + # data_frame = pd.DataFrame(data_dict) + + # data_frame = data_frame[ + # (data_frame['X'] >= xlims_zoom[0]) + # & (data_frame['X'] <= xlims_zoom[1]) + # & (data_frame['Y'] >= ylims_zoom[0]) + # & (data_frame['Y'] <= ylims_zoom[1]) + # ] + # field_data = data_frame['field'].values[:] + # coords_X = data_frame['X'].values[:] + # coords_Y = data_frame['Y'].values[:] + + field_data, coords_X, coords_Y = \ + reshape_gusto_data(field_data, coords_X, coords_Y) + + cmap, lines = tomplot_cmap(contours, colour_scheme, remove_contour=0.0) + + # Plot data ---------------------------------------------------------------- + cf, _ = plot_contoured_field( + ax, coords_X, coords_Y, field_data, contour_method, contours, + cmap=cmap, line_contours=lines + ) + + add_colorbar_ax(ax, cf, field_label, location='bottom') + tomplot_field_title( + ax, None, minmax=True, field_data=field_data, minmax_format='.3f' + ) + + # Labels ------------------------------------------------------------------- + ax.set_xlabel(r'$x$ (km)', labelpad=-10) + + if i in [0, 1]: + ax.set_xlim(xlims) + ax.set_ylim(ylims) + ax.set_xticks(xlims) + ax.set_xticklabels(xlims) + else: + ax.set_xlim(xlims_zoom) + ax.set_ylim(ylims_zoom) + ax.set_xticks(xlims_zoom) + ax.set_xticklabels(xlims_zoom) + + if i == 0: + ax.set_ylabel(r'$z$ (km)', labelpad=-20) + ax.set_yticks(ylims) + ax.set_yticklabels(ylims) + + elif i == 2: + ax.set_ylabel(r'$z$ (km)', labelpad=-20) + ax.set_yticks(ylims_zoom) + ax.set_yticklabels(ylims_zoom) + + +# Save figure ------------------------------------------------------------------ +fig.suptitle(f't = {time:.1f} s') +fig.subplots_adjust(wspace=0.25) +plot_name = f'{plot_stem}_final.png' +print(f'Saving figure to {plot_name}') +fig.savefig(plot_name, bbox_inches='tight') +plt.close() diff --git a/plotting/compressible_euler/plot_skamarock_klemp_nonhydrostatic.py b/plotting/compressible_euler/plot_skamarock_klemp_nonhydrostatic.py index 668d75dc8..566948b41 100644 --- a/plotting/compressible_euler/plot_skamarock_klemp_nonhydrostatic.py +++ b/plotting/compressible_euler/plot_skamarock_klemp_nonhydrostatic.py @@ -3,7 +3,7 @@ This plots the initial conditions @ t = 0 s, with (a) theta perturbation, (b) theta -and the final state @ t = 3600 s, with +and the final state @ t = 3000 s, with (a) theta perturbation, (b) a 1D slice through the wave """ @@ -11,6 +11,7 @@ import matplotlib.pyplot as plt import numpy as np from netCDF4 import Dataset +import pandas as pd from tomplot import ( set_tomplot_style, tomplot_cmap, plot_contoured_field, add_colorbar_ax, tomplot_field_title, extract_gusto_coords, @@ -49,7 +50,6 @@ # General options # ---------------------------------------------------------------------------- # contour_method = 'tricontour' -xlims = [0, 300.0] ylims = [0, 10.0] # Things that are likely the same for all plots -------------------------------- @@ -59,6 +59,8 @@ # ---------------------------------------------------------------------------- # # INITIAL PLOTTING # ---------------------------------------------------------------------------- # +xlims = [0, 300.0] + fig, axarray = plt.subplots(1, 2, figsize=(12, 6), sharex='all', sharey='all') time_idx = 0 @@ -107,6 +109,9 @@ # ---------------------------------------------------------------------------- # # FINAL PLOTTING # ---------------------------------------------------------------------------- # +x_offset = -3000.0*20/1000.0 +xlims = [-x_offset, 300.0-x_offset] + fig, axarray = plt.subplots(2, 1, figsize=(8, 8), sharex='all') time_idx = -1 @@ -115,6 +120,21 @@ coords_X, coords_Y = extract_gusto_coords(data_file, final_field_name) time = data_file['time'][time_idx] +# Wave has wrapped around periodic boundary, so shift the coordinates +coords_X = np.where(coords_X < xlims[0], coords_X + 300.0, coords_X) + +# Sort data given the change in coordinates +data_dict = { + 'X': coords_X, + 'Y': coords_Y, + 'field': field_data +} +data_frame = pd.DataFrame(data_dict) +data_frame.sort_values(by=['X', 'Y'], inplace=True) +coords_X = data_frame['X'].values[:] +coords_Y = data_frame['Y'].values[:] +field_data = data_frame['field'].values[:] + # Plot 2D data ----------------------------------------------------------------- ax = axarray[0]