diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1fe23df --- /dev/null +++ b/.clang-format @@ -0,0 +1,94 @@ +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Allman +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + - Regex: '^(<|"(gtest|isl|json)/)' + Priority: 3 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '$' +IndentCaseLabels: false +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 8 +UseTab: Never diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0d08e26 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..65947d3 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..15bed8e --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,42 @@ +name: Release + +on: [push, pull_request] + +jobs: + dist: + runs-on: ubuntu-latest + container: ghcr.io/fenics/dolfinx/dolfinx:nightly + env: + DEB_PYTHON_INSTALL_LAYOUT: deb_system + + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + run: python3 -m pip install --upgrade build twine + + - name: Build SDist and wheel + run: python3 -m build --no-isolation --sdist + + - uses: actions/upload-artifact@v4 + with: + path: dist/* + + - name: Check metadata + run: python3 -m twine check dist/* + + publish: + needs: [dist] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags') + environment: pypi + permissions: + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + name: artifact + path: dist + + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c861bde..6040e3f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,4 +23,4 @@ jobs: run: python3 -m pytest -v - name: Run tests in parallel - run: mpiexec -n 4 python3 -m pytest -v \ No newline at end of file + run: mpiexec -n 4 python3 -m pytest -v diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d5c7359 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-docstring-first + - id: debug-statements + - id: check-toml + + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: 'v0.6.5' + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.2 + hooks: + - id: mypy + files: ^src/|^tests/ + args: ["--config-file", "pyproject.toml"] + + + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: 'v14.0.6' # Use the sha / tag you want to point at + hooks: + - id: clang-format diff --git a/CMakeLists.txt b/CMakeLists.txt index 48af5a1..ebd0bb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,4 +60,4 @@ target_link_libraries(_scifem PRIVATE dolfinx) set_target_properties(_scifem PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE) # Install directive for scikit-build-core -install(TARGETS _scifem LIBRARY DESTINATION ${PROJECT_NAME}) \ No newline at end of file +install(TARGETS _scifem LIBRARY DESTINATION ${PROJECT_NAME}) diff --git a/README.md b/README.md index b508060..53f4201 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This package contains a collection of tools for scientific computing with a focu Many users that are transitioning from legacy FEniCS to FEniCSx may find the transition difficult due to the lack of some functionalities in FEniCSx. This package aims to provide some of the functionalities that are missing in FEniCSx. -The package is still in its early stages and many functionalities are still missing. +The package is still in its early stages and many functionalities are still missing. ## Features - Real-space implementation for usage in DOLFINx (>=v0.8.0) @@ -28,6 +28,3 @@ python3 -m pip install --no-build-isolation git+https://github.com/scientificcom ## Having issues or want to contribute? If you are having issues, feature request or would like to contribute, please let us know. You can do so by opening an issue on the [issue tracker](https://github.com/scientificcomputing/scifem/issues). - - - diff --git a/pyproject.toml b/pyproject.toml index c1a2ac9..38dd355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ repository = "https://github.com/scientificcomputing/scifem.git" [project.optional-dependencies] docs = [] +dev = ["ruff", "mypy", "bump-my-version", "pre-commit"] test = ["pytest", "petsc4py"] @@ -88,3 +89,43 @@ max-complexity = 10 [tool.ruff.lint.pydocstyle] convention = "google" + + +[tool.ruff.lint.isort] +known-first-party = ["scifem"] +known-third-party = [ + "basix", + "dolfinx", + "ffcx", + "ufl", + "gmsh", + "numpy", + "pytest", +] +section-order = [ + "future", + "standard-library", + "mpi", + "third-party", + "first-party", + "local-folder", +] +[tool.ruff.lint.isort.sections] +"mpi" = ["mpi4py", "petsc4py"] + + +[tool.bumpversion] +allow_dirty = false +commit = true +message = "Bump version: {current_version} → {new_version}" +tag = true +sign_tags = false +tag_name = "v{new_version}" +tag_message = "Bump version: {current_version} → {new_version}" +current_version = "0.1.0" + + +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'version = "{current_version}"' +replace = 'version = "{new_version}"' diff --git a/src/scifem.cpp b/src/scifem.cpp index 5b83355..a9dd984 100644 --- a/src/scifem.cpp +++ b/src/scifem.cpp @@ -8,9 +8,9 @@ #include #include #include +#include #include #include -#include #include #include @@ -18,58 +18,71 @@ using namespace dolfinx; namespace scifem { - template - dolfinx::fem::FunctionSpace create_real_functionspace(std::shared_ptr> mesh, std::vector value_shape) - { +template +dolfinx::fem::FunctionSpace +create_real_functionspace(std::shared_ptr> mesh, + std::vector value_shape) +{ - basix::FiniteElement e_v = basix::create_element( - basix::element::family::P, mesh::cell_type_to_basix_type(mesh->topology()->cell_type()), 0, - basix::element::lagrange_variant::unset, basix::element::dpc_variant::unset, true); + basix::FiniteElement e_v = basix::create_element( + basix::element::family::P, + mesh::cell_type_to_basix_type(mesh->topology()->cell_type()), 0, + basix::element::lagrange_variant::unset, + basix::element::dpc_variant::unset, true); - // NOTE: Optimize input source/dest later as we know this a priori - std::int32_t num_dofs = (dolfinx::MPI::rank(MPI_COMM_WORLD) == 0) ? 1 : 0; - std::int32_t num_ghosts = (dolfinx::MPI::rank(MPI_COMM_WORLD) != 0) ? 1 : 0; - std::vector ghosts(num_ghosts, 0); - ghosts.reserve(1); - std::vector owners(num_ghosts, 0); - owners.reserve(1); - std::shared_ptr imap = - std::make_shared(MPI_COMM_WORLD, num_dofs, ghosts, owners); - std::size_t value_size = std::accumulate(value_shape.begin(), value_shape.end(), 1, - std::multiplies{}); - int index_map_bs = value_size; - int bs = value_size; - // Element dof layout - fem::ElementDofLayout dof_layout(value_size, e_v.entity_dofs(), e_v.entity_closure_dofs(), {}, {}); - std::size_t num_cells_on_process = mesh->topology()->index_map(mesh->topology()->dim())->size_local() + - mesh->topology()->index_map(mesh->topology()->dim())->num_ghosts(); + // NOTE: Optimize input source/dest later as we know this a priori + std::int32_t num_dofs = (dolfinx::MPI::rank(MPI_COMM_WORLD) == 0) ? 1 : 0; + std::int32_t num_ghosts = (dolfinx::MPI::rank(MPI_COMM_WORLD) != 0) ? 1 : 0; + std::vector ghosts(num_ghosts, 0); + ghosts.reserve(1); + std::vector owners(num_ghosts, 0); + owners.reserve(1); + std::shared_ptr imap + = std::make_shared( + MPI_COMM_WORLD, num_dofs, ghosts, owners); + std::size_t value_size = std::accumulate( + value_shape.begin(), value_shape.end(), 1, std::multiplies{}); + int index_map_bs = value_size; + int bs = value_size; + // Element dof layout + fem::ElementDofLayout dof_layout(value_size, e_v.entity_dofs(), + e_v.entity_closure_dofs(), {}, {}); + std::size_t num_cells_on_process + = mesh->topology()->index_map(mesh->topology()->dim())->size_local() + + mesh->topology()->index_map(mesh->topology()->dim())->num_ghosts(); - std::vector dofmap(num_cells_on_process, 0); - dofmap.reserve(1); - std::shared_ptr real_dofmap = - std::make_shared(dof_layout, imap, index_map_bs, dofmap, bs); + std::vector dofmap(num_cells_on_process, 0); + dofmap.reserve(1); + std::shared_ptr real_dofmap + = std::make_shared(dof_layout, imap, + index_map_bs, dofmap, bs); - std::shared_ptr> d_el = - std::make_shared>(e_v, value_size, false); + std::shared_ptr> d_el + = std::make_shared>(e_v, value_size, + false); - return dolfinx::fem::FunctionSpace(mesh, d_el, real_dofmap, value_shape); - } + return dolfinx::fem::FunctionSpace(mesh, d_el, real_dofmap, value_shape); } +} // namespace scifem namespace scifem_wrapper { - template - void declare_real_function_space(nanobind::module_ &m, std::string type) - { - std::string pyfunc_name = "create_real_functionspace_" + type; - m.def(pyfunc_name.c_str(), [](std::shared_ptr> mesh, std::vector value_shape) - { return scifem::create_real_functionspace(mesh, value_shape); }, "Create a real function space"); - } +template +void declare_real_function_space(nanobind::module_& m, std::string type) +{ + std::string pyfunc_name = "create_real_functionspace_" + type; + m.def( + pyfunc_name.c_str(), + [](std::shared_ptr> mesh, + std::vector value_shape) + { return scifem::create_real_functionspace(mesh, value_shape); }, + "Create a real function space"); +} } // namespace scifem_wrapper NB_MODULE(_scifem, m) { - scifem_wrapper::declare_real_function_space(m, "float64"); - scifem_wrapper::declare_real_function_space(m, "float32"); -} \ No newline at end of file + scifem_wrapper::declare_real_function_space(m, "float64"); + scifem_wrapper::declare_real_function_space(m, "float32"); +} diff --git a/src/scifem/__init__.py b/src/scifem/__init__.py index 03851ef..fceab6f 100644 --- a/src/scifem/__init__.py +++ b/src/scifem/__init__.py @@ -1,10 +1,12 @@ import dolfinx import basix import numpy as np -from . import _scifem +from . import _scifem # type: ignore -def create_real_functionspace(mesh: dolfinx.mesh.Mesh, value_shape: tuple[int, ...]=()) -> dolfinx.fem.FunctionSpace: +def create_real_functionspace( + mesh: dolfinx.mesh.Mesh, value_shape: tuple[int, ...] = () +) -> dolfinx.fem.FunctionSpace: """Create a real function space. Args: @@ -19,11 +21,11 @@ def create_real_functionspace(mesh: dolfinx.mesh.Mesh, value_shape: tuple[int, . """ dtype = mesh.geometry.x.dtype - ufl_e = basix.ufl.element("P", mesh.basix_cell(), 0, dtype=dtype, discontinuous=True, - shape=value_shape) - - if (dtype:=mesh.geometry.x.dtype) == np.float64: - + ufl_e = basix.ufl.element( + "P", mesh.basix_cell(), 0, dtype=dtype, discontinuous=True, shape=value_shape + ) + + if (dtype := mesh.geometry.x.dtype) == np.float64: cppV = _scifem.create_real_functionspace_float64(mesh._cpp_object, value_shape) elif dtype == np.float32: cppV = _scifem.create_real_functionspace_float32(mesh._cpp_object, value_shape) diff --git a/tests/test_real_functionspace.py b/tests/test_real_functionspace.py index 59aa856..d2a9d96 100644 --- a/tests/test_real_functionspace.py +++ b/tests/test_real_functionspace.py @@ -10,14 +10,18 @@ @pytest.mark.parametrize("L", [0.1, 0.2, 0.3]) @pytest.mark.parametrize("H", [1.3, 0.8, 0.2]) -@pytest.mark.parametrize("cell_type", [dolfinx.mesh.CellType.triangle, dolfinx.mesh.CellType.quadrilateral]) +@pytest.mark.parametrize( + "cell_type", [dolfinx.mesh.CellType.triangle, dolfinx.mesh.CellType.quadrilateral] +) @pytest.mark.parametrize("dtype", [np.float64, np.float32]) def test_real_function_space_mass(L, H, cell_type, dtype): """ Check that real space mass matrix is the same as assembling the volume of the mesh """ - mesh = dolfinx.mesh.create_rectangle(MPI.COMM_WORLD, [[0.,0.],[L,H]], [7,9],cell_type, dtype=dtype) + mesh = dolfinx.mesh.create_rectangle( + MPI.COMM_WORLD, [[0.0, 0.0], [L, H]], [7, 9], cell_type, dtype=dtype + ) V = scifem.create_real_functionspace(mesh) u = ufl.TrialFunction(V) @@ -30,17 +34,19 @@ def test_real_function_space_mass(L, H, cell_type, dtype): assert len(A.data) == 1 if MPI.COMM_WORLD.rank == 0: - assert np.isclose(A.data[0], L*H, atol=tol) + assert np.isclose(A.data[0], L * H, atol=tol) + @pytest.mark.parametrize("dtype", [np.float64, np.float32]) -@pytest.mark.parametrize("cell_type", [dolfinx.mesh.CellType.tetrahedron, dolfinx.mesh.CellType.hexahedron]) +@pytest.mark.parametrize( + "cell_type", [dolfinx.mesh.CellType.tetrahedron, dolfinx.mesh.CellType.hexahedron] +) def test_real_function_space_vector(cell_type, dtype): """ Test that assembling against a real space test function is equivalent to assembling a vector """ - - mesh = dolfinx.mesh.create_unit_cube(MPI.COMM_WORLD, 2,3,5, cell_type, dtype=dtype) + mesh = dolfinx.mesh.create_unit_cube(MPI.COMM_WORLD, 2, 3, 5, cell_type, dtype=dtype) V = dolfinx.fem.functionspace(mesh, ("Lagrange", 3)) v = ufl.TrialFunction(V) @@ -70,14 +76,17 @@ def test_real_function_space_vector(cell_type, dtype): np.testing.assert_allclose(A_R.indices, np.arange(num_dofs_global)) np.testing.assert_allclose(b.array[:num_dofs], A_R.data[:num_dofs], atol=tol) else: - assert num_local_rows == 0 + assert num_local_rows == 0 + @pytest.mark.parametrize("dtype", [PETSc.RealType]) @pytest.mark.parametrize("tensor", [0, 1, 2]) @pytest.mark.parametrize("degree", range(1, 5)) def test_singular_poisson(tensor, degree, dtype): M = 25 - mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, M, M, dolfinx.mesh.CellType.triangle, dtype=dtype) + mesh = dolfinx.mesh.create_unit_square( + MPI.COMM_WORLD, M, M, dolfinx.mesh.CellType.triangle, dtype=dtype + ) if tensor == 0: value_shape = () @@ -86,7 +95,7 @@ def test_singular_poisson(tensor, degree, dtype): else: value_shape = (3, 2) - V = dolfinx.fem.functionspace(mesh, ("Lagrange", degree, value_shape)) + V = dolfinx.fem.functionspace(mesh, ("Lagrange", degree, value_shape)) R = scifem.create_real_functionspace(mesh, value_shape) u = ufl.TrialFunction(V) @@ -94,23 +103,27 @@ def test_singular_poisson(tensor, degree, dtype): c = ufl.TrialFunction(R) d = ufl.TestFunction(R) x = ufl.SpatialCoordinate(mesh) - pol = x[0]**degree - 2*x[1]**degree + pol = x[0] ** degree - 2 * x[1] ** degree # Compute average value of polynomial to make mean 0 - C = mesh.comm.allreduce(dolfinx.fem.assemble_scalar(dolfinx.fem.form(pol*ufl.dx, dtype=dtype)), op=MPI.SUM) + C = mesh.comm.allreduce( + dolfinx.fem.assemble_scalar(dolfinx.fem.form(pol * ufl.dx, dtype=dtype)), op=MPI.SUM + ) u_scalar = pol - dolfinx.fem.Constant(mesh, dtype(C)) if tensor == 0: u_ex = u_scalar zero = dolfinx.fem.Constant(mesh, dtype(0.0)) elif tensor == 1: u_ex = ufl.as_vector([u_scalar, -u_scalar]) - zero = dolfinx.fem.Constant(mesh, dtype((0.0,0.0))) + zero = dolfinx.fem.Constant(mesh, dtype((0.0, 0.0))) else: - u_ex = ufl.as_tensor([[u_scalar, 2*u_scalar], [3*u_scalar, -u_scalar], - [u_scalar, 2*u_scalar], - ]) - zero = dolfinx.fem.Constant(mesh, dtype(((0.0,0.0), - (0.0,0.0), - (0.0,0.0)))) + u_ex = ufl.as_tensor( + [ + [u_scalar, 2 * u_scalar], + [3 * u_scalar, -u_scalar], + [u_scalar, 2 * u_scalar], + ] + ) + zero = dolfinx.fem.Constant(mesh, dtype(((0.0, 0.0), (0.0, 0.0), (0.0, 0.0)))) dx = ufl.Measure("dx", domain=mesh) f = -ufl.div(ufl.grad(u_ex)) @@ -120,13 +133,12 @@ def test_singular_poisson(tensor, degree, dtype): a01 = ufl.inner(c, v) * dx a10 = ufl.inner(u, d) * dx - L0 = ufl.inner(f , v) * dx + ufl.inner(g, v) * ufl.ds - L1 = ufl.inner(zero, d) * dx - + L0 = ufl.inner(f, v) * dx + ufl.inner(g, v) * ufl.ds + L1 = ufl.inner(zero, d) * dx + a = dolfinx.fem.form([[a00, a01], [a10, None]], dtype=dtype) L = dolfinx.fem.form([L0, L1], dtype=dtype) - A = dolfinx.fem.petsc.assemble_matrix_block(a) A.assemble() b = dolfinx.fem.petsc.create_vector_block(L) @@ -134,7 +146,6 @@ def test_singular_poisson(tensor, degree, dtype): loc.set(0) dolfinx.fem.petsc.assemble_vector_block(b, L, a, bcs=[]) - ksp = PETSc.KSP().create(mesh.comm) ksp.setOperators(A) ksp.setType("preonly") @@ -146,9 +157,11 @@ def test_singular_poisson(tensor, degree, dtype): ksp.solve(b, x) x.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD) uh = dolfinx.fem.Function(V) - x_local = dolfinx.cpp.la.petsc.get_local_vectors(x, [(V.dofmap.index_map, V.dofmap.index_map_bs), - (R.dofmap.index_map, R.dofmap.index_map_bs)]) - uh.x.array[:len(x_local[0])] = x_local[0] + x_local = dolfinx.cpp.la.petsc.get_local_vectors( + x, + [(V.dofmap.index_map, V.dofmap.index_map_bs), (R.dofmap.index_map, R.dofmap.index_map_bs)], + ) + uh.x.array[: len(x_local[0])] = x_local[0] uh.x.scatter_forward() error = dolfinx.fem.form(ufl.inner(u_ex - uh, u_ex - uh) * dx, dtype=dtype) @@ -184,6 +197,5 @@ def test_complex_real_space(ftype, stype): b_const.scatter_reverse(dolfinx.la.InsertMode.add) b_const.scatter_forward() - tol = 100 * np.finfo(stype).eps - np.testing.assert_allclose(b.array, b_const.array, atol=tol) \ No newline at end of file + np.testing.assert_allclose(b.array, b_const.array, atol=tol)