Beispiel #1
0
def _get_scalar_lump():
    from mirgecom.eos import IdealSingleGas
    eos = IdealSingleGas()

    from mirgecom.initializers import MulticomponentLump
    init = MulticomponentLump(dim=2,
                              nspecies=3,
                              velocity=np.ones(2),
                              spec_y0s=np.ones(3),
                              spec_amplitudes=np.ones(3))

    from meshmode.mesh import BTAG_ALL
    from mirgecom.boundary import PrescribedInviscidBoundary
    boundaries = {
        BTAG_ALL: PrescribedInviscidBoundary(fluid_solution_func=init)
    }

    return eos, init, boundaries, 1e-12
Beispiel #2
0
def _get_scalar_lump():
    from mirgecom.eos import IdealSingleGas
    from mirgecom.gas_model import GasModel, make_fluid_state
    gas_model = GasModel(eos=IdealSingleGas())

    from mirgecom.initializers import MulticomponentLump
    init = MulticomponentLump(
        dim=2, nspecies=3, velocity=np.ones(2), spec_y0s=np.ones(3),
        spec_amplitudes=np.ones(3))

    def _my_boundary(discr, btag, gas_model, state_minus, **kwargs):
        actx = state_minus.array_context
        bnd_discr = discr.discr_from_dd(btag)
        nodes = thaw(bnd_discr.nodes(), actx)
        return make_fluid_state(init(x_vec=nodes, eos=gas_model.eos,
                                     **kwargs), gas_model)

    from meshmode.mesh import BTAG_ALL
    from mirgecom.boundary import PrescribedFluidBoundary
    boundaries = {
        BTAG_ALL: PrescribedFluidBoundary(boundary_state_func=_my_boundary)
    }

    return gas_model, init, boundaries, 5e-12
Beispiel #3
0
def main(ctx_factory=cl.create_some_context, use_logmgr=True,
         use_leap=False, use_profiling=False, casename=None,
         rst_filename=None, actx_class=PyOpenCLArrayContext):
    """Drive example."""
    cl_ctx = ctx_factory()

    if casename is None:
        casename = "mirgecom"

    from mpi4py import MPI
    comm = MPI.COMM_WORLD
    rank = comm.Get_rank()
    nparts = comm.Get_size()

    logmgr = initialize_logmgr(use_logmgr,
        filename=f"{casename}.sqlite", mode="wu", mpi_comm=comm)

    if use_profiling:
        queue = cl.CommandQueue(
            cl_ctx, properties=cl.command_queue_properties.PROFILING_ENABLE)
    else:
        queue = cl.CommandQueue(cl_ctx)

    actx = actx_class(
        queue,
        allocator=cl_tools.MemoryPool(cl_tools.ImmediateAllocator(queue)))

    # timestepping control
    current_step = 0
    if use_leap:
        from leap.rk import RK4MethodBuilder
        timestepper = RK4MethodBuilder("state")
    else:
        timestepper = rk4_step
    t_final = 0.005
    current_cfl = 1.0
    current_dt = .001
    current_t = 0
    constant_cfl = False

    # some i/o frequencies
    nstatus = 1
    nrestart = 5
    nviz = 1
    nhealth = 1

    dim = 2
    rst_path = "restart_data/"
    rst_pattern = (
        rst_path + "{cname}-{step:04d}-{rank:04d}.pkl"
    )
    if rst_filename:  # read the grid from restart data
        rst_filename = f"{rst_filename}-{rank:04d}.pkl"
        from mirgecom.restart import read_restart_data
        restart_data = read_restart_data(actx, rst_filename)
        local_mesh = restart_data["local_mesh"]
        local_nelements = local_mesh.nelements
        global_nelements = restart_data["global_nelements"]
        assert restart_data["nparts"] == nparts
    else:  # generate the grid from scratch
        box_ll = -5.0
        box_ur = 5.0
        nel_1d = 16
        from meshmode.mesh.generation import generate_regular_rect_mesh
        generate_mesh = partial(generate_regular_rect_mesh, a=(box_ll,)*dim,
                                b=(box_ur,) * dim, nelements_per_axis=(nel_1d,)*dim)
        local_mesh, global_nelements = generate_and_distribute_mesh(comm,
                                                                    generate_mesh)
        local_nelements = local_mesh.nelements

    order = 3
    discr = EagerDGDiscretization(
        actx, local_mesh, order=order, mpi_communicator=comm
    )
    nodes = thaw(actx, discr.nodes())

    vis_timer = None

    if logmgr:
        logmgr_add_device_name(logmgr, queue)
        logmgr_add_device_memory_usage(logmgr, queue)
        logmgr_add_many_discretization_quantities(logmgr, discr, dim,
                             extract_vars_for_logging, units_for_logging)

        vis_timer = IntervalTimer("t_vis", "Time spent visualizing")
        logmgr.add_quantity(vis_timer)

        logmgr.add_watches([
            ("step.max", "step = {value}, "),
            ("t_sim.max", "sim time: {value:1.6e} s\n"),
            ("min_pressure", "------- P (min, max) (Pa) = ({value:1.9e}, "),
            ("max_pressure",    "{value:1.9e})\n"),
            ("t_step.max", "------- step walltime: {value:6g} s, "),
            ("t_log.max", "log walltime: {value:6g} s")
        ])

    # soln setup and init
    nspecies = 4
    centers = make_obj_array([np.zeros(shape=(dim,)) for i in range(nspecies)])
    spec_y0s = np.ones(shape=(nspecies,))
    spec_amplitudes = np.ones(shape=(nspecies,))
    eos = IdealSingleGas()
    velocity = np.ones(shape=(dim,))

    initializer = MulticomponentLump(dim=dim, nspecies=nspecies,
                                     spec_centers=centers, velocity=velocity,
                                     spec_y0s=spec_y0s,
                                     spec_amplitudes=spec_amplitudes)
    boundaries = {
        BTAG_ALL: PrescribedInviscidBoundary(fluid_solution_func=initializer)
    }

    if rst_filename:
        current_t = restart_data["t"]
        current_step = restart_data["step"]
        current_state = restart_data["state"]
        if logmgr:
            from mirgecom.logging_quantities import logmgr_set_time
            logmgr_set_time(logmgr, current_step, current_t)
    else:
        # Set the current state from time 0
        current_state = initializer(nodes)

    visualizer = make_visualizer(discr)
    initname = initializer.__class__.__name__
    eosname = eos.__class__.__name__
    init_message = make_init_message(dim=dim, order=order,
                                     nelements=local_nelements,
                                     global_nelements=global_nelements,
                                     dt=current_dt, t_final=t_final, nstatus=nstatus,
                                     nviz=nviz, cfl=current_cfl,
                                     constant_cfl=constant_cfl, initname=initname,
                                     eosname=eosname, casename=casename)
    if rank == 0:
        logger.info(init_message)

    def my_write_status(component_errors):
        if rank == 0:
            logger.info(
                "------- errors="
                + ", ".join("%.3g" % en for en in component_errors))

    def my_write_viz(step, t, state, dv=None, exact=None, resid=None):
        if dv is None:
            dv = eos.dependent_vars(state)
        if exact is None:
            exact = initializer(x_vec=nodes, eos=eos, time=t)
        if resid is None:
            resid = state - exact
        viz_fields = [("cv", state),
                      ("dv", dv),
                      ("exact", exact),
                      ("resid", resid)]
        from mirgecom.simutil import write_visfile
        write_visfile(discr, viz_fields, visualizer, vizname=casename,
                      step=step, t=t, overwrite=True, vis_timer=vis_timer)

    def my_write_restart(step, t, state):
        rst_fname = rst_pattern.format(cname=casename, step=step, rank=rank)
        if rst_fname != rst_filename:
            rst_data = {
                "local_mesh": local_mesh,
                "state": state,
                "t": t,
                "step": step,
                "order": order,
                "global_nelements": global_nelements,
                "num_parts": nparts
            }
            from mirgecom.restart import write_restart_file
            write_restart_file(actx, rst_data, rst_fname, comm)

    def my_health_check(pressure, component_errors):
        health_error = False
        from mirgecom.simutil import check_naninf_local, check_range_local
        if check_naninf_local(discr, "vol", pressure) \
           or check_range_local(discr, "vol", pressure, .99999999, 1.00000001):
            health_error = True
            logger.info(f"{rank=}: Invalid pressure data found.")

        exittol = .09
        if max(component_errors) > exittol:
            health_error = True
            if rank == 0:
                logger.info("Solution diverged from exact soln.")

        return health_error

    def my_pre_step(step, t, dt, state):
        try:
            dv = None
            exact = None
            component_errors = None

            if logmgr:
                logmgr.tick_before()

            from mirgecom.simutil import check_step
            do_viz = check_step(step=step, interval=nviz)
            do_restart = check_step(step=step, interval=nrestart)
            do_health = check_step(step=step, interval=nhealth)
            do_status = check_step(step=step, interval=nstatus)

            if do_health:
                dv = eos.dependent_vars(state)
                exact = initializer(x_vec=nodes, eos=eos, time=t)
                from mirgecom.simutil import compare_fluid_solutions
                component_errors = compare_fluid_solutions(discr, state, exact)
                from mirgecom.simutil import allsync
                health_errors = allsync(
                    my_health_check(dv.pressure, component_errors),
                    comm, op=MPI.LOR
                )
                if health_errors:
                    if rank == 0:
                        logger.info("Fluid solution failed health check.")
                    raise MyRuntimeError("Failed simulation health check.")

            if do_restart:
                my_write_restart(step=step, t=t, state=state)

            if do_viz:
                if dv is None:
                    dv = eos.dependent_vars(state)
                if exact is None:
                    exact = initializer(x_vec=nodes, eos=eos, time=t)
                resid = state - exact
                my_write_viz(step=step, t=t, state=state, dv=dv, exact=exact,
                             resid=resid)

            if do_status:
                if component_errors is None:
                    if exact is None:
                        exact = initializer(x_vec=nodes, eos=eos, time=t)
                    from mirgecom.simutil import compare_fluid_solutions
                    component_errors = compare_fluid_solutions(discr, state, exact)
                my_write_status(component_errors)

        except MyRuntimeError:
            if rank == 0:
                logger.info("Errors detected; attempting graceful exit.")
            my_write_viz(step=step, t=t, state=state)
            my_write_restart(step=step, t=t, state=state)
            raise

        dt = get_sim_timestep(discr, state, t, dt, current_cfl, eos, t_final,
                              constant_cfl)
        return state, dt

    def my_post_step(step, t, dt, state):
        # Logmgr needs to know about EOS, dt, dim?
        # imo this is a design/scope flaw
        if logmgr:
            set_dt(logmgr, dt)
            set_sim_state(logmgr, dim, state, eos)
            logmgr.tick_after()
        return state, dt

    def my_rhs(t, state):
        return euler_operator(discr, cv=state, time=t,
                              boundaries=boundaries, eos=eos)

    current_dt = get_sim_timestep(discr, current_state, current_t, current_dt,
                                  current_cfl, eos, t_final, constant_cfl)

    current_step, current_t, current_state = \
        advance_state(rhs=my_rhs, timestepper=timestepper,
                      pre_step_callback=my_pre_step, dt=current_dt,
                      post_step_callback=my_post_step,
                      state=current_state, t=current_t, t_final=t_final)

    # Dump the final data
    if rank == 0:
        logger.info("Checkpointing final state ...")

    final_dv = eos.dependent_vars(current_state)
    final_exact = initializer(x_vec=nodes, eos=eos, time=current_t)
    final_resid = current_state - final_exact
    my_write_viz(step=current_step, t=current_t, state=current_state, dv=final_dv,
                 exact=final_exact, resid=final_resid)
    my_write_restart(step=current_step, t=current_t, state=current_state)

    if logmgr:
        logmgr.close()
    elif use_profiling:
        print(actx.tabulate_profiling_data())

    finish_tol = 1e-16
    time_err = current_t - t_final
    if np.abs(time_err) > finish_tol:
        raise ValueError(f"Simulation did not finish at expected time {time_err=}.")
Beispiel #4
0
def main(ctx_factory=cl.create_some_context, use_leap=False):
    """Drive example."""
    cl_ctx = ctx_factory()
    queue = cl.CommandQueue(cl_ctx)
    actx = PyOpenCLArrayContext(queue,
                                allocator=cl_tools.MemoryPool(
                                    cl_tools.ImmediateAllocator(queue)))

    dim = 3
    nel_1d = 16
    order = 3
    exittol = .09
    t_final = 0.01
    current_cfl = 1.0
    current_dt = .001
    current_t = 0
    constant_cfl = False
    nstatus = 1
    nviz = 1
    rank = 0
    checkpoint_t = current_t
    current_step = 0
    if use_leap:
        from leap.rk import RK4MethodBuilder
        timestepper = RK4MethodBuilder("state")
    else:
        timestepper = rk4_step
    box_ll = -5.0
    box_ur = 5.0

    from mpi4py import MPI
    comm = MPI.COMM_WORLD
    rank = comm.Get_rank()

    nspecies = 4
    centers = make_obj_array(
        [np.zeros(shape=(dim, )) for i in range(nspecies)])
    spec_y0s = np.ones(shape=(nspecies, ))
    spec_amplitudes = np.ones(shape=(nspecies, ))
    eos = IdealSingleGas()

    velocity = np.ones(shape=(dim, ))

    initializer = MulticomponentLump(dim=dim,
                                     nspecies=nspecies,
                                     spec_centers=centers,
                                     velocity=velocity,
                                     spec_y0s=spec_y0s,
                                     spec_amplitudes=spec_amplitudes)
    boundaries = {BTAG_ALL: PrescribedBoundary(initializer)}

    casename = "mixture-lump"

    from meshmode.mesh.generation import generate_regular_rect_mesh
    generate_mesh = partial(generate_regular_rect_mesh,
                            a=(box_ll, ) * dim,
                            b=(box_ur, ) * dim,
                            nelements_per_axis=(nel_1d, ) * dim)
    local_mesh, global_nelements = generate_and_distribute_mesh(
        comm, generate_mesh)
    local_nelements = local_mesh.nelements

    discr = EagerDGDiscretization(actx,
                                  local_mesh,
                                  order=order,
                                  mpi_communicator=comm)
    nodes = thaw(actx, discr.nodes())
    current_state = initializer(nodes)

    visualizer = make_visualizer(discr)
    initname = initializer.__class__.__name__
    eosname = eos.__class__.__name__
    init_message = make_init_message(dim=dim,
                                     order=order,
                                     nelements=local_nelements,
                                     global_nelements=global_nelements,
                                     dt=current_dt,
                                     t_final=t_final,
                                     nstatus=nstatus,
                                     nviz=nviz,
                                     cfl=current_cfl,
                                     constant_cfl=constant_cfl,
                                     initname=initname,
                                     eosname=eosname,
                                     casename=casename)
    if rank == 0:
        logger.info(init_message)

    get_timestep = partial(inviscid_sim_timestep,
                           discr=discr,
                           t=current_t,
                           dt=current_dt,
                           cfl=current_cfl,
                           eos=eos,
                           t_final=t_final,
                           constant_cfl=constant_cfl)

    def my_rhs(t, state):
        return euler_operator(discr,
                              cv=state,
                              t=t,
                              boundaries=boundaries,
                              eos=eos)

    def my_checkpoint(step, t, dt, state):
        return sim_checkpoint(discr,
                              visualizer,
                              eos,
                              cv=state,
                              exact_soln=initializer,
                              vizname=casename,
                              step=step,
                              t=t,
                              dt=dt,
                              nstatus=nstatus,
                              nviz=nviz,
                              exittol=exittol,
                              constant_cfl=constant_cfl,
                              comm=comm)

    try:
        (current_step, current_t, current_state) = \
            advance_state(rhs=my_rhs, timestepper=timestepper,
                checkpoint=my_checkpoint,
                get_timestep=get_timestep, state=current_state,
                t=current_t, t_final=t_final)
    except ExactSolutionMismatch as ex:
        current_step = ex.step
        current_t = ex.t
        current_state = ex.state

    #    if current_t != checkpoint_t:
    if rank == 0:
        logger.info("Checkpointing final state ...")
    my_checkpoint(current_step,
                  t=current_t,
                  dt=(current_t - checkpoint_t),
                  state=current_state)

    if current_t - t_final < 0:
        raise ValueError("Simulation exited abnormally")
Beispiel #5
0
def test_multilump(ctx_factory, dim):
    """Test the multi-component lump initializer."""
    cl_ctx = ctx_factory()
    queue = cl.CommandQueue(cl_ctx)
    actx = PyOpenCLArrayContext(queue)

    nel_1d = 4
    nspecies = 10

    from meshmode.mesh.generation import generate_regular_rect_mesh

    mesh = generate_regular_rect_mesh(a=(-1.0, ) * dim,
                                      b=(1.0, ) * dim,
                                      nelements_per_axis=(nel_1d, ) * dim)

    order = 3
    logger.info(f"Number of elements: {mesh.nelements}")

    discr = EagerDGDiscretization(actx, mesh, order=order)
    nodes = thaw(actx, discr.nodes())

    rho0 = 1.5
    centers = make_obj_array(
        [np.zeros(shape=(dim, )) for i in range(nspecies)])
    spec_y0s = np.ones(shape=(nspecies, ))
    spec_amplitudes = np.ones(shape=(nspecies, ))

    for i in range(nspecies):
        centers[i][0] = (.1 * i)
        spec_y0s[i] += (.1 * i)
        spec_amplitudes[i] += (.1 * i)

    velocity = np.zeros(shape=(dim, ))
    velocity[0] = 1

    lump = MulticomponentLump(dim=dim,
                              nspecies=nspecies,
                              rho0=rho0,
                              spec_centers=centers,
                              velocity=velocity,
                              spec_y0s=spec_y0s,
                              spec_amplitudes=spec_amplitudes)

    cv = lump(nodes)
    numcvspec = get_num_species(dim, cv.join())
    print(f"get_num_species = {numcvspec}")

    assert get_num_species(dim, cv.join()) == nspecies
    assert discr.norm(cv.mass - rho0) == 0.0

    p = 0.4 * (cv.energy - 0.5 * np.dot(cv.momentum, cv.momentum) / cv.mass)
    exp_p = 1.0
    errmax = discr.norm(p - exp_p, np.inf)
    assert len(cv.species_mass) == nspecies
    species_mass = cv.species_mass

    spec_r = make_obj_array([nodes - centers[i] for i in range(nspecies)])
    r2 = make_obj_array(
        [np.dot(spec_r[i], spec_r[i]) for i in range(nspecies)])
    expfactor = make_obj_array(
        [spec_amplitudes[i] * actx.np.exp(-r2[i]) for i in range(nspecies)])
    exp_mass = make_obj_array(
        [rho0 * (spec_y0s[i] + expfactor[i]) for i in range(nspecies)])
    mass_resid = species_mass - exp_mass

    print(f"exp_mass = {exp_mass}")
    print(f"mass_resid = {mass_resid}")

    assert discr.norm(mass_resid, np.inf) == 0.0

    logger.info(f"lump_soln = {cv}")
    logger.info(f"pressure = {p}")

    assert errmax < 1e-15
Beispiel #6
0
def test_multilump_rhs(actx_factory, dim, order, v0):
    """Test the Euler rhs using the non-trivial 1, 2, and 3D mass lump case.

    The case is tested against the analytic expressions of the RHS. Checks several
    different orders and refinement levels to check error behavior.
    """
    actx = actx_factory()
    nspecies = 10
    tolerance = 1e-8
    maxxerr = 0.0

    from pytools.convergence import EOCRecorder

    eoc_rec = EOCRecorder()

    for nel_1d in [4, 8, 16]:
        from meshmode.mesh.generation import (
            generate_regular_rect_mesh, )

        mesh = generate_regular_rect_mesh(
            a=(-1, ) * dim,
            b=(1, ) * dim,
            nelements_per_axis=(nel_1d, ) * dim,
        )

        logger.info(f"Number of elements: {mesh.nelements}")

        discr = EagerDGDiscretization(actx, mesh, order=order)
        nodes = thaw(actx, discr.nodes())

        centers = make_obj_array(
            [np.zeros(shape=(dim, )) for i in range(nspecies)])
        spec_y0s = np.ones(shape=(nspecies, ))
        spec_amplitudes = np.ones(shape=(nspecies, ))

        velocity = np.zeros(shape=(dim, ))
        velocity[0] = v0
        rho0 = 2.0

        lump = MulticomponentLump(dim=dim,
                                  nspecies=nspecies,
                                  rho0=rho0,
                                  spec_centers=centers,
                                  velocity=velocity,
                                  spec_y0s=spec_y0s,
                                  spec_amplitudes=spec_amplitudes)

        lump_soln = lump(nodes)
        boundaries = {
            BTAG_ALL: PrescribedInviscidBoundary(fluid_solution_func=lump)
        }

        inviscid_rhs = euler_operator(discr,
                                      eos=IdealSingleGas(),
                                      boundaries=boundaries,
                                      cv=lump_soln,
                                      time=0.0)
        expected_rhs = lump.exact_rhs(discr, cv=lump_soln, time=0)

        print(f"inviscid_rhs = {inviscid_rhs}")
        print(f"expected_rhs = {expected_rhs}")
        err_max = discr.norm((inviscid_rhs - expected_rhs).join(), np.inf)
        if err_max > maxxerr:
            maxxerr = err_max

        eoc_rec.add_data_point(1.0 / nel_1d, err_max)

        logger.info(f"Max error: {maxxerr}")

    logger.info(f"Error for (dim,order) = ({dim},{order}):\n" f"{eoc_rec}")

    assert (eoc_rec.order_estimate() >= order - 0.5
            or eoc_rec.max_error() < tolerance)
Beispiel #7
0
def test_multilump_rhs(actx_factory, dim, order, v0, use_overintegration):
    """Test the Euler rhs using the non-trivial 1, 2, and 3D mass lump case.

    The case is tested against the analytic expressions of the RHS. Checks several
    different orders and refinement levels to check error behavior.
    """
    actx = actx_factory()
    nspecies = 10
    tolerance = 1e-8
    maxxerr = 0.0

    from pytools.convergence import EOCRecorder

    eoc_rec = EOCRecorder()

    for nel_1d in [4, 8, 12]:
        from meshmode.mesh.generation import (
            generate_regular_rect_mesh,
        )

        mesh = generate_regular_rect_mesh(
            a=(-1,) * dim, b=(1,) * dim, nelements_per_axis=(nel_1d,) * dim,
        )

        logger.info(f"Number of elements: {mesh.nelements}")

        from grudge.dof_desc import DISCR_TAG_BASE, DISCR_TAG_QUAD
        from meshmode.discretization.poly_element import \
            default_simplex_group_factory, QuadratureSimplexGroupFactory

        discr = EagerDGDiscretization(
            actx, mesh,
            discr_tag_to_group_factory={
                DISCR_TAG_BASE: default_simplex_group_factory(
                    base_dim=dim, order=order),
                DISCR_TAG_QUAD: QuadratureSimplexGroupFactory(2*order + 1)
            }
        )

        if use_overintegration:
            quadrature_tag = DISCR_TAG_QUAD
        else:
            quadrature_tag = None

        nodes = thaw(discr.nodes(), actx)

        centers = make_obj_array([np.zeros(shape=(dim,)) for i in range(nspecies)])
        spec_y0s = np.ones(shape=(nspecies,))
        spec_amplitudes = np.ones(shape=(nspecies,))

        velocity = np.zeros(shape=(dim,))
        velocity[0] = v0
        rho0 = 2.0

        lump = MulticomponentLump(dim=dim, nspecies=nspecies, rho0=rho0,
                                  spec_centers=centers, velocity=velocity,
                                  spec_y0s=spec_y0s, spec_amplitudes=spec_amplitudes)

        lump_soln = lump(nodes)
        gas_model = GasModel(eos=IdealSingleGas())
        fluid_state = make_fluid_state(lump_soln, gas_model)

        def _my_boundary(discr, btag, gas_model, state_minus, **kwargs):
            actx = state_minus.array_context
            bnd_discr = discr.discr_from_dd(btag)
            nodes = thaw(bnd_discr.nodes(), actx)
            return make_fluid_state(lump(x_vec=nodes, **kwargs), gas_model)

        boundaries = {
            BTAG_ALL: PrescribedFluidBoundary(boundary_state_func=_my_boundary)
        }

        inviscid_rhs = euler_operator(
            discr, state=fluid_state, gas_model=gas_model, boundaries=boundaries,
            time=0.0, quadrature_tag=quadrature_tag
        )
        expected_rhs = lump.exact_rhs(discr, cv=lump_soln, time=0)

        print(f"inviscid_rhs = {inviscid_rhs}")
        print(f"expected_rhs = {expected_rhs}")

        err_max = actx.to_numpy(
            discr.norm((inviscid_rhs-expected_rhs), np.inf))
        if err_max > maxxerr:
            maxxerr = err_max

        eoc_rec.add_data_point(1.0 / nel_1d, err_max)

        logger.info(f"Max error: {maxxerr}")

    logger.info(
        f"Error for (dim,order) = ({dim},{order}):\n"
        f"{eoc_rec}"
    )

    assert (
        eoc_rec.order_estimate() >= order - 0.5
        or eoc_rec.max_error() < tolerance
    )