diff --git a/.cspell_dict.txt b/.cspell_dict.txt index 639a8b2..7c269d4 100644 --- a/.cspell_dict.txt +++ b/.cspell_dict.txt @@ -3,6 +3,8 @@ allreduce argsort astype atol +basix +cellname cofac dirichletbc dofs @@ -11,6 +13,7 @@ fenics fenicsx fenicsx_pulse finsberg +functionspace gradu Holzapfel holzapfelogden @@ -36,3 +39,8 @@ subplus varepsilon Venant XDMF +PYVISTA +TRAME +libgl +libxrender +xvfb diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 5983e43..31da26b 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -2,10 +2,10 @@ name: Deploy static content to Pages on: - # Runs on pushes targeting the default branch + pull_request: push: - branches: - - "**" + branches: [main] + # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -29,10 +29,17 @@ jobs: env: DEB_PYTHON_INSTALL_LAYOUT: deb_system PUBLISH_DIR: ./_build/html + PYVISTA_TRAME_SERVER_PROXY_PREFIX: "/proxy/" + PYVISTA_TRAME_SERVER_PROXY_ENABLED: "True" + PYVISTA_OFF_SCREEN: false + PYVISTA_JUPYTER_BACKEND: "html" steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Install dependencies for pyvista + run: apt-get update && apt-get install -y libgl1-mesa-glx libxrender1 xvfb - name: Install dependencies run: python3 -m pip install ".[docs]" @@ -41,7 +48,7 @@ jobs: run: jupyter book build -W . - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: ${{ env.PUBLISH_DIR }} @@ -57,12 +64,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Pages - uses: actions/configure-pages@v2 + uses: actions/configure-pages@v4 - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9ff6765..bcb5775 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,6 +1,10 @@ name: Pre-commit -on: [push] +on: + pull_request: + push: + branches: [main] + jobs: @@ -8,7 +12,7 @@ jobs: runs-on: ubuntu-22.04 steps: # This action sets the current path to the root of your github repo - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pre-commit run: python3 -m pip install pre-commit diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 0e93190..a6a0483 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,17 +1,21 @@ name: Release -on: [push] +on: + pull_request: + push: + branches: [main] + jobs: dist: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build SDist and wheel run: pipx run build - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: path: dist/* @@ -27,7 +31,7 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: artifact path: dist diff --git a/.github/workflows/test_package_coverage.yml b/.github/workflows/test_package_coverage.yml index cba9788..94023ff 100644 --- a/.github/workflows/test_package_coverage.yml +++ b/.github/workflows/test_package_coverage.yml @@ -12,7 +12,7 @@ jobs: steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install package run: python3 -m pip install .[test] @@ -28,14 +28,14 @@ jobs: echo "total=$TOTAL" >> $GITHUB_ENV - name: Upload HTML report. - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: html-report path: htmlcov - name: Create coverage Badge if: github.ref == 'refs/heads/main' - uses: schneegans/dynamic-badges-action@v1.6.0 + uses: schneegans/dynamic-badges-action@v1.7.0 with: auth: ${{ secrets.GIST_SECRET }} gistID: a7290de789564f03eb6b1ee122fce423 diff --git a/_config.yml b/_config.yml index 447e275..6bb09f8 100644 --- a/_config.yml +++ b/_config.yml @@ -30,6 +30,12 @@ parse: sphinx: config: + html_last_updated_fmt: "%b %d, %Y" + nb_custom_formats: # https://jupyterbook.org/en/stable/file-types/jupytext.html#file-types-custom + .py: + - jupytext.reads + - fmt: py + suppress_warnings: ["mystnb.unknown_mime_type"] html_js_files: - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js @@ -38,4 +44,4 @@ sphinx: - 'sphinx.ext.napoleon' - 'sphinx.ext.viewcode' -exclude_patterns: [".pytest_cache/*", ".github/*"] +exclude_patterns: [".pytest_cache/*", ".github/*", ".tox/*"] diff --git a/demo/unit_cube.ipynb b/demo/unit_cube.ipynb deleted file mode 100644 index d0044d4..0000000 --- a/demo/unit_cube.ipynb +++ /dev/null @@ -1,363 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "f0905235", - "metadata": {}, - "source": [ - "# Unit Cube\n", - "\n", - "In this demo we will use `fenicsx_pulse` to solve a simple contracting cube with one fixed side and with the opposite side having a traction force.\n", - "\n", - "First let us do the necessary imports" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "669b7d12", - "metadata": { - "lines_to_next_cell": 2 - }, - "outputs": [], - "source": [ - "import dolfinx\n", - "import numpy as np\n", - "import fenicsx_pulse\n", - "from mpi4py import MPI\n", - "from petsc4py import PETSc" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "b2923a14", - "metadata": {}, - "source": [ - "Then we can create unit cube mesh" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "84e77495", - "metadata": {}, - "outputs": [], - "source": [ - "mesh = dolfinx.mesh.create_unit_cube(MPI.COMM_WORLD, 3, 3, 3)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "85ec6a83", - "metadata": {}, - "source": [ - "Next let up specify a list of boundary markers where we will set the different boundary conditions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "22806878", - "metadata": {}, - "outputs": [], - "source": [ - "boundaries = [\n", - " fenicsx_pulse.Marker(marker=1, dim=2, locator=lambda x: np.isclose(x[0], 0)),\n", - " fenicsx_pulse.Marker(marker=2, dim=2, locator=lambda x: np.isclose(x[0], 1)),\n", - "]" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "673b51e3", - "metadata": {}, - "source": [ - "Now collect the boundaries and mesh in to a geometry object\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7a9683b9", - "metadata": {}, - "outputs": [], - "source": [ - "geo = fenicsx_pulse.Geometry(\n", - " mesh=mesh,\n", - " boundaries=boundaries,\n", - " metadata={\"quadrature_degree\": 4},\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "a8ff97d9", - "metadata": {}, - "source": [ - "We would also need to to create a passive material model. Here we will used the Holzapfel and Ogden material model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ce4147cd", - "metadata": {}, - "outputs": [], - "source": [ - "material_params = fenicsx_pulse.HolzapfelOgden.transversely_isotropic_parameters()\n", - "f0 = dolfinx.fem.Constant(mesh, PETSc.ScalarType((1.0, 0.0, 0.0)))\n", - "s0 = dolfinx.fem.Constant(mesh, PETSc.ScalarType((0.0, 1.0, 0.0)))\n", - "material = fenicsx_pulse.HolzapfelOgden(f0=f0, s0=s0, **material_params)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f144a998", - "metadata": {}, - "source": [ - "We also need to create a model for the active contraction. Here we use an active stress model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "263cd5c5", - "metadata": {}, - "outputs": [], - "source": [ - "Ta = dolfinx.fem.Constant(mesh, PETSc.ScalarType(0.0))\n", - "active_model = fenicsx_pulse.ActiveStress(f0, activation=Ta)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "a3a062f9", - "metadata": {}, - "source": [ - "We also need to specify whether the model what type of compressibility we want for our model. Here we use a full incompressible model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9b61f308", - "metadata": {}, - "outputs": [], - "source": [ - "comp_model = fenicsx_pulse.Incompressible()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "35761872", - "metadata": {}, - "outputs": [], - "source": [ - "# Finally we collect all the models into a cardiac model\n", - "model = fenicsx_pulse.CardiacModel(\n", - " material=material,\n", - " active=active_model,\n", - " compressibility=comp_model,\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "4d9abc61", - "metadata": {}, - "source": [ - "Now we need to specify the different boundary conditions. \n", - "\n", - "We can specify the dirichlet boundary conditions using a function that takes the state space as input and return a list of dirichlet boundary conditions. Since we are using the an incompressible formulation the state space have two subspaces where the first subspace represents the displacement. Here we set the displacement to zero on the boundary with marker 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ebf4e957", - "metadata": {}, - "outputs": [], - "source": [ - "def dirichlet_bc(\n", - " state_space: dolfinx.fem.FunctionSpace,\n", - ") -> list[dolfinx.fem.bcs.DirichletBC]:\n", - " V, _ = state_space.sub(0).collapse()\n", - " facets = geo.facet_tags.find(1) # Specify the marker used on the boundary\n", - " dofs = dolfinx.fem.locate_dofs_topological((state_space.sub(0), V), 2, facets)\n", - " u_fixed = dolfinx.fem.Function(V)\n", - " u_fixed.x.array[:] = 0.0\n", - " return [dolfinx.fem.dirichletbc(u_fixed, dofs, state_space.sub(0))]" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "548fb825", - "metadata": {}, - "source": [ - "We als set a traction on the opposite boundary" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a542c020", - "metadata": {}, - "outputs": [], - "source": [ - "traction = dolfinx.fem.Constant(mesh, PETSc.ScalarType(-1.0))\n", - "neumann = fenicsx_pulse.NeumannBC(traction=traction, marker=2)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "df2bb4c9", - "metadata": {}, - "source": [ - "Finally we collect all the boundary conditions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b04f745a", - "metadata": {}, - "outputs": [], - "source": [ - "bcs = fenicsx_pulse.BoundaryConditions(dirichlet=(dirichlet_bc,), neumann=(neumann,))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "75866130", - "metadata": {}, - "source": [ - "and create a mechanics problem" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5ea7493c", - "metadata": {}, - "outputs": [], - "source": [ - "problem = fenicsx_pulse.MechanicsProblem(model=model, geometry=geo, bcs=bcs)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "eae42ef5", - "metadata": {}, - "source": [ - "We also set a value for the active stress" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ecbe39db", - "metadata": {}, - "outputs": [], - "source": [ - "Ta.value = 2.0" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "918218d7", - "metadata": {}, - "source": [ - "And solve the problem" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "873c4b2b", - "metadata": {}, - "outputs": [], - "source": [ - "problem.solve()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "8ac1918e", - "metadata": {}, - "source": [ - "We can get the solution (displacement)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1559fdd9", - "metadata": {}, - "outputs": [], - "source": [ - "u = problem.state.sub(0).collapse()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "22a49f57", - "metadata": {}, - "source": [ - "and save it to XDMF for visualization in Paraview" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ce9e17bf", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "07f36d36", - "metadata": {}, - "outputs": [], - "source": [ - "from fenicsx_plotly import plot\n", - "plot(u, component=\"magnitude\")" - ] - } - ], - "metadata": { - "jupytext": { - "cell_metadata_filter": "-all", - "main_language": "python", - "notebook_metadata_filter": "-all" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.9.15" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/demo/unit_cube.py b/demo/unit_cube.py new file mode 100644 index 0000000..a53f4bd --- /dev/null +++ b/demo/unit_cube.py @@ -0,0 +1,130 @@ +# # Unit Cube +# +# In this demo we will use `fenicsx_pulse` to solve a simple contracting cube with one fixed +# side and with the opposite side having a traction force. +# +# First let us do the necessary imports + +import dolfinx +import numpy as np +import fenicsx_pulse +from mpi4py import MPI +from petsc4py import PETSc + + +# Then we can create unit cube mesh + +mesh = dolfinx.mesh.create_unit_cube(MPI.COMM_WORLD, 3, 3, 3) + +# Next let up specify a list of boundary markers where we will set the different boundary conditions + +boundaries = [ + fenicsx_pulse.Marker(marker=1, dim=2, locator=lambda x: np.isclose(x[0], 0)), + fenicsx_pulse.Marker(marker=2, dim=2, locator=lambda x: np.isclose(x[0], 1)), +] + +# Now collect the boundaries and mesh in to a geometry object +# + +geo = fenicsx_pulse.Geometry( + mesh=mesh, + boundaries=boundaries, + metadata={"quadrature_degree": 4}, +) + +# We would also need to to create a passive material model. +# Here we will used the Holzapfel and Ogden material model + +material_params = fenicsx_pulse.HolzapfelOgden.transversely_isotropic_parameters() +f0 = dolfinx.fem.Constant(mesh, PETSc.ScalarType((1.0, 0.0, 0.0))) +s0 = dolfinx.fem.Constant(mesh, PETSc.ScalarType((0.0, 1.0, 0.0))) +material = fenicsx_pulse.HolzapfelOgden(f0=f0, s0=s0, **material_params) # type: ignore + +# We also need to create a model for the active contraction. Here we use an active stress model + +Ta = dolfinx.fem.Constant(mesh, PETSc.ScalarType(0.0)) +active_model = fenicsx_pulse.ActiveStress(f0, activation=Ta) + +# We also need to specify whether the model what type of compressibility we want for our model. +# Here we use a full incompressible model + +comp_model = fenicsx_pulse.Incompressible() + +# Finally we collect all the models into a cardiac model +model = fenicsx_pulse.CardiacModel( + material=material, + active=active_model, + compressibility=comp_model, +) + + +# Now we need to specify the different boundary conditions. +# +# We can specify the dirichlet boundary conditions using a function that takes the state +# space as input and return a list of dirichlet boundary conditions. Since we are using +# the an incompressible formulation the state space have two subspaces where the first +# subspace represents the displacement. Here we set the displacement to zero on the +# boundary with marker 1 + + +def dirichlet_bc( + state_space: dolfinx.fem.FunctionSpace, +) -> list[dolfinx.fem.bcs.DirichletBC]: + V, _ = state_space.sub(0).collapse() + facets = geo.facet_tags.find(1) # Specify the marker used on the boundary + mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim) + dofs = dolfinx.fem.locate_dofs_topological((state_space.sub(0), V), 2, facets) + u_fixed = dolfinx.fem.Function(V) + u_fixed.x.array[:] = 0.0 + return [dolfinx.fem.dirichletbc(u_fixed, dofs, state_space.sub(0))] + + +# We als set a traction on the opposite boundary + +traction = dolfinx.fem.Constant(mesh, PETSc.ScalarType(-1.0)) +neumann = fenicsx_pulse.NeumannBC(traction=traction, marker=2) + +# Finally we collect all the boundary conditions + +bcs = fenicsx_pulse.BoundaryConditions(dirichlet=(dirichlet_bc,), neumann=(neumann,)) + +# and create a mechanics problem + +problem = fenicsx_pulse.MechanicsProblem(model=model, geometry=geo, bcs=bcs) + +# We also set a value for the active stress + +Ta.value = 2.0 + +# And solve the problem + +problem.solve() + +# We can get the solution (displacement) + +u = problem.state.sub(0).collapse() + +# and visualize it using pyvista + +import pyvista + +pyvista.start_xvfb() + +# Create plotter and pyvista grid +p = pyvista.Plotter() + +topology, cell_types, geometry = dolfinx.plot.vtk_mesh( + problem.state_space.sub(0).collapse()[0], +) +grid = pyvista.UnstructuredGrid(topology, cell_types, geometry) + +# Attach vector values to grid and warp grid by vectora +grid["u"] = u.x.array.reshape((geometry.shape[0], 3)) +actor_0 = p.add_mesh(grid, style="wireframe", color="k") +warped = grid.warp_by_vector("u", factor=1.5) +actor_1 = p.add_mesh(warped, show_edges=True) +p.show_axes() +if not pyvista.OFF_SCREEN: + p.show() +else: + figure_as_array = p.screenshot("displacement.png") diff --git a/pyproject.toml b/pyproject.toml index f0739e7..8a7acbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,8 +25,7 @@ dev = [ docs = [ "jupyter-book", "jupytext", - "ipython!=8.7.0", - "fenicsx-plotly" + "pyvista" ] all = [ diff --git a/src/fenicsx_pulse/mechanicsproblem.py b/src/fenicsx_pulse/mechanicsproblem.py index 90364aa..f2fe035 100644 --- a/src/fenicsx_pulse/mechanicsproblem.py +++ b/src/fenicsx_pulse/mechanicsproblem.py @@ -5,6 +5,7 @@ import dolfinx.fem.petsc import dolfinx.nls.petsc import ufl +import basix from . import kinematics from .boundary_conditions import BoundaryConditions @@ -13,7 +14,7 @@ @dataclass(slots=True) -class MechanicsProblem: +class BaseMechanicsProblem: model: CardiacModel geometry: Geometry bcs: BoundaryConditions = field(default_factory=BoundaryConditions) @@ -22,49 +23,33 @@ class MechanicsProblem: state_space: dolfinx.fem.FunctionSpace = field(init=False, repr=False) state: dolfinx.fem.Function = field(init=False, repr=False) test_state: dolfinx.fem.Function = field(init=False, repr=False) - _virtual_work: ufl.form.Form = field(init=False, repr=False) - _dirichlet_bc: typing.Sequence[dolfinx.fem.bcs.DirichletBC] = field( + virtual_work: ufl.form.Form = field(init=False, repr=False) + _dirichlet_bc: typing.Sequence[dolfinx.fem.bcs.DirichletBC] | None = field( + default=None, init=False, repr=False, ) + @property + def dirichlet_bc(self) -> typing.Sequence[dolfinx.fem.bcs.DirichletBC]: + return self._dirichlet_bc or [] + + @dirichlet_bc.setter + def dirichlet_bc(self, value: typing.Sequence[dolfinx.fem.bcs.DirichletBC]) -> None: + self._dirichlet_bc = value + self._set_dirichlet_bc() + self._init_solver() + def __post_init__(self): self._init_space() self._init_form() self._init_solver() - def _init_space(self) -> None: - P2 = ufl.VectorElement("Lagrange", self.geometry.mesh.ufl_cell(), 2) - P1 = ufl.FiniteElement("Lagrange", self.geometry.mesh.ufl_cell(), 1) - - self.state_space = dolfinx.fem.FunctionSpace(self.geometry.mesh, P2 * P1) - self.state = dolfinx.fem.Function(self.state_space) - self.test_state = ufl.TestFunction(self.state_space) - - def _init_form(self) -> None: - u, p = ufl.split(self.state) - v, _ = ufl.split(self.test_state) - - self.model.compressibility.register(p) - - F = kinematics.DeformationGradient(u) - psi = self.model.strain_energy(F, p) - self._virtual_work = ufl.derivative( - psi * self.geometry.dx, - coefficient=self.state, - argument=self.test_state, - ) - external_work = self._external_work(u, v) - if external_work is not None: - self._virtual_work += external_work - - self._set_dirichlet_bc() - def _init_solver(self) -> None: self._problem = dolfinx.fem.petsc.NonlinearProblem( - self._virtual_work, + self.virtual_work, self.state, - self._dirichlet_bc, + self.dirichlet_bc, ) self._solver = dolfinx.nls.petsc.NewtonSolver( self.geometry.mesh.comm, @@ -114,3 +99,43 @@ def _set_dirichlet_bc(self) -> None: def solve(self): return self._solver.solve(self.state) + + +@dataclass(slots=True) +class MechanicsProblem(BaseMechanicsProblem): + def _init_space(self) -> None: + P2 = basix.ufl.element( + family="Lagrange", + cell=self.geometry.mesh.ufl_cell().cellname(), + degree=2, + shape=(self.geometry.mesh.ufl_cell().topological_dimension(),), + ) + P1 = basix.ufl.element( + family="Lagrange", + cell=self.geometry.mesh.ufl_cell().cellname(), + degree=1, + ) + element = basix.ufl.mixed_element([P2, P1]) + + self.state_space = dolfinx.fem.functionspace(self.geometry.mesh, element) + self.state = dolfinx.fem.Function(self.state_space) + self.test_state = ufl.TestFunction(self.state_space) + + def _init_form(self) -> None: + u, p = ufl.split(self.state) + v, _ = ufl.split(self.test_state) + + self.model.compressibility.register(p) + + F = kinematics.DeformationGradient(u) + psi = self.model.strain_energy(F, p) + self.virtual_work = ufl.derivative( + psi * self.geometry.dx, + coefficient=self.state, + argument=self.test_state, + ) + external_work = self._external_work(u, v) + if external_work is not None: + self.virtual_work += external_work + + self._set_dirichlet_bc() diff --git a/tests/conftest.py b/tests/conftest.py index 915a810..6af89b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,12 +10,12 @@ def mesh(): @pytest.fixture(scope="session") def P1(mesh): - return dolfinx.fem.FunctionSpace(mesh, ("Lagrange", 1)) + return dolfinx.fem.functionspace(mesh, ("Lagrange", 1)) @pytest.fixture(scope="session") def P2(mesh): - return dolfinx.fem.VectorFunctionSpace(mesh, ("Lagrange", 2)) + return dolfinx.fem.functionspace(mesh, ("Lagrange", 2, (mesh.geometry.dim,))) @pytest.fixture diff --git a/tests/test_mechanicsproblem.py b/tests/test_mechanicsproblem.py index a668b17..938d0ea 100644 --- a/tests/test_mechanicsproblem.py +++ b/tests/test_mechanicsproblem.py @@ -37,6 +37,7 @@ def dirichlet_bc( ) -> list[dolfinx.fem.bcs.DirichletBC]: V, _ = state_space.sub(0).collapse() facets = geo.facet_tags.find(1) + mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim) dofs = dolfinx.fem.locate_dofs_topological((state_space.sub(0), V), 2, facets) u_fixed = dolfinx.fem.Function(V) u_fixed.x.array[:] = 0.0