diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..79025f5 --- /dev/null +++ b/.flake8 @@ -0,0 +1,11 @@ +[flake8] +max_line_length = 120 + +exclude = + __pycache__, + .git, + .github, + .venv, + build, + output, + venv, diff --git a/.github/workflows/test-force-example.yml b/.github/workflows/test-force-example.yml new file mode 100644 index 0000000..c4687c9 --- /dev/null +++ b/.github/workflows/test-force-example.yml @@ -0,0 +1,65 @@ +name: Test Force Example + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +concurrency: + group: test-force-example-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-force-example: + 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 }} + # Yes to proceed, Yes to cell_based, No to crypt/heart/lung, Yes to Python bindings, No to SBML, Yes to confirm + printf 'y\ny\nn\nn\nn\ny\nn\ny\n' | python3 setup_project.py + + - name: Add MyForce to the project bindings + run: | + cd ${{ env.PROJECT_DIR }} + cp examples/my_force/MyForce.hpp examples/my_force/MyForce.cpp src/ + sed -i.bak 's/^source_includes:$/&\n - MyForce.hpp/' dynamic/config.yaml + sed -i.bak 's/^ classes:$/&\n - name: MyForce/' dynamic/config.yaml + # Import PyChaste's module and declare AbstractForce as an external base + sed -i.bak 's/^ - name: all$/&\n imports:/' dynamic/config.yaml + sed -i.bak 's/^ imports:$/&\n - chaste._pychaste_all/' dynamic/config.yaml + sed -i.bak 's/^ - chaste._pychaste_all$/&\n external_bases:/' dynamic/config.yaml + sed -i.bak 's/^ external_bases:$/&\n - AbstractForce/' dynamic/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 }}/scripts/bindings_install.sh + + - name: Run the MyForce simulation from Python + run: ${{ env.PROJECT_DIR }}/.virtualenv/bin/python ${{ env.PROJECT_DIR }}/examples/my_force/run_my_force.py 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..461eed6 --- /dev/null +++ b/.github/workflows/test-python-project.yml @@ -0,0 +1,64 @@ +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: + 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 }} + # Yes to Python bindings, no to SBML, yes to final confirmation + printf '\n\n\n\n\ny\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: Install + run: | + ${{ env.PROJECT_DIR }}/scripts/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!') + " diff --git a/.github/workflows/test-sbml-project.yml b/.github/workflows/test-sbml-project.yml new file mode 100644 index 0000000..ca1834b --- /dev/null +++ b/.github/workflows/test-sbml-project.yml @@ -0,0 +1,66 @@ +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 and curl + run: apt-get update && apt-get install -y clang-format curl + + - 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 SBML code generator + run: ${{ env.PROJECT_DIR }}/scripts/sbml_install.sh + + - name: Import the Goldbeter 1991 model + run: | + cd ${{ env.PROJECT_DIR }} + # Use the vendored model (examples/goldbeter_1991/Goldbeter1991.xml) so the + # job does not depend on reaching BioModels at run time. + cp examples/goldbeter_1991/Goldbeter1991.xml . + .virtualenv/bin/chaste-sbml generate Goldbeter1991.xml --model-type srn --output-dir src/ + cp examples/goldbeter_1991/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 c446709..e2b3ca3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}/dynamic") + add_subdirectory(dynamic) +endif() diff --git a/README.md b/README.md index a8d13a4..581a31f 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,206 @@ 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. + +## SBML models + +This template can turn an [SBML](https://sbml.org/) model into a Chaste model using +[chaste-codegen-sbml](https://github.com/Chaste/chaste-codegen-sbml). For a complete, +worked example see [examples/goldbeter_1991/README.md](examples/goldbeter_1991/README.md). + +### 1. Enable SBML support + +When you run `setup_project.py`, answer **yes** to: + +``` +Do you want to create an SBML user project? +``` + +### 2. Activate the virtualenv + +```sh +source .virtualenv/bin/activate +``` + +(If you ever need to (re)install the generator or refresh the base classes by hand, run +`scripts/sbml_install.sh`.) + +### 3. Convert an SBML model into a Chaste model + +```sh +chaste-sbml generate my_model.xml --model-type srn --output-dir src/ +``` + +* `--model-type` is one of `generic`, `srn` (sub-cellular reaction network), or + `cell-cycle`. +* 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}`, plus + `MyModelSbmlSrnModel.{hpp,cpp}` (for `srn`) or `MyModelSbmlCellCycleModel.{hpp,cpp}` + (for `cell-cycle`). + +### 4. Write 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. + +### 5. Build and run the test + +From the project directory (with `CHASTE_SOURCE_DIR` pointing at your Chaste source): + +```sh +scripts/configure.sh # register the project and configure the Chaste build +scripts/compile.sh # build the project +scripts/test.sh # run the project's tests +``` + +### The SBML base classes + +`setup_project.py` copies these into `src/` from the installed `chaste-sbml` package (via +`chaste-sbml copy-base-classes`), so they always match the generator version. 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,cpp}` | Math helper functions used by generated equations. | +| `fortests/SbmlTestHelpers.{hpp,cpp}` | Utility helpers for tests. | +| `fortests/SbmlTestOdeSolution.{hpp,cpp}` | `OdeSolution` recording per-step parameters, for tests. | + +## Python bindings + +This template can build [PyChaste](https://chaste.github.io/) Python bindings for your +project's C++ classes using [cppwg](https://github.com/Chaste/cppwg), so you can drive +your project from Python. For a complete, worked example — writing a new C++ `Force` and +using it in a Python simulation — see +[examples/my_force/README.md](examples/my_force/README.md). + +### Prerequisites + +Building and installing the bindings needs, in addition to a Chaste source tree: + +* [cppwg](https://github.com/Chaste/cppwg) (with cross-module inheritance support — the + `imports` and `external_bases` config keys), used at configure time to generate the + wrappers, and +* PyChaste's native runtime dependencies — `petsc4py`, `mpi4py` and `vtk` — importable from + the Python interpreter used to run your bindings. + +The latest [`chaste/base`](https://hub.docker.com/r/chaste/base) Docker image already provides all +of these, and `scripts/bindings_install.sh` creates the project virtualenv with +`--system-site-packages` so it can see them. Pull the latest image with `docker pull chaste/base`. If you are **not** working inside that image, +`pip install` these dependencies into the system Python (or the project virtualenv) yourself +before running configuration: `petsc4py`, `mpi4py`, and `vtk`, which all build against the versions of PETSc, MPI, and VTK on your system. + +### 1. 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: + +* `dynamic/config.yaml` — the cppwg configuration listing the classes to wrap, +* `dynamic/CMakeLists.txt` — builds the bindings as part of the project, +* `src/py/` — the installable Python package (renamed to `/`). + +If you answer no, this scaffolding is removed and the project is a plain C++ project. + +### 2. Configure and compile + +From the project directory (with `CHASTE_SOURCE_DIR` pointing at your Chaste source): + +```sh +scripts/configure.sh # registers the project and configures the Chaste build +scripts/compile.sh # builds the project, including the Python bindings +``` + +`configure.sh` automatically enables PyChaste when `dynamic/config.yaml` is present. + +### 3. Install the bindings into the project virtualenv + +```sh +scripts/bindings_install.sh +``` + +This creates a project virtualenv in `.virtualenv/` and installs both PyChaste and your +project's bindings package into it. + +### 4. Activate the virtualenv and use your project from Python + +```sh +source .virtualenv/bin/activate +``` + +```python +import myproject # replace with your project name +hello = myproject.Hello_myproject("Hello from Python!") +print(hello.GetMessage()) +``` + +### 5. Add your own C++ classes to the bindings + +To expose a new class, add it to `src/`, then list it in `dynamic/config.yaml`: + +* add the header to `source_includes:`, and +* add `- name: YourClass` under the `all` module's `classes:`. + +Then recompile (`scripts/compile.sh`) and reinstall (`scripts/bindings_install.sh`). + +If your class inherits from a Chaste class that is wrapped in PyChaste (for example a +custom `AbstractForce` subclass), import PyChaste's compiled module under the `all` +module's `imports:` and name the base class under `external_bases:` so cppwg can link the +inheritance across modules: + +```yaml +modules: + - name: all + imports: + - chaste._pychaste_all + external_bases: + - AbstractForce + classes: + - name: YourClass +``` + +> **Class names for templated classes.** A templated class is wrapped once per +> dimension, with the dimensions appended after an underscore. For example a class +> templated over `` becomes `YourClass_2` / `YourClass_3`, and one templated +> over `` becomes `YourClass_2_2` / `YourClass_3_3`. (PyChaste's own +> classes additionally expose no-underscore aliases such as `OffLatticeSimulation2_2`.) If +> you are unsure of a generated name, run +> `print([n for n in dir(myproject) if "YourClass" in n])`. + +See [examples/my_force/README.md](examples/my_force/README.md) for a full walkthrough of +this process. + +### Troubleshooting the bindings + +**Wrapper generation fails during `scripts/configure.sh`.** cppwg runs at configure time, so +an error in `dynamic/config.yaml` (a misspelt class, a missing header, an unmatched template +signature) fails the configure step immediately. The full cppwg output is written to +`cppwg.log` in the project's build tree, at +`${CHASTE_BUILD_DIR}/projects//dynamic/cppwg.log`; read it to see which class +or header caused the failure, fix `dynamic/config.yaml`, and re-run `scripts/configure.sh`. +Wrappers are regenerated on every configure, so your edits are always picked up. + +**Configure fails with "No Python wrapper sources were generated".** cppwg ran but produced +no wrappers — usually because no classes under the `all` module actually matched (for +example the header was not found on the include path, or every class name was misspelt). +Check the `classes:` and `source_includes:` entries in `dynamic/config.yaml` against +`cppwg.log`, then re-configure. + +**Compilation fails with missing PyChaste or Chaste headers.** Make sure PyChaste is enabled +(it is automatically when `dynamic/config.yaml` is present) and that your Chaste source tree +is built with PyChaste support. To force a clean rebuild of just the wrappers, +`make _wrappers` from the build directory, or run `scripts/clean.sh` followed by `scripts/configure.sh`. + +**`import myproject` fails at runtime**, typically with an error importing `petsc4py`, +`mpi4py` or `vtk`. Those are PyChaste's native runtime dependencies and must be importable +from the interpreter running your script — see [Prerequisites](#prerequisites). Activate the +project virtualenv (`source .virtualenv/bin/activate`), which is created with +`--system-site-packages` so it can see them, or install them yourself when working outside +the `chaste/base` image. diff --git a/dynamic/CMakeLists.txt b/dynamic/CMakeLists.txt new file mode 100644 index 0000000..cd40779 --- /dev/null +++ b/dynamic/CMakeLists.txt @@ -0,0 +1,146 @@ +# 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) + +# Add VTK 9+ includes +if(TARGET VTK::CommonCore) + get_target_property(_include_dirs VTK::CommonCore INTERFACE_INCLUDE_DIRECTORIES) + foreach(_dir IN LISTS _include_dirs) + if(_dir MATCHES "^\\$$") + # VTK 9.5+ have $ generator expressions in their includes + list(APPEND PROJECT_INCLUDE_DIRS "${CMAKE_MATCH_1}") + elseif(NOT _dir MATCHES "^\\$<") + # VTK 9.0 to 9.4 have plain include paths, no generator expressions + list(APPEND PROJECT_INCLUDE_DIRS "${_dir}") + endif() + endforeach() +endif() + +# Add PETSc4Py includes +list(APPEND PROJECT_INCLUDE_DIRS ${PETSC4PY_INCLUDES}) + +# Add the project's own source directory so cppwg can find project headers +list(APPEND PROJECT_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/../src) + +# Add pychaste typecaster includes +header_dirs("${CMAKE_SOURCE_DIR}/pychaste/dynamic" _include_dirs) +list(APPEND PROJECT_INCLUDE_DIRS ${_include_dirs}) + +# Command for generating wrappers +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 c++17 +) + +# Generate wrappers +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 + ${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 dynamic/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 + ${Python3_LIBRARIES} + Chaste_COMMON_DEPS + chaste_project_template_project + chaste_pychaste +) + +target_include_directories(_template_project_all PRIVATE ${PROJECT_INCLUDE_DIRS}) + +target_compile_options(_template_project_all PRIVATE -Wno-unused-local-typedefs) + +set_target_properties(_template_project_all PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON) + +add_dependencies(_template_project_all pychaste) +add_dependencies(project_template_project _template_project_all) + +set_target_properties(_template_project_all PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/package/template_project +) + +################################ +#### Copy Python source +################################ + +file(GLOB_RECURSE _py_sources + CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/../src/py/* +) +list(FILTER _py_sources EXCLUDE REGEX "\\.(dll|dylib|pyc|so)$") + +foreach(_src ${_py_sources}) + file(RELATIVE_PATH _rel ${CMAKE_CURRENT_SOURCE_DIR}/../src/py ${_src}) + configure_file(${_src} ${CMAKE_CURRENT_BINARY_DIR}/package/${_rel} COPYONLY) +endforeach() diff --git a/dynamic/config.yaml b/dynamic/config.yaml new file mode 100644 index 0000000..3fdee7d --- /dev/null +++ b/dynamic/config.yaml @@ -0,0 +1,63 @@ +# 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://github.com/Chaste/cppwg for documentation. +# 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 + +source_includes: + - SmartPointers.hpp + - Hello.hpp + +template_substitutions: + - signature: + replacement: [[2], [3]] + - signature: + replacement: [[2], [3]] + - signature: + replacement: [[2, 2], [3, 3]] + - signature: + replacement: [[2, 2], [3, 3]] + +modules: + - name: all + source_locations: + - src/ + classes: + - name: Hello diff --git a/examples/goldbeter_1991/Goldbeter1991.xml b/examples/goldbeter_1991/Goldbeter1991.xml new file mode 100644 index 0000000..74d9201 --- /dev/null +++ b/examples/goldbeter_1991/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/examples/goldbeter_1991/README.md b/examples/goldbeter_1991/README.md new file mode 100644 index 0000000..2fdddec --- /dev/null +++ b/examples/goldbeter_1991/README.md @@ -0,0 +1,166 @@ +# Walkthrough: importing the Goldbeter 1991 model from BioModels + +This walkthrough 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. + +## 1. Activate the virtualenv + +```sh +source .virtualenv/bin/activate +``` + +## 2. Get the SBML model with a clean name + +The generated C++ class names come from the **file name**, so the model needs a +C++-friendly name. Here we use `Goldbeter1991`. + +A copy of the model is included next to this walkthrough, so just copy it in: + +```sh +cp examples/goldbeter_1991/Goldbeter1991.xml . +``` + +Alternatively, download it from BioModels and rename it yourself (this needs network +access to `www.ebi.ac.uk`): + +```sh +curl -L "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000003?filename=BIOMD0000000003_url.xml" -o Goldbeter1991.xml +``` + +## 3. Convert the model into a Chaste model + +Goldbeter 1991 is a sub-cellular reaction network, so use `--model-type srn`: + +```sh +chaste-sbml generate 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`). + +## 4. Create the test file + +Create `test/TestGoldbeter1991SbmlSrnModel.hpp` with the following contents. It +builds a cell carrying the imported SRN model, integrates it to a steady state, +and checks the concentrations of cyclin (`C`), active cdc2 kinase (`M`), and +active cyclin protease (`X`). + +```cpp +#ifndef TESTGOLDBETER1991SBMLSRNMODEL_HPP_ +#define TESTGOLDBETER1991SBMLSRNMODEL_HPP_ + +#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 header generated from Goldbeter1991.xml in step 3. +#include "Goldbeter1991SbmlSrnModel.hpp" + +// This is a serial test. +#include "FakePetscSetup.hpp" + +class TestGoldbeter1991SbmlSrnModel : public AbstractCellBasedTestSuite +{ +public: + void TestSteadyStateSimulation() + { + // Run until t = 100 with dt = 0.001, by which time the mitotic oscillator + // has settled to the reference steady state. + SimulationTime* p_simulation_time = SimulationTime::Instance(); + double end_time = 100; + double dt = 0.001; + unsigned num_steps = (unsigned)(end_time / dt); + 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 the simulation to the end time. + while (!p_simulation_time->IsFinished()) + { + p_simulation_time->IncrementTimeOneStep(); + if (p_cell->ReadyToDivide()) + { + p_cell->Divide(); + } + } + + // Check the steady state of the mitotic oscillator. + Goldbeter1991SbmlSrnModel* p_srn = dynamic_cast(p_cell->GetSrnModel()); + TS_ASSERT_DELTA(p_srn->GetStateVariable("C"), 0.5470, 1e-2); + TS_ASSERT_DELTA(p_srn->GetStateVariable("M"), 0.2936, 1e-2); + TS_ASSERT_DELTA(p_srn->GetStateVariable("X"), 0.0067, 1e-3); + } +}; + +#endif /*TESTGOLDBETER1991SBMLSRNMODEL_HPP_*/ +``` + +A ready-made copy of this file lives next to this walkthrough at +[`TestGoldbeter1991SbmlSrnModel.hpp`](TestGoldbeter1991SbmlSrnModel.hpp); you can simply +copy it into your `test/` directory: + +```sh +cp examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp test/ +``` + +Then register the test by adding its file name to `test/ContinuousTestPack.txt`: + +``` +TestGoldbeter1991SbmlSrnModel.hpp +``` + +## 5. Build and run the test + +With `CHASTE_SOURCE_DIR` pointing at your Chaste source tree: + +```sh +scripts/configure.sh # register the project and configure the Chaste 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 the steady-state values: + +``` +C ≈ 0.547 M ≈ 0.294 X ≈ 0.0067 +``` + +## Next steps + +* To import a different model, repeat steps 2–4 with your own `.xml` file, choosing + `--model-type generic`, `srn`, or `cell-cycle` to match the model. +* See [chaste-codegen-sbml](https://github.com/Chaste/chaste-codegen-sbml) + for more information. diff --git a/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp b/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp new file mode 100644 index 0000000..cdad0c7 --- /dev/null +++ b/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp @@ -0,0 +1,66 @@ +#ifndef TESTGOLDBETER1991SBMLSRNMODEL_HPP_ +#define TESTGOLDBETER1991SBMLSRNMODEL_HPP_ + +#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 header generated from Goldbeter1991.xml by chaste-sbml. +#include "Goldbeter1991SbmlSrnModel.hpp" + +// This is a serial test. +#include "FakePetscSetup.hpp" + +class TestGoldbeter1991SbmlSrnModel : public AbstractCellBasedTestSuite +{ +public: + void TestSteadyStateSimulation() + { + // Run until t = 100 with dt = 0.001, by which time the mitotic oscillator + // has settled to the reference steady state. + SimulationTime* p_simulation_time = SimulationTime::Instance(); + double end_time = 100; + double dt = 0.001; + unsigned num_steps = (unsigned)(end_time / dt); + 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 the simulation to the end time. + while (!p_simulation_time->IsFinished()) + { + p_simulation_time->IncrementTimeOneStep(); + if (p_cell->ReadyToDivide()) + { + p_cell->Divide(); + } + } + + // Check the steady state of the mitotic oscillator. + Goldbeter1991SbmlSrnModel* p_srn = dynamic_cast(p_cell->GetSrnModel()); + TS_ASSERT_DELTA(p_srn->GetStateVariable("C"), 0.5470, 1e-2); + TS_ASSERT_DELTA(p_srn->GetStateVariable("M"), 0.2936, 1e-2); + TS_ASSERT_DELTA(p_srn->GetStateVariable("X"), 0.0067, 1e-3); + } +}; + +#endif /*TESTGOLDBETER1991SBMLSRNMODEL_HPP_*/ diff --git a/examples/my_force/MyForce.cpp b/examples/my_force/MyForce.cpp new file mode 100644 index 0000000..8de9c9d --- /dev/null +++ b/examples/my_force/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/examples/my_force/MyForce.hpp b/examples/my_force/MyForce.hpp new file mode 100644 index 0000000..4244682 --- /dev/null +++ b/examples/my_force/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/examples/my_force/README.md b/examples/my_force/README.md new file mode 100644 index 0000000..7bda8fd --- /dev/null +++ b/examples/my_force/README.md @@ -0,0 +1,147 @@ +# Walkthrough: a new C++ Force used from Python + +This walkthrough 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/) +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 `dynamic/config.yaml`, +`dynamic/CMakeLists.txt` and `src/py/` are present. + +Run every command below from your project's root directory, and replace `myproject` with +your project's name throughout. + +## 1. Add the force to your project's source + +Copy the example force class into your project's `src/` directory: + +```sh +cp examples/my_force/MyForce.hpp examples/my_force/MyForce.cpp src/ +``` + +[`MyForce`](MyForce.hpp) is templated over the spatial dimension and subclasses +`AbstractForce`. Its `AddForceContribution()` applies a constant force, in the +positive x-direction, to every node in the population — a deliberately simple force whose +only job is to demonstrate the C++ → Python workflow. + +## 2. Expose the force to the Python bindings + +Tell cppwg to wrap the new class by editing `dynamic/config.yaml`: + +* add its header under `source_includes:` + + ```yaml + source_includes: + - SmartPointers.hpp + - Hello_ProjectName.hpp + - MyForce.hpp + ``` + +* under the `all` module, add the `MyForce` to `classes:`, then tell cppwg that + `AbstractForce` is wrapped in PyChaste 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_ProjectName + - name: MyForce #<-- new +``` + + This is what makes the cross-module inheritance work: `MyForce` subclasses + `AbstractForce`, which is wrapped in PyChaste (a different package). Listing + `AbstractForce` under `external_bases` tells cppwg it is registered there, so + cppwg references it as the base of `MyForce`; `imports` then imports + `chaste._pychaste_all` so that base type is registered before `MyForce`. + Without this, `OffLatticeSimulation.AddForce(my_force)` would reject the force + because Python would not recognise `MyForce` as an `AbstractForce`. + + > Use the latest version of [cppwg](https://github.com/Chaste/cppwg) to + > ensure it has cross-module inheritance support. + +Because `MyForce` is templated over ``, it is wrapped once per +dimension and exposed in Python as `MyForce_2` (2D) and `MyForce_3` (3D). + +## 3. Compile and install the bindings + +With `CHASTE_SOURCE_DIR` pointing at your Chaste source: + +```sh +scripts/configure.sh # only needed the first time +scripts/compile.sh # rebuilds the project and its bindings +scripts/bindings_install.sh # installs PyChaste + your project into .virtualenv/ +``` + +## 4. Activate the virtualenv + +```sh +source .virtualenv/bin/activate +``` + +## 5. Use the force in a Python simulation + +A ready-to-run script lives next to this walkthrough at +[`run_my_force.py`](run_my_force.py). It 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.cell_based.SimulationTime.Instance().SetStartTime(0.0) + +# Build a small node-based cell population. +generator = chaste.mesh.HoneycombMeshGenerator(5, 5) +mesh = chaste.mesh.NodesOnlyMesh_2() +mesh.ConstructNodesWithoutMesh(generator.GetMesh(), 1.5) + +transit_type = chaste.cell_based.TransitCellProliferativeType() +cell_generator = chaste.cell_based.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) +simulator.AddForce(chaste.cell_based.GeneralisedLinearSpringForce_2_2()) +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 examples/my_force/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 note in the top-level README). + +## Troubleshooting +See the main [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 `dynamic/config.yaml` the same way. +* See the [PyChaste tutorials](https://chaste.github.io/docs/python-tutorials/) for more + complete cell-based simulations. diff --git a/examples/my_force/run_my_force.py b/examples/my_force/run_my_force.py new file mode 100644 index 0000000..3577213 --- /dev/null +++ b/examples/my_force/run_my_force.py @@ -0,0 +1,67 @@ +"""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 examples/my_force/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.core.OutputFileHandler("Python/MyForce") + + # The cell-cycle models need the simulation clock to exist before cells are created. + chaste.cell_based.SimulationTime.Instance().SetStartTime(0.0) + + # Build a small node-based cell population on a honeycomb mesh. + generator = chaste.mesh.HoneycombMeshGenerator(5, 5) + generating_mesh = generator.GetMesh() + + mesh = chaste.mesh.NodesOnlyMesh_2() + mesh.ConstructNodesWithoutMesh(generating_mesh, 1.5) + + transit_type = chaste.cell_based.TransitCellProliferativeType() + cell_generator = chaste.cell_based.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) + + # A standard spring force keeps neighbouring cells interacting... + spring_force = chaste.cell_based.GeneralisedLinearSpringForce_2_2() + simulator.AddForce(spring_force) + + # ...and 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/scripts/bindings_install.sh b/scripts/bindings_install.sh new file mode 100755 index 0000000..7da7848 --- /dev/null +++ b/scripts/bindings_install.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import common variables and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/common.sh" + +# Abort if Python bindings are not set up for this project. +if [[ ! -f "${PROJECT_ROOT}/dynamic/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}/dynamic/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 virtualenv (with --system-site-packages) if it does not already exist, so it +# can see PyChaste's native runtime dependencies (petsc4py, mpi4py, vtk) from the system +# Python. These are not pip-installable here. +"${common_dir}/create_venv.sh" + +# 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/scripts/clean.sh b/scripts/clean.sh new file mode 100755 index 0000000..6b135c3 --- /dev/null +++ b/scripts/clean.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $(basename "$0")" >&2 +} + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + usage + exit 1 +fi + +# Import common variables and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/common.sh" + +# Clean the build directory. +safe_rm "${CHASTE_BUILD_DIR}" +mkdir -p "${CHASTE_BUILD_DIR}" + +echo "Cleaned build in '${CHASTE_BUILD_DIR}'." diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 0000000..acc54d6 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Shared variables and helpers for the project scripts. +# Source this file (do not execute it directly) from the other scripts. + +# Resolve paths relative to this file so they hold regardless of the caller. +common_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd -- "${common_dir}/.." && pwd)" + +# The project virtualenv, shared by the Python bindings and SBML install scripts. +# Keep this path in sync with VENV_DIR in setup_project.py, which defines it independently. +VENV_DIR="${PROJECT_ROOT}/.virtualenv" + +# If the Chaste source directory is not set, try to find it in a few common locations. +# It is left empty if not found: sourcing this file must not require Chaste (create_venv.sh +# uses it before the build is set up). Scripts that need it call require_source to enforce it. +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 +fi +CHASTE_SOURCE_DIR="${CHASTE_SOURCE_DIR:-}" + +CHASTE_PROJECTS_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 + +# The name of this project is the name of the project directory. +PROJECT_NAME="$(basename "${PROJECT_ROOT}")" + +Chaste_UPDATE_PROVENANCE="${Chaste_UPDATE_PROVENANCE:-OFF}" + +if [[ -f "${PROJECT_ROOT}/dynamic/config.yaml" ]]; then + # Enable pychaste if this project has Python bindings set up. + Chaste_ENABLE_PYCHASTE=ON + BUILD_PROJECT_PYTHON_BINDINGS=ON +else + Chaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE:-OFF}" + BUILD_PROJECT_PYTHON_BINDINGS=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 +} + +# Remove a file or directory (recursively), refusing unsafe targets (an empty +# path, the filesystem root, or the project root) to guard against catastrophic +# deletes. For a symlink, only the link itself is removed, not its target. +safe_rm() { + local path="$1" + if [[ "${EUID}" -eq 0 ]]; then + echo "Error: refusing to remove '${path}' as a privileged user." >&2 + exit 1 + fi + if [[ -z "${path}" || "${path}" == "/" || "${path}" == "${PROJECT_ROOT}" || "${path}" == "${CHASTE_SOURCE_DIR}" ]]; then + echo "Error: refusing to remove unsafe path '${path}'." >&2 + exit 1 + fi + rm -rf "${path}" +} diff --git a/scripts/compile.sh b/scripts/compile.sh new file mode 100755 index 0000000..0c00552 --- /dev/null +++ b/scripts/compile.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $(basename "$0")" >&2 +} + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + usage + exit 1 +fi + +# Import common variables and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/common.sh" + +# Check that cmake is available. +require_command cmake + +# Check that the build directory has been configured. +require_configured + +# Build. +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..a1a13ef --- /dev/null +++ b/scripts/configure.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Abort if number of arguments is incorrect. +if [[ $# -gt 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import common variables and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/common.sh" + +# Check that cmake is available and the Chaste source exists. +require_command cmake +require_source + +# Ensure this project is registered under Chaste/projects/. +"${common_dir}/register.sh" + +# 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/create_venv.sh b/scripts/create_venv.sh new file mode 100755 index 0000000..1e2c85e --- /dev/null +++ b/scripts/create_venv.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Create the project virtualenv if it does not already exist. +# +# Usage: create_venv.sh +# +# Creates virtual environment with --system-site-packages so it can see native +# packages provided by the system Python (e.g. petsc4py, mpi4py and vtk) + +if [[ $# -ne 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import common variables and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/common.sh" + +require_command python3 + +if [[ ! -d "${VENV_DIR}" ]]; then + python3 -m venv --system-site-packages "${VENV_DIR}" +fi diff --git a/scripts/register.sh b/scripts/register.sh new file mode 100755 index 0000000..00cb165 --- /dev/null +++ b/scripts/register.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Register this project with Chaste. +# The project is only built if it appears under the Chaste/projects/ directory +# or a symlink there points back here. + +# Abort if number of arguments is incorrect. +if [[ $# -gt 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import common variables and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/common.sh" + +# Check that the Chaste source directory exists. +require_source + +# Register the project +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 register.sh." >&2 + exit 1 +else + # Create a new symlink under Chaste/projects/. + ln -s "${PROJECT_ROOT}" "${project_link}" + echo "Registered project '${PROJECT_NAME}' under '${CHASTE_PROJECTS_DIR}'." +fi diff --git a/scripts/sbml_install.sh b/scripts/sbml_install.sh new file mode 100755 index 0000000..ce05584 --- /dev/null +++ b/scripts/sbml_install.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Create the project virtualenv and install the SBML code generator into it. + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + echo "Usage: $(basename "$0")" >&2 + exit 1 +fi + +# Import common variables and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/common.sh" + +require_command python3 + +# The SBML generator formats its output with clang-format; warn (non-fatal) 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 (shared with the Python +# bindings, hence --system-site-packages inside create_venv.sh so it never hides PyChaste's +# native packages when a project has both SBML and Python bindings). +"${common_dir}/create_venv.sh" + +# Install the SBML code generator from GitHub. +"${VENV_DIR}/bin/pip" install --upgrade pip +"${VENV_DIR}/bin/pip" install "git+https://github.com/Chaste/chaste-codegen-sbml@develop" + +# Copy the C++ base classes the generated code depends on into the project's src/, so they +# always match the installed version of the generator. +"${VENV_DIR}/bin/chaste-sbml" copy-base-classes --output-dir "${PROJECT_ROOT}/src" + +echo "" +echo "Installed the SBML code generator 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/test.sh b/scripts/test.sh new file mode 100755 index 0000000..e32aac7 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $(basename "$0")" >&2 +} + +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then + usage + exit 1 +fi + +# Import common variables and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/common.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 +cd "${CHASTE_BUILD_DIR}" +ctest -j"${NCORES}" -V -L "project_${PROJECT_NAME}$" diff --git a/setup_project.py b/setup_project.py index 1984c34..f0e9117 100644 --- a/setup_project.py +++ b/setup_project.py @@ -1,155 +1,379 @@ -"""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")] + + # Python binding template files (substituted if Python bindings opted in, deleted otherwise). + self.PYTHON_BINDING_FILES = [ + os.path.join(self.PROJECT_ROOT, "dynamic", "config.yaml"), + os.path.join(self.PROJECT_ROOT, "dynamic", "CMakeLists.txt"), + os.path.join(self.PROJECT_ROOT, "src", "py", "MANIFEST.in"), + os.path.join(self.PROJECT_ROOT, "src", "py", "setup.cfg"), + os.path.join(self.PROJECT_ROOT, "src", "py", "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 dynamic/config.yaml. + self.PYTHON_CONFIG_SUBSTITUTIONS = { + "Hello.hpp": f"Hello_{self.PROJECT_NAME}.hpp", + "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.PROJECT_ROOT, "src", "py", "template_project") + + # The project virtualenv and the script that creates it (shared by the bindings and SBML paths). + # Keep VENV_DIR in sync with VENV_DIR in scripts/common.sh, which defines it independently. + self.VENV_DIR = os.path.join(self.PROJECT_ROOT, ".virtualenv") + self.CREATE_VENV_SCRIPT = os.path.join(self.PROJECT_ROOT, "scripts", "create_venv.sh") + + # The script that creates the project virtualenv, installs the SBML code generator, + # and copies the SBML base classes into src/ (via chaste-sbml copy-base-classes). + self.SBML_INSTALL_SCRIPT = os.path.join(self.PROJECT_ROOT, "scripts", "sbml_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) -# Appends text_to_append before the file extension -def append_to_file_name(text_to_append, file): - new_name = file.replace('.', text_to_append + '.') +def append_to_file_name(text_to_append: str, file: str) -> str: + """Insert text_to_append before the file's extension and rename it. + + 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__)) - - # 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 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) + + +def create_virtualenv(settings: Settings) -> bool: + """Create the project virtualenv, shared by the Python bindings and SBML paths. + + Runs scripts/create_venv.sh, which creates .virtualenv/ with --system-site-packages so + it can see native pip packages provided by the system Python (e.g. petsc4py, mpi4py and vtk ). + """ + try: + subprocess.run([settings.CREATE_VENV_SCRIPT], check=True) + except (subprocess.CalledProcessError, OSError) as error: + print("") + print(f"WARNING: could not create the project virtualenv automatically ({error}).") + print("Create it manually with:") + print(f" python3 -m venv --system-site-packages {settings.VENV_DIR}") + return False + return True + + +def warn_missing_pychaste_deps(settings: Settings) -> None: + """Warn (non-fatal) if PyChaste's native runtime dependencies are not visible. + + petsc4py, mpi4py and vtk are provided by the system Python (e.g. in the chaste/base + Docker image), not pip-installed; the bindings will fail to import at runtime without + them. Only relevant to the Python bindings, so it is not run for the SBML path. + """ + venv_python = os.path.join(settings.VENV_DIR, "bin", "python") + missing = [ + module + for module in ("petsc4py", "mpi4py", "vtk") + if subprocess.run([venv_python, "-c", f"import {module}"], capture_output=True).returncode != 0 + ] + if missing: + print("") + print(f"WARNING: PyChaste runtime dependencies not found: {', '.join(missing)}.") + print("These are provided by the system Python (e.g. in the chaste/base image) and are") + print("needed to import the bindings. Install them on your system before using the bindings.") + + +def install_sbml_codegen(settings: Settings) -> None: + """Create the project virtualenv and set up SBML support in it. + + Creates the shared virtualenv via create_virtualenv(), then runs scripts/sbml_install.sh + to install the code generator and copy the SBML base classes into src/. On any failure + this is non-fatal: it prints the manual commands so the user can finish the install + themselves. + """ + if not create_virtualenv(settings): + return + try: + subprocess.run([settings.SBML_INSTALL_SCRIPT], check=True) + except (subprocess.CalledProcessError, OSError) as error: + pip = os.path.join(settings.VENV_DIR, "bin", "pip") + chaste_sbml = os.path.join(settings.VENV_DIR, "bin", "chaste-sbml") + src_dir = os.path.join(settings.PROJECT_ROOT, "src") + print("") + print(f"WARNING: could not set up SBML support automatically ({error}).") + print("Set it up manually with:") + print(f" {pip} install 'git+https://github.com/Chaste/chaste-codegen-sbml@develop'") + print(f" {chaste_sbml} copy-base-classes --output-dir {src_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)) + + # Set up or remove Python bindings + if python_bindings: + # 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.PROJECT_ROOT, "dynamic", "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.PROJECT_ROOT, "src", "py", settings.PROJECT_NAME) + os.rename(settings.PYTHON_PKG_TEMPLATE_DIR, new_pkg_dir) + # Create the project virtualenv (the compiled bindings are installed later + # by scripts/bindings_install.sh). + if create_virtualenv(settings): + warn_missing_pychaste_deps(settings) + else: + # Remove the Python binding template files + shutil.rmtree(os.path.join(settings.PROJECT_ROOT, "dynamic")) + shutil.rmtree(os.path.join(settings.PROJECT_ROOT, "src", "py")) + + # Set up SBML support: install the code generator and copy the base classes into src/. + if sbml: + install_sbml_codegen(settings) + + # 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 components: + print(f"* Set Chaste components in CMakeLists.txt to: {', '.join(components)}.") + print("* Renamed the template files:") + + for original, renamed in zip(settings.TEMPLATE_SOURCE_FILES, appended_file_names): + print(f" - {os.path.basename(original)} -> {os.path.basename(renamed)}") + + if python_bindings: + print("* Set up Python bindings in dynamic/ and src/py/.") + print("* Created the project virtualenv in .virtualenv/.") + else: + print("* Removed Python bindings template files in dynamic/ and src/py/.") - # 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) - - # Amend the components - components_list = [] - - if ask_for_response("Does this project depend on the cell_based component? [Y/n] "): - components_list.append('cell_based') - - if ask_for_response("Does this project depend on the crypt component? [Y/n] "): - components_list.append('crypt') - - if ask_for_response("Does this project depend on the heart component? [Y/n] "): - components_list.append('heart') - - if ask_for_response("Does this project depend on the lung component? [Y/n] "): - components_list.append('lung') + if sbml: + print("* Installed the SBML code generator into .virtualenv and copied the SBML base classes into src/.") + print(" See the README and examples/goldbeter_1991/ for how to import an SBML model.") - # 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/py/MANIFEST.in b/src/py/MANIFEST.in new file mode 100644 index 0000000..8dc9c7f --- /dev/null +++ b/src/py/MANIFEST.in @@ -0,0 +1 @@ +recursive-include template_project *.so diff --git a/src/py/pyproject.toml b/src/py/pyproject.toml new file mode 100644 index 0000000..aff5d71 --- /dev/null +++ b/src/py/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/src/py/setup.cfg b/src/py/setup.cfg new file mode 100644 index 0000000..e3bae8a --- /dev/null +++ b/src/py/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/src/py/setup.py b/src/py/setup.py new file mode 100644 index 0000000..c3c7732 --- /dev/null +++ b/src/py/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/src/py/template_project/__init__.py b/src/py/template_project/__init__.py new file mode 100644 index 0000000..9a81e4a --- /dev/null +++ b/src/py/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/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