diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..0d08e261 --- /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/build_docs.yml b/.github/workflows/build_docs.yml new file mode 100644 index 00000000..19920215 --- /dev/null +++ b/.github/workflows/build_docs.yml @@ -0,0 +1,36 @@ +name: Build documentation + +on: + pull_request: + branches: [main] + workflow_call: + workflow_dispatch: + +env: + DEB_PYTHON_INSTALL_LAYOUT: deb_system + DISPLAY: ":99.0" + +jobs: + + build: + runs-on: ubuntu-22.04 + container: ghcr.io/scientificcomputing/fenics-gmsh:2023-02-20 + env: + PUBLISH_DIR: ./docs/_build/html + + steps: + - name: Checkout + uses: actions/checkout@v4 + + + - name: Install dependencies + run: | + python3 -m pip install ".[docs, gui]" + + - name: Build docs + run: make docs + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ${{ env.PUBLISH_DIR }} diff --git a/.github/workflows/convergence-test.yml b/.github/workflows/convergence-test.yml new file mode 100644 index 00000000..318757d4 --- /dev/null +++ b/.github/workflows/convergence-test.yml @@ -0,0 +1,71 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Run convergence test + +on: + workflow_dispatch: + push: + branches: + - "!*" + tags: + - "v*" + + +jobs: + job_1: + name: run convergence tests + runs-on: ubuntu-latest + strategy: + matrix: + dx: [0.1, 0.2, 0.4] + dt: [0.05] + include: + - dt: 0.025 + dx: 0.2 + - dt: 0.1 + dx: 0.2 + + + container: + image: ghcr.io/scientificcomputing/fenics-gmsh:2023-02-20 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + python3 -m pip install -e "." + + - name: Run benchmark with dx = ${{ matrix.dx }} and dt = ${{ matrix.dt }} + run: python3 -m simcardems.benchmark run convergence_test --dt=${{ matrix.dt }} --dx=${{ matrix.dx }} --sha=${{ github.sha }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: "results-${{ matrix.dx }}-${{ matrix.dt }}" + path: convergence_test/results_dx*.json + + job_2: + name: Download repost and upload to gist + needs: job_1 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: convergence_test + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install dependencies + run: python3 -m pip install requests + + - name: Print files + run: python3 scripts/upload-data.py convergence_test --token=${{ secrets.CONV_TOKEN }} diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 00000000..03d37b23 --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,41 @@ +name: Publish documentation + +on: + push: + branches: [main] + workflow_dispatch: + + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + + build: + uses: ./.github/workflows/build_docs.yml + + deploy: + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 2ec3fa52..a4be84fd 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -25,16 +25,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Log in to the Container registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -42,12 +42,12 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: context: . push: true diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml deleted file mode 100644 index 23a2c5ad..00000000 --- a/.github/workflows/github-pages.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: github pages - -on: [push] - -jobs: - deploy: - runs-on: ubuntu-latest - container: - image: ghcr.io/scientificcomputing/fenics-gmsh:2023-02-20 - steps: - - uses: actions/checkout@v2 - - - name: Upgrade pip - run: | - # install pip=>20.1 to use "pip cache dir" - python3 -m pip install --upgrade pip - - - name: Install dependencies - run: | - python3 -m pip install -e ".[docs,gui]" - - - name: Build docs - run: | - make docs - - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - if: github.ref == 'refs/heads/main' - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_build/html diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c9139f17..fca7b663 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,11 +10,11 @@ jobs: image: ghcr.io/scientificcomputing/fenics-gmsh:2023-02-20 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Cache id: cache-primes - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: | ~/.cache/instant @@ -34,12 +34,18 @@ jobs: run: | python3 -m pytest - - name: Coverage report - uses: codecov/codecov-action@v3 - if: github.ref == 'refs/heads/main' + - name: Extract Coverage + run: | + python3 -m coverage report | sed 's/^/ /' >> $GITHUB_STEP_SUMMARY + python3 -m coverage json + export TOTAL=$(python3 -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") + echo "total=$TOTAL" >> $GITHUB_ENV + + - name: Upload HTML report. + uses: actions/upload-artifact@v4 with: - fail_ci_if_error: true - files: ./coverage.xml + name: html-report + path: htmlcov - name: Install pypa/build run: >- diff --git a/.github/workflows/mpi.yml b/.github/workflows/mpi.yml deleted file mode 100644 index 44967cc5..00000000 --- a/.github/workflows/mpi.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: CI mpi - -on: [push] - -jobs: - test: - name: Run tests with in paralell - runs-on: ubuntu-latest - timeout-minutes: 20 - container: - image: ghcr.io/scientificcomputing/fenics-gmsh:2023-02-20 - - steps: - - uses: actions/checkout@v3 - - - name: Install dependencies - run: | - python3 -m pip install --upgrade pip - python3 -m pip install h5py --no-binary=h5py - python3 -m pip install -e ".[dev]" - - - name: Test with pytest - run: | - mpirun -n 2 python3 -m pytest --no-cov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d8c476ac..b6cdfe97 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,11 +25,6 @@ repos: hooks: - id: flake8 - - repo: https://github.com/asottile/add-trailing-comma - rev: v2.4.0 - hooks: - - id: add-trailing-comma - - repo: https://github.com/asottile/setup-cfg-fmt rev: v2.2.0 hooks: diff --git a/demos/stiffness_sensitivity.py b/demos/stiffness_sensitivity.py new file mode 100644 index 00000000..dceef204 --- /dev/null +++ b/demos/stiffness_sensitivity.py @@ -0,0 +1,76 @@ +# # Simple demo +# +# In this demo we show the most simple usage of the `simcardems` library using the python API. +# +# Import the necessary libraries +# + +from pathlib import Path +import simcardems +import matplotlib.pyplot as plt +import numpy as np + +here = Path(__file__).absolute().parent +geometry_path = here / "geometries/slab.h5" +geometry_schema_path = here / "geometries/slab.json" + + +def run(outdir: Path, a: float = 2.28): + outdir.mkdir(exist_ok=True, parents=True) + results_file = outdir.joinpath("results.h5") + if results_file.exists(): + values = np.load(outdir.joinpath("values.npy"), allow_pickle=True).item() + + t = values["time"] + lmbda = values["mechanics"]["lambda"] + return t, lmbda + config = simcardems.Config( + outdir=outdir, + geometry_path=geometry_path, + geometry_schema_path=geometry_schema_path, + coupling_type="fully_coupled_ORdmm_Land", + T=1000, + material_parameter_a=a, + ) + runner = simcardems.Runner(config) + runner.solve(T=config.T, save_freq=config.save_freq, show_progress_bar=True) + + loader = simcardems.datacollector.DataLoader(results_file) + values = simcardems.postprocess.extract_traces(loader, reduction="center") + np.save(outdir.joinpath("values.npy"), values) + + t = values["time"] + lmbda = values["mechanics"]["lambda"] + fig, ax = plt.subplots() + ax.plot(t, lmbda) + ax.set_xlabel("Time [ms]") + ax.set_ylabel("Lambda") + ax.set_title(f"Material parameter a = {a}") + fig.savefig(outdir.joinpath(f"lambda_{a}.png")) + + return t, lmbda + # simcardems.postprocess.plot_state_traces(outdir.joinpath("results.h5"), "center") + + +def main(): + outdir = here / "results_stiffness_sensitivity" + fig, ax = plt.subplots() + a_default = 2.28 + for a in [ + a_default / 4.0, + a_default / 2.0, + a_default, + a_default * 2.0, + a_default * 4.0, + ]: + t, lmbda = run(outdir=outdir / f"a_{a}", a=a) + ax.plot(t, lmbda, label=f"a = {a}") + ax.legend() + ax.set_xlabel("Time [ms]") + ax.set_ylabel("Lambda") + ax.set_title("Stiffness sensitivity") + fig.savefig(outdir / "stiffness_sensitivity.png") + + +if __name__ == "__main__": + main() diff --git a/demos/timestep_sensitivity.py b/demos/timestep_sensitivity.py new file mode 100644 index 00000000..c3066692 --- /dev/null +++ b/demos/timestep_sensitivity.py @@ -0,0 +1,76 @@ +# # Simple demo +# +# In this demo we show the most simple usage of the `simcardems` library using the python API. +# +# Import the necessary libraries +# + +from pathlib import Path +import simcardems +import matplotlib.pyplot as plt +import numpy as np + +here = Path(__file__).absolute().parent +geometry_path = here / "geometries/slab.h5" +geometry_schema_path = here / "geometries/slab.json" + + +def run(outdir: Path, dt_mech: float = 2.28): + outdir.mkdir(exist_ok=True, parents=True) + results_file = outdir.joinpath("results.h5") + if results_file.exists(): + values = np.load(outdir.joinpath("values.npy"), allow_pickle=True).item() + + t = values["time"] + lmbda = values["mechanics"]["lambda"] + return t, lmbda + config = simcardems.Config( + outdir=outdir, + geometry_path=geometry_path, + geometry_schema_path=geometry_schema_path, + coupling_type="fully_coupled_ORdmm_Land", + T=1000, + dt_mech=dt_mech, + ) + runner = simcardems.Runner(config) + runner.solve(T=config.T, save_freq=config.save_freq, show_progress_bar=True) + + loader = simcardems.datacollector.DataLoader(results_file) + values = simcardems.postprocess.extract_traces(loader, reduction="center") + np.save(outdir.joinpath("values.npy"), values) + + t = values["time"] + lmbda = values["mechanics"]["lambda"] + fig, ax = plt.subplots() + ax.plot(t, lmbda) + ax.set_xlabel("Time [ms]") + ax.set_ylabel("Lambda") + ax.set_title(f"Timestep = {dt_mech}") + fig.savefig(outdir.joinpath(f"lambda_{dt_mech}.png")) + + return t, lmbda + # simcardems.postprocess.plot_state_traces(outdir.joinpath("results.h5"), "center") + + +def main(): + outdir = here / "results_timestep_sensitivity" + fig, ax = plt.subplots() + dt_mech_default = 1.0 + for dt in [ + dt_mech_default / 4.0, + dt_mech_default / 2.0, + dt_mech_default, + dt_mech_default * 2.0, + dt_mech_default * 4.0, + ]: + t, lmbda = run(outdir=outdir / f"dt_{dt}", dt_mech=dt) + ax.plot(t, lmbda, label=f"dt = {dt}") + ax.legend() + ax.set_xlabel("Time [ms]") + ax.set_ylabel("Lambda") + ax.set_title("Timestep sensitivity") + fig.savefig(outdir / "timestep_sensitivity.png") + + +if __name__ == "__main__": + main() diff --git a/demos/timestep_threshold_sensitivity.py b/demos/timestep_threshold_sensitivity.py new file mode 100644 index 00000000..9e228a41 --- /dev/null +++ b/demos/timestep_threshold_sensitivity.py @@ -0,0 +1,76 @@ +# # Simple demo +# +# In this demo we show the most simple usage of the `simcardems` library using the python API. +# +# Import the necessary libraries +# + +from pathlib import Path +import simcardems +import matplotlib.pyplot as plt +import numpy as np + +here = Path(__file__).absolute().parent +geometry_path = here / "geometries/slab.h5" +geometry_schema_path = here / "geometries/slab.json" + + +def run(outdir: Path, mech_threshold: float = 0.05): + outdir.mkdir(exist_ok=True, parents=True) + results_file = outdir.joinpath("results.h5") + if results_file.exists(): + values = np.load(outdir.joinpath("values.npy"), allow_pickle=True).item() + + t = values["time"] + lmbda = values["mechanics"]["lambda"] + return t, lmbda + config = simcardems.Config( + outdir=outdir, + geometry_path=geometry_path, + geometry_schema_path=geometry_schema_path, + coupling_type="fully_coupled_ORdmm_Land", + T=1000, + mech_threshold=mech_threshold, + ) + runner = simcardems.Runner(config) + runner.solve(T=config.T, save_freq=config.save_freq, show_progress_bar=True) + + loader = simcardems.datacollector.DataLoader(results_file) + values = simcardems.postprocess.extract_traces(loader, reduction="center") + np.save(outdir.joinpath("values.npy"), values) + + t = values["time"] + lmbda = values["mechanics"]["lambda"] + fig, ax = plt.subplots() + ax.plot(t, lmbda) + ax.set_xlabel("Time [ms]") + ax.set_ylabel("Lambda") + ax.set_title(f"Threshold = {mech_threshold}") + fig.savefig(outdir.joinpath(f"lambda_{mech_threshold}.png")) + + return t, lmbda + # simcardems.postprocess.plot_state_traces(outdir.joinpath("results.h5"), "center") + + +def main(): + outdir = here / "results_timestep_threshold_sensitivity" + fig, ax = plt.subplots() + threshold_default = 0.05 + for thr in [ + threshold_default / 4.0, + threshold_default / 2.0, + threshold_default, + threshold_default * 2.0, + threshold_default * 4.0, + ]: + t, lmbda = run(outdir=outdir / f"thr_{thr}", mech_threshold=thr) + ax.plot(t, lmbda, label=f"thr = {thr}") + ax.legend() + ax.set_xlabel("Time [ms]") + ax.set_ylabel("Lambda") + ax.set_title("Timestep sensitivity") + fig.savefig(outdir / "timestep_threshold_sensitivity.png") + + +if __name__ == "__main__": + main() diff --git a/setup.cfg b/setup.cfg index 98746856..17a5c857 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,7 @@ install_requires = fenics-pulse h5py matplotlib - numpy + numpy<2.0 scipy tqdm typing-extensions diff --git a/src/simcardems/config.py b/src/simcardems/config.py index 10e4edce..ae5ce306 100644 --- a/src/simcardems/config.py +++ b/src/simcardems/config.py @@ -54,6 +54,7 @@ class Config: "explicit_ORdmm_Land", "pureEP_ORdmm_Land", ] = "fully_coupled_ORdmm_Land" + material_parameter_a: float = 2.28 def as_dict(self): return {k: v for k, v in self.__dict__.items()} diff --git a/src/simcardems/geometry.py b/src/simcardems/geometry.py index 19036910..2144112a 100644 --- a/src/simcardems/geometry.py +++ b/src/simcardems/geometry.py @@ -75,7 +75,7 @@ def refine_mesh( ) -> dolfin.Mesh: dolfin.parameters["refinement_algorithm"] = "plaza_with_parent_facets" for i in range(num_refinements): - logger.info(f"Performing refinement {i+1}") + logger.info(f"Performing refinement {i + 1}") mesh = dolfin.refine(mesh, redistribute=redistribute) return mesh @@ -159,8 +159,8 @@ def default_parameters() -> Dict[str, Any]: def default_stimulus_domain(mesh: dolfin.Mesh) -> StimulusDomain: # Default is to stimulate the entire tissue marker = 1 - #domain = dolfin.MeshFunction("size_t", mesh, mesh.topology().dim()) - #domain.set_all(marker) + # domain = dolfin.MeshFunction("size_t", mesh, mesh.topology().dim()) + # domain.set_all(marker) subdomain = dolfin.CompiledSubDomain("x[0] < 1.0") domain = dolfin.MeshFunction("size_t", mesh, mesh.topology().dim()) domain.set_all(0) diff --git a/src/simcardems/mechanics_model.py b/src/simcardems/mechanics_model.py index bd97329c..c20b9a9e 100644 --- a/src/simcardems/mechanics_model.py +++ b/src/simcardems/mechanics_model.py @@ -34,6 +34,7 @@ def setup_solver( linear_solver="mumps", use_custom_newton_solver: bool = config.Config.mechanics_use_custom_newton_solver, state_prev=None, + material_parameter_a: float = 2.28, ): """Setup mechanics model with dirichlet boundary conditions or rigid motion.""" @@ -42,8 +43,9 @@ def setup_solver( logger.info("Set up mechanics model") # Use parameters from Biaxial test in Holzapfel 2019 (Table 1). + material_parameters = dict( - a=2.28, + a=material_parameter_a, a_f=1.686, b=9.726, b_f=15.779, diff --git a/src/simcardems/models/em_model.py b/src/simcardems/models/em_model.py index c175a360..42b8a622 100644 --- a/src/simcardems/models/em_model.py +++ b/src/simcardems/models/em_model.py @@ -70,6 +70,7 @@ def setup_EM_model( use_custom_newton_solver=config.mechanics_use_custom_newton_solver, debug_mode=config.debug_mode, ActiveModel=cls_ActiveModel, + material_parameter_a=config.material_parameter_a, ) if mech_state_init is not None: mech_heart.state.assign(mech_state_init) diff --git a/src/simcardems/slabgeometry.py b/src/simcardems/slabgeometry.py index 743d7cb0..cb9d5f6e 100644 --- a/src/simcardems/slabgeometry.py +++ b/src/simcardems/slabgeometry.py @@ -98,13 +98,15 @@ def validate(self): pass def __repr__(self) -> str: + name = self.__class__.__name__ return ( - f"{self.__class__.__name__}(" + f"{name}(" f"lx={self.parameters['lx']}, " f"ly={self.parameters['ly']}, " f"lz={self.parameters['lz']}, " f"dx={self.parameters['dx']}, " - f"num_refinements={self.parameters['num_refinements']})" + f"num_refinements={self.parameters['num_refinements']}" + ")" ) diff --git a/tests/conftest.py b/tests/conftest.py index 3dc0c569..7768e1e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ def by_slow_marker(item): - return item.get_closest_marker("slow", default=empty_mark) + return int(item.get_closest_marker("slow", default=empty_mark).name == "slow") def pytest_collection_modifyitems(items):