diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..87ef98d --- /dev/null +++ b/.flake8 @@ -0,0 +1,13 @@ +[flake8] +max_line_length = 120 + +exclude = + __pycache__, + .git, + .github, + .venv, + .virtualenv, + build, + output, + venv, + virtualenv, diff --git a/.github/workflows/test-project.yml b/.github/workflows/test-project.yml new file mode 100644 index 0000000..1d881e0 --- /dev/null +++ b/.github/workflows/test-project.yml @@ -0,0 +1,50 @@ +name: Test Project + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +concurrency: + group: test-project-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-project: + runs-on: ubuntu-latest + + container: + image: chaste/base + options: --user root + + env: + CHASTE_SOURCE_DIR: /tmp/Chaste + CHASTE_BUILD_DIR: /tmp/build + CHASTE_TEST_OUTPUT: /tmp/testoutput + PROJECT_DIR: /tmp/myproject + + steps: + - name: Checkout template + uses: actions/checkout@v7 + + - name: Checkout Chaste source + run: | + git clone --depth 1 --branch develop https://github.com/Chaste/Chaste.git "${CHASTE_SOURCE_DIR}" + git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" + + - name: Create test project + run: | + cp -r . ${{ env.PROJECT_DIR }} + cd ${{ env.PROJECT_DIR }} + # Prompt defaults (no components, no Python bindings, no SBML), yes to confirm + printf '\n\n\n\n\nn\nn\ny\n' | python3 setup_project.py + + - name: Configure + run: ${{ env.PROJECT_DIR }}/scripts/configure.sh + + - name: Compile + run: ${{ env.PROJECT_DIR }}/scripts/compile.sh + + - name: Test + run: ${{ env.PROJECT_DIR }}/scripts/test.sh diff --git a/.github/workflows/test-python-project.yml b/.github/workflows/test-python-project.yml new file mode 100644 index 0000000..a931605 --- /dev/null +++ b/.github/workflows/test-python-project.yml @@ -0,0 +1,98 @@ +name: Test Python Project + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +concurrency: + group: test-python-project-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-python-project: + # Two configurations: + # - default-components: the python wrapper template exactly as shipped. + # - force-example: the bindings/example walkthrough. + name: ${{ matrix.variant }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + # Prompt defaults (proceed, no optional components), yes to Python + # bindings, no to SBML, yes to confirm + - variant: default-components + answers: '\n\n\n\n\ny\nn\ny\n' + # Yes to proceed, yes to cell_based, no to crypt/heart/lung, yes to + # Python bindings, no to SBML, yes to confirm + - variant: force-example + answers: 'y\ny\nn\nn\nn\ny\nn\ny\n' + + container: + image: chaste/base + options: --user root + + env: + CHASTE_SOURCE_DIR: /tmp/Chaste + CHASTE_BUILD_DIR: /tmp/build + CHASTE_TEST_OUTPUT: /tmp/testoutput + PROJECT_DIR: /tmp/myproject + + steps: + - name: Checkout template + uses: actions/checkout@v7 + + - name: Checkout Chaste source + run: | + git clone --depth 1 --branch develop https://github.com/Chaste/Chaste.git "${CHASTE_SOURCE_DIR}" + git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" + + - name: Create test project + run: | + cp -r . ${{ env.PROJECT_DIR }} + cd ${{ env.PROJECT_DIR }} + printf '%b' '${{ matrix.answers }}' | python3 setup_project.py + + - name: Add MyForce to the project bindings + if: matrix.variant == 'force-example' + run: | + cd ${{ env.PROJECT_DIR }} + cp bindings/example/MyForce.hpp bindings/example/MyForce.cpp src/ + sed -i.bak 's/^ classes:$/&\n - name: MyForce/' bindings/config.yaml + # Import PyChaste's module and declare AbstractForce as an external base + sed -i.bak 's/^ - name: all$/&\n imports:/' bindings/config.yaml + sed -i.bak 's/^ imports:$/&\n - chaste._pychaste_all/' bindings/config.yaml + sed -i.bak 's/^ - chaste._pychaste_all$/&\n external_bases:/' bindings/config.yaml + sed -i.bak 's/^ external_bases:$/&\n - AbstractForce/' bindings/config.yaml + # The seds above anchor on the shipped formatting of config.yaml; fail loudly + # here rather than later with a confusing "MyForce_2 not found" at runtime. + grep -q '^ - name: MyForce$' bindings/config.yaml + grep -q '^ - chaste._pychaste_all$' bindings/config.yaml + grep -q '^ - AbstractForce$' bindings/config.yaml + + - name: Configure + run: ${{ env.PROJECT_DIR }}/scripts/configure.sh + + - name: Compile + run: ${{ env.PROJECT_DIR }}/scripts/compile.sh + + - name: Install + run: ${{ env.PROJECT_DIR }}/bindings/install.sh + + - name: Test Python bindings + run: | + ${{ env.PROJECT_DIR }}/.virtualenv/bin/python -c " + import myproject + h = myproject.Hello_myproject('Hello from Python!') + message = h.GetMessage() + assert message == 'Hello from Python!', f'Expected \"Hello from Python!\" but got \"{message}\"' + print(f'GetMessage() returned: {message}') + print('Test passed!') + " + + - name: Run the MyForce simulation from Python + if: matrix.variant == 'force-example' + run: ${{ env.PROJECT_DIR }}/.virtualenv/bin/python ${{ env.PROJECT_DIR }}/bindings/example/run_my_force.py diff --git a/.github/workflows/test-sbml-project.yml b/.github/workflows/test-sbml-project.yml new file mode 100644 index 0000000..f144db2 --- /dev/null +++ b/.github/workflows/test-sbml-project.yml @@ -0,0 +1,64 @@ +name: Test SBML Project + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +concurrency: + group: test-sbml-project-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-sbml-project: + runs-on: ubuntu-latest + + container: + image: chaste/base + options: --user root + + env: + CHASTE_SOURCE_DIR: /tmp/Chaste + CHASTE_BUILD_DIR: /tmp/build + CHASTE_TEST_OUTPUT: /tmp/testoutput + PROJECT_DIR: /tmp/myproject + + steps: + - name: Checkout template + uses: actions/checkout@v7 + + - name: Checkout Chaste source + run: | + git clone --depth 1 --branch develop https://github.com/Chaste/Chaste.git "${CHASTE_SOURCE_DIR}" + git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" + + - name: Install clang-format + run: apt-get update && apt-get install -y clang-format + + - name: Create test project + run: | + cp -r . ${{ env.PROJECT_DIR }} + cd ${{ env.PROJECT_DIR }} + # Prompt defaults for proceed/components/no-python-bindings, yes to SBML, yes to confirm + printf '\n\n\n\n\n\ny\ny\n' | python3 setup_project.py + + - name: Install chaste-sbml + run: ${{ env.PROJECT_DIR }}/sbml/install.sh + + - name: Import the Goldbeter 1991 model + run: | + cd ${{ env.PROJECT_DIR }} + cp sbml/example/Goldbeter1991.xml . + .virtualenv/bin/chaste-sbml Goldbeter1991.xml --model-type srn --output-dir src/ + cp sbml/example/TestGoldbeter1991SbmlSrnModel.hpp test/ + echo "TestGoldbeter1991SbmlSrnModel.hpp" >> test/ContinuousTestPack.txt + + - name: Configure + run: ${{ env.PROJECT_DIR }}/scripts/configure.sh + + - name: Compile + run: ${{ env.PROJECT_DIR }}/scripts/compile.sh + + - name: Test + run: ${{ env.PROJECT_DIR }}/scripts/test.sh diff --git a/.gitignore b/.gitignore index dc84959..bbe0e20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,42 @@ build/ +output/ +.vscode/ + +.DS_Store +Thumbs.db + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.eggs/ +.virtualenv/ +.venv/ +venv/ +env/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# C++ +*.o +*.obj +*.lo +*.slo +*.a +*.lib +*.so +*.dylib +*.dll +*.exe +*.out +*.gch +*.pch + +# CMake +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +CTestTestfile.cmake +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt index bff0df2..de94bcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2005-2023, University of Oxford. +# Copyright (c) 2005-2026, University of Oxford. # All rights reserved. # # University of Oxford means the Chancellor, Masters and Scholars of the @@ -52,3 +52,7 @@ find_package(Chaste COMPONENTS continuum_mechanics global io linalg mesh ode pde # Change the project name in the line below to match the folder this file is in, # i.e. the name of your project. chaste_do_project(template_project) + +if(Chaste_ENABLE_PYCHASTE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/bindings") + add_subdirectory(bindings) +endif() diff --git a/README.md b/README.md index a8d13a4..19cb530 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,29 @@ Alternatively, if you aren't a github user, you can download a zip (see Releases Then see the [User Projects](https://chaste.github.io/docs/user-guides/user-projects/) guide page on the Chaste website for more information. -If you clone this repository, you should make sure to rename the template_project folder with your project name and run the 'setup_project.py' script to avoid conflicts if you have multiple projects. +If you clone this repository, you should make sure to rename the **template_project** folder with your project name and run the `setup_project.py` script to avoid conflicts if you have multiple projects. + +## What's in here + +| Path | | +| --- | --- | +| `src/`, `test/`, `apps/` | your project's C++ code; only `src/` is compiled into the project library | +| `scripts/` | configure / compile / test helpers | +| `bindings/` | optional PyChaste Python bindings; see [bindings/README.md](bindings/README.md) | +| `sbml/` | optional SBML model import; see [sbml/README.md](sbml/README.md) | + +`setup_project.py` asks whether you want each optional capability and removes the +directory if you decline, so a plain C++ project keeps neither. + +## Building + +There are simple convenience scripts available to help with the build process. From the project directory, with `CHASTE_SOURCE_DIR` pointing at your Chaste source: + +```sh +scripts/configure.sh # symlink the project into Chaste/projects and configure the build +scripts/compile.sh # build the project +scripts/test.sh # run the project's tests +``` + +If you need more detailed control of the build process, see the +[full Chaste build guide](https://chaste.github.io/docs/dev-guides/cmake-build-guide/). diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt new file mode 100644 index 0000000..364c0c5 --- /dev/null +++ b/bindings/CMakeLists.txt @@ -0,0 +1,168 @@ +# Copyright (c) 2005-2026, University of Oxford. +# All rights reserved. +# +# University of Oxford means the Chancellor, Masters and Scholars of the +# University of Oxford, having an administrative office at Wellington +# Square, Oxford OX1 2JD, UK. +# +# This file is part of Chaste. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the University of Oxford nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +if(NOT Chaste_ENABLE_PYCHASTE) + return() +endif() + +################################ +#### Generate Python wrappers +################################ + +# Get includes from the project's chaste target +get_target_property(PROJECT_INCLUDE_DIRS chaste_project_template_project INCLUDE_DIRECTORIES) + +# Inherit the include configuration that PyChaste exposes for +# VTK, PETSc4Py and the pybind11 typecasters. +get_target_property(PYCHASTE_INTERFACE_INCLUDE_DIRS chaste_pychaste INTERFACE_INCLUDE_DIRECTORIES) +if(NOT PYCHASTE_INTERFACE_INCLUDE_DIRS) + message(FATAL_ERROR + "Cannot find chaste_pychaste INTERFACE include directories; update your Chaste source tree.") +endif() +foreach(_dir IN LISTS PYCHASTE_INTERFACE_INCLUDE_DIRS) + if(_dir MATCHES "^\\$$") + list(APPEND PROJECT_INCLUDE_DIRS "${CMAKE_MATCH_1}") + elseif(NOT _dir MATCHES "^\\$<") + list(APPEND PROJECT_INCLUDE_DIRS "${_dir}") + endif() +endforeach() + +# Add the project's own source directory so cppwg can find project headers +list(APPEND PROJECT_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/../src) + +list(REMOVE_DUPLICATES PROJECT_INCLUDE_DIRS) + +# To parse C++ headers, castxml mimics a compiler. PyChaste searches for clang, +# which has better castxml support, and caches the result in Chaste_CASTXML_COMPILER, +# which this project inherits. The fallback is set to the normal compiler below. +# The library itself is built with the normal compiler either way. +if(NOT Chaste_CASTXML_COMPILER) + set(Chaste_CASTXML_COMPILER ${CMAKE_CXX_COMPILER}) + message(WARNING "clang not found. Using ${CMAKE_CXX_COMPILER} as the castxml" + " compiler. Wrapper generation may fail") +endif() + +# Command for generating wrappers. +# - gnu++17 (rather than c++17) and -Wno-missing-template-arg-list-after-template-kw +# silence a castxml/LLVM error on use of the template keyword without a +# following argument list, which appears in some boost headers. +set(WRAPPER_GENERATION_COMMAND + ${chaste_python3_venv}/cppwg ${CMAKE_CURRENT_SOURCE_DIR}/.. + -w ${CMAKE_CURRENT_BINARY_DIR}/wrappers + -p ${CMAKE_CURRENT_SOURCE_DIR}/config.yaml + -i ${PROJECT_INCLUDE_DIRS} + -l ${CMAKE_CURRENT_BINARY_DIR}/cppwg.log + --std "gnu++17" + --castxml_cflags="-Wno-missing-template-arg-list-after-template-kw" + --castxml_compiler ${Chaste_CASTXML_COMPILER} +) + +# Generate wrappers. Unlike PyChaste, which generates once, a user project +# regenerates on every configure so that edits to config.yaml and to the +# project's own classes are picked up. +execute_process( + COMMAND ${WRAPPER_GENERATION_COMMAND} + COMMAND_ERROR_IS_FATAL ANY +) + +# Target for manually regenerating wrappers +add_custom_target(template_project_wrappers + COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_CURRENT_BINARY_DIR}/wrappers + COMMAND ${WRAPPER_GENERATION_COMMAND} +) + +# Add generated wrapper headers to includes +header_dirs("${CMAKE_CURRENT_BINARY_DIR}/wrappers" _include_dirs) +list(APPEND PROJECT_INCLUDE_DIRS ${_include_dirs}) + +################################ +#### Build Python module +################################ +# Creates a `_template_project_all` library from the Python wrappers. +# When built, it can be used in Python as `from template_project._template_project_all import *`. + +file(GLOB_RECURSE WRAPPER_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/wrappers/all/*.cpp + ${CMAKE_CURRENT_BINARY_DIR}/wrappers/all/*.hpp +) + +if(NOT WRAPPER_SOURCES) + message(FATAL_ERROR + "No Python wrapper sources were generated for the project. " + "Check the classes listed in bindings/config.yaml and the cppwg log at: " + " ${CMAKE_CURRENT_BINARY_DIR}/cppwg.log") +endif() + +pybind11_add_module(_template_project_all OPT_SIZE ${WRAPPER_SOURCES}) + +target_link_libraries(_template_project_all PRIVATE + Chaste_COMMON_DEPS + chaste_project_template_project + chaste_pychaste +) + +target_include_directories(_template_project_all PRIVATE ${PROJECT_INCLUDE_DIRS}) + +# -g0: Disable debug symbols in the Python modules as they are not needed. +target_compile_options(_template_project_all PRIVATE -Wno-unused-local-typedefs -g0) + +# Perform link-time optimization for Release builds only. +set_target_properties(_template_project_all PROPERTIES INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # clang ThinLTO needs lld. + target_link_options(_template_project_all PRIVATE -fuse-ld=lld) +endif() + +set_target_properties(_template_project_all PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/package/template_project +) + +add_dependencies(_template_project_all pychaste) +add_dependencies(project_template_project _template_project_all) + +################################ +#### Copy Python source +################################ + +file(GLOB_RECURSE _py_sources + CONFIGURE_DEPENDS + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/package + ${CMAKE_CURRENT_SOURCE_DIR}/package/* +) +list(FILTER _py_sources EXCLUDE REGEX "\\.(dll|dylib|pyc|so)$") + +foreach(_src ${_py_sources}) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/package/${_src} + ${CMAKE_CURRENT_BINARY_DIR}/package/${_src} + COPYONLY + ) +endforeach() diff --git a/bindings/README.md b/bindings/README.md new file mode 100644 index 0000000..32035b5 --- /dev/null +++ b/bindings/README.md @@ -0,0 +1,119 @@ +# Python bindings + +This template builds [PyChaste](https://chaste.github.io/pychaste/) Python bindings for your +project's C++ classes using [cppwg](https://github.com/Chaste/cppwg), so you can use +your project from Python. For a complete example, see +[example/README.md](example/README.md). + +Run every command below from your project's root directory. See the +[top-level README](../README.md) for the project layout and the build cycle. + +## Prerequisites +Creating bindings requires: + +* [cppwg](https://github.com/Chaste/cppwg): used at configure time to generate the + wrappers +* PyChaste's native runtime dependencies: `petsc4py` and `vtk` (Python VTK wrappers). + +The [`chaste/base`](https://hub.docker.com/r/chaste/base) Docker image provides all +of these. The user project virtualenv is created with the +`--system-site-packages` flag so it can see them, as they are installed as system packages. + +If you are **not** working inside the chaste/base image, +install these dependencies into the system Python (or `pip install` them into the project virtualenv) yourself +before running configuration: `petsc4py`, and `vtk`. Note that the versions must match the versions of PETSc and VTK on your system. + +## Enable Python bindings + +When you run `setup_project.py`, answer **yes** to: + +``` +Do you want to create Python bindings for this project? +``` + +This keeps the binding scaffolding and wires it to your project name: + +* `bindings/config.yaml`: the cppwg configuration listing the classes to wrap, +* `bindings/CMakeLists.txt`: builds the bindings as part of the project, +* `bindings/package/`: the installable Python package (its `template_project/` + subdirectory is renamed to `/`). + +If you answer no, the whole `bindings/` directory is removed and the project is a plain C++ project. + +## Configure and compile + +From the project directory, with `CHASTE_SOURCE_DIR` pointing at your Chaste source: + +```sh +scripts/configure.sh # configure the build and generate the Python bindings +scripts/compile.sh # build the project, including the Python bindings +``` + +## Install the bindings into the project virtualenv + +```sh +bindings/install.sh +``` + +This creates the project virtualenv and installs both PyChaste and your +project's bindings package into it. + +## Use your project from Python + +Activate the virtualenv + +```sh +source .virtualenv/bin/activate +``` + +```python +import myproject # replace with your project name +hello = myproject.Hello_myproject("Hello from Python!") +print(hello.GetMessage()) +``` + +## Add your own C++ classes to the bindings + +To expose a new class created in `src/`, add it in `bindings/config.yaml` under `classes:` + +For example: + +```yaml +modules: + - name: all + source_locations: + - src/ + classes: + - name: Hello_myproject +``` + +If your class inherits from one that PyChaste already wraps, list that base +under `external_bases` and add PyChaste's module under `imports`, so that +cppwg registers the base class before yours. See the +[cppwg docs on inheritance](https://chaste.github.io/cppwg/inheritance.html). + +See [example/README.md](example/README.md) for a full example. + +## Troubleshooting the bindings + +**Wrapper generation fails during `scripts/configure.sh`.** cppwg runs at +configure time, and the full cppwg output is written to `cppwg.log` in the project's +build tree, at `${CHASTE_BUILD_DIR}/projects//bindings/cppwg.log`; +read it to see which class or header caused the failure. +This could be due to an error in `bindings/config.yaml` e.g. a misspelt class. Fix `config.yaml`, +and re-run `configure.sh` to regenerate the wrappers. + +**Configure fails with "No Python wrapper sources were generated".** cppwg ran +but produced no wrappers, usually because no classes in the list were matched from `bindings/config.yaml`. +Check the `classes:` and `source_locations:` +entries in `config.yaml` against `cppwg.log`, then re-configure. + +**Compilation fails with missing PyChaste or Chaste headers.** Make sure PyChaste is enabled. Try also deleting the build directory (`$CHASTE_BUILD_DIR`, by default +`build/` in the project) and re-run `configure.sh`. + +**`import myproject` fails at runtime**, typically with an error importing +`petsc4py` or `vtk`. Those are PyChaste's native runtime dependencies +and must be available. See +[Prerequisites](#prerequisites). Try activating the project virtualenv if not already active, or try installing the dependencies yourself. + +> See also https://chaste.github.io/pychaste/dev-guide/ diff --git a/bindings/config.yaml b/bindings/config.yaml new file mode 100644 index 0000000..6df3596 --- /dev/null +++ b/bindings/config.yaml @@ -0,0 +1,85 @@ +# Copyright (c) 2005-2026, University of Oxford. +# All rights reserved. +# +# University of Oxford means the Chancellor, Masters and Scholars of the +# University of Oxford, having an administrative office at Wellington +# Square, Oxford OX1 2JD, UK. +# +# This file is part of Chaste. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the University of Oxford nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# CPPWG configuration for template_project Python bindings. +# See https://chaste.github.io/cppwg/ for documentation, and the Chaste tree's +# pychaste/dynamic/config.yaml for a large worked example. Add classes here as +# you add them to your project. + +name: template_project + +smart_ptr_type: boost::shared_ptr +pointer_call_policy: reference +reference_call_policy: reference_internal + +common_include_file: OFF + +# Automatically include the headers each wrapper needs. A class's own header is +# always included, so only extra headers need listing under source_includes. +auto_includes: True + +# Skip bindings for methods that only override a virtual base class method that +# is already wrapped on the base class as these are redundant. +exclude_inherited_overrides: True + +# Auto-include type-caster headers in wrappers that use their types. These are the +# casters PyChaste ships; cppwg adds each one only to the wrappers that need it. +typecasters: + - header: PybindPetscTypeCaster.hpp + types: [Vec, Mat] + - header: PybindUblasTypeCaster.hpp + types: [boost::numeric::ublas::c_vector] + - header: PybindVTKTypeCaster.hpp + types: [vtkSmartPointer, vtkNew] + +# Headers added to every class wrapper. +source_includes: + - SmartPointers.hpp + +# Wrap the explicit template instantiations declared in the project's own .cpp files, +# e.g. `template class MyForce<2>;` wraps MyForce as MyForce_2. If a class's +# instantiations cannot be discovered this way, list them per class instead with +# template_substitutions (see the cppwg docs on templates). +discover_template_instantiations: True +discover_arg_excludes: + DIM: [0] + SPACE_DIM: [0] + ELEMENT_DIM: [0] + +modules: + - name: all + # Where cppwg looks for the classes named below. If a header basename appears in more + # than one scanned location the last match wins, ordered by path, so keep wrapped + # headers in src/ rather than adding a location that sorts after it. + source_locations: + - src/ + classes: + - name: Hello diff --git a/bindings/example/MyForce.cpp b/bindings/example/MyForce.cpp new file mode 100644 index 0000000..8de9c9d --- /dev/null +++ b/bindings/example/MyForce.cpp @@ -0,0 +1,48 @@ +#include "MyForce.hpp" + +template +MyForce::MyForce(double forceMagnitude) + : AbstractForce(), + mForceMagnitude(forceMagnitude) +{ +} + +template +void MyForce::AddForceContribution(AbstractCellPopulation& rCellPopulation) +{ + // Build a constant force vector pointing in the positive x-direction. + c_vector force = zero_vector(DIM); + force[0] = mForceMagnitude; + + // Apply it to every node in the population. + for (typename AbstractCellPopulation::Iterator cell_iter = rCellPopulation.Begin(); + cell_iter != rCellPopulation.End(); + ++cell_iter) + { + unsigned node_index = rCellPopulation.GetLocationIndexUsingCell(*cell_iter); + rCellPopulation.GetNode(node_index)->AddAppliedForceContribution(force); + } +} + +template +double MyForce::GetForceMagnitude() const +{ + return mForceMagnitude; +} + +template +void MyForce::OutputForceParameters(out_stream& rParamsFile) +{ + *rParamsFile << "\t\t\t" << mForceMagnitude << "\n"; + + // Call the method on the direct parent class. + AbstractForce::OutputForceParameters(rParamsFile); +} + +// Explicit instantiation +template class MyForce<1>; +template class MyForce<2>; +template class MyForce<3>; + +#include "SerializationExportWrapper.hpp" +EXPORT_TEMPLATE_CLASS_SAME_DIMS(MyForce) diff --git a/bindings/example/MyForce.hpp b/bindings/example/MyForce.hpp new file mode 100644 index 0000000..4244682 --- /dev/null +++ b/bindings/example/MyForce.hpp @@ -0,0 +1,68 @@ +#ifndef MYFORCE_HPP_ +#define MYFORCE_HPP_ + +#include "AbstractCellPopulation.hpp" +#include "AbstractForce.hpp" + +#include "ChasteSerialization.hpp" +#include + +/** + * An example user-defined force for a Chaste cell-based simulation. + * + * It applies a constant force of a given magnitude, in the positive x-direction, to every + * node in the cell population. This is deliberately simple: it exists to show how to add a + * new C++ class to a user project and then drive it from Python via the project bindings. + */ +template +class MyForce : public AbstractForce +{ +private: + /** The magnitude of the constant force applied to each node. */ + double mForceMagnitude; + + /** Needed for serialization. */ + friend class boost::serialization::access; + + /** + * Archive the object and its member variables. + * + * @param archive the archive + * @param version the current version of this class + */ + template + void serialize(Archive& archive, const unsigned int version) + { + archive& boost::serialization::base_object >(*this); + archive& mForceMagnitude; + } + +public: + /** + * Constructor. + * + * @param forceMagnitude the magnitude of the constant force (defaults to 1.0) + */ + MyForce(double forceMagnitude = 1.0); + + /** + * Overridden AddForceContribution() method. + * + * @param rCellPopulation reference to the cell population + */ + void AddForceContribution(AbstractCellPopulation& rCellPopulation) override; + + /** + * @return the magnitude of the applied force. + */ + double GetForceMagnitude() const; + + /** + * Overridden OutputForceParameters() method. + * + * @param rParamsFile the file stream to which the parameters are output + */ + void OutputForceParameters(out_stream& rParamsFile) override; +}; + +#endif /*MYFORCE_HPP_*/ diff --git a/bindings/example/README.md b/bindings/example/README.md new file mode 100644 index 0000000..f42b196 --- /dev/null +++ b/bindings/example/README.md @@ -0,0 +1,143 @@ +# Bindings Example: a new C++ Force used from Python + +This example adds a new C++ cell-based `Force` to your project, exposes it through the +project's Python bindings, and then uses it in a [PyChaste](https://chaste.github.io/pychaste/) +simulation driven from Python. + +It assumes you have already created your project from this template and answered **yes** to +the Python bindings prompt and **yes** to the cell-based prompt in `setup_project.py`, so that the `bindings/` folder is present, containing a `config.yaml`, +a `CMakeLists.txt` and a `package/` subfolder. + +Run every command below from your project's root directory, and replace `myproject` with +your project's name throughout. + +## Add the force to your project's source + +Copy the example force class into your project's `src/` directory: + +```sh +cp bindings/example/MyForce.?pp src/ +``` + +[`MyForce`](MyForce.hpp) subclasses +`AbstractForce` and is templated over the spatial dimension. Its `AddForceContribution()` applies a constant force in the +positive x-direction to every node in the population. + +## Expose the force to the Python bindings + +Tell [cppwg](https://github.com/Chaste/cppwg) (the wrapper generator) to wrap the new class by editing +`bindings/config.yaml` and adding `MyForce` to `classes:`. + +`MyForce` subclasses `AbstractForce`, which is wrapped in PyChaste, so tell +cppwg this by adding the `chaste._pychaste_all` module under `imports:` and +listing `AbstractForce` under `external_bases`: + +```yaml +modules: + - name: all + imports: #<-- new + - chaste._pychaste_all #<-- new + external_bases: #<-- new + - AbstractForce #<-- new + source_locations: + - src/ + classes: + - name: Hello_myproject + - name: MyForce #<-- new +``` + +Cross-package inheritance works even though +`AbstractForce` is wrapped in PyChaste, which is a separate Python package from +the user project's Python package. Listing `AbstractForce` under `external_bases` +tells cppwg it is registered outside the module, and specifying `chaste._pychaste_all` +as an import ensures that `AbstractForce` is registered before `MyForce`. +Without this, Python would not recognise `MyForce` as an `AbstractForce`. +As a result, for example, `OffLatticeSimulation.AddForce(my_force)` would reject +the force. + +Because `MyForce` is templated over ``, it is wrapped once per +dimension. The setting `discover_template_instantiations: True` in `config.yaml` +picks up the explicit instantiations at the bottom of [`MyForce.cpp`](MyForce.cpp), +`template class MyForce<1>;` and so on. The class is exposed in Python as +`MyForce_1`, `MyForce_2` (2D) and `MyForce_3` (3D). + +## Compile and install the bindings + +With `CHASTE_SOURCE_DIR` pointing at your Chaste source: + +```sh +scripts/configure.sh # configure the build and generate the Python bindings +scripts/compile.sh # rebuild the project and its bindings +bindings/install.sh # install PyChaste + your project into .virtualenv/ +``` + + +## Use the force in a Python simulation + +Activate the virtualenv + +```sh +source .virtualenv/bin/activate +``` + +The example [`run_my_force.py`](run_my_force.py) script builds a small node-based cell population and runs +an off-lattice simulation that adds both a standard spring force and our custom `MyForce`: + +```python +import chaste +import chaste.cell_based +import chaste.mesh + +chaste.init() + +import myproject # provides MyForce_2 + +# The cell-cycle models need the simulation clock to exist before cells are created. +chaste.SimulationTime.Instance().SetStartTime(0.0) + +# Build a small node-based cell population. The same distance sets how far apart +# cells interact and where the force below is cut off, so they must agree. +cutoff_length = 1.5 +generator = chaste.HoneycombMeshGenerator(5, 5) +mesh = chaste.mesh.NodesOnlyMesh_2() +mesh.ConstructNodesWithoutMesh(generator.GetMesh(), cutoff_length) + +transit_type = chaste.TransitCellProliferativeType() +cell_generator = chaste.CellsGenerator["UniformCellCycleModel", "2"]() +cells = cell_generator.GenerateBasicRandom(mesh.GetNumNodes(), transit_type) +cell_population = chaste.cell_based.NodeBasedCellPopulation_2(mesh, cells) + +# Run an off-lattice simulation using a standard force and our custom force. +simulator = chaste.cell_based.OffLatticeSimulation_2_2(cell_population) +simulator.SetOutputDirectory("Python/MyForce") +simulator.SetEndTime(1.0) +spring_force = chaste.cell_based.PathmanathanInteractionForce_2_2() +spring_force.SetCutOffLength(cutoff_length) +simulator.AddForce(spring_force) +simulator.AddForce(myproject.MyForce_2(1.0)) # <-- our new force, from C++ +simulator.Solve() +``` + +Edit the `import myproject` line to your project name, then run it: + +```sh +python bindings/example/run_my_force.py +``` + +You should see the simulation run to completion and print the number of cells. The custom +force pushes the whole population in the x-direction over the course of the simulation, +confirming that your new C++ class is callable from Python. + +> If `myproject.MyForce_2` is not found, list the generated names with +> `print([n for n in dir(myproject) if "MyForce" in n])`. The dimension suffix depends on +> how the class is templated (see the [cppwg docs](https://chaste.github.io/cppwg/templates.html)). + +## Troubleshooting +See the [bindings README](../README.md#troubleshooting-the-bindings) for steps to fix problems with adding Python bindings. + +## Next steps + +* Give `MyForce` more parameters or a different `AddForceContribution()` and rebuild. +* Add more of your own classes to `bindings/config.yaml` the same way. +* See the [PyChaste tutorials](https://chaste.github.io/pychaste/tutorials/) for more + complete cell-based simulations. diff --git a/bindings/example/run_my_force.py b/bindings/example/run_my_force.py new file mode 100644 index 0000000..b00312f --- /dev/null +++ b/bindings/example/run_my_force.py @@ -0,0 +1,71 @@ +"""Run a PyChaste node-based cell simulation that uses MyForce from the project bindings. + +Before running this, build and install your project's Python bindings (see the README in +this directory), then activate the virtualenv: + + source .virtualenv/bin/activate + python bindings/example/run_my_force.py + +Replace ``myproject`` below with your project's name. +""" + +import chaste +import chaste.cell_based +import chaste.mesh + +chaste.init() + +# The project bindings, which provide MyForce. Replace with your project name. +import myproject + +# A templated class is wrapped once per dimension. cppwg names the 2D instantiation of +# MyForce as MyForce_2 (the dimensions follow an underscore). If this name is wrong +# for your build, run: print([n for n in dir(myproject) if "MyForce" in n]) +MyForce_2 = myproject.MyForce_2 + + +def main(): + # Choose where results are written. + chaste.OutputFileHandler("Python/MyForce") + + # The cell-cycle models need the simulation clock to exist before cells are created. + chaste.SimulationTime.Instance().SetStartTime(0.0) + + # Build a small node-based cell population on a honeycomb mesh. + generator = chaste.HoneycombMeshGenerator(5, 5) + generating_mesh = generator.GetMesh() + + cutoff_length = 1.5 # The distance at which cells interact. + + mesh = chaste.mesh.NodesOnlyMesh_2() + mesh.ConstructNodesWithoutMesh(generating_mesh, cutoff_length) + + transit_type = chaste.TransitCellProliferativeType() + cell_generator = chaste.CellsGenerator["UniformCellCycleModel", "2"]() + cells = cell_generator.GenerateBasicRandom(mesh.GetNumNodes(), transit_type) + + cell_population = chaste.cell_based.NodeBasedCellPopulation_2(mesh, cells) + + # Set up an off-lattice simulation. + simulator = chaste.cell_based.OffLatticeSimulation_2_2(cell_population) + simulator.SetOutputDirectory("Python/MyForce") + simulator.SetSamplingTimestepMultiple(12) + simulator.SetEndTime(1.0) + + # PathmanathanInteractionForce keeps neighbouring cells apart. Its repulsion is + # logarithmic and its attraction decays exponentially with separation. + spring_force = chaste.cell_based.PathmanathanInteractionForce_2_2() + spring_force.SetCutOffLength(cutoff_length) + simulator.AddForce(spring_force) + + # Our custom force from the project bindings pushes every cell in +x. + my_force = MyForce_2(1.0) + simulator.AddForce(my_force) + + simulator.Solve() + + print(f"Simulation complete with {cell_population.GetNumRealCells()} cells.") + + +if __name__ == "__main__": + main() diff --git a/bindings/install.sh b/bindings/install.sh new file mode 100755 index 0000000..928208e --- /dev/null +++ b/bindings/install.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install this project's compiled Python bindings into the project virtualenv. +# +# Usage: install.sh +# +# Run after configure.sh and compile.sh. + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import the project environment and helpers. +_here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +source "${_here}/scripts/env.sh" + +# Abort if Python bindings are not set up for this project. +if [[ ! -f "${PROJECT_ROOT}/bindings/config.yaml" ]]; then + echo "Error: Python bindings are not set up for this project." >&2 + echo "Run setup_project.py with Python bindings enabled." >&2 + exit 1 +fi + +require_command python3 +require_configured + +# Check that the project's Python bindings have been compiled. +project_pkg="${CHASTE_BUILD_DIR}/projects/${PROJECT_NAME}/bindings/package" +if [[ ! -d "${project_pkg}" ]]; then + echo "Error: Python bindings package not found at '${project_pkg}'." >&2 + echo "Run compile.sh first." >&2 + exit 1 +fi + +# Check that PyChaste has been compiled. +pychaste_pkg="${CHASTE_BUILD_DIR}/pychaste/package" +if [[ ! -d "${pychaste_pkg}" ]]; then + echo "Error: PyChaste package not found at '${pychaste_pkg}'." >&2 + echo "Run compile.sh first." >&2 + exit 1 +fi + +# Create the project virtualenv if it does not already exist. +# --system-site-packages lets it see native packages provided by the system +# Python (petsc4py and vtk), which are not pip-installable here. The +# virtualenv is shared with SBML if enabled. +if [[ ! -d "${VENV_DIR}" ]]; then + python3 -m venv --system-site-packages "${VENV_DIR}" +fi + +# Warn if PyChaste's runtime dependencies are not visible from the virtualenv. +missing="" +for module in petsc4py vtk; do + "${VENV_DIR}/bin/python" -c "import ${module}" >/dev/null 2>&1 || missing="${missing} ${module}" +done +if [[ -n "${missing}" ]]; then + echo "Warning: PyChaste runtime dependencies not found:${missing}." >&2 + echo "These are provided by the system Python (e.g. in the chaste/base image) and are" >&2 + echo "needed to import the bindings. Install them on your system before using them." >&2 +fi + +# Install PyChaste first (it is a dependency of the project bindings). +"${VENV_DIR}/bin/pip" install "${pychaste_pkg}" + +# Install the project Python bindings. +"${VENV_DIR}/bin/pip" install "${project_pkg}" + +echo "Installed Python bindings to '${VENV_DIR}'." +echo "Activate with: source '${VENV_DIR}/bin/activate'" diff --git a/bindings/package/MANIFEST.in b/bindings/package/MANIFEST.in new file mode 100644 index 0000000..8dc9c7f --- /dev/null +++ b/bindings/package/MANIFEST.in @@ -0,0 +1 @@ +recursive-include template_project *.so diff --git a/bindings/package/pyproject.toml b/bindings/package/pyproject.toml new file mode 100644 index 0000000..aff5d71 --- /dev/null +++ b/bindings/package/pyproject.toml @@ -0,0 +1,34 @@ +# Copyright (c) 2005-2026, University of Oxford. +# All rights reserved. + +# University of Oxford means the Chancellor, Masters and Scholars of the +# University of Oxford, having an administrative office at Wellington +# Square, Oxford OX1 2JD, UK. + +# This file is part of Chaste. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the University of Oxford nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/bindings/package/setup.cfg b/bindings/package/setup.cfg new file mode 100644 index 0000000..e3bae8a --- /dev/null +++ b/bindings/package/setup.cfg @@ -0,0 +1,58 @@ +# Copyright (c) 2005-2026, University of Oxford. +# All rights reserved. + +# University of Oxford means the Chancellor, Masters and Scholars of the +# University of Oxford, having an administrative office at Wellington +# Square, Oxford OX1 2JD, UK. + +# This file is part of Chaste. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the University of Oxford nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +[metadata] +name = template_project +version = 0.1 +author = Chaste User +author_email = example@email.com +description = Python bindings for template_project, a Chaste user project. +keywords = computational biology, scientific +license = BSD-3-Clause +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Science/Research + Topic :: Scientific/Engineering + Operating System :: POSIX + Programming Language :: Python :: 3 + Programming Language :: Python :: Implementation :: CPython + +[options] +zip_safe = False +include_package_data = True +packages = find: +python_requires = >=3.9 +install_requires = + chaste + +[options.packages.find] +exclude = + doc diff --git a/bindings/package/setup.py b/bindings/package/setup.py new file mode 100644 index 0000000..c3c7732 --- /dev/null +++ b/bindings/package/setup.py @@ -0,0 +1,44 @@ +"""Copyright (c) 2005-2026, University of Oxford. +All rights reserved. + +University of Oxford means the Chancellor, Masters and Scholars of the +University of Oxford, having an administrative office at Wellington +Square, Oxford OX1 2JD, UK. + +This file is part of Chaste. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the University of Oxford nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +from setuptools import setup, Distribution + + +class BinaryDistribution(Distribution): + def is_pure(self): + return False + + def has_ext_modules(self): + return True + + +setup(distclass=BinaryDistribution) diff --git a/bindings/package/template_project/__init__.py b/bindings/package/template_project/__init__.py new file mode 100644 index 0000000..9a81e4a --- /dev/null +++ b/bindings/package/template_project/__init__.py @@ -0,0 +1,32 @@ +# Copyright (c) 2005-2026, University of Oxford. +# All rights reserved. +# +# University of Oxford means the Chancellor, Masters and Scholars of the +# University of Oxford, having an administrative office at Wellington +# Square, Oxford OX1 2JD, UK. +# +# This file is part of Chaste. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the University of Oxford nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from ._template_project_all import * diff --git a/sbml/README.md b/sbml/README.md new file mode 100644 index 0000000..eeb8685 --- /dev/null +++ b/sbml/README.md @@ -0,0 +1,74 @@ +# SBML models + +This template turns [SBML](https://sbml.org/) models into Chaste models using +[chaste-sbml](https://github.com/Chaste/chaste-sbml) (see +[documentation](https://chaste.github.io/chaste-sbml/)). For a worked example +see [example/README.md](example/README.md). + +Run every command below from your project's root directory. See the +[top-level README](../README.md) for information about project layout and the build process. + +## Enable SBML support + +When you run `setup_project.py`, answer **yes** to: + +``` +Do you want to create an SBML user project? +``` + +## Convert an SBML model into a Chaste model + +> If you ever need to re-install chaste-sbml or refresh the SBML base classes, run +> `sbml/install.sh`. This is already run during user project setup by `setup_project.py`. + +Activate the virtualenv + +```sh +source .virtualenv/bin/activate +``` + +Convert the model + +```sh +chaste-sbml my_model.xml --output-dir src/ +``` + +> The generated class and file names are derived from the input file name, suffixed +> with `Sbml`. For example `my_model.xml` produces `MyModelSbmlOdeSystem.{hpp,cpp}`. + +See the [command-line options](https://chaste.github.io/chaste-sbml/command-line.html) for +the rest of what `chaste-sbml` can do, including `--model-type` to generate a sub-cellular +reaction network or cell-cycle model, and `--timescale` to set the model's native time unit. + +## Add a test + +Add a test under `test/` that `#include`s the generated model, then list it in +`test/ContinuousTestPack.txt`. See the walkthrough for a full example. + +## Build and run the test + +From the project directory, with `CHASTE_SOURCE_DIR` pointing at your Chaste source: + +```sh +scripts/configure.sh # configure the build +scripts/compile.sh # build the project +scripts/test.sh # run the project's tests +``` + +## The SBML base classes + +`sbml/install.sh` copies these into `src/` from the installed `chaste-sbml` package (via +`chaste-sbml --copy-base-classes`). They are +required by the generated code: + +| File | Purpose | +| --- | --- | +| `AbstractSbmlOdeSystem.{hpp,cpp}` | Base ODE system for a generated SBML model. | +| `AbstractSbmlSrnModel.{hpp,cpp}` | Base sub-cellular reaction network (SRN) model. | +| `AbstractSbmlCellCycleModel.{hpp,cpp}` | Base cell-cycle model. | +| `SbmlEventType.hpp` | Enum of SBML event types (e.g. cell division). | +| `SbmlMath.hpp` | Math helper functions used by generated equations. | +| `SbmlOdeSolverSetup.hpp` | ODE solver setup shared by the generated models. | +| `fortests/SbmlTestHelpers.{hpp,cpp}` | Utility helpers for tests. | +| `fortests/SbmlTestOdeSolution.{hpp,cpp}` | `OdeSolution` recording per-step parameters, for tests. | + diff --git a/sbml/example/Goldbeter1991.xml b/sbml/example/Goldbeter1991.xml new file mode 100644 index 0000000..74d9201 --- /dev/null +++ b/sbml/example/Goldbeter1991.xml @@ -0,0 +1,464 @@ + + + + + +
Goldbeter - Min Mit Oscil
+

Minimal cascade model for the mitotic oscillator involving cyclin and cdc2 kinase.

+
+

This model has been generated by MathSBML 2.4.6 (14-January-2005) 14-January-2005 18:33:39.806932.

+
+

This model is described in the article:

+ +
Goldbeter A.
+
Proc. Natl. Acad. Sci. U.S.A. 1991; 88(20):9107-11
+

Abstract:

+

A minimal model for the mitotic oscillator is presented. The model, built on recent experimental advances, is based on the cascade of post-translational modification that modulates the activity of cdc2 kinase during the cell cycle. The model pertains to the situation encountered in early amphibian embryos, where the accumulation of cyclin suffices to trigger the onset of mitosis. In the first cycle of the bicyclic cascade model, cyclin promotes the activation of cdc2 kinase through reversible dephosphorylation, and in the second cycle, cdc2 kinase activates a cyclin protease by reversible phosphorylation. That cyclin activates cdc2 kinase while the kinase triggers the degradation of cyclin has suggested that oscillations may originate from such a negative feedback loop [Félix, M. A., Labbé, J. C., Dorée, M., Hunt, T. & Karsenti, E. (1990) Nature (London) 346, 379-382]. This conjecture is corroborated by the model, which indicates that sustained oscillations of the limit cycle type can arise in the cascade, provided that a threshold exists in the activation of cdc2 kinase by cyclin and in the activation of cyclin proteolysis by cdc2 kinase. The analysis shows how miototic oscillations may readily arise from time lags associated with these thresholds and from the delayed negative feedback provided by cdc2-induced cyclin degradation. A mechanism for the origin of the thresholds is proposed in terms of the phenomenon of zero-order ultrasensitivity previously described for biochemical systems regulated by covalent modification.

+
+
+

This model is hosted on BioModels Database + and identified by: BIOMD0000000003 + .

+

To cite BioModels Database, please use: BioModels Database: An enhanced, curated and annotated resource for published quantitative kinetic models + .

+
+

To the extent possible under law, all copyright and related or neighbouring rights to this encoded model have been dedicated to the public domain worldwide. Please refer to CC0 Public Domain Dedication + for more information.

+
+ + +
+ + + + + + + + Shapiro + Bruce + + bshapiro@jpl.nasa.gov + + NASA Jet Propulsion Laboratory + + + + + Chelliah + Vijayalakshmi + + viji@ebi.ac.uk + + EMBL-EBI + + + + + + 2005-02-06T23:39:40Z + + + 2013-05-16T14:38:01Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + C + VM1 + + + + + C + Kc + + -1 + + + + + + + + + M + VM3 + + + + + + + + + + + + + + + + + + + + + + + + + cell + vi + + + + + + + + + + + + + + + + + + + + + + + + + + + C + cell + kd + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + C + cell + vd + X + + + + + C + Kd + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + cell + + + 1 + + + -1 + M + + + V1 + + + + + K1 + + + -1 + M + + 1 + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + cell + M + V2 + + + + + K2 + M + + -1 + + + + + + + + + + + + + + + + + + cell + V3 + + + 1 + + + -1 + X + + + + + + + K3 + + + -1 + X + + 1 + + -1 + + + + + + + + + + + + + + + + + cell + V4 + X + + + + + K4 + X + + -1 + + + + + + + + + + +
+
\ No newline at end of file diff --git a/sbml/example/README.md b/sbml/example/README.md new file mode 100644 index 0000000..5f19853 --- /dev/null +++ b/sbml/example/README.md @@ -0,0 +1,102 @@ +# SBML Example: importing the Goldbeter 1991 model from BioModels + +This example imports [BIOMD0000000003](https://biomodels.org/BIOMD0000000003), the +**Goldbeter 1991** minimal cascade model of the mitotic oscillator, into a Chaste user +project as a sub-cellular reaction network (SRN) model, then builds and tests it. + +It assumes you have already created your project from this template and answered **yes** +to the SBML prompt in `setup_project.py`, so that: + +* the SBML base classes are present in `src/`, +* `cell_based` is listed in `CMakeLists.txt`, and +* `chaste-sbml` is installed in `.virtualenv/`. + +Run every command below from your project's root directory. + +## Get the SBML model + +A copy of the model is included next to this walkthrough, so you can just copy it in: + +```sh +cp sbml/example/Goldbeter1991.xml . +``` + +Alternatively, download it from BioModels and rename it yourself. + +> [https://www.biomodels.org/BIOMD0000000003](https://www.biomodels.org/BIOMD0000000003) + +The generated C++ class names come from the file name, so it is a good idea to give the model file a +C++-friendly name. Here we use `Goldbeter1991`. + +## Convert the model into a Chaste model + +Activate the virtualenv + +```sh +source .virtualenv/bin/activate +``` + +We want to create a sub-cellular reaction network for Goldbeter 1991, so use `--model-type srn`: + +```sh +chaste-sbml Goldbeter1991.xml --model-type srn --output-dir src/ +``` + +This generates four files in `src/`: + +* `Goldbeter1991SbmlOdeSystem.hpp` / `.cpp`: the ODE system (state variables `C`, `M`, `X`). +* `Goldbeter1991SbmlSrnModel.hpp` / `.cpp`: the SRN model that wraps the ODE system. + +These have the base classes +- `AbstractSbmlOdeSystem` +- `AbstractSbmlSrnModel` + +## Create the test file + +Copy the example test, +[`TestGoldbeter1991SbmlSrnModel.hpp`](TestGoldbeter1991SbmlSrnModel.hpp), into +your `test/` directory: + +```sh +cp sbml/example/TestGoldbeter1991SbmlSrnModel.hpp test/ +``` + +It builds a cell carrying the imported SRN model, runs it, and checks the behaviour of +cyclin (`C`), active cdc2 kinase (`M`) and active cyclin protease (`X`). + +**Times are written in the model's native units.** Goldbeter 1991 declares no +time unit, so `chaste-sbml` assumes seconds (the default for SBML Level 2, which the model is written in) and therefore +scales the generated derivatives by a `TIMESCALE_MULTIPLIER` factor of 3600 because Chaste integrates in hours. +The test divides its times by that same factor, so `100.0 / TIMESCALE_MULTIPLIER` +means 100 seconds: + +```cpp +constexpr double TIMESCALE_MULTIPLIER = Goldbeter1991SbmlOdeSystem::TIMESCALE_MULTIPLIER; +``` + +> Pass `--timescale ms|s|m|h` when generating if your model's real unit differs. + +Then register the test by adding its file name to `test/ContinuousTestPack.txt`: + +``` +TestGoldbeter1991SbmlSrnModel.hpp +``` + +## Build and run the test + +With `CHASTE_SOURCE_DIR` pointing at your Chaste source tree: + +```sh +scripts/configure.sh # configure the build +scripts/compile.sh # build the project (including the generated model) +scripts/test.sh # run the project's tests +``` + +You should see `TestGoldbeter1991SbmlSrnModel` pass, confirming that the model behaves as expected. + +## Next steps + +* To import a different model, repeat the steps above with your own `.xml` file. +* See the [command-line options](https://chaste.github.io/chaste-sbml/command-line.html) + for the full set, and the [chaste-sbml documentation](https://chaste.github.io/chaste-sbml/) + for everything else. diff --git a/sbml/example/TestGoldbeter1991SbmlSrnModel.hpp b/sbml/example/TestGoldbeter1991SbmlSrnModel.hpp new file mode 100644 index 0000000..216f8c6 --- /dev/null +++ b/sbml/example/TestGoldbeter1991SbmlSrnModel.hpp @@ -0,0 +1,121 @@ +#ifndef TESTGOLDBETER1991SBMLSRNMODEL_HPP_ +#define TESTGOLDBETER1991SBMLSRNMODEL_HPP_ + +#include +#include + +#include + +#include "AbstractCellBasedTestSuite.hpp" +#include "Cell.hpp" +#include "CellPropertyRegistry.hpp" +#include "FixedG1GenerationalCellCycleModel.hpp" +#include "SimulationTime.hpp" +#include "SmartPointers.hpp" +#include "TransitCellProliferativeType.hpp" +#include "WildTypeCellMutationState.hpp" + +// The headers generated from Goldbeter1991.xml by chaste-sbml. The SRN model header +// pulls in the ODE system header, but include it explicitly since we name it below. +#include "Goldbeter1991SbmlOdeSystem.hpp" +#include "Goldbeter1991SbmlSrnModel.hpp" + +// This is a serial test. +#include "FakePetscSetup.hpp" + +namespace +{ +// Native time units per hour. The model declares no time unit, so chaste-sbml +// assumes seconds (the SBML Level 2 default) and scales the derivatives by this +// factor, because Chaste integrates in hours. +constexpr double TIMESCALE_MULTIPLIER = Goldbeter1991SbmlOdeSystem::TIMESCALE_MULTIPLIER; +} // namespace + +class TestGoldbeter1991SbmlSrnModel : public AbstractCellBasedTestSuite +{ +public: + /** + * Goldbeter 1991 is a mitotic *oscillator*: it settles onto a limit cycle + * rather than to a fixed point. We track each variable's range over several + * whole cycles and check that envelope, which is a property of the cycle + * itself and so does not depend on where we stop. + */ + void TestLimitCycleEnvelope() + { + // Written in the model's native units (seconds); converted to hours. + const double sample_time = 100.0 / TIMESCALE_MULTIPLIER; // 4 cycles (~25s each) + const double settle_time = 100.0 / TIMESCALE_MULTIPLIER; // discard the transient start + const double dt = 0.001 / TIMESCALE_MULTIPLIER; + + const double end_time = settle_time + sample_time; + unsigned num_steps = (unsigned)(end_time / dt); + + SimulationTime* p_simulation_time = SimulationTime::Instance(); + p_simulation_time->SetEndTimeAndNumberOfTimeSteps(end_time, num_steps + 1); + + // Create a cell carrying the imported SRN model. + boost::shared_ptr p_healthy_state( + CellPropertyRegistry::Instance()->Get()); + boost::shared_ptr p_transit_type( + CellPropertyRegistry::Instance()->Get()); + + FixedG1GenerationalCellCycleModel* p_cell_model = new FixedG1GenerationalCellCycleModel(); + Goldbeter1991SbmlSrnModel* p_srn_model = new Goldbeter1991SbmlSrnModel(); + + CellPtr p_cell(new Cell(p_healthy_state, p_cell_model, p_srn_model, false, CellPropertyCollection())); + p_cell->SetCellProliferativeType(p_transit_type); + p_cell->InitialiseCellCycleModel(); + p_cell->InitialiseSrnModel(); + + // Step to the end time + constexpr double inf = std::numeric_limits::infinity(); + double min_C = inf, max_C = -inf; + double min_M = inf, max_M = -inf; + double min_X = inf, max_X = -inf; + + while (!p_simulation_time->IsFinished()) + { + p_simulation_time->IncrementTimeOneStep(); + if (p_cell->ReadyToDivide()) + { + p_cell->Divide(); + } + + if (p_simulation_time->GetTime() < settle_time) + { + continue; + } + + // Start recording each variable's range once the transient start has passed. + Goldbeter1991SbmlSrnModel* p_srn + = dynamic_cast(p_cell->GetSrnModel()); + const double C = p_srn->GetStateVariable("C"); // cyclin + const double M = p_srn->GetStateVariable("M"); // active cdc2 kinase + const double X = p_srn->GetStateVariable("X"); // active cyclin protease + + min_C = std::min(min_C, C); + max_C = std::max(max_C, C); + min_M = std::min(min_M, M); + max_M = std::max(max_M, M); + min_X = std::min(min_X, X); + max_X = std::max(max_X, X); + } + + // The limit cycle each variable traces out. A model that stopped + // oscillating or changed amplitude would fail. + TS_ASSERT_DELTA(min_C, 0.1895, 2e-2); + TS_ASSERT_DELTA(max_C, 0.5815, 2e-2); + TS_ASSERT_DELTA(min_M, 0.0060, 1e-2); + TS_ASSERT_DELTA(max_M, 0.7093, 2e-2); + TS_ASSERT_DELTA(min_X, 0.0001, 1e-2); + TS_ASSERT_DELTA(max_X, 0.6416, 2e-2); + + // It must actually be oscillating: a fixed point would collapse each + // range to zero. + TS_ASSERT_LESS_THAN(0.3, max_C - min_C); + TS_ASSERT_LESS_THAN(0.5, max_M - min_M); + TS_ASSERT_LESS_THAN(0.5, max_X - min_X); + } +}; + +#endif /*TESTGOLDBETER1991SBMLSRNMODEL_HPP_*/ diff --git a/sbml/install.sh b/sbml/install.sh new file mode 100755 index 0000000..c976db0 --- /dev/null +++ b/sbml/install.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install chaste-sbml and the Chaste SBML C++ base classes. +# +# Usage: install.sh +# +# This script is run by setup_project.py to install the SBML dependencies. +# Re-run it to refresh chaste-sbml or the base classes. + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import the project environment and helpers. +_here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +source "${_here}/scripts/env.sh" + +# The chaste-sbml release to install. +# Override with: CHASTE_SBML_VERSION=develop sbml/install.sh +CHASTE_SBML_VERSION="${CHASTE_SBML_VERSION:-0.0.1}" + +require_command python3 + +# The SBML generator formats its output with clang-format; warn if absent. +if ! command -v clang-format >/dev/null 2>&1; then + echo "Warning: clang-format is not on PATH; chaste-sbml needs it to format generated code." >&2 +fi + +# Create the project virtualenv if it does not already exist. +# --system-site-packages lets it see native packages provided by the system +# Python (petsc4py and vtk), which are not pip-installable here. The +# virtualenv is shared with Python bindings if enabled. +if [[ ! -d "${VENV_DIR}" ]]; then + python3 -m venv --system-site-packages "${VENV_DIR}" +fi + +# Install chaste-sbml from GitHub. +"${VENV_DIR}/bin/pip" install --upgrade pip +"${VENV_DIR}/bin/pip" install "git+https://github.com/Chaste/chaste-sbml@${CHASTE_SBML_VERSION}" + +# Copy the C++ base classes the generated code depends on into the project's src/, so they +# always match the installed version of chaste-sbml. +"${VENV_DIR}/bin/chaste-sbml" --copy-base-classes --output-dir "${PROJECT_ROOT}/src" + +echo "" +echo "Installed chaste-sbml into '${VENV_DIR}'." +echo "Copied the SBML base classes into '${PROJECT_ROOT}/src'." +echo "Activate the virtualenv with: source '${VENV_DIR}/bin/activate'" +echo "Then convert an SBML model with: chaste-sbml --help" diff --git a/scripts/compile.sh b/scripts/compile.sh new file mode 100755 index 0000000..70e7d39 --- /dev/null +++ b/scripts/compile.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build this project's library, apps and tests. +# +# Usage: compile.sh +# +# Run after configure.sh. Builds with NCORES parallel jobs. + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import the project environment and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/env.sh" + +# Check that cmake is available. +require_command cmake + +# Check that the build directory has been configured. +require_configured + +# Build. +echo "NCORES=${NCORES}" +cd "${CHASTE_BUILD_DIR}" +cmake --build . --target "project_${PROJECT_NAME}" --parallel "${NCORES}" diff --git a/scripts/configure.sh b/scripts/configure.sh new file mode 100755 index 0000000..1be5965 --- /dev/null +++ b/scripts/configure.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Configure the Chaste build for this project. +# +# Usage: configure.sh +# +# Symlinks the project into ${CHASTE_SOURCE_DIR}/projects, where Chaste looks for +# user projects, then runs cmake in ${CHASTE_BUILD_DIR}. With Python bindings +# enabled the cppwg wrappers are generated here too, so re-run this after editing +# bindings/config.yaml. + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import the project environment and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/env.sh" + +# Check that cmake is available and the Chaste source exists. +require_command cmake +require_source + +# Ensure this project is registered under Chaste/projects/. +register_project + +# Create the build directory +mkdir -p "${CHASTE_BUILD_DIR}" + +# Configure +cd "${CHASTE_BUILD_DIR}" +cmake "${CHASTE_SOURCE_DIR}" \ + -DChaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE}" \ + -DChaste_UPDATE_PROVENANCE="${Chaste_UPDATE_PROVENANCE}" diff --git a/scripts/env.sh b/scripts/env.sh new file mode 100644 index 0000000..ee3cfeb --- /dev/null +++ b/scripts/env.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# Shared variables and helpers for the project scripts. +# Source this file from the other scripts (don't run it directly). + +# Resolve all paths relative to this file. +_env_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd -- "${_env_dir}/.." && pwd)" + +# The name of this project is the name of the project directory. +PROJECT_NAME="$(basename "${PROJECT_ROOT}")" + +# The shared project virtualenv, used by the Python bindings and SBML install +# scripts which create it on demand. +VENV_DIR="${PROJECT_ROOT}/.virtualenv" + +# If CHASTE_SOURCE_DIR is not set, try to find it in a few common locations. +if [[ -z "${CHASTE_SOURCE_DIR:-}" ]]; then + for _candidate in \ + "${PROJECT_ROOT}/../Chaste" \ + "${PROJECT_ROOT}/../../Chaste" \ + "${HOME}/Chaste" \ + "/home/chaste/src" + do + if [[ -f "${_candidate}/CMakeLists.txt" ]]; then + CHASTE_SOURCE_DIR="$(cd -- "${_candidate}" && pwd)" + break + fi + done +elif [[ ! -f "${CHASTE_SOURCE_DIR}/CMakeLists.txt" ]]; then + # Warn if the supplied CHASTE_SOURCE_DIR doesn't look right. + echo "Warning: CHASTE_SOURCE_DIR='${CHASTE_SOURCE_DIR}' is not a Chaste source directory." >&2 +fi +CHASTE_SOURCE_DIR="${CHASTE_SOURCE_DIR:-}" + +# Left empty when no Chaste source was found, so it is never a bare "/projects". +CHASTE_PROJECTS_DIR="${CHASTE_SOURCE_DIR:+${CHASTE_SOURCE_DIR}/projects}" + +CHASTE_BUILD_DIR="${CHASTE_BUILD_DIR:-${PROJECT_ROOT}/build}" +if [[ "${CHASTE_BUILD_DIR}" == "${PROJECT_ROOT}" || "${CHASTE_BUILD_DIR}" == "${CHASTE_SOURCE_DIR}" ]]; then + echo "Error: CHASTE_BUILD_DIR must not be the project root or Chaste source directory." >&2 + echo "Set CHASTE_BUILD_DIR to a separate build directory." >&2 + exit 1 +fi + +if [[ -z "${CHASTE_TEST_OUTPUT:-}" ]]; then + export CHASTE_TEST_OUTPUT="${PROJECT_ROOT}/output" +fi + +Chaste_UPDATE_PROVENANCE="${Chaste_UPDATE_PROVENANCE:-OFF}" + +# Enable PyChaste if this project has Python bindings set up. The root +# CMakeLists.txt gates add_subdirectory(bindings) on the same setting. +if [[ -f "${PROJECT_ROOT}/bindings/config.yaml" ]]; then + Chaste_ENABLE_PYCHASTE=ON +else + Chaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE:-OFF}" +fi + +# Set the number of parallel jobs for building and testing +if ! [[ "${NCORES:-}" =~ ^[1-9][0-9]*$ ]]; then + NCORES="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1)" +fi + +# Abort with an error if the given command is not on PATH. +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Error: $1 is not available on PATH." >&2 + exit 1 + fi +} + +# Abort unless the build has been configured. +require_configured() { + if [[ ! -f "${CHASTE_BUILD_DIR}/CMakeCache.txt" ]]; then + echo "Error: build is not configured at '${CHASTE_BUILD_DIR}'. Run configure.sh first." >&2 + exit 1 + fi +} + +# Abort unless the Chaste source directory exists. +require_source() { + if [[ ! -f "${CHASTE_SOURCE_DIR}/CMakeLists.txt" ]]; then + echo "Error: '${CHASTE_SOURCE_DIR}' is not a Chaste source directory." >&2 + echo "Set CHASTE_SOURCE_DIR to the location of your Chaste source." >&2 + exit 1 + fi +} + +# Create a symlink for this project under Chaste/projects/ if necessary. +# Chaste only builds projects that appear there. Project directories can be +# either placed directly under Chaste/projects/ or symlinked there. +register_project() { + require_source + + local _project_link="${CHASTE_PROJECTS_DIR}/${PROJECT_NAME}" + + mkdir -p "${CHASTE_PROJECTS_DIR}" + + if [[ -L "${_project_link}" && "${_project_link}" -ef "${PROJECT_ROOT}" ]]; then + : # Already registered: a symlink under Chaste/projects/ points back to this project. + elif [[ ! -L "${_project_link}" && "${_project_link}" -ef "${PROJECT_ROOT}" ]]; then + : # Already registered: the project itself lives directly under Chaste/projects/. + elif [[ -L "${_project_link}" ]]; then + # Repoint a stale/dangling symlink. + ln -sfn "${PROJECT_ROOT}" "${_project_link}" + echo "Re-registered project '${PROJECT_NAME}' under '${CHASTE_PROJECTS_DIR}'." + elif [[ -e "${_project_link}" ]]; then + # Another project already exists with this name. + echo "Error: '${_project_link}' already exists and is not this project." >&2 + echo "Remove or rename it, then re-run configuration." >&2 + exit 1 + else + # Create a new symlink under Chaste/projects/. + ln -s "${PROJECT_ROOT}" "${_project_link}" + echo "Symlinked project '${PROJECT_NAME}' under '${CHASTE_PROJECTS_DIR}'." + fi +} diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..ff52c15 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Run this project's tests. +# +# Usage: test.sh +# +# Run after compile.sh. Runs the tests labelled for this project with NCORES +# parallel cores. Simulation output is written to ${CHASTE_TEST_OUTPUT}. + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import the project environment and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/env.sh" + +# Check that ctest is available +require_command ctest + +# Check that the build directory has been configured +require_configured + +# Make sure the test output directory exists +mkdir -p "${CHASTE_TEST_OUTPUT}" +echo "CHASTE_TEST_OUTPUT=${CHASTE_TEST_OUTPUT}" + +# Run tests +echo "NCORES=${NCORES}" +cd "${CHASTE_BUILD_DIR}" +ctest -j"${NCORES}" -V -L "project_${PROJECT_NAME}$" diff --git a/setup_project.py b/setup_project.py index 1984c34..e21e24a 100644 --- a/setup_project.py +++ b/setup_project.py @@ -1,155 +1,352 @@ -"""Copyright (c) 2005-2023, University of Oxford. -All rights reserved. - -University of Oxford means the Chancellor, Masters and Scholars of the -University of Oxford, having an administrative office at Wellington -Square, Oxford OX1 2JD, UK. - -This file is part of Chaste. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of the University of Oxford nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) 2005-2026, University of Oxford. +# All rights reserved. +# +# University of Oxford means the Chancellor, Masters and Scholars of the +# University of Oxford, having an administrative office at Wellington +# Square, Oxford OX1 2JD, UK. +# +# This file is part of Chaste. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the University of Oxford nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Set up a Chaste user project from this template. + +Run this script from the project directory once it has been renamed to your project name. """ import os -from functools import partial - - -def find_and_replace(filename, old_string, new_string): - # Read the file - f = open(filename, 'r') - file_contents = f.read() - f.close() - - # Write to the file - f = open(filename, 'w') - f.write(file_contents.replace(old_string, new_string)) - f.close() - - return - - -def ask_for_response(question): - # Display the question - print(question) - +import re +import shutil +import subprocess + + +class Settings: + """Paths and substitutions for this template.""" + + # The Chaste components the template depends on by default. + DEFAULT_COMPONENTS = ["continuum_mechanics", "global", "io", "linalg", "mesh", "ode", "pde"] + + # The optional Chaste components the user can choose to depend on. + OPTIONAL_COMPONENTS = ["cell_based", "crypt", "heart", "lung"] + + def __init__(self) -> None: + """Set the paths and substitutions from the current project directory.""" + # The project directory and its name (taken from the directory this script lives in). + self.PROJECT_ROOT = os.path.dirname(os.path.realpath(__file__)) + self.PROJECT_NAME = os.path.basename(self.PROJECT_ROOT) + + # Full paths to the example source files. + self.TEMPLATE_SOURCE_FILES = [ + os.path.join(self.PROJECT_ROOT, "apps", "src", "ExampleApp.cpp"), + os.path.join(self.PROJECT_ROOT, "src", "Hello.cpp"), + os.path.join(self.PROJECT_ROOT, "src", "Hello.hpp"), + os.path.join(self.PROJECT_ROOT, "test", "TestHello.hpp"), + ] + + # Substitutions for the project name in the CMakeLists.txt files. + self.BASE_CMAKELISTS = os.path.join(self.PROJECT_ROOT, "CMakeLists.txt") + self.APPS_CMAKELISTS = os.path.join(self.PROJECT_ROOT, "apps", "CMakeLists.txt") + self.TEST_CMAKELISTS = os.path.join(self.PROJECT_ROOT, "test", "CMakeLists.txt") + + # Map of template text -> project text applied to the source and test files. + # These are deliberately specific to avoid rewriting the printed "Hello world" message. + self.SOURCE_SUBSTITUTIONS = { + " TestHello": f" TestHello_{self.PROJECT_NAME}", + "TestHello.hpp": f"TestHello_{self.PROJECT_NAME}.hpp", + "HELLO": f"HELLO_{self.PROJECT_NAME.upper()}", + "Hello world(": f"Hello_{self.PROJECT_NAME} world(", + "class Hello": f"class Hello_{self.PROJECT_NAME}", + "Hello::": f"Hello_{self.PROJECT_NAME}::", + "Hello(": f"Hello_{self.PROJECT_NAME}(", + "Hello.hpp": f"Hello_{self.PROJECT_NAME}.hpp", + } + + self.TEST_PACK_FILES = [os.path.join(self.PROJECT_ROOT, "test", "ContinuousTestPack.txt")] + + # SBML and Python bindings capabilities live in separate directories. + self.BINDINGS_DIR = os.path.join(self.PROJECT_ROOT, "bindings") + self.SBML_DIR = os.path.join(self.PROJECT_ROOT, "sbml") + + # Python binding template files. + self.PYTHON_BINDING_FILES = [ + os.path.join(self.BINDINGS_DIR, "config.yaml"), + os.path.join(self.BINDINGS_DIR, "CMakeLists.txt"), + os.path.join(self.BINDINGS_DIR, "package", "MANIFEST.in"), + os.path.join(self.BINDINGS_DIR, "package", "setup.cfg"), + os.path.join(self.BINDINGS_DIR, "package", "template_project", "__init__.py"), + ] + + # Substitutions applied to all Python binding files. + self.PYTHON_BINDING_SUBSTITUTIONS = { + "template_project": self.PROJECT_NAME, + } + + # Additional substitutions applied only to bindings/config.yaml. + self.PYTHON_CONFIG_SUBSTITUTIONS = { + "name: Hello": f"name: Hello_{self.PROJECT_NAME}", + } + + # Python package template directory (renamed to / during setup). + self.PYTHON_PKG_TEMPLATE_DIR = os.path.join(self.BINDINGS_DIR, "package", "template_project") + + # The SBML install script for chaste-sbml, used to set up SBML support. + self.SBML_INSTALL_SCRIPT = os.path.join(self.SBML_DIR, "install.sh") + + +def find_and_replace(filename: str, old_string: str, new_string: str) -> None: + """Replace every occurrence of old_string with new_string in a file, in place.""" + with open(filename, "r") as f: + contents = f.read() + with open(filename, "w") as f: + f.write(contents.replace(old_string, new_string)) + + +def ask_for_response(question: str, default: bool = False) -> bool: + """Prompt the user with a yes/no question and return the answer as a bool. + + An empty response returns default; any unrecognised response re-prompts. + """ # Define permitted yes/no answers - yes = {'yes', 'y', 'ye', ''} - no = {'no', 'n'} + yes = {"yes", "y", "ye"} + no = {"no", "n"} - # Take the lower case raw input - choice = input().lower() + # Show the default option in uppercase + options = "[Y/n]" if default else "[y/N]" + choice = input(f"{question} {options} ").lower() # Decide on the choice - if choice in yes: + if choice == "": + return default + elif choice in yes: return True elif choice in no: return False else: - ask_for_response("Please respond with yes or no:") + return ask_for_response("Please respond with yes or no:", default) + +def append_to_file_name(text_to_append: str, file: str) -> str: + """Insert text_to_append before the file's extension and rename it. -# Appends text_to_append before the file extension -def append_to_file_name(text_to_append, file): - new_name = file.replace('.', text_to_append + '.') + Returns the new path, e.g. 'Hello.cpp' -> 'Hello_myproject.cpp'. + """ + root, ext = os.path.splitext(file) + new_name = root + text_to_append + ext os.rename(file, new_name) return new_name -# Converts a project name to something that will be valid to append to a C++ class name -def sanitize_project_name(project_name): - return ''.join(filter(lambda char: char.isalpha() or char.isnumeric(), project_name)) - - -def main(): - # The absolute path to the project directory - path_to_project = os.path.dirname(os.path.realpath(__file__)) +def print_banner(*lines: str) -> None: + """Print the given lines framed in a banner box.""" + width = max(len(line) for line in lines) + border = "*" * (width + 4) + print(border) + for line in lines: + print(f"* {line.ljust(width)} *") + print(border) - # Identify the name of the project - project_name = os.path.basename(path_to_project) - # Paths to the CMakeLists.txt files - base_cmakelists = os.path.join(path_to_project, 'CMakeLists.txt') - apps_cmakelists = os.path.join(path_to_project, 'apps', 'CMakeLists.txt') - test_cmakelists = os.path.join(path_to_project, 'test', 'CMakeLists.txt') - - # Files to append project name to - this avoids conflicts if mutliple projects are generated from the template project - files_requiring_append = [os.path.join(path_to_project, 'apps', 'src', 'ExampleApp.cpp'), - os.path.join(path_to_project, 'src', 'Hello.cpp'), - os.path.join(path_to_project, 'src', 'Hello.hpp'), - os.path.join(path_to_project, 'test', 'TestHello.hpp')] - - # Append project name to required files - append_project_name = partial(append_to_file_name, '_' + project_name) - appended_file_names = list(map(append_project_name, files_requiring_append)) - - # Perform the find-and-replace tasks to update the template project source - sanitized_name = sanitize_project_name(project_name) - - substitutions = { # These are very specific to avoid rewriting the printed out "Hello world" message - " TestHello": " TestHello_" + sanitized_name, - "TestHello.hpp": "TestHello_" + project_name + ".hpp", - "HELLO": "HELLO_" + sanitized_name.upper(), - "Hello world(": "Hello_" + sanitized_name + " world(", - "class Hello": "class Hello_" + sanitized_name, - "Hello::": "Hello_" + sanitized_name + "::", - "Hello(": "Hello_" + sanitized_name + "(", - "TestHello.hpp": "TestHello_" + project_name + ".hpp", - "Hello.hpp": "Hello_" + project_name + ".hpp" - } - - files_to_sub = appended_file_names + [str(os.path.join(path_to_project, 'test', 'ContinuousTestPack.txt'))] +def setup_bindings(settings: Settings) -> None: + """Wire the Python bindings scaffolding in bindings/ to this project's name.""" + # Substitute the project name into all Python binding files + for file in settings.PYTHON_BINDING_FILES: + for old, new in settings.PYTHON_BINDING_SUBSTITUTIONS.items(): + find_and_replace(file, old, new) + # Substitute class names and headers into config.yaml + config_yaml = os.path.join(settings.BINDINGS_DIR, "config.yaml") + for old, new in settings.PYTHON_CONFIG_SUBSTITUTIONS.items(): + find_and_replace(config_yaml, old, new) + + # Rename the Python package directory (template_project/ -> /) + new_pkg_dir = os.path.join(settings.BINDINGS_DIR, "package", settings.PROJECT_NAME) + os.rename(settings.PYTHON_PKG_TEMPLATE_DIR, new_pkg_dir) + + # The virtualenv is created later, by bindings/install.sh, once there is a + # compiled package to install into it. + + +def remove_bindings(settings: Settings) -> None: + """Remove the whole bindings directory. + + Its example and install script are only meaningful with bindings enabled, so a + project that declines them keeps none of it. + """ + shutil.rmtree(settings.BINDINGS_DIR) + + +def setup_sbml(settings: Settings) -> None: + """Set up SBML support by running sbml/install.sh. + + The script creates the project virtualenv if needed, installs chaste-sbml into it, + and copies the SBML base classes into src/. On any failure this is non-fatal: it + names the script so the user can finish the install themselves. + """ + try: + subprocess.run([settings.SBML_INSTALL_SCRIPT], check=True) + except (subprocess.CalledProcessError, OSError) as error: + print("") + print(f"WARNING: could not set up SBML support automatically ({error}).") + print("Set it up manually by re-running:") + print(f" {settings.SBML_INSTALL_SCRIPT}") + + +def remove_sbml(settings: Settings) -> None: + """Remove the whole sbml directory, as for the bindings.""" + shutil.rmtree(settings.SBML_DIR) + + +def is_setup(settings: Settings) -> bool: + """Return True if the project has already been set up (any of the example files are renamed).""" + return not all(os.path.exists(file) for file in settings.TEMPLATE_SOURCE_FILES) + + +def setup(settings: Settings) -> None: + """Customise the template for this project, after confirming the chosen settings.""" + # Abort if the project has already been configured. + if is_setup(settings): + print_banner( + "ERROR: This Chaste user project has already been set up.", + "If you want to run setup again, use a fresh copy of the template.", + "", + "Alternatively, try the steps below to reset this template.", + "Note that any changes you have made will be lost forever!!!", + "1. Run 'git checkout -- .' in the project directory to restore the original files.", + "2. Run 'git clean -f -- .' in the project directory to remove all new files.", + "3. Run this script again to set up the project.", + ) + raise SystemExit(1) + + # Confirm the template directory has been renamed to the project name before making any changes. + print("Make sure to rename the 'template_project' directory to your project name before running this script.") + print(f"The current project name is '{settings.PROJECT_NAME}' (same as the current directory name).") + if not ask_for_response("Do you want to proceed?", default=True): + return + + # Check that the project name is a valid C++ name. + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", settings.PROJECT_NAME): + print( + f"ERROR: the project name '{settings.PROJECT_NAME}' is not a valid C++ name. " + "Renaming the directory is recommended." + ) + raise SystemExit(1) + + # Ask which Chaste components this project depends on + components: list[str] = [] + for component in settings.OPTIONAL_COMPONENTS: + if ask_for_response(f"Does this project depend on the {component} component?"): + components.append(component) + + # Ask whether to create Python bindings + python_bindings = ask_for_response("Do you want to create Python bindings for this project?") + + # Ask whether to create an SBML user project + sbml = ask_for_response("Do you want to create an SBML user project?") + if sbml and "cell_based" not in components: + # The SBML base classes subclass cell_based classes, so the component is required. + components.append("cell_based") + + # Summarise the chosen options and confirm before making any changes + print("") + print("Summary:") + print(f" Project name: {settings.PROJECT_NAME}") + print(f" Chaste components: {', '.join(components) if components else '(template default)'}") + print(f" Python bindings: {'Yes' if python_bindings else 'No'}") + print(f" SBML project: {'Yes' if sbml else 'No'}") + print("") + if not ask_for_response("Proceed with these settings?"): + print("No changes made.") + return + + # Append the project name to the example source files (avoids clashes between projects) + suffix = "_" + settings.PROJECT_NAME + appended_file_names = [append_to_file_name(suffix, file) for file in settings.TEMPLATE_SOURCE_FILES] + + # Substitute the project name into the source and test files + files_to_sub = appended_file_names + settings.TEST_PACK_FILES for file in files_to_sub: - for old, new in substitutions.items(): + for old, new in settings.SOURCE_SUBSTITUTIONS.items(): find_and_replace(file, old, new) + # Set the project name in the CMakeLists.txt files + find_and_replace( + settings.BASE_CMAKELISTS, "chaste_do_project(template_project)", f"chaste_do_project({settings.PROJECT_NAME})" + ) + find_and_replace( + settings.APPS_CMAKELISTS, + "chaste_do_apps_project(template_project)", + f"chaste_do_apps_project({settings.PROJECT_NAME})", + ) + find_and_replace( + settings.TEST_CMAKELISTS, + "chaste_do_test_project(template_project)", + f"chaste_do_test_project({settings.PROJECT_NAME})", + ) + + # Replace the default components if any optional components were selected + if components: + find_and_replace(settings.BASE_CMAKELISTS, " ".join(settings.DEFAULT_COMPONENTS), " ".join(components)) + + # Apply each optional capability, or remove its directory entirely. + if python_bindings: + setup_bindings(settings) + else: + remove_bindings(settings) - # Perform the find-and-replace tasks to update the template project cmake - find_and_replace(base_cmakelists, 'chaste_do_project(template_project', 'chaste_do_project(' + project_name) - find_and_replace(apps_cmakelists, 'chaste_do_apps_project(template_project', 'chaste_do_apps_project(' + project_name) - find_and_replace(test_cmakelists, 'chaste_do_test_project(template_project', 'chaste_do_test_project(' + project_name) + if sbml: + setup_sbml(settings) + else: + remove_sbml(settings) - # Amend the components - components_list = [] + # Summarise the changes that were made + print("") + print("Setup complete.") + print(f"The following changes were made for project '{settings.PROJECT_NAME}':") + print("* Substituted the project name in all files.") - if ask_for_response("Does this project depend on the cell_based component? [Y/n] "): - components_list.append('cell_based') + if components: + print(f"* Set Chaste components in CMakeLists.txt to: {', '.join(components)}.") + print("* Renamed the template files:") - if ask_for_response("Does this project depend on the crypt component? [Y/n] "): - components_list.append('crypt') + for original, renamed in zip(settings.TEMPLATE_SOURCE_FILES, appended_file_names): + print(f" - {os.path.basename(original)} -> {os.path.basename(renamed)}") - if ask_for_response("Does this project depend on the heart component? [Y/n] "): - components_list.append('heart') + if python_bindings: + print("* Set up Python bindings in bindings/.") + else: + print("* Removed the Python bindings scaffolding in bindings/.") - if ask_for_response("Does this project depend on the lung component? [Y/n] "): - components_list.append('lung') + if sbml: + print("* Installed chaste-sbml into the project virtualenv and copied the SBML base classes into src/.") + print(" See sbml/README.md and sbml/example/ for how to import an SBML model.") + else: + print("* Removed the SBML scaffolding in sbml/.") - # If the list is non-empty, replace the default components - if components_list: - components_string = ' '.join(components_list) - default_components = 'continuum_mechanics global io linalg mesh ode pde' - find_and_replace(base_cmakelists, default_components, components_string) +def main() -> None: + """Set up the project from the template.""" + settings = Settings() + setup(settings) if __name__ == "__main__": diff --git a/src/Hello.cpp b/src/Hello.cpp index d7434d2..b468217 100644 --- a/src/Hello.cpp +++ b/src/Hello.cpp @@ -1,6 +1,6 @@ /* -Copyright (c) 2005-2023, University of Oxford. +Copyright (c) 2005-2026, University of Oxford. All rights reserved. University of Oxford means the Chancellor, Masters and Scholars of the diff --git a/src/Hello.hpp b/src/Hello.hpp index 356a2eb..fb13e6d 100644 --- a/src/Hello.hpp +++ b/src/Hello.hpp @@ -1,6 +1,6 @@ /* -Copyright (c) 2005-2023, University of Oxford. +Copyright (c) 2005-2026, University of Oxford. All rights reserved. University of Oxford means the Chancellor, Masters and Scholars of the diff --git a/test/ContinuousTestPack.txt b/test/ContinuousTestPack.txt index 0e80dc8..0608b55 100644 --- a/test/ContinuousTestPack.txt +++ b/test/ContinuousTestPack.txt @@ -1 +1 @@ -TestHello.hpp \ No newline at end of file +TestHello.hpp diff --git a/test/TestHello.hpp b/test/TestHello.hpp index bd48d32..7c32e1d 100644 --- a/test/TestHello.hpp +++ b/test/TestHello.hpp @@ -1,6 +1,6 @@ /* -Copyright (c) 2005-2023, University of Oxford. +Copyright (c) 2005-2026, University of Oxford. All rights reserved. University of Oxford means the Chancellor, Masters and Scholars of the