Closed jorgensd closed 1 year ago
Issue with tentative velocity term, i.e. u_ab is written in the wrong way. Reference code for simpler CN AB solver:
from IPython import embed
import ufl
import dolfinx
import numpy as np
from mpi4py import MPI
from petsc4py import PETSc
class NaiveScheme():
def __init__(self, mesh, u_deg, p_deg, dt, nu):
self._mesh = mesh
self.V = dolfinx.fem.VectorFunctionSpace(mesh, ("Lagrange", u_deg))
self.Q = dolfinx.fem.FunctionSpace(mesh, ("Lagrange", p_deg))
self.u = dolfinx.fem.Function(self.V, name="u")
self.ut = dolfinx.fem.Function(self.V, name="ut")
self.u1 = dolfinx.fem.Function(self.V, name="u1")
self.u2 = dolfinx.fem.Function(self.V, name="u2")
self.p = dolfinx.fem.Function(self.Q, name="p")
self.phi = dolfinx.fem.Function(self.Q, name="phi")
self.create_forms(dt, nu)
self.create_solvers()
def create_forms(self, k, nu):
u = ufl.TrialFunction(self.V)
v = ufl.TestFunction(self.V)
dt = dolfinx.fem.Constant(self._mesh, k)
dx = ufl.Measure("dx", domain=self._mesh)
u_ab = 1.5 * self.u1 - 0.5 * self.u2
self.a1 = 1./dt * ufl.inner(u, v) * dx
self.a1 += ufl.inner(0.5*ufl.grad(u)*u_ab, v)*dx
self.a1 += nu/2*ufl.inner(ufl.grad(u), ufl.grad(v))*dx
self.L1 = 1./dt * ufl.inner(self.u1, v)*ufl.dx
self.L1 -= ufl.inner((0.5*ufl.grad(self.u1)*u_ab), v)*dx
self.L1 -= nu/2*ufl.inner(ufl.grad(self.u1), ufl.grad(v))*dx
self.L1 += self.p * ufl.div(v)*dx
# u_avg = 0.5 * (u + self.u1)
# F = 1./dt * ufl.inner((u - self.u1), v) * dx
# F += ufl.inner(ufl.dot(u_ab, ufl.grad(u_avg)), v) * dx
# F += nu * ufl.inner(ufl.grad(u_avg), ufl.grad(v)) * dx
# F -= self.p*ufl.div(v)*dx
# self.a1, self.L1 = ufl.system(F)
# w_time = dolfinx.fem.Constant(self._mesh, PETSc.ScalarType(3. / (2. * dt)))
# w_diffusion = dolfinx.fem.Constant(self._mesh, PETSc.ScalarType(nu))
# self.a1 = (w_time * ufl.inner(u, v) + w_diffusion
# * ufl.inner(ufl.grad(u), ufl.grad(v))) * dx
# self.L1 = ufl.inner(self.p, ufl.div(v)) * dx
# self.L1 += dolfinx.fem.Constant(mesh, PETSc.ScalarType(1. / (2. * dt))) *\
# ufl.inner(4 * self.u1 - self.u2, v) * dx
# BDF2 with implicit Adams-Bashforth
# bs = 2 * self.u1-self.u2
# self.a1 += ufl.inner(ufl.grad(u) * bs, v) * dx
# self.a1 += 0.5 * ufl.div(bs) * ufl.inner(u, v) * dx
p = ufl.TrialFunction(self.Q)
q = ufl.TestFunction(self.Q)
self.a2 = ufl.inner(ufl.grad(p), ufl.grad(q))*ufl.dx
self.L2 = -1./dt*ufl.div(self.ut)*q*ufl.dx
# self.a2 = ufl.inner(ufl.grad(p), ufl.grad(q)) * dx
# self.L2 = - w_time * ufl.inner(ufl.div(self.ut), q) * dx
nullspace = PETSc.NullSpace().create(constant=True)
F3 = 1/dt*ufl.inner(u-self.ut, v)*dx + ufl.inner(ufl.grad(self.phi), v)*dx
self.a3, self.L3 = ufl.system(F3)
def create_solvers(self):
boundary_facets = dolfinx.mesh.exterior_facet_indices(self._mesh.topology)
dofs = dolfinx.fem.locate_dofs_topological(
self.V, self._mesh.topology.dim-1, boundary_facets)
self.u_bc = dolfinx.fem.Function(self.V)
bc = dolfinx.fem.dirichletbc(self.u_bc, dofs)
self.problem1 = dolfinx.fem.petsc.LinearProblem(self.a1, self.L1, [bc], self.ut, petsc_options={"ksp_type": "preonly",
"pc_type": "lu"})
self.A2 = dolfinx.fem.petsc.assemble_matrix(dolfinx.fem.form(self.a2))
nullspace = PETSc.NullSpace().create(constant=True, comm=self._mesh.comm)
self.A2.setNearNullSpace(nullspace)
self.A2.setNullSpace(nullspace)
self.A2.assemble()
self.problem2 = PETSc.KSP().create(self._mesh.comm)
self.problem2.setOperators(self.A2)
# opts = PETSc.Options()
# opts["ksp_type"] = "preonly"
# opts["pc_type"] = "lu"
# opts["pc_factor_mat_solver_type"] = "mumps"
# opts["mat_mumps_icntl_24"] = 1
# opts["mat_mumps_icntl_25"] = 0
# opts["ksp_error_if_not_converged"] = True
self.problem2.setFromOptions()
self.problem3 = dolfinx.fem.petsc.LinearProblem(self.a3, self.L3, [], self.u, petsc_options={"ksp_type": "preonly",
"pc_type": "lu"})
def update_bc(self, bc_expr):
self.u_bc.interpolate(bc_expr)
def solve(self):
uh = self.problem1.solve()
b2 = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(self.L2))
b2.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
self.A2.getNullSpace().remove(b2)
self.problem2.solve(b2, self.phi.vector)
self.phi.x.scatter_forward()
vol = mesh.comm.allreduce(
dolfinx.fem.assemble_scalar(dolfinx.fem.form(1*ufl.dx(domain=self._mesh))), op=MPI.SUM)
phi_avg = mesh.comm.allreduce(
dolfinx.fem.assemble_scalar(dolfinx.fem.form(self.phi*ufl.dx)),
op=MPI.SUM)/vol
self.phi.x.array[:] -= phi_avg
u3 = self.problem3.solve()
return (uh, self.phi, u3)
class U():
def __init__(self, t, nu):
self.t = t
self.nu = nu
def eval_x(self, x):
return - np.cos(np.pi * x[0]) * np.sin(np.pi * x[1]) * np.exp(-2.0 * self.nu * np.pi**2 * self.t)
def eval_y(self, x):
return np.cos(np.pi * x[1]) * np.sin(np.pi * x[0]) * np.exp(-2.0 * self.nu * np.pi**2 * self.t)
def eval(self, x):
return (self.eval_x(x), self.eval_y(x))
N = 25
mesh = dolfinx.mesh.create_rectangle(
MPI.COMM_WORLD, [[-1, -1], [1, 1]], [N, N], cell_type=dolfinx.mesh.CellType.triangle)
dim = mesh.topology.dim - 1
mesh.topology.create_connectivity(dim, dim+1)
facets = dolfinx.mesh.exterior_facet_indices(mesh.topology)
value = 3
values = np.full_like(facets, value, dtype=np.int32)
sort = np.argsort(facets)
facet_tags = dolfinx.mesh.meshtags(mesh, dim, facets[sort], values[sort])
dt = 1e-2
nu = 0.01
T = 1
solver = NaiveScheme(mesh, 2, 1, dt, nu)
# Create expression for error calculation and initial conditions
x = ufl.SpatialCoordinate(mesh)
p_time = dolfinx.fem.Constant(mesh, -dt/2)
man_p = -0.25 * (ufl.cos(2*ufl.pi*x[0])+ufl.cos(2*ufl.pi*x[1]))*ufl.exp(-4*ufl.pi**2*nu*p_time)
p_expr = dolfinx.fem.Expression(man_p, solver.Q.element.interpolation_points())
u_time = dolfinx.fem.Constant(mesh, 0.)
man_u = ufl.as_vector((
- ufl.sin(ufl.pi*x[1])*ufl.cos(ufl.pi*x[0]),
ufl.sin(ufl.pi*x[0])*ufl.cos(ufl.pi*x[1])))*ufl.exp(-2*ufl.pi**2*nu*u_time)
u_expr = dolfinx.fem.Expression(man_u, solver.V.element.interpolation_points())
# Create initial conditions at time 0 and -dt
u_time.value = -dt
solver.u2.interpolate(u_expr)
u_time.value = 0
solver.u1.interpolate(u_expr)
p_time.value = -dt/2.
solver.p.interpolate(p_expr)
bc_expr = U(0., nu)
# Create error computation
diff_u = solver.u-man_u
L2_u = dolfinx.fem.form(ufl.inner(diff_u, diff_u) * ufl.dx)
diff_p = solver.p - man_p
L2_p = dolfinx.fem.form(ufl.inner(diff_p, diff_p) * ufl.dx)
u_vtx = dolfinx.io.VTXWriter(mesh.comm, "u.bp", [solver.u])
ut_vtx = dolfinx.io.VTXWriter(mesh.comm, "ut.bp", [solver.ut])
p_vtx = dolfinx.io.VTXWriter(mesh.comm, "p.bp", [solver.p])
phi_vtx = dolfinx.io.VTXWriter(mesh.comm, "phi.bp", [solver.phi])
p_ex = dolfinx.fem.Function(solver.Q)
pex_vtx = dolfinx.io.VTXWriter(mesh.comm, "p_ex.bp", [p_ex])
i = 0
# p_vtx.write(float(0))
p_ex.interpolate(p_expr)
# pex_vtx.write(float(0))
while bc_expr.t <= T+1e-14:
bc_expr.t += dt
u_time.value += dt
p_time.value += dt
solver.update_bc(bc_expr.eval)
u_t, phi, uh = solver.solve()
solver.p.x.array[:] += phi.x.array
solver.u2.x.array[:] = solver.u1.x.array[:]
solver.u1.x.array[:] = solver.u.x.array[:]
u_vtx.write(float(u_time.value))
p_vtx.write(float(p_time.value))
ut_vtx.write(float(u_time.value))
phi_vtx.write(float(p_time.value))
p_ex.interpolate(p_expr)
pex_vtx.write(float(p_time.value))
print(f"u_err {float(u_time.value):.2e}, {np.sqrt(mesh.comm.allreduce(dolfinx.fem.assemble_scalar(L2_u), op=MPI.SUM))}")
print(f"p_err {float(p_time.value):.2e}, {np.sqrt(mesh.comm.allreduce(dolfinx.fem.assemble_scalar(L2_p), op=MPI.SUM))}")
print("*"*10)
pex_vtx.close()
u_vtx.close()
p_vtx.close()
ut_vtx.close()
from IPython import embed
import ufl
import dolfinx
import numpy as np
from mpi4py import MPI
from petsc4py import PETSc
import argparse
import logging
desc = "Taylor-Green convergence demo"
parser = argparse.ArgumentParser(description=desc,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-N", "--refinement", type=int, dest="Ns", action="append",
help="The number of elements in x and y direction", required=True)
parser.add_argument("-T0", "--T-start", dest="T_start", type=float,
help="Start time of simulation", default=0)
parser.add_argument("-T1", "--T-end", dest="T_end", type=float,
help="End time of simulation", default=1)
parser.add_argument("-dt", dest="dt", type=float, help="Time step", default=0.1)
parser.add_argument("-nu", dest="nu", type=float, help="Kinematic viscosity", default=0.01)
parser.add_argument("-u", dest="u_deg", type=int, help="Degree of velocity space", default=2)
parser.add_argument("-p", dest="p_deg", type=int, help="Degree of pressure space", default=1)
parser.add_argument("-lm", "--low-memory", dest="lm", action="store_true",
default=False, help="Use low memory version of Oasisx")
inputs = parser.parse_args()
logger = logging.getLogger("Oasisx")
class NaiveScheme():
def __init__(self, mesh, u_deg, p_deg, dt, nu):
self._mesh = mesh
self.V = dolfinx.fem.VectorFunctionSpace(mesh, ("Lagrange", u_deg))
self.Q = dolfinx.fem.FunctionSpace(mesh, ("Lagrange", p_deg))
self.u = dolfinx.fem.Function(self.V, name="u")
self.ut = dolfinx.fem.Function(self.V, name="ut")
self.u1 = dolfinx.fem.Function(self.V, name="u1")
self.u2 = dolfinx.fem.Function(self.V, name="u2")
self.p = dolfinx.fem.Function(self.Q, name="p")
self.phi = dolfinx.fem.Function(self.Q, name="phi")
self.create_forms(dt, nu)
self.create_solvers()
def create_forms(self, k, nu):
u = ufl.TrialFunction(self.V)
v = ufl.TestFunction(self.V)
dt = dolfinx.fem.Constant(self._mesh, k)
dx = ufl.Measure("dx", domain=self._mesh)
u_ab = 1.5 * self.u1 - 0.5 * self.u2
self.a1 = 1./dt * ufl.inner(u, v) * dx
self.a1 += ufl.inner(0.5*ufl.grad(u)*u_ab, v)*dx
self.a1 += nu/2*ufl.inner(ufl.grad(u), ufl.grad(v))*dx
self.L1 = 1./dt * ufl.inner(self.u1, v)*ufl.dx
self.L1 -= ufl.inner((0.5*ufl.grad(self.u1)*u_ab), v)*dx
self.L1 -= nu/2*ufl.inner(ufl.grad(self.u1), ufl.grad(v))*dx
self.L1 += self.p * ufl.div(v)*dx
# u_avg = 0.5 * (u + self.u1)
# F = 1./dt * ufl.inner((u - self.u1), v) * dx
# F += ufl.inner(ufl.dot(u_ab, ufl.grad(u_avg)), v) * dx
# F += nu * ufl.inner(ufl.grad(u_avg), ufl.grad(v)) * dx
# F -= self.p*ufl.div(v)*dx
# self.a1, self.L1 = ufl.system(F)
# w_time = dolfinx.fem.Constant(self._mesh, PETSc.ScalarType(3. / (2. * dt)))
# w_diffusion = dolfinx.fem.Constant(self._mesh, PETSc.ScalarType(nu))
# self.a1 = (w_time * ufl.inner(u, v) + w_diffusion
# * ufl.inner(ufl.grad(u), ufl.grad(v))) * dx
# self.L1 = ufl.inner(self.p, ufl.div(v)) * dx
# self.L1 += dolfinx.fem.Constant(mesh, PETSc.ScalarType(1. / (2. * dt))) *\
# ufl.inner(4 * self.u1 - self.u2, v) * dx
# BDF2 with implicit Adams-Bashforth
# bs = 2 * self.u1-self.u2
# self.a1 += ufl.inner(ufl.grad(u) * bs, v) * dx
# self.a1 += 0.5 * ufl.div(bs) * ufl.inner(u, v) * dx
p = ufl.TrialFunction(self.Q)
q = ufl.TestFunction(self.Q)
self.a2 = ufl.inner(ufl.grad(p), ufl.grad(q))*ufl.dx
self.L2 = -1./dt*ufl.div(self.ut)*q*ufl.dx
# self.a2 = ufl.inner(ufl.grad(p), ufl.grad(q)) * dx
# self.L2 = - w_time * ufl.inner(ufl.div(self.ut), q) * dx
nullspace = PETSc.NullSpace().create(constant=True)
F3 = 1/dt*ufl.inner(u-self.ut, v)*dx + ufl.inner(ufl.grad(self.phi), v)*dx
self.a3, self.L3 = ufl.system(F3)
def create_solvers(self):
boundary_facets = dolfinx.mesh.exterior_facet_indices(self._mesh.topology)
dofs = dolfinx.fem.locate_dofs_topological(
self.V, self._mesh.topology.dim-1, boundary_facets)
self.u_bc = dolfinx.fem.Function(self.V)
bc = dolfinx.fem.dirichletbc(self.u_bc, dofs)
self.problem1 = dolfinx.fem.petsc.LinearProblem(self.a1, self.L1, [bc], self.ut, petsc_options={"ksp_type": "preonly",
"pc_type": "lu"})
self.A2 = dolfinx.fem.petsc.assemble_matrix(dolfinx.fem.form(self.a2))
nullspace = PETSc.NullSpace().create(constant=True, comm=self._mesh.comm)
self.A2.setNearNullSpace(nullspace)
self.A2.setNullSpace(nullspace)
self.A2.assemble()
self.problem2 = PETSc.KSP().create(self._mesh.comm)
self.problem2.setOperators(self.A2)
opts = PETSc.Options()
opts["ksp_type"] = "preonly"
opts["pc_type"] = "lu"
opts["pc_factor_mat_solver_type"] = "mumps"
opts["mat_mumps_icntl_24"] = 1
opts["mat_mumps_icntl_25"] = 0
opts["ksp_error_if_not_converged"] = True
self.problem2.setFromOptions()
self.problem3 = dolfinx.fem.petsc.LinearProblem(self.a3, self.L3, [], self.u, petsc_options={"ksp_type": "preonly",
"pc_type": "lu"})
def update_bc(self, bc_expr):
self.u_bc.interpolate(bc_expr)
def solve(self):
uh = self.problem1.solve()
b2 = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(self.L2))
b2.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
self.A2.getNullSpace().remove(b2)
self.problem2.solve(b2, self.phi.vector)
self.phi.x.scatter_forward()
vol = mesh.comm.allreduce(
dolfinx.fem.assemble_scalar(dolfinx.fem.form(1*ufl.dx(domain=self._mesh))), op=MPI.SUM)
phi_avg = mesh.comm.allreduce(
dolfinx.fem.assemble_scalar(dolfinx.fem.form(self.phi*ufl.dx)),
op=MPI.SUM)/vol
self.phi.x.array[:] -= phi_avg
u3 = self.problem3.solve()
return (uh, self.phi, u3)
class U():
def __init__(self, t, nu):
self.t = t
self.nu = nu
def eval_x(self, x):
return - np.cos(np.pi * x[0]) * np.sin(np.pi * x[1]) * np.exp(-2.0 * self.nu * np.pi**2 * float(self.t))
def eval_y(self, x):
return np.cos(np.pi * x[1]) * np.sin(np.pi * x[0]) * np.exp(-2.0 * self.nu * np.pi**2 * float(self.t))
def eval(self, x):
return (self.eval_x(x), self.eval_y(x))
dt = inputs.dt
nu = inputs.nu
assert (inputs.T_start < inputs.T_end)
T_end = inputs.T_end
T_start = inputs.T_start
num_steps = int((T_end-T_start)//dt)
assert inputs.u_deg > inputs.p_deg
solver_options = {"tentative": {"ksp_type": "preonly", "pc_type": "lu"},
"pressure": {"ksp_type": "preonly", "pc_type": "lu"},
"scalar": {"ksp_type": "preonly", "pc_type": "lu"}}
space_errors = np.empty((2, len(inputs.Ns)), dtype=np.float64)
hs = np.empty(len(inputs.Ns), dtype=np.float64)
for n, N in enumerate(inputs.Ns):
mesh = dolfinx.mesh.create_rectangle(
MPI.COMM_WORLD, [[-1, -1], [1, 1]], [N, N], cell_type=dolfinx.mesh.CellType.triangle)
dim = mesh.topology.dim - 1
mesh.topology.create_connectivity(dim, dim+1)
facets = dolfinx.mesh.exterior_facet_indices(mesh.topology)
value = 3
values = np.full_like(facets, value, dtype=np.int32)
sort = np.argsort(facets)
facet_tags = dolfinx.mesh.meshtags(mesh, dim, facets[sort], values[sort])
solver = NaiveScheme(mesh, inputs.u_deg, inputs.p_deg, dt, nu)
# Create expression for error calculation and initial conditions
x = ufl.SpatialCoordinate(mesh)
p_time = dolfinx.fem.Constant(mesh, -dt/2)
man_p = -0.25 * (ufl.cos(2*ufl.pi*x[0])+ufl.cos(2*ufl.pi*x[1]))*ufl.exp(-4*ufl.pi**2*nu*p_time)
p_expr = dolfinx.fem.Expression(man_p, solver.Q.element.interpolation_points())
u_time = dolfinx.fem.Constant(mesh, 0.)
man_u = ufl.as_vector((
- ufl.sin(ufl.pi*x[1])*ufl.cos(ufl.pi*x[0]),
ufl.sin(ufl.pi*x[0])*ufl.cos(ufl.pi*x[1])))*ufl.exp(-2*ufl.pi**2*nu*u_time)
u_expr = dolfinx.fem.Expression(man_u, solver.V.element.interpolation_points())
# Create initial conditions at time 0 and -dt
u_time.value = -dt
solver.u2.interpolate(u_expr)
u_time.value = 0
solver.u1.interpolate(u_expr)
p_time.value = -dt/2.
solver.p.interpolate(p_expr)
bc_expr = U(u_time, nu)
# Create error computation
diff_u = solver.u-man_u
L2_u = dolfinx.fem.form(ufl.inner(diff_u, diff_u) * ufl.dx)
diff_p = solver.p - man_p
L2_p = dolfinx.fem.form(ufl.inner(diff_p, diff_p) * ufl.dx)
u_vtx = dolfinx.io.VTXWriter(mesh.comm, "u.bp", [solver.u])
ut_vtx = dolfinx.io.VTXWriter(mesh.comm, "ut.bp", [solver.ut])
p_vtx = dolfinx.io.VTXWriter(mesh.comm, "p.bp", [solver.p])
phi_vtx = dolfinx.io.VTXWriter(mesh.comm, "phi.bp", [solver.phi])
p_ex = dolfinx.fem.Function(solver.Q)
pex_vtx = dolfinx.io.VTXWriter(mesh.comm, "p_ex.bp", [p_ex])
i = 0
error_space_time = np.empty((2, num_steps), dtype=np.float64)
for i in range(num_steps):
u_time.value += dt
p_time.value += dt
solver.update_bc(bc_expr.eval)
u_t, phi, uh = solver.solve()
solver.p.x.array[:] += phi.x.array
solver.u2.x.array[:] = solver.u1.x.array[:]
solver.u1.x.array[:] = solver.u.x.array[:]
L2_u_loc = dolfinx.fem.assemble_scalar(L2_u)
error_u = mesh.comm.allreduce(L2_u_loc, op=MPI.SUM)
L2_p_loc = dolfinx.fem.assemble_scalar(L2_p)
error_p = mesh.comm.allreduce(L2_p_loc, op=MPI.SUM)
logger.debug(f"{i=} {float(u_time.value)}, {error_u=}")
logger.debug(f"{i=} {float(p_time.value)}, {error_p=}")
error_space_time[:, i] = [error_u, error_p]
logger.debug("*"*10)
hmax_loc = np.max(mesh.h(mesh.topology.dim, np.arange(
mesh.topology.index_map(mesh.topology.dim).size_local, dtype=np.int32)))
hmax = mesh.comm.allreduce(hmax_loc, op=MPI.MAX)
space_time_u_L2 = np.sqrt(dt*np.sum(error_space_time[0, :]))
space_time_p_L2 = np.sqrt(dt*np.sum(error_space_time[1, :]))
logger.setLevel(logging.INFO)
logger.info(f"{hmax=} {space_time_u_L2=} {space_time_p_L2=}")
hs[n] = hmax
space_errors[:, n] = [space_time_u_L2, space_time_p_L2]
order = np.argsort(hs)[::-1]
logger.setLevel(logging.INFO)
logger.info(f"{hmax=} {space_time_u_L2=} {space_time_p_L2=}")
hs[n] = hmax
space_errors[:, n] = [space_time_u_L2, space_time_p_L2]
order = np.argsort(hs)[::-1]
hs = hs[order]
space_errors[0, :] = space_errors[0, order]
space_errors[1, :] = space_errors[1, order]
logger.info(
f"Convergence rates u: {np.log(space_errors[0, 1:] / space_errors[0, :-1]) / np.log(hs[1:]/hs[:-1])}")
logger.info(
f"Convergence rates p: {np.log(space_errors[1, 1:] / space_errors[1, :-1]) / np.log(hs[1:]/hs[:-1])}")
gives
root@dokken-XPS-9320:~/shared/oasisx/demo# python3 mwe2.py -N 15 -N 30 -N 60 -dt 0.01
INFO:Oasisx:hmax=0.18856180831641278 space_time_u_L2=0.009238574053141763 space_time_p_L2=0.011946135014718771
INFO:Oasisx:hmax=0.09428090415820647 space_time_u_L2=0.0005878265124763631 space_time_p_L2=0.002767463093457577
INFO:Oasisx:hmax=0.047140452079103314 space_time_u_L2=6.124957864698556e-05 space_time_p_L2=0.000681973256128087
INFO:Oasisx:hmax=0.047140452079103314 space_time_u_L2=6.124957864698556e-05 space_time_p_L2=0.000681973256128087
INFO:Oasisx:Convergence rates u: [3.97420786 3.26261861]
INFO:Oasisx:Convergence rates p: [2.10990795 2.02077701]
Add consistent treatment of pressure from external forces on outlets.