# 3D problem (sphere) Navier-Stokes

Hi,

I try to solve a simple 3D Navier-Stokes 3D (sphere) problem.
And for this, I have considered this example:Test problem 2: Flow past a cylinder (DFG 2D-3 benchmark) โ FEniCSx tutorial
However, I am more used to FEniCS 2019, where I just mark the BC. But, in FEniCS-X ns_code2 example, the BC is marked with facets.

I have managed to read the mesh and create the function space in FEniCS-X

``````import dolfinx
import numpy as np
from mpi4py import MPI
from dolfinx.cpp.mesh import CellType
from dolfinx.io import (XDMFFile, extract_gmsh_geometry,
extract_gmsh_topology_and_markers, ufl_mesh_from_gmsh)
from dolfinx.fem import (Constant, DirichletBC, Function, FunctionSpace, apply_lifting, assemble_matrix,
assemble_scalar, assemble_vector, create_vector, locate_dofs_topological, set_bc)
from petsc4py import PETSc
from ufl import (FacetNormal, FiniteElement, Identity, Measure, TestFunction, TrialFunction, VectorElement,

print(f"DOLFINx version: {dolfinx.__version__} is installed")

filename="sphere-2.xdmf"
with dolfinx.io.XDMFFile(MPI.COMM_WORLD, "sphere-2.xdmf", "r") as xdmf:

t = 0
T = 1 #8                    # Final time
dt = 1/1600                 # Time step size
num_steps = int(T/dt)
k = Constant(mesh, dt)
mu = Constant(mesh, 0.001)  # Dynamic viscosity
rho = Constant(mesh, 1)     # Density

v_cg2 = VectorElement("CG", mesh.ufl_cell(), 2)
s_cg1 = FiniteElement("CG", mesh.ufl_cell(), 1)
V = FunctionSpace(mesh, v_cg2)
Q = FunctionSpace(mesh, s_cg1)

fdim = mesh.topology.dim

``````

For a similar problem in FEniCS 2019, I have set the BC:

``````# Define function spaces for pressure and velocity
V = VectorFunctionSpace(mesh, "Lagrange", degree=2, dim=3)
Q = FunctionSpace(mesh, "Lagrange", 1)

class Inflow(SubDomain):
def inside(self, x, on_boundary):
return on_boundary and (abs(x[0] - 0.) < DOLFIN_EPS)

class Outflow(SubDomain):
def inside(self, x, on_boundary):
return on_boundary and (abs(x[0] - 10.) < DOLFIN_EPS)

class Walls(SubDomain):
def inside(self, x, on_boundary):
result = on_boundary and ((abs(x[1] + 0.) < DOLFIN_EPS) or (abs(x[1] - 5.) < DOLFIN_EPS) or
(abs(x[2] - 0.) < DOLFIN_EPS) or (abs(x[2] - 5.) < DOLFIN_EPS))
print(result)
return result

class Sphere(SubDomain):
def inside(self, x, on_boundary):
result = on_boundary\
and x[0] > 2.0 - DOLFIN_EPS and x[0] < 4.0 + DOLFIN_EPS\
and x[1] > 2.0 - DOLFIN_EPS and x[1] < 4.0 + DOLFIN_EPS\
and x[2] > 2.0 - DOLFIN_EPS and x[2] < 4.0 + DOLFIN_EPS
return result

boundaries = MeshFunction("size_t", mesh, mesh.topology().dim() , 0)

Inflow().mark(boundaries, 1)
Outflow().mark(boundaries, 2)
Walls().mark(boundaries, 3)
Sphere().mark(boundaries, 4)

inflow_profile = ('1.', '0.', '0.')

bcu_inflow = DirichletBC(V, Expression(inflow_profile, degree=3), boundaries, 1)
bcp_outflow = DirichletBC(Q, Constant(0.), boundaries, 2)
bcu_walls = DirichletBC(V, Constant((0., 0., 0.)), boundaries, 3)
bcu_sphere = DirichletBC(V, Constant((0., 0., 0.)), boundaries, 4)
bcu = [bcu_inflow, bcu_walls, bcu_sphere]
bcp = [bcp_outflow]
``````

Could you please tell me how to do it in a similar way in FEniCS-X or is there any other smart way to do set the BC?

See for instance: Implementation โ FEniCSx tutorial
which uses `dolfinx.fem.locate_dofs_geometrical`, which is similar to the previous subdomain, but using a python function instead of a specific class.

Thank you!
Now I have formulated to as you suggested. However, I am not sure if this is correct.
Moreover, I am having an error with the `Expression` function. I do not know if this is deprecated or do not know from where to import it.

``````import dolfinx
import numpy as np
from mpi4py import MPI
from dolfinx.cpp.mesh import CellType
from dolfinx.io import (XDMFFile, extract_gmsh_geometry,
extract_gmsh_topology_and_markers, ufl_mesh_from_gmsh)
from dolfinx.fem import (Constant, DirichletBC, Function, FunctionSpace, apply_lifting, assemble_matrix,
assemble_scalar, assemble_vector, create_vector, locate_dofs_topological, set_bc)
from petsc4py import PETSc
from ufl import (FacetNormal, FiniteElement, Identity, Measure, TestFunction, TrialFunction, VectorElement,
as_vector, div, dot, ds, dx, inner, lhs, grad, nabla_grad, rhs, sym, Expression)

print(f"DOLFINx version: {dolfinx.__version__} is installed")

filename="sphere-2.xdmf"
with dolfinx.io.XDMFFile(MPI.COMM_WORLD, "sphere-2.xdmf", "r") as xdmf:

t = 0
T = 1 #8                    # Final time
dt = 1/1600                 # Time step size
num_steps = int(T/dt)
k = Constant(mesh, dt)
mu = Constant(mesh, 0.001)  # Dynamic viscosity
rho = Constant(mesh, 1)     # Density

import dolfinx.fem as fem
V = fem.FunctionSpace(mesh, ("CG", 2))
Q = fem.FunctionSpace(mesh, ("CG", 1))

fdim = mesh.topology.dim

DOLFIN_EPS = 0.000001

def Inflow(x):
return Inflow and (abs(x[0] - 0.) < DOLFIN_EPS)

def Outflow(x):
return Outflow and (abs(x[0] - 5.) < DOLFIN_EPS)

def Walls(x):
result = Walls and ((abs(x[1] + 0.) < DOLFIN_EPS) or (abs(x[1] - 5.) < DOLFIN_EPS) or
(abs(x[2] - 0.) < DOLFIN_EPS) or (abs(x[2] - 5.) < DOLFIN_EPS))
return result

def Sphere(x):
result = Sphere\
and x[0] > 2.0 - DOLFIN_EPS and x[0] < 4.0 + DOLFIN_EPS\
and x[1] > 2.0 - DOLFIN_EPS and x[1] < 4.0 + DOLFIN_EPS\
and x[2] > 2.0 - DOLFIN_EPS and x[2] < 4.0 + DOLFIN_EPS
return result

inflow_profile = ('1.', '0.', '0.')

inflow_boundary_dofs = fem.locate_dofs_geometrical(V, Inflow)
outflow_boundary_dofs = fem.locate_dofs_geometrical(Q, Outflow)
walls_boundary_dofs = fem.locate_dofs_geometrical(V, Walls)
sphere_boundary_dofs = fem.locate_dofs_geometrical(V, Sphere)

bcu_inflow = fem.DirichletBC(inflow_boundary_dofs, Expression(inflow_profile, degree=3))
bcu_outflow = fem.DirichletBC(outflow_boundary_dofs, Constant(0.))
bcu_walls = fem.DirichletBC(walls_boundary_dofs, Constant(0.))
bcu_sphere = fem.DirichletBC(sphere_boundary_dofs, Constant(0.))

bcu = [bcu_inflow, bcu_walls, bcu_sphere]
bcp = [bcp_outflow]

``````

This should simply be

``````def Inflow(x):
return (abs(x[0] - 0.) < DOLFIN_EPS)
``````

and similarly for all other functions used to locate boundary conditions.

The `Expression` class in dolfin has been deprecated, as it hid alot of magic (including interpolation into an appropriate space).
See:
https://jorgensd.github.io/dolfinx-tutorial/chapter1/fundamentals_code.html#defining-the-boundary-conditions
For for details on how to implement spatially varying boundary conditions

Thank you!
I am still not able to further continue with walls and sphere.

``````def Inflow(x):
return (abs(x[0] - 0.) < DOLFIN_EPS)

def Outflow(x):
return (abs(x[0] - 5.) < DOLFIN_EPS)

def Walls(x):
return (abs(x[1] + 0.) < DOLFIN_EPS) or (abs(x[1] - 5.) < DOLFIN_EPS) or\
(abs(x[2] - 0.) < DOLFIN_EPS) or (abs(x[2] - 5.) < DOLFIN_EPS)

def Sphere(x):
return x[0] > 2.0 - DOLFIN_EPS and x[0] < 4.0 + DOLFIN_EPS\
and x[1] > 2.0 - DOLFIN_EPS and x[1] < 4.0 + DOLFIN_EPS\
and x[2] > 2.0 - DOLFIN_EPS and x[2] < 4.0 + DOLFIN_EPS

``````

And I am getting the following error:

``````DOLFINx version: 0.3.1.0 is installed
Traceback (most recent call last):
File "fluid.py", line 55, in <module>
walls_boundary_dofs = fem.locate_dofs_geometrical(V, Walls)
File "/python3.8/site-packages/dolfinx/fem/dirichletbc.py", line 66, in locate_dofs_geometrical
return cpp.fem.locate_dofs_geometrical(_V, marker)
File "fluid.py", line 42, in Walls
return (abs(x[1] + 0.) < DOLFIN_EPS) or (abs(x[1] - 5.) < DOLFIN_EPS) or\
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

``````

And I have tried to used the `(a<b).any()` `(a<b).all()`, then I am getting some sort of memory error:

``````DOLFINx version: 0.3.1.0 is installed
Traceback (most recent call last):
File "fluid.py", line 51, in <module>
walls_boundary_dofs = fem.locate_dofs_geometrical(V, Walls)
File "/python3.8/site-packages/dolfinx/fem/dirichletbc.py", line 66, in locate_dofs_geometrical
return cpp.fem.locate_dofs_geometrical(_V, marker)

``````

Please read carefully through the tutorial, as it explains most of these steps.
Iโve attached a minimal code that achieves what you want to do:

``````import dolfinx.fem as fem
import dolfinx
import numpy as np
from mpi4py import MPI

from dolfinx.generation import UnitCubeMesh
print(f"DOLFINx version: {dolfinx.__version__} is installed")

mesh = UnitCubeMesh(MPI.COMM_WORLD, 10, 10, 10)
t = 0
T = 1  # 8                    # Final time
dt = 1 / 1600                 # Time step size
num_steps = int(T / dt)
k = fem.Constant(mesh, dt)
mu = fem.Constant(mesh, 0.001)  # Dynamic viscosity
rho = fem.Constant(mesh, 1)     # Density

V = fem.VectorFunctionSpace(mesh, ("CG", 2))
Q = fem.FunctionSpace(mesh, ("CG", 1))

fdim = mesh.topology.dim

DOLFIN_EPS = 0.000001

def Inflow(x):
return np.abs(x[0] - 0.) < DOLFIN_EPS

def Outflow(x):
return np.abs(x[0] - 5.) < DOLFIN_EPS

def Walls(x):
result = np.logical_or(np.abs(x[1] + 0.) < DOLFIN_EPS, np.abs(x[1] - 5.) < DOLFIN_EPS)
result2 = np.logical_or(np.abs(x[2] - 0.) < DOLFIN_EPS, np.abs(x[2] - 5.) < DOLFIN_EPS)
return np.logical_or(result, result2)

def Sphere(x):
result = np.logical_and(x[0] > 2.0 - DOLFIN_EPS, x[0] < 4.0 + DOLFIN_EPS)
result2 = np.logical_and(x[1] > 2.0 - DOLFIN_EPS, x[1] < 4.0 + DOLFIN_EPS)
result3 = np.logical_and(x[2] > 2.0 - DOLFIN_EPS, x[2] < 4.0 + DOLFIN_EPS)
return np.logical_and(np.logical_and(result, result2), result3)

def inflow_profile(x):
values = np.zeros(x.shape)
values[0, :] = 1
return values

u_in = fem.Function(V)
u_in.interpolate(inflow_profile)

u_zero = fem.Function(V)
u_zero.x.array[:] = 0
p_zero = fem.Function(Q)
p_zero.x.array[:] = 0

inflow_boundary_dofs = fem.locate_dofs_geometrical(V, Inflow)
outflow_boundary_dofs = fem.locate_dofs_geometrical(Q, Outflow)
walls_boundary_dofs = fem.locate_dofs_geometrical(V, Walls)
sphere_boundary_dofs = fem.locate_dofs_geometrical(V, Sphere)

bcu_inflow = fem.DirichletBC(u_in, inflow_boundary_dofs)
bcp_outflow = fem.DirichletBC(p_zero, outflow_boundary_dofs)
bcu_walls = fem.DirichletBC(u_zero, walls_boundary_dofs)
bcu_sphere = fem.DirichletBC(u_zero, sphere_boundary_dofs)

bcu = [bcu_inflow, bcu_walls, bcu_sphere]
bcp = [bcp_outflow]
``````

Many thanks for the minimal code!
But, still, I am having some issues with the `interpolate` function:

``````\$ python3 fluid.py
DOLFINx version: 0.3.1.0 is installed
DOLFINx version: 0.3.1.0 is installed
[[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]]
[[1. 1. 1. ... 1. 1. 1.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]]
Traceback (most recent call last):
File "/python3.8/site-packages/dolfinx/fem/function.py", line 301, in _interpolate
self._cpp_object.interpolate(u._cpp_object)
AttributeError: 'function' object has no attribute '_cpp_object'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "fluid.py", line 64, in <module>
u_in.interpolate(inflow_profile)
File "/python3.8/site-packages/dolfinx/fem/function.py", line 309, in interpolate
_interpolate(u)
File "/Python/3.8.6-GCCcore-10.2.0/lib/python3.8/functools.py", line 875, in wrapper
return dispatch(args[0].__class__)(*args, **kw)
File "python3.8/site-packages/dolfinx/fem/function.py", line 303, in _interpolate
self._cpp_object.interpolate(u)
RuntimeError: Interpolation data has the wrong shape.
``````

I have referred to another issue, similar to this one.
Should I use: ` values = np.zeros(x.shape[1])`

As you have not provided a minimal example to reproduce your issue I cannot help you any further.
Please consider the minimal code I sent you, as it executes with no error messages.

Compared to your original code, I replaced

with

as you want the velocity to be a vector function.

Thanks! that problem solved. Now I am getting error with PETSc comm.

``````import dolfinx
import numpy as np
from mpi4py import MPI
from dolfinx.cpp.mesh import CellType
from dolfinx.io import (XDMFFile, extract_gmsh_geometry,
extract_gmsh_topology_and_markers, ufl_mesh_from_gmsh)
from dolfinx.fem import (Constant, DirichletBC, Function, FunctionSpace, apply_lifting, assemble_matrix,
assemble_scalar, assemble_vector, create_vector, locate_dofs_topological, set_bc)
from petsc4py import PETSc
from ufl import (FacetNormal, FiniteElement, Identity, Measure, TestFunction, TrialFunction, VectorElement,

print(f"DOLFINx version: {dolfinx.__version__} is installed")

filename="sphere-2.xdmf"
with dolfinx.io.XDMFFile(MPI.COMM_WORLD, "sphere-2.xdmf", "r") as xdmf:

#### local mesh ####
## mesh = UnitCubeMesh(MPI.COMM_WORLD, 10, 10, 10)
###################

t = 0
T = 1 #8                    # Final time
dt = 1/1600                 # Time step size
num_steps = int(T/dt)
k = Constant(mesh, dt)
mu = Constant(mesh, 0.001)  # Dynamic viscosity
rho = Constant(mesh, 1)     # Density

import dolfinx.fem as fem
V = fem.VectorFunctionSpace(mesh, ("CG", 2))
Q = fem.FunctionSpace(mesh, ("CG", 1))

DOLFIN_EPS = 0.000001

def Inflow(x):
return np.abs(x[0] - 0.) < DOLFIN_EPS

def Outflow(x):
return np.abs(x[0] - 5.) < DOLFIN_EPS

def Walls(x):
result = np.logical_or(np.abs(x[1] + 0.) < DOLFIN_EPS, np.abs(x[1] - 5.) < DOLFIN_EPS)
result2 = np.logical_or(np.abs(x[2] - 0.) < DOLFIN_EPS, np.abs(x[2] - 5.) < DOLFIN_EPS)
return np.logical_or(result, result2)

def Sphere(x):
result = np.logical_and(x[0] > 2.0 - DOLFIN_EPS, x[0] < 4.0 + DOLFIN_EPS)
result2 = np.logical_and(x[1] > 2.0 - DOLFIN_EPS, x[1] < 4.0 + DOLFIN_EPS)
result3 = np.logical_and(x[2] > 2.0 - DOLFIN_EPS, x[2] < 4.0 + DOLFIN_EPS)
return np.logical_and(np.logical_and(result, result2), result3)

def inflow_profile(x):
values = np.zeros(x.shape)
values[0,:]=1
return values

u_in = fem.Function(V)
u_in.interpolate(inflow_profile)

u_zero = fem.Function(V)
u_zero.x.array[:] = 0
p_zero = fem.Function(Q)
p_zero.x.array[:] = 0

inflow_boundary_dofs = fem.locate_dofs_geometrical(V, Inflow)
outflow_boundary_dofs = fem.locate_dofs_geometrical(Q, Outflow)
walls_boundary_dofs = fem.locate_dofs_geometrical(V, Walls)
sphere_boundary_dofs = fem.locate_dofs_geometrical(V, Sphere)

bcu_inflow = fem.DirichletBC(u_in, inflow_boundary_dofs)
bcp_outflow = fem.DirichletBC(p_zero, outflow_boundary_dofs)
bcu_walls = fem.DirichletBC(u_zero, walls_boundary_dofs)
bcu_sphere = fem.DirichletBC(u_zero, sphere_boundary_dofs)

bcu = [bcu_inflow, bcu_walls, bcu_sphere]
bcp = [bcp_outflow]

print(f"DOLFINx version: {dolfinx.__version__} is installed")

# Define trial and test functions
u = TrialFunction(V)
v = TestFunction(V)
p = TrialFunction(Q)
q = TestFunction(Q)

# Define functions for solutions at previous and current time steps
u_n = Function(V)
u_ = Function(V)
p_n = Function(Q)
p_ = Function(Q)

# Define expressions used in variational forms
U = 0.5 * (u_n + u)
n = FacetNormal(mesh)
f = Constant(mesh, PETSc.ScalarType((0,0,0)))
k = Constant(mesh, PETSc.ScalarType(dt))
mu = Constant(mesh, PETSc.ScalarType(.001))
rho = Constant(mesh, PETSc.ScalarType(1))

def epsilon(u):

# Define stress tensor
def sigma(u, p):
return 2 * mu * epsilon(u) - p * Identity(len(u))

# Define variational problem for step 1
F1 = rho * dot((u - u_n) / k, v) * dx \
+ rho * dot(dot(u_n, nabla_grad(u_n)), v) * dx \
+ inner(sigma(U, p_n), epsilon(v)) * dx \
+ dot(p_n * n, v) * ds - dot(mu * nabla_grad(U) * n, v) * ds \
- dot(f, v) * dx
a1 = lhs(F1)
L1 = rhs(F1)

# Define variational problem for step 2
L2 = dot(nabla_grad(p_n), nabla_grad(q)) * dx - (1 / k) * div(u_) * q * dx

# Define variational problem for step 3
a3 = dot(u, v) * dx
L3 = dot(u_, v) * dx - k * dot(nabla_grad(p_ - p_n), v) * dx

# Assemble matrices
A1 = assemble_matrix(a1, bcs=bcu)
A1.assemble()
b1 = create_vector(L1)
A2 = assemble_matrix(a2, bcs=bcp)
A2.assemble()
b2 = create_vector(L2)
A3 = assemble_matrix(a3)
A3.assemble()
b3 = create_vector(L3)

print(f"DOLFINx version: {dolfinx.__version__} is installed")

# Solver for step 1
solver1 = PETSc.KSP().create(mesh.comm)
solver1.setOperators(A1)
solver1.setType(PETSc.KSP.Type.BCGS)
pc1 = solver1.getPC()
pc1.setType(PETSc.PC.Type.JACOBI)

# Solver for step 2
solver2 = PETSc.KSP().create(mesh.comm)
solver2.setOperators(A2)
solver2.setType(PETSc.KSP.Type.MINRES)
pc2 = solver2.getPC()
pc2.setType(PETSc.PC.Type.HYPRE)
pc2.setHYPREType("boomeramg")

# Solver for step 3
solver3 = PETSc.KSP().create(mesh.comm)
solver3.setOperators(A3)
solver3.setType(PETSc.KSP.Type.CG)
pc3 = solver3.getPC()
pc3.setType(PETSc.PC.Type.SOR)
``````

And I am getting the following error:

``````Traceback (most recent call last):
File "fluid.py", line 156, in <module>
solver1 = PETSc.KSP().create(mesh.comm)
AttributeError: 'dolfinx.cpp.mesh.Mesh' object has no attribute 'comm'

``````

Could you supply the output of the following/

``````python3 -c "import dolfinx; import dolfinx.common; print(dolfinx.__version__, dolfinx.common.git_commit_hash)"
``````

Here it is:

``````\$ python3 -c "import dolfinx; import dolfinx.common; print(dolfinx.__version__, dolfinx.common.git_commit_hash)"
0.3.1.0 7f15bfc9a3b72a2ab6ab7a991aa2a439c7523438
``````

How did you install dolfinx? This commit hash is not in our git system as far as I can tell.

I have installed using Jake installation script: GitHub - jhale/fenicsx-iris-cluster: Scripts for building FEniCSX on the University of Luxembourg iris cluster

However, if I replace `mesh.comm` with `solver1 = PETSc.KSP().create(MPI.COMM_WORLD) ` it is working.

Maybe @jackhale can shed some light on what version of dolfinx that script builds. Seems like it is using a slightly outdated dolfinx API.

Sure, thanks! will check that. I am also having another question, I have not used .xdmf files before in Paraview or VisIt. Is there any way to write a time series solution in .vtk or .vtu files in FEniCS-X or in the .xdmf file? Or is it good to use the .xdmf file for visualization, if so why I am not able to see velocity and pressure contours?

We have support for VTK, VTX and Fides as well. See dolfinx/python/test/unit/io at main ยท FEniCS/dolfinx ยท GitHub
For how to use them.
XDMF is preferrable to VTK when executing in parallel as it uses `hdf5` to store the data, while vtk writes everything to file in plain-text.

I have explained how to extract blocks from XDMFFiles for visualization at: Using Paraview for visualization โ FEniCSx tutorial

1 Like

Thanks! I was able to see the solution using Paraview. However, I am getting errors, solutions, probably I am making the mistake when I do BC. Or does it have anything to do with choosing iterative solvers? Moreover, I am trying just laminar flow.

If the problem comes from defining the BC, perhaps, I can name the mesh (inlet, outlet, walls, and sphere), in that case, how can I define those naming in BC from exported mesh files.

Start by making a minimal example.
Finally, start by using direct solvers to eliminate the solver as a source of error.
With all these steps in place you should be able to make a more accurate minimal example pinpointing your issue

1 Like

Thanks!

• Start by making a minimal example.
The example I am trying is already a minimal one
• Then check your boundary conditions.
There was some error as of now, the outlet should be at 10.0 not at 5.0
• Finally, start by using direct solvers to eliminate the solver as a source of error.
I see that you have put up some examples for LU decomposion, will try that

The problem is when I try to define the BC for the obstacle/sphere in the fluid domain.

``````def Sphere(x):
result = np.logical_and(x[0] > 2.0 - DOLFIN_EPS, x[0] < 4.0 + DOLFIN_EPS)
result2 = np.logical_and(x[1] > 2.0 - DOLFIN_EPS, x[1] < 4.0 + DOLFIN_EPS)
result3 = np.logical_and(x[2] > 2.0 - DOLFIN_EPS, x[2] < 4.0 + DOLFIN_EPS)
return np.logical_and(np.logical_and(result, result2), result3)
``````

Could you please tell me if I am making some mistake in here? Or now I have tagged mesh (inlet, outlet, walls, and obstacle), would that work if I try one of your options ( 3. How to load msh files into dolfin-X),
then I can try BC as you defined in here: flow past obstacle