From e26d52e0d5fd87acba4968b2b974d0c62d0689da Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 16 Jun 2026 14:16:46 +0000 Subject: [PATCH 01/51] #3 Fix minor setup bugs --- setup_project.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup_project.py b/setup_project.py index 1984c34..f601285 100644 --- a/setup_project.py +++ b/setup_project.py @@ -65,12 +65,13 @@ def ask_for_response(question): 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:") # Appends text_to_append before the file extension def append_to_file_name(text_to_append, file): - new_name = file.replace('.', text_to_append + '.') + root, ext = os.path.splitext(file) + new_name = root + text_to_append + ext os.rename(file, new_name) return new_name @@ -113,7 +114,6 @@ def main(): "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" } From f7ed54daaaef58ede54218c20de35acd808c29ee Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 09:11:48 +0000 Subject: [PATCH 02/51] #3 Add convenience scripts --- scripts/clean.sh | 22 ++++++++++++++ scripts/common.sh | 41 +++++++++++++++++++++++++ scripts/compile.sh | 26 ++++++++++++++++ scripts/configure.sh | 25 ++++++++++++++++ scripts/register.sh | 47 +++++++++++++++++++++++++++++ scripts/reset.sh | 71 ++++++++++++++++++++++++++++++++++++++++++++ scripts/test.sh | 30 +++++++++++++++++++ 7 files changed, 262 insertions(+) create mode 100755 scripts/clean.sh create mode 100644 scripts/common.sh create mode 100755 scripts/compile.sh create mode 100755 scripts/configure.sh create mode 100755 scripts/register.sh create mode 100755 scripts/reset.sh create mode 100755 scripts/test.sh diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100755 index 0000000..9a8c993 --- /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. +rm -rf "${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..42dbb83 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,41 @@ +#!/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)" +repo_root="$(cd -- "${common_dir}/.." && pwd)" + +CHASTE_BUILD_DIR="${CHASTE_BUILD_DIR:-${repo_root}/build}" + +CHASTE_SOURCE_DIR="${CHASTE_SOURCE_DIR:-${repo_root}/../Chaste}" +CHASTE_PROJECTS_DIR="${CHASTE_SOURCE_DIR}/projects" + +CHASTE_TEST_OUTPUT="${CHASTE_TEST_OUTPUT:-${repo_root}/output}" +export CHASTE_TEST_OUTPUT="${CHASTE_TEST_OUTPUT}" + +# The name of this project is the name of the project directory. +PROJECT_NAME="$(basename "${repo_root}")" + +# Set the number of parallel jobs for building and testing. +NCORES="${NCORES:-4}" +if ! [[ "${NCORES}" =~ ^[0-9]+$ ]] || [[ "${NCORES}" -lt 1 ]]; then + echo "Error: NCORES must be a positive integer (got '${NCORES}')." >&2 + exit 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 +} 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..177d15d --- /dev/null +++ b/scripts/configure.sh @@ -0,0 +1,25 @@ +#!/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. +require_command cmake + +# 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}" diff --git a/scripts/register.sh b/scripts/register.sh new file mode 100755 index 0000000..284d5fb --- /dev/null +++ b/scripts/register.sh @@ -0,0 +1,47 @@ +#!/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. +if [[ ! -d "${CHASTE_SOURCE_DIR}" ]]; then + echo "Error: Chaste source directory not found at '${CHASTE_SOURCE_DIR}'." >&2 + echo "Set CHASTE_SOURCE_DIR to override the default sibling checkout path." >&2 + exit 1 +fi + +# Register the project +project_link="${CHASTE_PROJECTS_DIR}/${PROJECT_NAME}" + +mkdir -p "${CHASTE_PROJECTS_DIR}" + +if [[ -L "${project_link}" && "${project_link}" -ef "${repo_root}" ]]; then + : # Already registered: a symlink under Chaste/projects/ points back to this project. +elif [[ ! -L "${project_link}" && "${project_link}" -ef "${repo_root}" ]]; then + : # Already registered: the project itself lives directly under Chaste/projects/. +elif [[ -L "${project_link}" ]]; then + # Repoint a stale/dangling symlink. + ln -sfn "${repo_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 "${repo_root}" "${project_link}" + echo "Registered project '${PROJECT_NAME}' under '${CHASTE_PROJECTS_DIR}'." +fi diff --git a/scripts/reset.sh b/scripts/reset.sh new file mode 100755 index 0000000..fa8f225 --- /dev/null +++ b/scripts/reset.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $(basename "$0") [-f|--force]" >&2 +} + +# Parse arguments. +force=0 +if [[ $# -gt 1 ]]; then + usage + exit 1 +fi +if [[ $# -eq 1 ]]; then + case "$1" in + -f | --force) force=1 ;; + *) usage; exit 1 ;; + esac +fi + +# Import common variables and helpers. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/common.sh" + +# Check that git is available and that this is the project's git repository. +require_command git +if ! git -C "${repo_root}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Error: '${repo_root}' is not a git repository." >&2 + exit 1 +fi + +# Confirm before discarding local changes, unless forced. +if [[ "${force}" -ne 1 ]]; then + echo "This will reset the template to its original (committed) state:" + echo " - all tracked files reset to HEAD (local changes discarded)" + echo " - '${CHASTE_BUILD_DIR}' and '${CHASTE_TEST_OUTPUT}' removed" + echo " - untracked files created by setup_project.py removed" + echo " - this project's Chaste registration symlink removed" + echo " ('${script_dir}' is preserved.)" + reply="" + read -r -p "Proceed? [y/N] " reply || true + case "${reply}" in + y | Y | yes | Yes) ;; + *) echo "Aborted."; exit 0 ;; + esac +fi + +# Remove this project's registration symlink under Chaste/projects/ (only if it +# is a symlink pointing back here, never a real directory living there). +project_link="${CHASTE_PROJECTS_DIR}/${PROJECT_NAME}" +if [[ -L "${project_link}" && "${project_link}" -ef "${repo_root}" ]]; then + rm -f "${project_link}" + echo "Removed Chaste registration symlink '${project_link}'." +fi + +# Remove generated build/output directories. These may resolve outside the repo +# (via CHASTE_BUILD_DIR/CHASTE_TEST_OUTPUT), so handle them explicitly and guard +# against deleting the repository root or the filesystem root. +for dir in "${CHASTE_BUILD_DIR}" "${CHASTE_TEST_OUTPUT}"; do + if [[ -n "${dir}" && "${dir}" != "/" && "${dir}" != "${repo_root}" ]]; then + rm -rf "${dir}" + fi +done + +# Restore tracked files to their committed state, then remove any remaining +# untracked/ignored files (e.g. files renamed by setup_project.py), keeping +# this scripts/ directory. +git -C "${repo_root}" reset --hard +git -C "${repo_root}" clean -fdx --exclude=/scripts + +echo "Reset template '${PROJECT_NAME}' to its original state." 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}$" From 444e6e86a07a94dd1f7406564e6bc3ea3a9e629a Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 09:12:10 +0000 Subject: [PATCH 03/51] #3 Update gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dc84959..b97a986 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ build/ - +output/ From ee9479c0d2aa5b67e7231eb3085ab32f30ff4ecc Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 09:48:14 +0000 Subject: [PATCH 04/51] #3 Add setup query about project name --- setup_project.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/setup_project.py b/setup_project.py index f601285..d98a47d 100644 --- a/setup_project.py +++ b/setup_project.py @@ -88,6 +88,15 @@ def main(): # Identify the name of the project project_name = os.path.basename(path_to_project) + # Confirm the template directory has been renamed to the project name before making any changes. + print("This project will be set up using '" + project_name + "' (the directory name) as the project name.") + if not ask_for_response("Do you want to proceed? [Y/n] "): + print("Rename the '" + project_name + "' directory to your project name, then run this script again.") + return + + # Refresh the project name + 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') From 871bf1e91cd6ab47d3a29f8b3fd1e5d4bd5bd02a Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 10:00:33 +0000 Subject: [PATCH 05/51] #3 Summarise chosen options --- setup_project.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/setup_project.py b/setup_project.py index d98a47d..200b150 100644 --- a/setup_project.py +++ b/setup_project.py @@ -102,6 +102,31 @@ def main(): apps_cmakelists = os.path.join(path_to_project, 'apps', 'CMakeLists.txt') test_cmakelists = os.path.join(path_to_project, 'test', 'CMakeLists.txt') + # Ask which Chaste components this project depends on + 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') + + # Summarise the chosen options and confirm before making any changes + print("") + print("Summary:") + print(" Project name: " + project_name) + print(" Chaste components: " + (', '.join(components_list) if components_list else "(template default)")) + print("") + if not ask_for_response("Proceed with these settings? [Y/n] "): + print("No changes made.") + return + # 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'), @@ -138,21 +163,6 @@ def main(): 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 the list is non-empty, replace the default components if components_list: components_string = ' '.join(components_list) From 16dc0e4a2c914fa3b741dac22b80700b70bb999b Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 10:15:42 +0000 Subject: [PATCH 06/51] #3 Add docstrings in setup.py --- setup_project.py | 151 ++++++++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 66 deletions(-) diff --git a/setup_project.py b/setup_project.py index 200b150..9685731 100644 --- a/setup_project.py +++ b/setup_project.py @@ -1,60 +1,69 @@ -"""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): +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.""" # Read the file - f = open(filename, 'r') + f = open(filename, "r") file_contents = f.read() f.close() # Write to the file - f = open(filename, 'w') + f = open(filename, "w") f.write(file_contents.replace(old_string, new_string)) f.close() return -def ask_for_response(question): +def ask_for_response(question: str) -> bool: + """Prompt the user with a yes/no question and return the answer as a bool. + + An empty response defaults to yes; any unrecognised response re-prompts. + """ # Display the question print(question) # 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() @@ -68,20 +77,24 @@ def ask_for_response(question): return ask_for_response("Please respond with yes or no:") -# Appends text_to_append before the file extension -def append_to_file_name(text_to_append, file): +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 sanitize_project_name(project_name: str) -> str: + """Strip non-alphanumeric characters so the name is safe to append to a C++ class name.""" + return "".join(filter(lambda char: char.isalpha() or char.isnumeric(), project_name)) -def main(): +def main() -> None: + """Customise the template for this project.""" # The absolute path to the project directory path_to_project = os.path.dirname(os.path.realpath(__file__)) @@ -98,75 +111,81 @@ def main(): 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') + 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") # Ask which Chaste components this project depends on components_list = [] if ask_for_response("Does this project depend on the cell_based component? [Y/n] "): - components_list.append('cell_based') + components_list.append("cell_based") if ask_for_response("Does this project depend on the crypt component? [Y/n] "): - components_list.append('crypt') + components_list.append("crypt") if ask_for_response("Does this project depend on the heart component? [Y/n] "): - components_list.append('heart') + components_list.append("heart") if ask_for_response("Does this project depend on the lung component? [Y/n] "): - components_list.append('lung') + components_list.append("lung") # Summarise the chosen options and confirm before making any changes print("") print("Summary:") print(" Project name: " + project_name) - print(" Chaste components: " + (', '.join(components_list) if components_list else "(template default)")) + print(" Chaste components: " + (", ".join(components_list) if components_list else "(template default)")) print("") if not ask_for_response("Proceed with these settings? [Y/n] "): print("No changes made.") return - # 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')] + # Files to append project name to - this avoids conflicts if multiple projects are generated from the template + 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) + 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, + # These are very specific to avoid rewriting the printed out "Hello world" message + substitutions = { + " 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 + "(", - "Hello.hpp": "Hello_" + 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'))] + files_to_sub = appended_file_names + [str(os.path.join(path_to_project, "test", "ContinuousTestPack.txt"))] for file in files_to_sub: for old, new in substitutions.items(): find_and_replace(file, old, new) - # 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) + find_and_replace(base_cmakelists, "chaste_do_project(template_project", "chaste_do_project(" + project_name) + find_and_replace( + apps_cmakelists, "chaste_do_apps_project(template_project", "chaste_do_apps_project(" + project_name + ) + find_and_replace( + test_cmakelists, "chaste_do_test_project(template_project", "chaste_do_test_project(" + project_name + ) # If 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' + components_string = " ".join(components_list) + default_components = "continuum_mechanics global io linalg mesh ode pde" find_and_replace(base_cmakelists, default_components, components_string) From c8cf2ed8b3a91c52d746620515999bef4b48278e Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 10:20:09 +0000 Subject: [PATCH 07/51] #3 Add linting, update gitignore --- .flake8 | 11 +++++++++++ .gitignore | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .flake8 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/.gitignore b/.gitignore index b97a986..e7ae214 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,41 @@ build/ output/ + +.vscode/ + +.DS_Store +Thumbs.db + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.eggs/ +.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 From 4e27e93928e6ad3194759549a88d8ab47efc2fc5 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 10:20:53 +0000 Subject: [PATCH 08/51] #3 Add safe delete in scripts --- scripts/clean.sh | 2 +- scripts/common.sh | 21 ++++++++++++++++----- scripts/register.sh | 8 ++++---- scripts/reset.sh | 17 +++++++---------- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/scripts/clean.sh b/scripts/clean.sh index 9a8c993..93b16ce 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -16,7 +16,7 @@ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" source "${script_dir}/common.sh" # Clean the build directory. -rm -rf "${CHASTE_BUILD_DIR}" +safe_remove_dir "${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 index 42dbb83..ca1e2e2 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -4,18 +4,18 @@ # Resolve paths relative to this file so they hold regardless of the caller. common_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -repo_root="$(cd -- "${common_dir}/.." && pwd)" +PROJECT_ROOT="$(cd -- "${common_dir}/.." && pwd)" -CHASTE_BUILD_DIR="${CHASTE_BUILD_DIR:-${repo_root}/build}" +CHASTE_BUILD_DIR="${CHASTE_BUILD_DIR:-${PROJECT_ROOT}/build}" -CHASTE_SOURCE_DIR="${CHASTE_SOURCE_DIR:-${repo_root}/../Chaste}" +CHASTE_SOURCE_DIR="${CHASTE_SOURCE_DIR:-${PROJECT_ROOT}/../Chaste}" CHASTE_PROJECTS_DIR="${CHASTE_SOURCE_DIR}/projects" -CHASTE_TEST_OUTPUT="${CHASTE_TEST_OUTPUT:-${repo_root}/output}" +CHASTE_TEST_OUTPUT="${CHASTE_TEST_OUTPUT:-${PROJECT_ROOT}/output}" export CHASTE_TEST_OUTPUT="${CHASTE_TEST_OUTPUT}" # The name of this project is the name of the project directory. -PROJECT_NAME="$(basename "${repo_root}")" +PROJECT_NAME="$(basename "${PROJECT_ROOT}")" # Set the number of parallel jobs for building and testing. NCORES="${NCORES:-4}" @@ -39,3 +39,14 @@ require_configured() { exit 1 fi } + +# Recursively remove a directory, refusing unsafe targets (an empty path, the +# filesystem root, or the project root) to guard against catastrophic deletes. +safe_remove_dir() { + local dir="$1" + if [[ -z "${dir}" || "${dir}" == "/" || "${dir}" == "${PROJECT_ROOT}" ]]; then + echo "Error: refusing to remove unsafe path '${dir}'." >&2 + exit 1 + fi + rm -rf "${dir}" +} diff --git a/scripts/register.sh b/scripts/register.sh index 284d5fb..9911c36 100755 --- a/scripts/register.sh +++ b/scripts/register.sh @@ -27,13 +27,13 @@ project_link="${CHASTE_PROJECTS_DIR}/${PROJECT_NAME}" mkdir -p "${CHASTE_PROJECTS_DIR}" -if [[ -L "${project_link}" && "${project_link}" -ef "${repo_root}" ]]; then +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 "${repo_root}" ]]; then +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 "${repo_root}" "${project_link}" + 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. @@ -42,6 +42,6 @@ elif [[ -e "${project_link}" ]]; then exit 1 else # Create a new symlink under Chaste/projects/. - ln -s "${repo_root}" "${project_link}" + ln -s "${PROJECT_ROOT}" "${project_link}" echo "Registered project '${PROJECT_NAME}' under '${CHASTE_PROJECTS_DIR}'." fi diff --git a/scripts/reset.sh b/scripts/reset.sh index fa8f225..936511c 100755 --- a/scripts/reset.sh +++ b/scripts/reset.sh @@ -24,8 +24,8 @@ source "${script_dir}/common.sh" # Check that git is available and that this is the project's git repository. require_command git -if ! git -C "${repo_root}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "Error: '${repo_root}' is not a git repository." >&2 +if ! git -C "${PROJECT_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Error: '${PROJECT_ROOT}' is not a git repository." >&2 exit 1 fi @@ -48,24 +48,21 @@ fi # Remove this project's registration symlink under Chaste/projects/ (only if it # is a symlink pointing back here, never a real directory living there). project_link="${CHASTE_PROJECTS_DIR}/${PROJECT_NAME}" -if [[ -L "${project_link}" && "${project_link}" -ef "${repo_root}" ]]; then +if [[ -L "${project_link}" && "${project_link}" -ef "${PROJECT_ROOT}" ]]; then rm -f "${project_link}" echo "Removed Chaste registration symlink '${project_link}'." fi # Remove generated build/output directories. These may resolve outside the repo -# (via CHASTE_BUILD_DIR/CHASTE_TEST_OUTPUT), so handle them explicitly and guard -# against deleting the repository root or the filesystem root. +# (via CHASTE_BUILD_DIR/CHASTE_TEST_OUTPUT), so handle them explicitly. for dir in "${CHASTE_BUILD_DIR}" "${CHASTE_TEST_OUTPUT}"; do - if [[ -n "${dir}" && "${dir}" != "/" && "${dir}" != "${repo_root}" ]]; then - rm -rf "${dir}" - fi + safe_remove_dir "${dir}" done # Restore tracked files to their committed state, then remove any remaining # untracked/ignored files (e.g. files renamed by setup_project.py), keeping # this scripts/ directory. -git -C "${repo_root}" reset --hard -git -C "${repo_root}" clean -fdx --exclude=/scripts +git -C "${PROJECT_ROOT}" reset --hard +git -C "${PROJECT_ROOT}" clean -fdx --exclude=/scripts echo "Reset template '${PROJECT_NAME}' to its original state." From 539336fe296f724dd746cdd87099c9aecb32dce7 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 10:32:51 +0000 Subject: [PATCH 09/51] #3 Update safe delete in scripts --- scripts/clean.sh | 2 +- scripts/common.sh | 15 ++++++++------- scripts/reset.sh | 11 ++++------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/scripts/clean.sh b/scripts/clean.sh index 93b16ce..6b135c3 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -16,7 +16,7 @@ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" source "${script_dir}/common.sh" # Clean the build directory. -safe_remove_dir "${CHASTE_BUILD_DIR}" +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 index ca1e2e2..56ec161 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -40,13 +40,14 @@ require_configured() { fi } -# Recursively remove a directory, refusing unsafe targets (an empty path, the -# filesystem root, or the project root) to guard against catastrophic deletes. -safe_remove_dir() { - local dir="$1" - if [[ -z "${dir}" || "${dir}" == "/" || "${dir}" == "${PROJECT_ROOT}" ]]; then - echo "Error: refusing to remove unsafe path '${dir}'." >&2 +# 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 [[ -z "${path}" || "${path}" == "/" || "${path}" == "${PROJECT_ROOT}" ]]; then + echo "Error: refusing to remove unsafe path '${path}'." >&2 exit 1 fi - rm -rf "${dir}" + rm -rf "${path}" } diff --git a/scripts/reset.sh b/scripts/reset.sh index 936511c..2c1c988 100755 --- a/scripts/reset.sh +++ b/scripts/reset.sh @@ -32,13 +32,10 @@ fi # Confirm before discarding local changes, unless forced. if [[ "${force}" -ne 1 ]]; then echo "This will reset the template to its original (committed) state:" - echo " - all tracked files reset to HEAD (local changes discarded)" - echo " - '${CHASTE_BUILD_DIR}' and '${CHASTE_TEST_OUTPUT}' removed" - echo " - untracked files created by setup_project.py removed" - echo " - this project's Chaste registration symlink removed" + echo "All uncommitted changes will be removed, and untracked files will be deleted." echo " ('${script_dir}' is preserved.)" reply="" - read -r -p "Proceed? [y/N] " reply || true + read -r -p "Proceed? [Y/n] " reply || true case "${reply}" in y | Y | yes | Yes) ;; *) echo "Aborted."; exit 0 ;; @@ -49,14 +46,14 @@ fi # is a symlink pointing back here, never a real directory living there). project_link="${CHASTE_PROJECTS_DIR}/${PROJECT_NAME}" if [[ -L "${project_link}" && "${project_link}" -ef "${PROJECT_ROOT}" ]]; then - rm -f "${project_link}" + safe_rm "${project_link}" echo "Removed Chaste registration symlink '${project_link}'." fi # Remove generated build/output directories. These may resolve outside the repo # (via CHASTE_BUILD_DIR/CHASTE_TEST_OUTPUT), so handle them explicitly. for dir in "${CHASTE_BUILD_DIR}" "${CHASTE_TEST_OUTPUT}"; do - safe_remove_dir "${dir}" + safe_rm "${dir}" done # Restore tracked files to their committed state, then remove any remaining From 9eebf7cb654a85ce3a4db1283bbe219a1619ec37 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 14:30:05 +0000 Subject: [PATCH 10/51] #3 Update reset, add setup settings --- scripts/reset.sh | 47 ++------ setup_project.py | 279 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 202 insertions(+), 124 deletions(-) diff --git a/scripts/reset.sh b/scripts/reset.sh index 2c1c988..9a3e356 100755 --- a/scripts/reset.sh +++ b/scripts/reset.sh @@ -2,48 +2,25 @@ set -euo pipefail usage() { - echo "Usage: $(basename "$0") [-f|--force]" >&2 + echo "Usage: $(basename "$0")" >&2 } -# Parse arguments. -force=0 -if [[ $# -gt 1 ]]; then +# Abort if number of arguments is incorrect. +if [[ $# -ne 0 ]]; then usage exit 1 fi -if [[ $# -eq 1 ]]; then - case "$1" in - -f | --force) force=1 ;; - *) usage; exit 1 ;; - esac -fi # Import common variables and helpers. script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" source "${script_dir}/common.sh" -# Check that git is available and that this is the project's git repository. -require_command git -if ! git -C "${PROJECT_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "Error: '${PROJECT_ROOT}' is not a git repository." >&2 - exit 1 -fi - -# Confirm before discarding local changes, unless forced. -if [[ "${force}" -ne 1 ]]; then - echo "This will reset the template to its original (committed) state:" - echo "All uncommitted changes will be removed, and untracked files will be deleted." - echo " ('${script_dir}' is preserved.)" - reply="" - read -r -p "Proceed? [Y/n] " reply || true - case "${reply}" in - y | Y | yes | Yes) ;; - *) echo "Aborted."; exit 0 ;; - esac -fi +# Restore the template to its original state (reverse setup). +require_command python3 +python3 "${PROJECT_ROOT}/setup_project.py" --reset -# Remove this project's registration symlink under Chaste/projects/ (only if it -# is a symlink pointing back here, never a real directory living there). +# Remove this project's symlink under Chaste/projects/ +# (only if it is a symlink, not a real directory). project_link="${CHASTE_PROJECTS_DIR}/${PROJECT_NAME}" if [[ -L "${project_link}" && "${project_link}" -ef "${PROJECT_ROOT}" ]]; then safe_rm "${project_link}" @@ -56,10 +33,4 @@ for dir in "${CHASTE_BUILD_DIR}" "${CHASTE_TEST_OUTPUT}"; do safe_rm "${dir}" done -# Restore tracked files to their committed state, then remove any remaining -# untracked/ignored files (e.g. files renamed by setup_project.py), keeping -# this scripts/ directory. -git -C "${PROJECT_ROOT}" reset --hard -git -C "${PROJECT_ROOT}" clean -fdx --exclude=/scripts - -echo "Reset template '${PROJECT_NAME}' to its original state." +echo "Reset project template to its original state." diff --git a/setup_project.py b/setup_project.py index 9685731..1d8446a 100644 --- a/setup_project.py +++ b/setup_project.py @@ -34,23 +34,103 @@ Run this script from the project directory once it has been renamed to your project name. """ +import argparse import os -from functools import partial - - -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.""" - # Read the file - f = open(filename, "r") - file_contents = f.read() - f.close() +import re +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: + self.update() + + def update(self) -> None: + """Recompute the paths and substitutions from the current project directory. + + Call this after the project directory is renamed so the values derived from it are refreshed. + """ + # 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") + + self.CMAKE_PROJECT_SUBSTITUTIONS = [ + ( + self.BASE_CMAKELISTS, + "chaste_do_project(template_project)", + f"chaste_do_project({self.PROJECT_NAME})", + ), + ( + self.APPS_CMAKELISTS, + "chaste_do_apps_project(template_project)", + f"chaste_do_apps_project({self.PROJECT_NAME})", + ), + ( + self.TEST_CMAKELISTS, + "chaste_do_test_project(template_project)", + f"chaste_do_test_project({self.PROJECT_NAME})", + ), + ] + + # 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")] + + +def find_and_replace(filename: str, pattern: str, replacement: str, regex: bool = False) -> None: + """Replace occurrences of pattern with replacement in a file, in place. + + By default pattern is treated as literal text. Pass regex=True to treat it as + a regular expression, matched with re.MULTILINE so ^ and $ anchor to line + boundaries. + """ + if not regex: + pattern = re.escape(pattern) + with open(filename, "r") as f: + contents = f.read() + with open(filename, "w") as f: + f.write(re.sub(pattern, replacement, contents, flags=re.MULTILINE)) - # Write to the file - f = open(filename, "w") - f.write(file_contents.replace(old_string, new_string)) - f.close() - return +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 ask_for_response(question: str) -> bool: @@ -88,106 +168,133 @@ def append_to_file_name(text_to_append: str, file: str) -> str: return new_name -def sanitize_project_name(project_name: str) -> str: - """Strip non-alphanumeric characters so the name is safe to append to a C++ class name.""" - return "".join(filter(lambda char: char.isalpha() or char.isnumeric(), project_name)) - - -def main() -> None: - """Customise the template for this project.""" - # The absolute path to the project directory - path_to_project = os.path.dirname(os.path.realpath(__file__)) +def is_git_repository(path: str) -> bool: + """Return True if path is inside a git working tree.""" + try: + result = subprocess.run( + ["git", "-C", path, "rev-parse", "--is-inside-work-tree"], + capture_output=True, + ) + except FileNotFoundError: + return False + return result.returncode == 0 - # Identify the name of the project - project_name = os.path.basename(path_to_project) +def setup(settings: Settings) -> None: + """Customise the template for this project, after confirming the chosen settings.""" # Confirm the template directory has been renamed to the project name before making any changes. - print("This project will be set up using '" + project_name + "' (the directory name) as the project name.") + print(f"This project will be set up using '{settings.PROJECT_NAME}' (the directory name) as the project name.") if not ask_for_response("Do you want to proceed? [Y/n] "): - print("Rename the '" + project_name + "' directory to your project name, then run this script again.") + print(f"Rename the '{settings.PROJECT_NAME}' directory to your project name, then run this script again.") return - # Refresh the project name - project_name = os.path.basename(path_to_project) + # Recompute settings in case the directory name has changed + settings.update() - # 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") + # 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}' contains characters other than letters, digits, + and underscores. Renaming the directory is recommended.""") + return # Ask which Chaste components this project depends on - 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") + components: list[str] = [] + for component in settings.OPTIONAL_COMPONENTS: + if ask_for_response(f"Does this project depend on the {component} component? [Y/n] "): + components.append(component) # Summarise the chosen options and confirm before making any changes print("") print("Summary:") - print(" Project name: " + project_name) - print(" Chaste components: " + (", ".join(components_list) if components_list else "(template default)")) + print(f" Project name: {settings.PROJECT_NAME}") + print(f" Chaste components: {', '.join(components) if components else '(template default)'}") print("") if not ask_for_response("Proceed with these settings? [Y/n] "): print("No changes made.") return - # Files to append project name to - this avoids conflicts if multiple projects are generated from the template - 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 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] - # 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)) + # 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 settings.SOURCE_SUBSTITUTIONS.items(): + find_and_replace(file, old, new) - # Perform the find-and-replace tasks to update the template project source - sanitized_name = sanitize_project_name(project_name) + # Set the project name in the CMakeLists.txt files + for cmake_file, template_text, project_text in settings.CMAKE_PROJECT_SUBSTITUTIONS: + find_and_replace(cmake_file, template_text, project_text) - # These are very specific to avoid rewriting the printed out "Hello world" message - substitutions = { - " 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 + "(", - "Hello.hpp": "Hello_" + project_name + ".hpp", - } + # Replace the default components if any optional components were selected + if components: + find_and_replace(settings.BASE_CMAKELISTS, " ".join(settings.DEFAULT_COMPONENTS), " ".join(components)) - files_to_sub = appended_file_names + [str(os.path.join(path_to_project, "test", "ContinuousTestPack.txt"))] - for file in files_to_sub: - for old, new in substitutions.items(): - find_and_replace(file, old, new) +def reset(settings: Settings) -> None: + """Reset the template to its original state using git, discarding setup's changes. - # 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 + The project must be a git repository: the tracked template files are restored + to their committed state and the setup-renamed example files are removed. + """ + # The project can only be reset from a git repository. + if not is_git_repository(settings.PROJECT_ROOT): + print("Error: the project can only be reset if it is a git repository.") + raise SystemExit(1) + + # Confirm before discarding any changes. + print_banner( + "Caution: This will reset the template to its original state!!!", + "Any changes to the example source and CMakeLists.txt files will be lost!!!", ) - find_and_replace( - test_cmakelists, "chaste_do_test_project(template_project", "chaste_do_test_project(" + project_name + if not ask_for_response("Proceed? [Y/n] "): + print("Aborted.") + raise SystemExit(1) + + # Remove the setup-renamed example source files (these are untracked by git). + suffix = "_" + settings.PROJECT_NAME + for original in settings.TEMPLATE_SOURCE_FILES: + root, ext = os.path.splitext(original) + renamed = root + suffix + ext + if os.path.exists(renamed): + os.remove(renamed) + + # Restore the tracked template files to their committed state. + tracked_files = settings.TEMPLATE_SOURCE_FILES + settings.TEST_PACK_FILES + [ + settings.BASE_CMAKELISTS, + settings.APPS_CMAKELISTS, + settings.TEST_CMAKELISTS, + ] + subprocess.run( + ["git", "-C", settings.PROJECT_ROOT, "checkout", "HEAD", "--", *tracked_files], + check=True, ) - # 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 parse_args() -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + prog="setup_project", + description="Set up a Chaste user project from the template.", + ) + parser.add_argument( + "--reset", + action="store_true", + help="Restore the template to its original state", + ) + return parser.parse_args() + + +def main() -> None: + """Set up the project from the template, or restore the template with --reset.""" + args = parse_args() + settings = Settings() + + if args.reset: + reset(settings) + else: + setup(settings) if __name__ == "__main__": From abf6caa333bc8dbcfcb9809888ff4dbba3a80b54 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 14:55:01 +0000 Subject: [PATCH 11/51] #3 Remove complicated reset --- scripts/reset.sh | 36 ----------------- setup_project.py | 100 +++-------------------------------------------- 2 files changed, 5 insertions(+), 131 deletions(-) delete mode 100755 scripts/reset.sh diff --git a/scripts/reset.sh b/scripts/reset.sh deleted file mode 100755 index 9a3e356..0000000 --- a/scripts/reset.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/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" - -# Restore the template to its original state (reverse setup). -require_command python3 -python3 "${PROJECT_ROOT}/setup_project.py" --reset - -# Remove this project's symlink under Chaste/projects/ -# (only if it is a symlink, not a real directory). -project_link="${CHASTE_PROJECTS_DIR}/${PROJECT_NAME}" -if [[ -L "${project_link}" && "${project_link}" -ef "${PROJECT_ROOT}" ]]; then - safe_rm "${project_link}" - echo "Removed Chaste registration symlink '${project_link}'." -fi - -# Remove generated build/output directories. These may resolve outside the repo -# (via CHASTE_BUILD_DIR/CHASTE_TEST_OUTPUT), so handle them explicitly. -for dir in "${CHASTE_BUILD_DIR}" "${CHASTE_TEST_OUTPUT}"; do - safe_rm "${dir}" -done - -echo "Reset project template to its original state." diff --git a/setup_project.py b/setup_project.py index 1d8446a..14fb7a3 100644 --- a/setup_project.py +++ b/setup_project.py @@ -34,10 +34,8 @@ Run this script from the project directory once it has been renamed to your project name. """ -import argparse import os import re -import subprocess class Settings: @@ -108,29 +106,12 @@ def update(self) -> None: self.TEST_PACK_FILES = [os.path.join(self.PROJECT_ROOT, "test", "ContinuousTestPack.txt")] -def find_and_replace(filename: str, pattern: str, replacement: str, regex: bool = False) -> None: - """Replace occurrences of pattern with replacement in a file, in place. - - By default pattern is treated as literal text. Pass regex=True to treat it as - a regular expression, matched with re.MULTILINE so ^ and $ anchor to line - boundaries. - """ - if not regex: - pattern = re.escape(pattern) +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(re.sub(pattern, replacement, contents, flags=re.MULTILINE)) - - -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) + f.write(contents.replace(old_string, new_string)) def ask_for_response(question: str) -> bool: @@ -168,18 +149,6 @@ def append_to_file_name(text_to_append: str, file: str) -> str: return new_name -def is_git_repository(path: str) -> bool: - """Return True if path is inside a git working tree.""" - try: - result = subprocess.run( - ["git", "-C", path, "rev-parse", "--is-inside-work-tree"], - capture_output=True, - ) - except FileNotFoundError: - return False - return result.returncode == 0 - - def setup(settings: Settings) -> None: """Customise the template for this project, after confirming the chosen settings.""" # Confirm the template directory has been renamed to the project name before making any changes. @@ -232,69 +201,10 @@ def setup(settings: Settings) -> None: find_and_replace(settings.BASE_CMAKELISTS, " ".join(settings.DEFAULT_COMPONENTS), " ".join(components)) -def reset(settings: Settings) -> None: - """Reset the template to its original state using git, discarding setup's changes. - - The project must be a git repository: the tracked template files are restored - to their committed state and the setup-renamed example files are removed. - """ - # The project can only be reset from a git repository. - if not is_git_repository(settings.PROJECT_ROOT): - print("Error: the project can only be reset if it is a git repository.") - raise SystemExit(1) - - # Confirm before discarding any changes. - print_banner( - "Caution: This will reset the template to its original state!!!", - "Any changes to the example source and CMakeLists.txt files will be lost!!!", - ) - if not ask_for_response("Proceed? [Y/n] "): - print("Aborted.") - raise SystemExit(1) - - # Remove the setup-renamed example source files (these are untracked by git). - suffix = "_" + settings.PROJECT_NAME - for original in settings.TEMPLATE_SOURCE_FILES: - root, ext = os.path.splitext(original) - renamed = root + suffix + ext - if os.path.exists(renamed): - os.remove(renamed) - - # Restore the tracked template files to their committed state. - tracked_files = settings.TEMPLATE_SOURCE_FILES + settings.TEST_PACK_FILES + [ - settings.BASE_CMAKELISTS, - settings.APPS_CMAKELISTS, - settings.TEST_CMAKELISTS, - ] - subprocess.run( - ["git", "-C", settings.PROJECT_ROOT, "checkout", "HEAD", "--", *tracked_files], - check=True, - ) - - -def parse_args() -> argparse.Namespace: - """Parse command-line arguments.""" - parser = argparse.ArgumentParser( - prog="setup_project", - description="Set up a Chaste user project from the template.", - ) - parser.add_argument( - "--reset", - action="store_true", - help="Restore the template to its original state", - ) - return parser.parse_args() - - def main() -> None: - """Set up the project from the template, or restore the template with --reset.""" - args = parse_args() + """Set up the project from the template.""" settings = Settings() - - if args.reset: - reset(settings) - else: - setup(settings) + setup(settings) if __name__ == "__main__": From 76e0f1b6b9e2ab0790215124b4efda8dfde0e86d Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 15:14:59 +0000 Subject: [PATCH 12/51] #3 Add end of setup message --- setup_project.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/setup_project.py b/setup_project.py index 14fb7a3..27cf9ec 100644 --- a/setup_project.py +++ b/setup_project.py @@ -119,15 +119,12 @@ def ask_for_response(question: str) -> bool: An empty response defaults to yes; any unrecognised response re-prompts. """ - # Display the question - print(question) - # Define permitted yes/no answers yes = {"yes", "y", "ye", ""} no = {"no", "n"} - # Take the lower case raw input - choice = input().lower() + # Display the question and take the lower case response + choice = input(question).lower() # Decide on the choice if choice in yes: @@ -152,9 +149,9 @@ def append_to_file_name(text_to_append: str, file: str) -> str: def setup(settings: Settings) -> None: """Customise the template for this project, after confirming the chosen settings.""" # Confirm the template directory has been renamed to the project name before making any changes. - print(f"This project will be set up using '{settings.PROJECT_NAME}' (the directory name) as the project name.") + print(f"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? [Y/n] "): - print(f"Rename the '{settings.PROJECT_NAME}' directory to your project name, then run this script again.") return # Recompute settings in case the directory name has changed @@ -200,6 +197,17 @@ def setup(settings: Settings) -> None: if components: find_and_replace(settings.BASE_CMAKELISTS, " ".join(settings.DEFAULT_COMPONENTS), " ".join(components)) + # Summarise the changes that were made + print("") + print(f"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(f"* 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)}") + def main() -> None: """Set up the project from the template.""" From 238ffc765a35476b9bd1a5e3dfd9ea57e79f5a11 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 15:38:13 +0000 Subject: [PATCH 13/51] #3 Check if project is already setup --- setup_project.py | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/setup_project.py b/setup_project.py index 27cf9ec..4dfb3da 100644 --- a/setup_project.py +++ b/setup_project.py @@ -48,13 +48,7 @@ class Settings: OPTIONAL_COMPONENTS = ["cell_based", "crypt", "heart", "lung"] def __init__(self) -> None: - self.update() - - def update(self) -> None: - """Recompute the paths and substitutions from the current project directory. - - Call this after the project directory is renamed so the values derived from it are refreshed. - """ + """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) @@ -146,17 +140,43 @@ def append_to_file_name(text_to_append: str, file: str) -> str: return new_name +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 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 setup.", + "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(f"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? [Y/n] "): return - # Recompute settings in case the directory name has changed - settings.update() - # 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}' contains characters other than letters, digits, From 23f60a05e99e80def52d976802eabb76aa8b7966 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 16:03:33 +0000 Subject: [PATCH 14/51] #3 Update setup answer defaults --- setup_project.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/setup_project.py b/setup_project.py index 4dfb3da..0301e98 100644 --- a/setup_project.py +++ b/setup_project.py @@ -108,25 +108,28 @@ def find_and_replace(filename: str, old_string: str, new_string: str) -> None: f.write(contents.replace(old_string, new_string)) -def ask_for_response(question: str) -> bool: +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 defaults to yes; any unrecognised response re-prompts. + An empty response returns default; any unrecognised response re-prompts. """ # Define permitted yes/no answers - yes = {"yes", "y", "ye", ""} + yes = {"yes", "y", "ye"} no = {"no", "n"} - # Display the question and take the lower case response - choice = input(question).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: - return ask_for_response("Please respond with yes or no:") + return ask_for_response("Please respond with yes or no:", default) def append_to_file_name(text_to_append: str, file: str) -> str: @@ -174,19 +177,19 @@ def setup(settings: Settings) -> None: # Confirm the template directory has been renamed to the project name before making any changes. print(f"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? [Y/n] "): + 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}' contains characters other than letters, digits, and underscores. Renaming the directory is recommended.""") - return + 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? [Y/n] "): + if ask_for_response(f"Does this project depend on the {component} component?"): components.append(component) # Summarise the chosen options and confirm before making any changes @@ -195,7 +198,7 @@ def setup(settings: Settings) -> None: print(f" Project name: {settings.PROJECT_NAME}") print(f" Chaste components: {', '.join(components) if components else '(template default)'}") print("") - if not ask_for_response("Proceed with these settings? [Y/n] "): + if not ask_for_response("Proceed with these settings?"): print("No changes made.") return From c3e5bf5f9965a69c4cba0d9b7b98c4462681d592 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 16:32:23 +0000 Subject: [PATCH 15/51] #3 Simplify cmake project substitutions --- setup_project.py | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/setup_project.py b/setup_project.py index 0301e98..2563f9d 100644 --- a/setup_project.py +++ b/setup_project.py @@ -66,24 +66,6 @@ def __init__(self) -> None: self.APPS_CMAKELISTS = os.path.join(self.PROJECT_ROOT, "apps", "CMakeLists.txt") self.TEST_CMAKELISTS = os.path.join(self.PROJECT_ROOT, "test", "CMakeLists.txt") - self.CMAKE_PROJECT_SUBSTITUTIONS = [ - ( - self.BASE_CMAKELISTS, - "chaste_do_project(template_project)", - f"chaste_do_project({self.PROJECT_NAME})", - ), - ( - self.APPS_CMAKELISTS, - "chaste_do_apps_project(template_project)", - f"chaste_do_apps_project({self.PROJECT_NAME})", - ), - ( - self.TEST_CMAKELISTS, - "chaste_do_test_project(template_project)", - f"chaste_do_test_project({self.PROJECT_NAME})", - ), - ] - # 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 = { @@ -175,15 +157,17 @@ def setup(settings: Settings) -> None: raise SystemExit(1) # Confirm the template directory has been renamed to the project name before making any changes. - print(f"Make sure to rename the 'template_project' directory to your project name before running this script.") + 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}' contains characters other than letters, digits, - and underscores. Renaming the directory is recommended.""") + 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 @@ -213,8 +197,19 @@ def setup(settings: Settings) -> None: find_and_replace(file, old, new) # Set the project name in the CMakeLists.txt files - for cmake_file, template_text, project_text in settings.CMAKE_PROJECT_SUBSTITUTIONS: - find_and_replace(cmake_file, template_text, project_text) + 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: @@ -222,12 +217,12 @@ def setup(settings: Settings) -> None: # Summarise the changes that were made print("") - print(f"Setup complete.") + 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(f"* Renamed the template files:") + 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)}") From fa118604cd82631f016e2d59486d07d2bab45cda Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 16:44:29 +0000 Subject: [PATCH 16/51] #3 Verify Chaste source directory exists --- scripts/common.sh | 9 +++++++++ scripts/configure.sh | 3 ++- scripts/register.sh | 6 +----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/common.sh b/scripts/common.sh index 56ec161..775d149 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -40,6 +40,15 @@ require_configured() { 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. diff --git a/scripts/configure.sh b/scripts/configure.sh index 177d15d..a30a6ee 100755 --- a/scripts/configure.sh +++ b/scripts/configure.sh @@ -11,8 +11,9 @@ fi script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" source "${script_dir}/common.sh" -# Check that cmake is available. +# 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" diff --git a/scripts/register.sh b/scripts/register.sh index 9911c36..ddb8513 100755 --- a/scripts/register.sh +++ b/scripts/register.sh @@ -16,11 +16,7 @@ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" source "${script_dir}/common.sh" # Check that the Chaste source directory exists. -if [[ ! -d "${CHASTE_SOURCE_DIR}" ]]; then - echo "Error: Chaste source directory not found at '${CHASTE_SOURCE_DIR}'." >&2 - echo "Set CHASTE_SOURCE_DIR to override the default sibling checkout path." >&2 - exit 1 -fi +require_source # Register the project project_link="${CHASTE_PROJECTS_DIR}/${PROJECT_NAME}" From a05b8cee7c389c5fbdcf8ef07f1d9438a3ddc942 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 21:48:18 +0000 Subject: [PATCH 17/51] #3 Add optional Python bindings support When setup_project.py is run with Python bindings opted in, it: - Substitutes the project name into dynamic/config.yaml and dynamic/CMakeLists.txt, which set up CPPWG-based wrapper generation - Renames src/py/template_project/ to src/py// and substitutes the project name into the Python package files - Leaves dynamic/ and src/py/ in place for the build system to use When opted out, dynamic/ and src/py/ are removed. The build scripts are extended accordingly: - common.sh auto-detects Chaste_ENABLE_PYCHASTE from dynamic/config.yaml and guards against CHASTE_BUILD_DIR pointing at the project root - configure.sh passes Chaste_ENABLE_PYCHASTE to cmake - compile.sh builds pychaste then __all when pychaste is enabled, or the C++ project library only when it is not - install.sh (new) creates .virtualenv/ and pip-installs the pychaste and project Python packages from the build directory - .github/workflows/test-python-project.yml (new) tests the full pipeline using the chaste/develop container Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test-python-project.yml | 58 +++++++++ .gitignore | 1 + CMakeLists.txt | 4 + dynamic/CMakeLists.txt | 144 ++++++++++++++++++++++ dynamic/config.yaml | 53 ++++++++ scripts/common.sh | 14 ++- scripts/compile.sh | 7 +- scripts/configure.sh | 5 +- scripts/install.sh | 51 ++++++++ setup_project.py | 55 ++++++++- src/py/MANIFEST.in | 1 + src/py/pyproject.toml | 34 +++++ src/py/setup.cfg | 58 +++++++++ src/py/setup.py | 44 +++++++ src/py/template_project/__init__.py | 32 +++++ 15 files changed, 555 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/test-python-project.yml create mode 100644 dynamic/CMakeLists.txt create mode 100644 dynamic/config.yaml create mode 100755 scripts/install.sh create mode 100644 src/py/MANIFEST.in create mode 100644 src/py/pyproject.toml create mode 100644 src/py/setup.cfg create mode 100644 src/py/setup.py create mode 100644 src/py/template_project/__init__.py diff --git a/.github/workflows/test-python-project.yml b/.github/workflows/test-python-project.yml new file mode 100644 index 0000000..cca79a0 --- /dev/null +++ b/.github/workflows/test-python-project.yml @@ -0,0 +1,58 @@ +name: Test Python Project + +on: + push: + branches: ["**"] + 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/develop + + env: + CHASTE_BUILD_DIR: ${{ runner.temp }}/build + CHASTE_TEST_OUTPUT: ${{ runner.temp }}/testoutput + PROJECT_DIR: ${{ runner.temp }}/myproject + + steps: + - name: Checkout template + uses: actions/checkout@v6 + + - name: Create test project + run: | + cp -r ${{ github.workspace }} ${{ env.PROJECT_DIR }} + cd ${{ env.PROJECT_DIR }} + printf '\n\n\n\n\ny\ny\n' | python3 setup_project.py + + - name: Configure + run: | + mkdir -p ${{ env.CHASTE_BUILD_DIR }} + mkdir -p ${{ env.CHASTE_TEST_OUTPUT }} + ${{ env.PROJECT_DIR }}/scripts/configure.sh + + - name: Compile + run: | + ${{ env.PROJECT_DIR }}/scripts/compile.sh + + - name: Install + run: | + ${{ env.PROJECT_DIR }}/scripts/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/.gitignore b/.gitignore index e7ae214..bbe0e20 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__/ *.py[cod] *.egg-info/ .eggs/ +.virtualenv/ .venv/ venv/ env/ diff --git a/CMakeLists.txt b/CMakeLists.txt index c446709..1f2ae8d 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) + add_subdirectory(dynamic) +endif() diff --git a/dynamic/CMakeLists.txt b/dynamic/CMakeLists.txt new file mode 100644 index 0000000..71427e6 --- /dev/null +++ b/dynamic/CMakeLists.txt @@ -0,0 +1,144 @@ +# 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) + list(APPEND PROJECT_INCLUDE_DIRS ${_include_dirs}) +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 if not already present +if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/wrappers") + execute_process(COMMAND ${WRAPPER_GENERATION_COMMAND}) +endif() + +# 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` shared 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 +) + +pybind11_add_module(_template_project_all SHARED ${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 -flto=auto -Wno-unused-local-typedefs) + +set_target_properties(_template_project_all PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/package/template_project +) + +################################ +#### Copy Python package +################################ + +# Get list of files in the Python package directory: src/py/ +set(package_file_paths "") +file( + GLOB_RECURSE + package_file_paths + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/../src/py + ${CMAKE_CURRENT_SOURCE_DIR}/../src/py/* +) + +# Exclude compiled files +list(FILTER package_file_paths EXCLUDE REGEX ".*\\.(dll|dylib|pyc|so)$") + +# Copy files to the build directory +foreach(file_path ${package_file_paths}) + if(file_path MATCHES ".*\\.(cfg|in|js|py|toml)$") + # Copy file and track changes + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/../src/py/${file_path} + ${CMAKE_CURRENT_BINARY_DIR}/package/${file_path} + COPYONLY + ) + else() + # Copy file, but don't track changes + file( + COPY ${CMAKE_CURRENT_SOURCE_DIR}/../src/py/${file_path} + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/package/ + ) + endif() +endforeach() diff --git a/dynamic/config.yaml b/dynamic/config.yaml new file mode 100644 index 0000000..ddd3831 --- /dev/null +++ b/dynamic/config.yaml @@ -0,0 +1,53 @@ +# 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 + +modules: + - name: all + source_locations: + - src/ + classes: + - name: Hello diff --git a/scripts/common.sh b/scripts/common.sh index 775d149..2fb2ebf 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -7,6 +7,11 @@ common_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd -- "${common_dir}/.." && pwd)" CHASTE_BUILD_DIR="${CHASTE_BUILD_DIR:-${PROJECT_ROOT}/build}" +if [[ "${CHASTE_BUILD_DIR}" == "${PROJECT_ROOT}" ]]; then + echo "Error: CHASTE_BUILD_DIR must not be the project root '${PROJECT_ROOT}'." >&2 + echo "Set CHASTE_BUILD_DIR to a separate build directory." >&2 + exit 1 +fi CHASTE_SOURCE_DIR="${CHASTE_SOURCE_DIR:-${PROJECT_ROOT}/../Chaste}" CHASTE_PROJECTS_DIR="${CHASTE_SOURCE_DIR}/projects" @@ -17,6 +22,13 @@ export CHASTE_TEST_OUTPUT="${CHASTE_TEST_OUTPUT}" # The name of this project is the name of the project directory. PROJECT_NAME="$(basename "${PROJECT_ROOT}")" +if [[ -f "${PROJECT_ROOT}/dynamic/config.yaml" ]]; then + # Enable pychaste if this project has Python bindings set up. + Chaste_ENABLE_PYCHASTE=ON +else + Chaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE:-OFF}" +fi + # Set the number of parallel jobs for building and testing. NCORES="${NCORES:-4}" if ! [[ "${NCORES}" =~ ^[0-9]+$ ]] || [[ "${NCORES}" -lt 1 ]]; then @@ -54,7 +66,7 @@ require_source() { # deletes. For a symlink, only the link itself is removed, not its target. safe_rm() { local path="$1" - if [[ -z "${path}" || "${path}" == "/" || "${path}" == "${PROJECT_ROOT}" ]]; then + if [[ -z "${path}" || "${path}" == "/" || "${path}" == "${PROJECT_ROOT}" || "${path}" == "${CHASTE_SOURCE_DIR}" ]]; then echo "Error: refusing to remove unsafe path '${path}'." >&2 exit 1 fi diff --git a/scripts/compile.sh b/scripts/compile.sh index 0c00552..0c10a33 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -23,4 +23,9 @@ require_configured # Build. cd "${CHASTE_BUILD_DIR}" -cmake --build . --target "project_${PROJECT_NAME}" --parallel "${NCORES}" +if [[ "${Chaste_ENABLE_PYCHASTE}" == "ON" ]]; then + cmake --build . --target pychaste --parallel "${NCORES}" + cmake --build . --target "_${PROJECT_NAME}_all" --parallel "${NCORES}" +else + cmake --build . --target "project_${PROJECT_NAME}" --parallel "${NCORES}" +fi diff --git a/scripts/configure.sh b/scripts/configure.sh index a30a6ee..40ebe69 100755 --- a/scripts/configure.sh +++ b/scripts/configure.sh @@ -21,6 +21,5 @@ require_source # Create the build directory mkdir -p "${CHASTE_BUILD_DIR}" -# Configure. -cd "${CHASTE_BUILD_DIR}" -cmake "${CHASTE_SOURCE_DIR}" +# Configure +cmake "${CHASTE_SOURCE_DIR}" -DChaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE:-OFF}" diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..8c03271 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,51 @@ +#!/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 if it does not already exist. +venv_dir="${PROJECT_ROOT}/.virtualenv" +python3 -m venv "${venv_dir}" + +# 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/setup_project.py b/setup_project.py index 2563f9d..ea0d723 100644 --- a/setup_project.py +++ b/setup_project.py @@ -36,6 +36,7 @@ import os import re +import shutil class Settings: @@ -81,6 +82,29 @@ def __init__(self) -> None: 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") + 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.""" @@ -176,11 +200,15 @@ def setup(settings: Settings) -> None: 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?") + # 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("") if not ask_for_response("Proceed with these settings?"): print("No changes made.") @@ -198,7 +226,7 @@ def setup(settings: Settings) -> None: # 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}))" + settings.BASE_CMAKELISTS, "chaste_do_project(template_project)", f"chaste_do_project({settings.PROJECT_NAME})" ) find_and_replace( settings.APPS_CMAKELISTS, @@ -215,17 +243,42 @@ def setup(settings: Settings) -> None: 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) + 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")) + # 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/.") + else: + print("* Removed Python bindings template files in dynamic/ and src/py/.") + def main() -> None: """Set up the project from the template.""" diff --git a/src/py/MANIFEST.in b/src/py/MANIFEST.in new file mode 100644 index 0000000..d0cf51e --- /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 * From dc6703e6e1d4664c2e2e315053bc4d7afe531495 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 21:54:44 +0000 Subject: [PATCH 18/51] #3 Fix test workflow --- .github/workflows/test-python-project.yml | 8 ++++---- scripts/configure.sh | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-python-project.yml b/.github/workflows/test-python-project.yml index cca79a0..7a4d846 100644 --- a/.github/workflows/test-python-project.yml +++ b/.github/workflows/test-python-project.yml @@ -18,13 +18,13 @@ jobs: image: chaste/develop env: - CHASTE_BUILD_DIR: ${{ runner.temp }}/build - CHASTE_TEST_OUTPUT: ${{ runner.temp }}/testoutput - PROJECT_DIR: ${{ runner.temp }}/myproject + CHASTE_BUILD_DIR: /tmp/build + CHASTE_TEST_OUTPUT: /tmp/testoutput + PROJECT_DIR: /tmp/myproject steps: - name: Checkout template - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Create test project run: | diff --git a/scripts/configure.sh b/scripts/configure.sh index 40ebe69..64b07bd 100755 --- a/scripts/configure.sh +++ b/scripts/configure.sh @@ -22,4 +22,5 @@ require_source mkdir -p "${CHASTE_BUILD_DIR}" # Configure +cd "${CHASTE_BUILD_DIR}" cmake "${CHASTE_SOURCE_DIR}" -DChaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE:-OFF}" From d257012cb9a6c57e5fa7f91d0481cd83300af8c8 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 22:04:20 +0000 Subject: [PATCH 19/51] #3 Default to checkout v6 --- .github/workflows/test-python-project.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-python-project.yml b/.github/workflows/test-python-project.yml index 7a4d846..c81e7ff 100644 --- a/.github/workflows/test-python-project.yml +++ b/.github/workflows/test-python-project.yml @@ -2,7 +2,7 @@ name: Test Python Project on: push: - branches: ["**"] + branches: ["main"] pull_request: branches: ["**"] @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout template - uses: actions/checkout@v7 + uses: actions/checkout@v6 - name: Create test project run: | From f1b6294e50b6251405848cee9b7d48f3e6548fdb Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Mon, 22 Jun 2026 22:18:17 +0000 Subject: [PATCH 20/51] #3 Use privileged test container --- .github/workflows/test-python-project.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-python-project.yml b/.github/workflows/test-python-project.yml index c81e7ff..2be6462 100644 --- a/.github/workflows/test-python-project.yml +++ b/.github/workflows/test-python-project.yml @@ -16,6 +16,7 @@ jobs: container: image: chaste/develop + options: --user root env: CHASTE_BUILD_DIR: /tmp/build @@ -24,7 +25,10 @@ jobs: steps: - name: Checkout template - uses: actions/checkout@v6 + uses: actions/checkout@v7 + + - name: Mark Chaste source as safe for git + run: git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" - name: Create test project run: | From 6151fd0fedad32399a122480d08c3a778e57f8f2 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 05:24:00 +0000 Subject: [PATCH 21/51] #3 Fix workflow template path --- .github/workflows/test-python-project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-python-project.yml b/.github/workflows/test-python-project.yml index 2be6462..d336948 100644 --- a/.github/workflows/test-python-project.yml +++ b/.github/workflows/test-python-project.yml @@ -32,7 +32,7 @@ jobs: - name: Create test project run: | - cp -r ${{ github.workspace }} ${{ env.PROJECT_DIR }} + cp -r . ${{ env.PROJECT_DIR }} cd ${{ env.PROJECT_DIR }} printf '\n\n\n\n\ny\ny\n' | python3 setup_project.py From a79d642881527f81df28d0bc136acef7d35fbb42 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 06:11:22 +0000 Subject: [PATCH 22/51] #3 Auto-set chaste source dir --- scripts/common.sh | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/scripts/common.sh b/scripts/common.sh index 2fb2ebf..3971522 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -6,6 +6,33 @@ common_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd -- "${common_dir}/.." && pwd)" +# All settings defer to environment variables if they are available. +# This simplifies working in a chaste docker container where the values are pre-set. + +# If the Chaste source directory is not set, try to find it in a few common locations: +# e.g. at same-level (../Chaste), up from Chaste/projects (../../Chaste), +# in home (~/Chaste), chaste docker path (/home/chaste/src). +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 + if [[ -z "${CHASTE_SOURCE_DIR:-}" ]]; then + echo "Error: could not find Chaste source directory." >&2 + echo "Set CHASTE_SOURCE_DIR to the location of your Chaste source." >&2 + exit 1 + fi +fi + +CHASTE_PROJECTS_DIR="${CHASTE_SOURCE_DIR}/projects" + CHASTE_BUILD_DIR="${CHASTE_BUILD_DIR:-${PROJECT_ROOT}/build}" if [[ "${CHASTE_BUILD_DIR}" == "${PROJECT_ROOT}" ]]; then echo "Error: CHASTE_BUILD_DIR must not be the project root '${PROJECT_ROOT}'." >&2 @@ -13,11 +40,9 @@ if [[ "${CHASTE_BUILD_DIR}" == "${PROJECT_ROOT}" ]]; then exit 1 fi -CHASTE_SOURCE_DIR="${CHASTE_SOURCE_DIR:-${PROJECT_ROOT}/../Chaste}" -CHASTE_PROJECTS_DIR="${CHASTE_SOURCE_DIR}/projects" - -CHASTE_TEST_OUTPUT="${CHASTE_TEST_OUTPUT:-${PROJECT_ROOT}/output}" -export CHASTE_TEST_OUTPUT="${CHASTE_TEST_OUTPUT}" +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}")" From 3ab6d49469e72c4eb84497e807255a3389a8ccc8 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 06:30:07 +0000 Subject: [PATCH 23/51] #3 Add standard project github workflow --- .github/workflows/test-project.yml | 47 +++++++++++++++++++++++ .github/workflows/test-python-project.yml | 3 +- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test-project.yml diff --git a/.github/workflows/test-project.yml b/.github/workflows/test-project.yml new file mode 100644 index 0000000..8402c3b --- /dev/null +++ b/.github/workflows/test-project.yml @@ -0,0 +1,47 @@ +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/develop + options: --user root + + env: + CHASTE_BUILD_DIR: /tmp/build + CHASTE_TEST_OUTPUT: /tmp/testoutput + PROJECT_DIR: /tmp/myproject + + steps: + - name: Checkout template + uses: actions/checkout@v7 + + - name: Mark Chaste source as safe for git + run: git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" + + - name: Create test project + run: | + cp -r . ${{ env.PROJECT_DIR }} + cd ${{ env.PROJECT_DIR }} + # Use prompt defaults, yes to final confirmation + printf '\n\n\n\n\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 index d336948..0a37457 100644 --- a/.github/workflows/test-python-project.yml +++ b/.github/workflows/test-python-project.yml @@ -34,12 +34,11 @@ jobs: run: | cp -r . ${{ env.PROJECT_DIR }} cd ${{ env.PROJECT_DIR }} + # Yes to python bindings prompt and final confirmation prompt printf '\n\n\n\n\ny\ny\n' | python3 setup_project.py - name: Configure run: | - mkdir -p ${{ env.CHASTE_BUILD_DIR }} - mkdir -p ${{ env.CHASTE_TEST_OUTPUT }} ${{ env.PROJECT_DIR }}/scripts/configure.sh - name: Compile From 6075a2ac1488b9f6cfc46fb64f139492740b0f28 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 07:16:28 +0000 Subject: [PATCH 24/51] #3 Add BUILD_PROJECT_PYTHON_BINDINGS script var --- CMakeLists.txt | 2 +- scripts/common.sh | 15 +++++---------- scripts/compile.sh | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f2ae8d..e2b3ca3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,6 @@ find_package(Chaste COMPONENTS continuum_mechanics global io linalg mesh ode pde # i.e. the name of your project. chaste_do_project(template_project) -if(Chaste_ENABLE_PYCHASTE) +if(Chaste_ENABLE_PYCHASTE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/dynamic") add_subdirectory(dynamic) endif() diff --git a/scripts/common.sh b/scripts/common.sh index 3971522..89bba85 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -6,12 +6,7 @@ common_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd -- "${common_dir}/.." && pwd)" -# All settings defer to environment variables if they are available. -# This simplifies working in a chaste docker container where the values are pre-set. - -# If the Chaste source directory is not set, try to find it in a few common locations: -# e.g. at same-level (../Chaste), up from Chaste/projects (../../Chaste), -# in home (~/Chaste), chaste docker path (/home/chaste/src). +# If the Chaste source directory is not set, try to find it in a few common locations. if [[ -z "${CHASTE_SOURCE_DIR:-}" ]]; then for _candidate in \ "${PROJECT_ROOT}/../Chaste" \ @@ -50,15 +45,15 @@ PROJECT_NAME="$(basename "${PROJECT_ROOT}")" 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. -NCORES="${NCORES:-4}" -if ! [[ "${NCORES}" =~ ^[0-9]+$ ]] || [[ "${NCORES}" -lt 1 ]]; then - echo "Error: NCORES must be a positive integer (got '${NCORES}')." >&2 - exit 1 +if ! [[ "${NCORES:-}" =~ ^[1-9][0-9]*$ ]]; then + NCORES="$(nproc)" fi # Abort with an error if the given command is not on PATH. diff --git a/scripts/compile.sh b/scripts/compile.sh index 0c10a33..824c2f6 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -23,7 +23,7 @@ require_configured # Build. cd "${CHASTE_BUILD_DIR}" -if [[ "${Chaste_ENABLE_PYCHASTE}" == "ON" ]]; then +if [[ "${BUILD_PROJECT_PYTHON_BINDINGS}" == "ON" ]]; then cmake --build . --target pychaste --parallel "${NCORES}" cmake --build . --target "_${PROJECT_NAME}_all" --parallel "${NCORES}" else From 5470b1fb554d5000235b2db1b817795072013a3b Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 08:43:37 +0000 Subject: [PATCH 25/51] #3 Simplify python bindings build --- dynamic/CMakeLists.txt | 3 +++ scripts/compile.sh | 7 +------ scripts/configure.sh | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/dynamic/CMakeLists.txt b/dynamic/CMakeLists.txt index 71427e6..7e35f30 100644 --- a/dynamic/CMakeLists.txt +++ b/dynamic/CMakeLists.txt @@ -105,6 +105,9 @@ target_include_directories(_template_project_all PRIVATE ${PROJECT_INCLUDE_DIRS} target_compile_options(_template_project_all PRIVATE -flto=auto -Wno-unused-local-typedefs) +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 ) diff --git a/scripts/compile.sh b/scripts/compile.sh index 824c2f6..0c00552 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -23,9 +23,4 @@ require_configured # Build. cd "${CHASTE_BUILD_DIR}" -if [[ "${BUILD_PROJECT_PYTHON_BINDINGS}" == "ON" ]]; then - cmake --build . --target pychaste --parallel "${NCORES}" - cmake --build . --target "_${PROJECT_NAME}_all" --parallel "${NCORES}" -else - cmake --build . --target "project_${PROJECT_NAME}" --parallel "${NCORES}" -fi +cmake --build . --target "project_${PROJECT_NAME}" --parallel "${NCORES}" diff --git a/scripts/configure.sh b/scripts/configure.sh index 64b07bd..a0c57f8 100755 --- a/scripts/configure.sh +++ b/scripts/configure.sh @@ -23,4 +23,4 @@ mkdir -p "${CHASTE_BUILD_DIR}" # Configure cd "${CHASTE_BUILD_DIR}" -cmake "${CHASTE_SOURCE_DIR}" -DChaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE:-OFF}" +cmake "${CHASTE_SOURCE_DIR}" -DChaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE}" From f4cb2d74dada9dd7045edc419604dab880d6083c Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 08:53:55 +0000 Subject: [PATCH 26/51] #3 Update safe_rm --- scripts/common.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/common.sh b/scripts/common.sh index 89bba85..7087f50 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -86,9 +86,18 @@ require_source() { # 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}" + if [[ -t 0 ]]; then + # Prompt for confirmation if in an interactive shell. + rm -rI "${path}" + else + rm -rf "${path}" + fi } From a1675f055ba9e4c8f1ab6dd03f0ef410e076a4d0 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 09:13:04 +0000 Subject: [PATCH 27/51] #3 Simplify python src copy --- dynamic/CMakeLists.txt | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/dynamic/CMakeLists.txt b/dynamic/CMakeLists.txt index 7e35f30..e5d6192 100644 --- a/dynamic/CMakeLists.txt +++ b/dynamic/CMakeLists.txt @@ -113,35 +113,16 @@ set_target_properties(_template_project_all PROPERTIES ) ################################ -#### Copy Python package +#### Copy Python source ################################ -# Get list of files in the Python package directory: src/py/ -set(package_file_paths "") -file( - GLOB_RECURSE - package_file_paths - RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/../src/py +file(GLOB_RECURSE _py_sources + CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../src/py/* ) +list(FILTER _py_sources EXCLUDE REGEX "\\.(dll|dylib|pyc|so)$") -# Exclude compiled files -list(FILTER package_file_paths EXCLUDE REGEX ".*\\.(dll|dylib|pyc|so)$") - -# Copy files to the build directory -foreach(file_path ${package_file_paths}) - if(file_path MATCHES ".*\\.(cfg|in|js|py|toml)$") - # Copy file and track changes - configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/../src/py/${file_path} - ${CMAKE_CURRENT_BINARY_DIR}/package/${file_path} - COPYONLY - ) - else() - # Copy file, but don't track changes - file( - COPY ${CMAKE_CURRENT_SOURCE_DIR}/../src/py/${file_path} - DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/package/ - ) - endif() +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() From efc28ca2ab045eb8b021e36d7af0c107642149d7 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 09:30:13 +0000 Subject: [PATCH 28/51] #3 Add chaste src dir guard --- scripts/common.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/common.sh b/scripts/common.sh index 7087f50..5a28c40 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -29,8 +29,8 @@ fi CHASTE_PROJECTS_DIR="${CHASTE_SOURCE_DIR}/projects" CHASTE_BUILD_DIR="${CHASTE_BUILD_DIR:-${PROJECT_ROOT}/build}" -if [[ "${CHASTE_BUILD_DIR}" == "${PROJECT_ROOT}" ]]; then - echo "Error: CHASTE_BUILD_DIR must not be the project root '${PROJECT_ROOT}'." >&2 +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 From 48dc02a948a0b5e34470c198e546295bfd9c1271 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 09:30:50 +0000 Subject: [PATCH 29/51] #3 Add pybind11 OPT_SIZE flag --- dynamic/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynamic/CMakeLists.txt b/dynamic/CMakeLists.txt index e5d6192..fc8d588 100644 --- a/dynamic/CMakeLists.txt +++ b/dynamic/CMakeLists.txt @@ -92,7 +92,7 @@ file(GLOB_RECURSE WRAPPER_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/wrappers/all/*.hpp ) -pybind11_add_module(_template_project_all SHARED ${WRAPPER_SOURCES}) +pybind11_add_module(_template_project_all SHARED OPT_SIZE ${WRAPPER_SOURCES}) target_link_libraries(_template_project_all PRIVATE ${Python3_LIBRARIES} From 9ba53897f44edae06f9a1ab82a2a2838c822a612 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Tue, 23 Jun 2026 11:15:42 +0100 Subject: [PATCH 30/51] #3 Fail configure if necessary wrappers don't generate Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- dynamic/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dynamic/CMakeLists.txt b/dynamic/CMakeLists.txt index fc8d588..ff340de 100644 --- a/dynamic/CMakeLists.txt +++ b/dynamic/CMakeLists.txt @@ -66,9 +66,11 @@ set(WRAPPER_GENERATION_COMMAND --std c++17 ) -# Generate wrappers if not already present if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/wrappers") - execute_process(COMMAND ${WRAPPER_GENERATION_COMMAND}) + execute_process( + COMMAND ${WRAPPER_GENERATION_COMMAND} + COMMAND_ERROR_IS_FATAL ANY + ) endif() # Target for manually regenerating wrappers From d53c804f991b133ccf47c2d62f7d051ea57577e3 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 11:39:47 +0000 Subject: [PATCH 31/51] #3 Simplify python cmake --- dynamic/CMakeLists.txt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/dynamic/CMakeLists.txt b/dynamic/CMakeLists.txt index ff340de..537d8b0 100644 --- a/dynamic/CMakeLists.txt +++ b/dynamic/CMakeLists.txt @@ -43,7 +43,15 @@ get_target_property(PROJECT_INCLUDE_DIRS chaste_project_template_project INCLUDE # Add VTK 9+ includes if(TARGET VTK::CommonCore) get_target_property(_include_dirs VTK::CommonCore INTERFACE_INCLUDE_DIRECTORIES) - list(APPEND PROJECT_INCLUDE_DIRS ${_include_dirs}) + 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 @@ -66,6 +74,7 @@ set(WRAPPER_GENERATION_COMMAND --std c++17 ) +# Generate wrappers if not already present if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/wrappers") execute_process( COMMAND ${WRAPPER_GENERATION_COMMAND} @@ -86,7 +95,7 @@ list(APPEND PROJECT_INCLUDE_DIRS ${_include_dirs}) ################################ #### Build Python module ################################ -# Creates a `_template_project_all` shared library from the Python wrappers. +# 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 @@ -94,7 +103,7 @@ file(GLOB_RECURSE WRAPPER_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/wrappers/all/*.hpp ) -pybind11_add_module(_template_project_all SHARED OPT_SIZE ${WRAPPER_SOURCES}) +pybind11_add_module(_template_project_all OPT_SIZE ${WRAPPER_SOURCES}) target_link_libraries(_template_project_all PRIVATE ${Python3_LIBRARIES} From 0b05c0c4287f26691948edfeb9b54ff473a3691c Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 11:40:35 +0000 Subject: [PATCH 32/51] #3 Default Chaste provenance to off --- scripts/common.sh | 2 ++ scripts/configure.sh | 4 +++- scripts/register.sh | 2 +- setup_project.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/common.sh b/scripts/common.sh index 5a28c40..a3e23e9 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -42,6 +42,8 @@ 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 diff --git a/scripts/configure.sh b/scripts/configure.sh index a0c57f8..a1a13ef 100755 --- a/scripts/configure.sh +++ b/scripts/configure.sh @@ -23,4 +23,6 @@ mkdir -p "${CHASTE_BUILD_DIR}" # Configure cd "${CHASTE_BUILD_DIR}" -cmake "${CHASTE_SOURCE_DIR}" -DChaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE}" +cmake "${CHASTE_SOURCE_DIR}" \ + -DChaste_ENABLE_PYCHASTE="${Chaste_ENABLE_PYCHASTE}" \ + -DChaste_UPDATE_PROVENANCE="${Chaste_UPDATE_PROVENANCE}" diff --git a/scripts/register.sh b/scripts/register.sh index ddb8513..00cb165 100755 --- a/scripts/register.sh +++ b/scripts/register.sh @@ -2,7 +2,7 @@ set -euo pipefail # Register this project with Chaste. -# The project is only built if it appears under the Chaste/projects/ directory +# 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. diff --git a/setup_project.py b/setup_project.py index ea0d723..53ee85d 100644 --- a/setup_project.py +++ b/setup_project.py @@ -189,7 +189,7 @@ def setup(settings: Settings) -> None: # 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." + f"ERROR: the project name '{settings.PROJECT_NAME}' is not a valid C++ name. " "Renaming the directory is recommended." ) raise SystemExit(1) From 66cb27d90b046bd4fb0520bc825b081eb4d10db6 Mon Sep 17 00:00:00 2001 From: Kwabena N Amponsah Date: Tue, 23 Jun 2026 15:07:15 +0000 Subject: [PATCH 33/51] #3 Add common template substitutions to config.yaml --- dynamic/config.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dynamic/config.yaml b/dynamic/config.yaml index ddd3831..0bba4fd 100644 --- a/dynamic/config.yaml +++ b/dynamic/config.yaml @@ -45,6 +45,18 @@ 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]] + - signature: + replacement: [[2, 2], [3, 3]] + modules: - name: all source_locations: From f892ff61b04d7c8ca9136a8e04ab3a56a3e86b6d Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Mon, 29 Jun 2026 19:20:26 +0100 Subject: [PATCH 34/51] #3 Add SBML template project --- .github/workflows/test-sbml-project.yml | 62 ++++ README.md | 64 ++++ examples/goldbeter_1991/README.md | 146 +++++++++ .../TestGoldbeter1991SbmlSrnModel.hpp | 55 ++++ scripts/sbml_install.sh | 42 +++ setup_project.py | 64 ++++ src/AbstractSbmlCellCycleModel.cpp | 165 ++++++++++ src/AbstractSbmlCellCycleModel.hpp | 165 ++++++++++ src/AbstractSbmlOdeSystem.cpp | 148 +++++++++ src/AbstractSbmlOdeSystem.hpp | 229 +++++++++++++ src/AbstractSbmlSrnModel.cpp | 124 +++++++ src/AbstractSbmlSrnModel.hpp | 138 ++++++++ src/SbmlEventType.hpp | 10 + src/SbmlMath.cpp | 190 +++++++++++ src/SbmlMath.hpp | 310 ++++++++++++++++++ src/fortests/SbmlTestHelpers.cpp | 143 ++++++++ src/fortests/SbmlTestHelpers.hpp | 195 +++++++++++ src/fortests/SbmlTestOdeSolution.cpp | 97 ++++++ src/fortests/SbmlTestOdeSolution.hpp | 104 ++++++ 19 files changed, 2451 insertions(+) create mode 100644 .github/workflows/test-sbml-project.yml create mode 100644 examples/goldbeter_1991/README.md create mode 100644 examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp create mode 100755 scripts/sbml_install.sh create mode 100644 src/AbstractSbmlCellCycleModel.cpp create mode 100644 src/AbstractSbmlCellCycleModel.hpp create mode 100644 src/AbstractSbmlOdeSystem.cpp create mode 100644 src/AbstractSbmlOdeSystem.hpp create mode 100644 src/AbstractSbmlSrnModel.cpp create mode 100644 src/AbstractSbmlSrnModel.hpp create mode 100644 src/SbmlEventType.hpp create mode 100644 src/SbmlMath.cpp create mode 100644 src/SbmlMath.hpp create mode 100644 src/fortests/SbmlTestHelpers.cpp create mode 100644 src/fortests/SbmlTestHelpers.hpp create mode 100644 src/fortests/SbmlTestOdeSolution.cpp create mode 100644 src/fortests/SbmlTestOdeSolution.hpp diff --git a/.github/workflows/test-sbml-project.yml b/.github/workflows/test-sbml-project.yml new file mode 100644 index 0000000..eb97927 --- /dev/null +++ b/.github/workflows/test-sbml-project.yml @@ -0,0 +1,62 @@ +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/develop + options: --user root + + env: + CHASTE_BUILD_DIR: /tmp/build + CHASTE_TEST_OUTPUT: /tmp/testoutput + PROJECT_DIR: /tmp/myproject + + steps: + - name: Checkout template + uses: actions/checkout@v7 + + - name: Mark Chaste source as safe for git + run: git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" + + - name: Install clang-format + run: apt-get update && apt-get install -y clang-format + + - name: Create test project + run: | + cp -r . ${{ env.PROJECT_DIR }} + cd ${{ env.PROJECT_DIR }} + # Prompt defaults for proceed/components/no-python-bindings, yes to SBML, yes to confirm + printf '\n\n\n\n\n\ny\ny\n' | python3 setup_project.py + + - name: Install SBML code generator + run: ${{ env.PROJECT_DIR }}/scripts/sbml_install.sh + + - name: Import the Goldbeter 1991 model + run: | + cd ${{ env.PROJECT_DIR }} + curl -L "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000003?filename=BIOMD0000000003_url.xml" \ + -o Goldbeter1991.xml + .virtualenv/bin/chaste_codegen_sbml 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/README.md b/README.md index a8d13a4..846f905 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,67 @@ 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 by hand, run `scripts/sbml_install.sh`.) + +### 3. Convert an SBML model into a Chaste model + +```sh +chaste_codegen_sbml 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 + +These live in `src/` and 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. | diff --git a/examples/goldbeter_1991/README.md b/examples/goldbeter_1991/README.md new file mode 100644 index 0000000..9dbeffe --- /dev/null +++ b/examples/goldbeter_1991/README.md @@ -0,0 +1,146 @@ +# 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-codegen-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. Download the SBML model and give it a clean name + +The generated C++ class names come from the **file name**, so download the model and +rename it to something C++-friendly. Here we use `Goldbeter1991`: + +```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_codegen_sbml Goldbeter1991.xml --model-type srn --output-dir src/ +``` + +This generates four files in `src/`: + +* `Goldbeter1991SbmlOdeSystem.hpp` / `.cpp` — the ODE system (state variables `C`, `M`, `X`). +* `Goldbeter1991SbmlSrnModel.hpp` / `.cpp` — the SRN model that wraps the ODE system. + +These have the base classes (`AbstractSbmlOdeSystem`, `AbstractSbmlSrnModel`). + +## 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 "NoCellCycleModel.hpp" +#include "SimulationTime.hpp" +#include "SmartPointers.hpp" +#include "StemCellProliferativeType.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() + { + // Integrate to t = 1000 in 1000 steps (AbstractCellBasedTestSuite has set the start time to 0). + SimulationTime* p_simulation_time = SimulationTime::Instance(); + p_simulation_time->SetEndTimeAndNumberOfTimeSteps(1000.0, 1000); + + // Create the SRN model from the imported SBML model and attach it to a cell. + Goldbeter1991SbmlSrnModel* p_srn_model = new Goldbeter1991SbmlSrnModel(); + + MAKE_PTR(WildTypeCellMutationState, p_state); + MAKE_PTR(StemCellProliferativeType, p_stem_type); + NoCellCycleModel* p_cc_model = new NoCellCycleModel(); + + CellPtr p_cell(new Cell(p_state, p_cc_model, p_srn_model)); + p_cell->SetCellProliferativeType(p_stem_type); + p_cell->InitialiseCellCycleModel(); + p_cell->InitialiseSrnModel(); + + // Step the simulation to the end time, advancing the SRN model each step. + while (!p_simulation_time->IsFinished()) + { + p_simulation_time->IncrementTimeOneStep(); + p_srn_model->SimulateToCurrentTime(); + } + + // Check the steady state of the mitotic oscillator. + TS_ASSERT_DELTA(p_srn_model->GetStateVariable("C"), 0.5470, 1e-2); + TS_ASSERT_DELTA(p_srn_model->GetStateVariable("M"), 0.2936, 1e-2); + TS_ASSERT_DELTA(p_srn_model->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..a3d8592 --- /dev/null +++ b/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp @@ -0,0 +1,55 @@ +#ifndef TESTGOLDBETER1991SBMLSRNMODEL_HPP_ +#define TESTGOLDBETER1991SBMLSRNMODEL_HPP_ + +#include + +#include "AbstractCellBasedTestSuite.hpp" +#include "Cell.hpp" +#include "NoCellCycleModel.hpp" +#include "SimulationTime.hpp" +#include "SmartPointers.hpp" +#include "StemCellProliferativeType.hpp" +#include "WildTypeCellMutationState.hpp" + +// The header generated from Goldbeter1991.xml by chaste_codegen_sbml. +#include "Goldbeter1991SbmlSrnModel.hpp" + +// This is a serial test. +#include "FakePetscSetup.hpp" + +class TestGoldbeter1991SbmlSrnModel : public AbstractCellBasedTestSuite +{ +public: + void TestSteadyStateSimulation() + { + // Integrate to t = 1000 in 1000 steps (AbstractCellBasedTestSuite has set the start time to 0). + SimulationTime* p_simulation_time = SimulationTime::Instance(); + p_simulation_time->SetEndTimeAndNumberOfTimeSteps(1000.0, 1000); + + // Create the SRN model from the imported SBML model and attach it to a cell. + Goldbeter1991SbmlSrnModel* p_srn_model = new Goldbeter1991SbmlSrnModel(); + + MAKE_PTR(WildTypeCellMutationState, p_state); + MAKE_PTR(StemCellProliferativeType, p_stem_type); + NoCellCycleModel* p_cc_model = new NoCellCycleModel(); + + CellPtr p_cell(new Cell(p_state, p_cc_model, p_srn_model)); + p_cell->SetCellProliferativeType(p_stem_type); + p_cell->InitialiseCellCycleModel(); + p_cell->InitialiseSrnModel(); + + // Step the simulation to the end time, advancing the SRN model each step. + while (!p_simulation_time->IsFinished()) + { + p_simulation_time->IncrementTimeOneStep(); + p_srn_model->SimulateToCurrentTime(); + } + + // Check the steady state of the mitotic oscillator. + TS_ASSERT_DELTA(p_srn_model->GetStateVariable("C"), 0.5470, 1e-2); + TS_ASSERT_DELTA(p_srn_model->GetStateVariable("M"), 0.2936, 1e-2); + TS_ASSERT_DELTA(p_srn_model->GetStateVariable("X"), 0.0067, 1e-3); + } +}; + +#endif /*TESTGOLDBETER1991SBMLSRNMODEL_HPP_*/ diff --git a/scripts/sbml_install.sh b/scripts/sbml_install.sh new file mode 100755 index 0000000..eacb584 --- /dev/null +++ b/scripts/sbml_install.sh @@ -0,0 +1,42 @@ +#!/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 + +# The project root is the parent of this script's directory. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd -- "${script_dir}/.." && pwd)" + +# 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 +} + +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_codegen_sbml needs it to format generated code." >&2 +fi + +# Create the project virtualenv (idempotent; shared with Python bindings if both are set up). +venv_dir="${PROJECT_ROOT}/.virtualenv" +python3 -m venv "${venv_dir}" + +# 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" + +echo "" +echo "Installed chaste-codegen-sbml into '${venv_dir}'." +echo "Activate the virtualenv with: source '${venv_dir}/bin/activate'" +echo "Then convert an SBML model with: chaste_codegen_sbml --help" diff --git a/setup_project.py b/setup_project.py index 53ee85d..94fa5c9 100644 --- a/setup_project.py +++ b/setup_project.py @@ -37,6 +37,7 @@ import os import re import shutil +import subprocess class Settings: @@ -105,6 +106,26 @@ def __init__(self) -> None: # Python package template directory (renamed to / during setup). self.PYTHON_PKG_TEMPLATE_DIR = os.path.join(self.PROJECT_ROOT, "src", "py", "template_project") + # SBML support files vendored from chaste-codegen-sbml (kept if SBML opted in, deleted otherwise). + # Their names are kept unchanged: generated model code #includes and subclasses them by name. + self.SBML_SUPPORT_FILES = [ + os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlOdeSystem.hpp"), + os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlOdeSystem.cpp"), + os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlSrnModel.hpp"), + os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlSrnModel.cpp"), + os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlCellCycleModel.hpp"), + os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlCellCycleModel.cpp"), + os.path.join(self.PROJECT_ROOT, "src", "SbmlEventType.hpp"), + os.path.join(self.PROJECT_ROOT, "src", "SbmlMath.hpp"), + os.path.join(self.PROJECT_ROOT, "src", "SbmlMath.cpp"), + ] + + # SBML test-helper directory (removed alongside SBML_SUPPORT_FILES if SBML is declined). + self.SBML_FORTESTS_DIR = os.path.join(self.PROJECT_ROOT, "src", "fortests") + + # The script that creates the project virtualenv and installs the SBML code generator. + 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.""" @@ -159,6 +180,26 @@ def print_banner(*lines: str) -> None: print(border) +def install_sbml_codegen(settings: Settings) -> None: + """Create the project virtualenv and install chaste-codegen-sbml into it. + + Runs scripts/sbml_install.sh. On any failure this is non-fatal: it prints the + manual commands so the user can finish the install themselves. + """ + try: + subprocess.run([settings.SBML_INSTALL_SCRIPT], check=True) + except (subprocess.CalledProcessError, OSError) as error: + venv_dir = os.path.join(settings.PROJECT_ROOT, ".virtualenv") + print("") + print(f"WARNING: could not install chaste-codegen-sbml automatically ({error}).") + print("Install it manually with:") + print(f" python3 -m venv {venv_dir}") + print( + f" {os.path.join(venv_dir, 'bin', 'pip')} install " + "'git+https://github.com/Chaste/chaste-codegen-sbml@develop'" + ) + + 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) @@ -203,12 +244,19 @@ def setup(settings: Settings) -> None: # 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.") @@ -261,6 +309,16 @@ def setup(settings: Settings) -> None: shutil.rmtree(os.path.join(settings.PROJECT_ROOT, "dynamic")) shutil.rmtree(os.path.join(settings.PROJECT_ROOT, "src", "py")) + # Set up or remove SBML support + if sbml: + # Keep the vendored SBML files (unchanged) and install the code generator into the venv. + install_sbml_codegen(settings) + else: + # Remove the vendored SBML support files and test helpers. + for file in settings.SBML_SUPPORT_FILES: + os.remove(file) + shutil.rmtree(settings.SBML_FORTESTS_DIR) + # Summarise the changes that were made print("") print("Setup complete.") @@ -279,6 +337,12 @@ def setup(settings: Settings) -> None: else: print("* Removed Python bindings template files in dynamic/ and src/py/.") + if sbml: + print("* Kept the SBML base classes in src/ and installed chaste-codegen-sbml into .virtualenv.") + print(" See the README and examples/goldbeter_1991/ for how to import an SBML model.") + else: + print("* Removed the SBML base classes from src/.") + def main() -> None: """Set up the project from the template.""" diff --git a/src/AbstractSbmlCellCycleModel.cpp b/src/AbstractSbmlCellCycleModel.cpp new file mode 100644 index 0000000..e7bc3f7 --- /dev/null +++ b/src/AbstractSbmlCellCycleModel.cpp @@ -0,0 +1,165 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#include "AbstractOdeBasedCellCycleModel.hpp" +#include "BackwardEulerIvpOdeSolver.hpp" +#include "CellCycleModelOdeSolver.hpp" +#include "CvodeAdaptor.hpp" +#include "SbmlEventType.hpp" +#include "StemCellProliferativeType.hpp" +#include "TransitCellProliferativeType.hpp" + +#include "AbstractSbmlCellCycleModel.hpp" + +AbstractSbmlCellCycleModel::AbstractSbmlCellCycleModel(boost::shared_ptr pOdeSolver) + : AbstractOdeBasedCellCycleModel(SimulationTime::Instance()->GetTime(), pOdeSolver) +{ + if (!mpOdeSolver) + { +#ifdef CHASTE_CVODE + // Default to CVODE where available + mpOdeSolver = CellCycleModelOdeSolver::Instance(); + mpOdeSolver->Initialise(); + mpOdeSolver->SetMaxSteps(10000); // Safe defaults + mpOdeSolver->SetTolerances(1e-6, 1e-8); + // CVODE needs to be instructed to check for stopping events + mpOdeSolver->CheckForStoppingEvents(); +#else + // Default to Chaste Runge-Kutta solver where CVODE is not available + mpOdeSolver = CellCycleModelOdeSolver::Instance(); + mpOdeSolver->Initialise(); + this->SetDt(0.0001); // Safe default +#endif // CHASTE_CVODE + } + + assert(mpOdeSolver->IsSetUp()); +} + +AbstractSbmlCellCycleModel::AbstractSbmlCellCycleModel(const AbstractSbmlCellCycleModel& rModel) + : AbstractOdeBasedCellCycleModel(rModel) +{ + /* + * Set each member variable of the new Cell Cycle model that inherits + * its value from the parent. + * + * Note 1: some of the new Cell Cycle model's member variables + * will already have been correctly initialized in its constructor. + * + * Note 2: one or more of the new Cell Cycle model's member variables + * may be set/overwritten as soon as InitialiseDaughterCell() is called on + * the new Cell Cycle model. + * + * Note 3: Only set the variables defined in this class. Variables defined + * in parent classes will be defined there. + */ +} + +AbstractSbmlCellCycleModel::~AbstractSbmlCellCycleModel() +{ +} + +void AbstractSbmlCellCycleModel::AdjustOdeParameters(double currentTime) +{ + static_cast(mpOdeSystem)->AdjustParameters(currentTime); +} + +bool AbstractSbmlCellCycleModel::CanCellTerminallyDifferentiate() +{ + return false; +} + +double AbstractSbmlCellCycleModel::GetAverageTransitCellCycleTime() +{ + // A default value, should be overridden in subclasses + return 1.25; +} + +double AbstractSbmlCellCycleModel::GetAverageStemCellCycleTime() +{ + // A default value, should be overridden in subclasses + return 1.25; +} + +double AbstractSbmlCellCycleModel::GetStateVariable(const std::string& rName) +{ + assert(mpOdeSystem != nullptr); + return mpOdeSystem->GetStateVariable(rName); +} + +void AbstractSbmlCellCycleModel::OutputCellCycleModelParameters(out_stream& rParamsFile) +{ + // No new parameters to output, so just call method on direct parent class + AbstractOdeBasedCellCycleModel::OutputCellCycleModelParameters(rParamsFile); +} + +bool AbstractSbmlCellCycleModel::ReadyToDivide() +{ + if (!mReadyToDivide) + { + bool was_ready_to_divide = mReadyToDivide; + double previous_divide_time = mDivideTime; + + // Solves ODE to current time and update cell division flag and time + bool stopping_event_occurred = AbstractOdeBasedCellCycleModel::ReadyToDivide(); + + if (stopping_event_occurred) + { + // Reset division flag and time if stopping event is not cell division + if (!static_cast(mpOdeSystem)->HasEventOccurred(SbmlEventType::CELL_DIVISION)) + { + mReadyToDivide = was_ready_to_divide; + mDivideTime = previous_divide_time; + } + } + } + return mReadyToDivide; +} + +void AbstractSbmlCellCycleModel::ResetForDivision() +{ + assert(mReadyToDivide); + AbstractOdeBasedCellCycleModel::ResetForDivision(); + + assert(mpOdeSystem != nullptr); + static_cast(mpOdeSystem)->ResetEventsOccurred(); +} + +// Register class with Boost serialization +#include "SerializationExportWrapperForCpp.hpp" +CHASTE_CLASS_EXPORT(AbstractSbmlCellCycleModel) + +// Register the CellCycleModel classes with Boost serialization +#include "CellCycleModelOdeSolverExportWrapper.hpp" +EXPORT_CELL_CYCLE_MODEL_ODE_SOLVER(AbstractSbmlCellCycleModel) diff --git a/src/AbstractSbmlCellCycleModel.hpp b/src/AbstractSbmlCellCycleModel.hpp new file mode 100644 index 0000000..b5b078b --- /dev/null +++ b/src/AbstractSbmlCellCycleModel.hpp @@ -0,0 +1,165 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#ifndef ABSTRACT_SBML_CELL_CYCLE_MODEL_HPP_ +#define ABSTRACT_SBML_CELL_CYCLE_MODEL_HPP_ + +#include + +#include "AbstractCellCycleModelOdeSolver.hpp" +#include "AbstractOdeBasedCellCycleModel.hpp" +#include "AbstractSbmlOdeSystem.hpp" +#include "ChasteSerialization.hpp" +#include "ClassIsAbstract.hpp" + +/** + * A base class for cell cycle models generated from SBML + */ + +class AbstractSbmlCellCycleModel : public AbstractOdeBasedCellCycleModel +{ +private: + /** Needed for serialization. */ + friend class boost::serialization::access; + /** + * Archive / unarchive the AbstractSbmlCellCycleModel. + * + * @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); + } + +protected: + /** + * Protected copy-constructor for use by CreateCellCycleModel(). + * + * The only way to copy an instance of a subclass of AbstractCellCycleModel is + * by calling CreateCellCycleModel(), which ensures that the instance is copied + * correctly. + * + * This copy-constructor helps subclasses of AbstractCellCycleModel to + * ensure that all their members are copied over correctly. It is primarily + * used during cell division to set member variables for a daughter cell. + * Note that the cell-cycle model of the parent cell will have run ResetForDivision() + * just before calling CreateCellCycleModel(), so performing an exact copy of the + * parent cell's cell-cycle model is suitable behaviour. Any further initialisation + * specific to the daughter cell can be completed via InitialiseDaughterCell(). + * + * @param rModel the cell-cycle model to copy. + */ + AbstractSbmlCellCycleModel(const AbstractSbmlCellCycleModel& rModel); + +public: + /** + * Default constructor calls base class. + * + * @param pOdeSolver An optional pointer to a cell-cycle model ODE solver object (allows the use of different ODE solvers) + */ + AbstractSbmlCellCycleModel(boost::shared_ptr pOdeSolver = boost::shared_ptr()); + + /** + * Destructor. + */ + virtual ~AbstractSbmlCellCycleModel(); + + /** + * Adjust any ODE parameters needed before solving until currentTime. + * + * @param currentTime the time up to which the system will be solved. + */ + void AdjustOdeParameters(double currentTime); + + /** + * Overridden CanCellTerminallyDifferentiate() method. + * @return whether cell can terminally differentiate + */ + bool CanCellTerminallyDifferentiate(); + + /** + * Overridden GetAverageStemCellCycleTime() method. + * @return time + */ + double GetAverageStemCellCycleTime(); + + /** + * Overridden GetAverageTransitCellCycleTime() method. + * @return time + */ + double GetAverageTransitCellCycleTime(); + + /** + * @return the value of a given state variable. + * + * @param rName the name of the state variable + */ + double GetStateVariable(const std::string& rName); + + /** + * Outputs cell-cycle model parameters to file. + * + * @param rParamsFile the file stream to which the parameters are output + */ + void OutputCellCycleModelParameters(out_stream& rParamsFile); + + /** + * See AbstractCellCycleModel::ResetForDivision() + * + * @return whether the cell is ready to divide (enter M phase). + */ + bool ReadyToDivide() override; + + /** + * Each cell-cycle model must be able to be reset 'after' a cell division. + * + * Actually, this method is called from Cell::Divide() to + * reset the cell cycle just before the daughter cell is created. + * CreateCellCycleModel() can then clone our state to generate a + * cell-cycle model instance for the daughter cell. + */ + void ResetForDivision() override; +}; + +// Register abstract class with Boost serialization +CLASS_IS_ABSTRACT(AbstractSbmlCellCycleModel) + +// Register the CellCycleModel classes with Boost serialization +#include "CellCycleModelOdeSolverExportWrapper.hpp" +EXPORT_CELL_CYCLE_MODEL_ODE_SOLVER(AbstractSbmlCellCycleModel) + +#endif // ABSTRACT_SBML_CELL_CYCLE_MODEL_HPP_ diff --git a/src/AbstractSbmlOdeSystem.cpp b/src/AbstractSbmlOdeSystem.cpp new file mode 100644 index 0000000..152cea3 --- /dev/null +++ b/src/AbstractSbmlOdeSystem.cpp @@ -0,0 +1,148 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#include "ChasteSerialization.hpp" +#include "SbmlEventType.hpp" + +#include "AbstractSbmlOdeSystem.hpp" + +AbstractSbmlOdeSystem::AbstractSbmlOdeSystem(unsigned numberOfStateVariables, unsigned numberOfParameters, unsigned numberOfEvents) + : AbstractOdeSystem(numberOfStateVariables), + mNumberOfParameters(numberOfParameters), + mNumberOfEvents(numberOfEvents) +{ + if (mNumberOfEvents > 0) + { + mEventType.resize(mNumberOfEvents, SbmlEventType::UNKNOWN); + + mEventSatisfied.resize(mNumberOfEvents, true); // Prevent events from triggering at the start + mEventClampActive.resize(mNumberOfEvents, true); // Re-derived at each Solve segment start + mEventTriggered.resize(mNumberOfEvents, false); + + if (mNumberOfStateVariables > 0) + { + mEventAdjustedStateVars.resize(mNumberOfStateVariables, false); + mEventAdjustedStateValues.resize(mNumberOfStateVariables, 0.0); + mEventAdjustedStatePriority.resize(mNumberOfStateVariables, 0.0); + } + + if (mNumberOfParameters > 0) + { + mEventAdjustedParameters.resize(mNumberOfParameters, false); + mEventAdjustedParameterValues.resize(mNumberOfParameters, 0.0); + mEventAdjustedParameterPriority.resize(mNumberOfParameters, 0.0); + } + } +} + +AbstractSbmlOdeSystem::~AbstractSbmlOdeSystem() +{ +} + +void AbstractSbmlOdeSystem::AdjustParameters(double time) +{ + for (unsigned i = 0; i < mEventAdjustedParameters.size(); ++i) + { + if (mEventAdjustedParameters[i]) + { + SetParameter(i, mEventAdjustedParameterValues[i]); + } + } + + for (unsigned i = 0; i < mEventAdjustedStateVars.size(); ++i) + { + if (mEventAdjustedStateVars[i]) + { + SetStateVariable(i, mEventAdjustedStateValues[i]); + mEventAdjustedStateVars[i] = false; + } + } +} + +double AbstractSbmlOdeSystem::CalculateRootFunction(double time, const std::vector& rY) +{ + return ProcessModelEvents(time, rY); +} + +bool AbstractSbmlOdeSystem::CalculateStoppingEvent(double time, const std::vector& rY) +{ + // Clear all event flags before a fresh BackwardEuler evaluation. ProcessModelEvents + // no longer resets these itself (so CVODE bisection doesn't erase a stored halving), + // so we must do it here to avoid stale flags from prior detections causing false + // positives on the initial-condition check at the start of the next Solve() call. + std::fill(mEventTriggered.begin(), mEventTriggered.end(), false); + std::fill(mEventAdjustedStateVars.begin(), mEventAdjustedStateVars.end(), false); + std::fill(mEventAdjustedParameters.begin(), mEventAdjustedParameters.end(), false); + + ProcessModelEvents(time, rY); + + // Freeze the clamp set for this Solve segment: clamp exactly those events whose trigger + // is active here (carried over from a previous segment) so CVODE reports no spurious root + // at the initial condition. ProcessModelEvents then clears each flag the instant its + // trigger first goes false, leaving the clamp stable across CVODE's in-step root + // bracketing so events localize at the true crossing rather than the step endpoint. + mEventClampActive = mEventSatisfied; + + for (unsigned i = 0; i < mEventTriggered.size(); ++i) + { + if (mEventTriggered[i]) return true; + } + return false; +} + +bool AbstractSbmlOdeSystem::HasEventOccurred(SbmlEventType eventType) +{ + for (unsigned i = 0; i < mEventTriggered.size(); ++i) + { + if (mEventTriggered[i] && mEventType[i] == eventType) + { + return true; + } + } + return false; +} + +void AbstractSbmlOdeSystem::ResetEventsOccurred() +{ + std::fill(mEventTriggered.begin(), mEventTriggered.end(), false); +} + +void AbstractSbmlOdeSystem::RunModelRules(double time, const std::vector& rY) +{ + UpdateStateVariables(time, rY); + UpdateParameters(time); + RunAssignmentRules(time); + RunReactions(time); +} diff --git a/src/AbstractSbmlOdeSystem.hpp b/src/AbstractSbmlOdeSystem.hpp new file mode 100644 index 0000000..7476e14 --- /dev/null +++ b/src/AbstractSbmlOdeSystem.hpp @@ -0,0 +1,229 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#ifndef ABSTRACT_SBML_ODE_SYSTEM_HPP_ +#define ABSTRACT_SBML_ODE_SYSTEM_HPP_ + +#include + +#include "AbstractOdeSystem.hpp" +#include "ChasteSerialization.hpp" +#include "ClassIsAbstract.hpp" +#include "SbmlEventType.hpp" + +/** + * Abstract SBML ODE System class. + * + * Sets up variables and functions for an ODE system imported from SBML. + * + * Instances can store event state internally in the mEvent* vectors - the + * vectors may be empty if the model does not have any events defined. + */ +class AbstractSbmlOdeSystem : public AbstractOdeSystem +{ +private: + /** Needed for serialization. */ + friend class boost::serialization::access; + /** + * Archive / unarchive the AbstractSbmlOdeSystem. + * + * @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 & mNumberOfParameters; + archive & mNumberOfEvents; + archive & mEventSatisfied; + archive & mEventClampActive; + archive & mEventTriggered; + archive & mEventType; + archive & mEventAdjustedStateVars; + archive & mEventAdjustedStateValues; + archive & mEventAdjustedStatePriority; + archive & mEventAdjustedParameters; + archive & mEventAdjustedParameterValues; + archive & mEventAdjustedParameterPriority; + } + + /** + * Process the events in the model. + * + * @param time The current time + * @param rY The current state variables + * + * @return How close we are to the time of the next event + */ + virtual double ProcessModelEvents(double time, const std::vector& rY) = 0; + + /** + * Run the assignment rules to update state. + * + * @param time The current time + * @param rY The current state variables + */ + virtual void RunAssignmentRules(double time) = 0; + + /** + * Run the initial assignments to set initial state. + * + * @param time The current time + */ + virtual void RunInitialAssignments(double time) = 0; + + /** + * Run the reactions to update state. + * + * @param time The current time + * @param rY The current state variables + */ + virtual void RunReactions(double time) = 0; + + /** + * Update variable parameters from current ODE system parameter settings. + * + * @param time The current time + */ + virtual void UpdateParameters(double time) = 0; + + /** + * Update state variables from the given ODE system state. + * + * @param time The current time + * @param rStateVariables The state variables to use + */ + virtual void UpdateStateVariables(double time, const std::vector& rStateVariables) = 0; + +protected: + /** The number of parameters in the model */ + unsigned mNumberOfParameters; + + /** The number of events in the model */ + unsigned mNumberOfEvents; + + // Event handling + std::vector mEventSatisfied; + /** + * Per-event clamp flag used only by the CVODE root function. Frozen at the start of each + * Solve segment (a snapshot of mEventSatisfied in CalculateStoppingEvent) and cleared + * permanently the instant an event's trigger first goes false (the event re-arms). While + * set, the event's root distance is forced large-negative so CVODE reports no spurious + * root for a trigger already active at the initial condition. Being monotonic within a + * segment (only ever cleared, never re-set) it stays constant across CVODE's in-step root + * bracketing, so events localize at the true crossing instead of the step endpoint, while + * an event that re-arms and re-fires within the same segment is still detected. + */ + std::vector mEventClampActive; + std::vector mEventTriggered; + std::vector mEventType; + std::vector mEventAdjustedStateVars; + std::vector mEventAdjustedStateValues; + std::vector mEventAdjustedStatePriority; + std::vector mEventAdjustedParameters; + std::vector mEventAdjustedParameterValues; + std::vector mEventAdjustedParameterPriority; + + /** + * Run the equations governing the model to update state. + * + * @param time The current time + * @param rY The current state variables + */ + void RunModelRules(double time, const std::vector& rY); + +public: + /** + * Constructor. + * + * @param numberOfStateVariables The number of state variables in the model + * @param numberOfParameters The number of parameters in the model + * @param numberOfEvents The number of events in the model + */ + AbstractSbmlOdeSystem(unsigned numberOfStateVariables, unsigned numberOfParameters, unsigned numberOfEvents); + + /** + * Destructor. + */ + virtual ~AbstractSbmlOdeSystem(); + + /** + * Adjust parameters and state variables after a stopping event + * + * @param time The current time + */ + void AdjustParameters(double time); + + /** + * Calculate whether the conditions to trigger an event have been met + * (Used by CVODE solver to find exact stopping position) + * + * @param time The current time + * @param rY The current state variables + * + * @return How close we are to the root of the stopping condition + */ + double CalculateRootFunction(double time, const std::vector& rY) override; + + /** + * Calculate whether the conditions to trigger an event have been met + * + * @param time The current time + * @param rY The current state variables + * + * @return True if conditions for an event are met, false otherwise + */ + bool CalculateStoppingEvent(double time, const std::vector& rY) override; + + /** + * Check if a specific type of event has occurred. + * + * @param eventType The type of event to check + * + * @return True if the type of event has occurred, false otherwise + */ + bool HasEventOccurred(SbmlEventType eventType); + + /** + * Reset the flags that indicate which events have been triggered. + */ + void ResetEventsOccurred(); +}; + +// Register abstract class with Boost serialization +CLASS_IS_ABSTRACT(AbstractSbmlOdeSystem) + +#endif // ABSTRACT_SBML_ODE_SYSTEM_HPP_ diff --git a/src/AbstractSbmlSrnModel.cpp b/src/AbstractSbmlSrnModel.cpp new file mode 100644 index 0000000..e08427c --- /dev/null +++ b/src/AbstractSbmlSrnModel.cpp @@ -0,0 +1,124 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#include "AbstractOdeSrnModel.hpp" +#include "AbstractSbmlOdeSystem.hpp" +#include "CellCycleModelOdeSolver.hpp" +#include "ChasteSerialization.hpp" +#include "CvodeAdaptor.hpp" +#include "RungeKutta4IvpOdeSolver.hpp" + +#include "AbstractSbmlSrnModel.hpp" + +AbstractSbmlSrnModel::AbstractSbmlSrnModel(unsigned stateSize, boost::shared_ptr pOdeSolver) + : AbstractOdeSrnModel(stateSize, pOdeSolver) +{ + if (mpOdeSolver == boost::shared_ptr()) + { +#ifdef CHASTE_CVODE + // Default to CVODE where available + mpOdeSolver = CellCycleModelOdeSolver::Instance(); + mpOdeSolver->Initialise(); + mpOdeSolver->SetMaxSteps(10000); // Safe default + mpOdeSolver->SetTolerances(1e-6, 1e-8); + // CVODE needs to be instructed to check for stopping events + mpOdeSolver->CheckForStoppingEvents(); +#else + // Default to Chaste Runge-Kutta solver where CVODE is not available + mpOdeSolver = CellCycleModelOdeSolver::Instance(); + mpOdeSolver->Initialise(); + this->SetDt(0.0001); // Safe default +#endif // CHASTE_CVODE + } + + assert(mpOdeSolver->IsSetUp()); +} + +AbstractSbmlSrnModel::AbstractSbmlSrnModel(const AbstractSbmlSrnModel& rModel) + : AbstractOdeSrnModel(rModel) +{ + /* + * Set each member variable of the new SRN model that inherits + * its value from the parent. + * + * Note 1: some of the new SRN model's member variables + * will already have been correctly initialized in its constructor. + * + * Note 2: one or more of the new SRN model's member variables + * may be set/overwritten as soon as InitialiseDaughterCell() is called on + * the new SRN model. + * + * Note 3: Only set the variables defined in this class. Variables defined + * in parent classes will be defined there. + */ +} + +AbstractSbmlSrnModel::~AbstractSbmlSrnModel() +{ +} + +double AbstractSbmlSrnModel::GetStateVariable(const std::string& rName) +{ + assert(mpOdeSystem != nullptr); + return mpOdeSystem->GetStateVariable(rName); +} + +void AbstractSbmlSrnModel::Initialise(AbstractSbmlOdeSystem* pOdeSystem) +{ + AbstractOdeSrnModel::Initialise(pOdeSystem); +} + +void AbstractSbmlSrnModel::OutputSrnModelParameters(out_stream& rParamsFile) +{ + // No new parameters to output, so just call method on direct parent class + AbstractOdeSrnModel::OutputSrnModelParameters(rParamsFile); +} + +void AbstractSbmlSrnModel::SimulateToCurrentTime() +{ + assert(mpOdeSystem != NULL); + assert(mpCell != NULL); + + // Run the ODE simulation as needed + AbstractOdeSrnModel::SimulateToCurrentTime(); +} + +// Register class with Boost serialization +#include "SerializationExportWrapperForCpp.hpp" +CHASTE_CLASS_EXPORT(AbstractSbmlSrnModel) + +// Register the CellCycleModel classes with Boost serialization +#include "CellCycleModelOdeSolverExportWrapper.hpp" +EXPORT_CELL_CYCLE_MODEL_ODE_SOLVER(AbstractSbmlSrnModel) diff --git a/src/AbstractSbmlSrnModel.hpp b/src/AbstractSbmlSrnModel.hpp new file mode 100644 index 0000000..db0dd18 --- /dev/null +++ b/src/AbstractSbmlSrnModel.hpp @@ -0,0 +1,138 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#ifndef ABSTRACT_SBML_SRN_MODEL_HPP_ +#define ABSTRACT_SBML_SRN_MODEL_HPP_ + +#include + +#include "AbstractOdeSrnModel.hpp" +#include "AbstractSbmlOdeSystem.hpp" +#include "ChasteSerialization.hpp" +#include "ClassIsAbstract.hpp" + +/** + * A base class for SRN models generated from SBML + */ + +class AbstractSbmlSrnModel : public AbstractOdeSrnModel +{ +private: + /** Needed for serialization. */ + friend class boost::serialization::access; + /** + * Archive / unarchive the AbstractSbmlSrnModel. + * + * @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); + } + +protected: + /** + * Protected copy-constructor for use by CreateSrnModel(). + * + * The only way to copy an instance of a subclass of AbstractCellCycleModel is + * by calling CreateSrnModel(), which ensures that the instance is copied + * correctly. + * + * This copy-constructor helps subclasses of AbstractSrnModel to + * ensure that all their members are copied over correctly. It is primarily + * used during cell division to set member variables for a daughter cell. + * Note that the SRN model of the parent cell will have run ResetForDivision() + * just before calling CreateSrnModel(), so performing an exact copy of the + * parent cell's SRN model is suitable behaviour. Any further initialisation + * specific to the daughter cell can be completed via InitialiseDaughterCell(). + * + * @param rModel the SRN model to copy. + */ + AbstractSbmlSrnModel(const AbstractSbmlSrnModel& rModel); + + using AbstractOdeSrnModel::Initialise; + /** + * Overridden Initialise() method to set up the ODE system. + * + * @param pOdeSystem pointer to an ODE system + */ + void Initialise(AbstractSbmlOdeSystem* pOdeSystem); + +public: + /** + * Constructor. + * + * @param stateSize The number of state variables in the ODE system. + * @param pOdeSolver An optional pointer to a cell-cycle model ODE solver + * object (allows the use of different ODE solvers) + */ + AbstractSbmlSrnModel(unsigned stateSize, + boost::shared_ptr pOdeSolver = boost::shared_ptr()); + + /** + * Destructor. + */ + virtual ~AbstractSbmlSrnModel(); + + /** + * @return the value of a given state variable. + * + * @param rName the name of the state variable + */ + double GetStateVariable(const std::string& rName); + + /** + * Outputs SRN model parameters to file. + * + * @param rParamsFile the file stream to which the parameters are output + */ + void OutputSrnModelParameters(out_stream& rParamsFile); + + /** + * Overridden SimulateToCurrentTime() method for custom behaviour + */ + void SimulateToCurrentTime(); +}; + +// Register abstract class with Boost serialization +CLASS_IS_ABSTRACT(AbstractSbmlSrnModel) + +// Register the CellCycleModel classes with Boost serialization +#include "CellCycleModelOdeSolverExportWrapper.hpp" +EXPORT_CELL_CYCLE_MODEL_ODE_SOLVER(AbstractSbmlSrnModel) + +#endif // ABSTRACT_SBML_SRN_MODEL_HPP_ diff --git a/src/SbmlEventType.hpp b/src/SbmlEventType.hpp new file mode 100644 index 0000000..e2b39b5 --- /dev/null +++ b/src/SbmlEventType.hpp @@ -0,0 +1,10 @@ +#ifndef SBML_EVENT_TYPE_HPP_ +#define SBML_EVENT_TYPE_HPP_ + +enum class SbmlEventType +{ + CELL_DIVISION, + UNKNOWN +}; + +#endif // SBML_EVENT_TYPE_HPP_ diff --git a/src/SbmlMath.cpp b/src/SbmlMath.cpp new file mode 100644 index 0000000..4a05778 --- /dev/null +++ b/src/SbmlMath.cpp @@ -0,0 +1,190 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#include + +#include "SbmlMath.hpp" + +// Arithmetic =================================== + +// divide +double sbmlmath::divide(double x, double y) +{ + return x / y; +} + +// minus +double sbmlmath::minus(double x, double y) +{ + return x - y; +} + +// plus +// times + +// Logs and exponents =========================== + +// log +double sbmlmath::log(double x) +{ + return std::log(x); +} + +double sbmlmath::log(double b, double x) +{ + return std::log(x) / std::log(b); +} + +// root +double sbmlmath::root(double n, double x) +{ + return std::pow(x, 1.0 / n); +} + +// sqr +double sbmlmath::sqr(double x) +{ + return x * x; +} + +// Logical ====================================== + +// and_ +// or_ + +// not_ +bool sbmlmath::not_(bool x) +{ + return !x; +} + +// xor_ + +// Relational =================================== + +// eq +// geq +// gt +// leq +// lt + +// neq +bool sbmlmath::neq(double x, double y) +{ + return x != y; +} + +// Trigonometry ================================= + +// cot, coth, acot, acoth +double sbmlmath::cot(double x) +{ + return 1.0 / std::tan(x); +} + +double sbmlmath::coth(double x) +{ + return 1.0 / std::tanh(x); +} + +double sbmlmath::acot(double x) +{ + return std::atan(1.0 / x); +} + +double sbmlmath::acoth(double x) +{ + return std::atanh(1.0 / x); +} + +// csc, csch, acsc, acsch +double sbmlmath::csc(double x) +{ + return 1.0 / std::sin(x); +} + +double sbmlmath::csch(double x) +{ + return 1.0 / std::sinh(x); +} + +double sbmlmath::acsc(double x) +{ + return std::asin(1.0 / x); +} + +double sbmlmath::acsch(double x) +{ + return std::asinh(1.0 / x); +} + +// sec, sech, asec, asech +double sbmlmath::sec(double x) +{ + return 1.0 / std::cos(x); +} + +double sbmlmath::sech(double x) +{ + return 1.0 / std::cosh(x); +} + +double sbmlmath::asec(double x) +{ + return std::acos(1.0 / x); +} + +double sbmlmath::asech(double x) +{ + return std::acosh(1.0 / x); +} + +// Other functions ============================== + +// factorial +double sbmlmath::factorial(double x) +{ + return std::tgamma(x + 1.0); +} + +// max +// min +// piecewise + +// quotient +double sbmlmath::quotient(double numer, double denom) +{ + return std::trunc(numer / denom); +} diff --git a/src/SbmlMath.hpp b/src/SbmlMath.hpp new file mode 100644 index 0000000..28422c8 --- /dev/null +++ b/src/SbmlMath.hpp @@ -0,0 +1,310 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#ifndef SBML_MATH_HPP_ +#define SBML_MATH_HPP_ + +/** + * SBML math functions. + */ + +#include + +namespace sbmlmath +{ +// Constants ================================== + +// SBML Level 3 recommended avogadro value: +// https://sbml.org/documents/specifications/level-3/ +inline constexpr double AVOGADRO = 6.02214179E23; + +// Note: Avogadro value has been updated in the most recent SI Brochure. +// Bureau International des Poids et Mesures (2019): +// The International System of Units (SI), 9th edition + +// Arithmetic ================================= + +// divide +double divide(double x, double y); + +// minus +double minus(double x, double y); + +// plus +template +constexpr double plus(Args... args); + +// times +template +constexpr double times(Args... args); + +// Logs and exponents ========================= + +// log +double log(double x); +double log(double b, double x); + +// root +double root(double n, double x); + +// sqr +double sqr(double x); + +// Logical ==================================== + +// and_ +template +constexpr bool and_(Args... args); + +// or_ +template +constexpr bool or_(Args... args); + +// not_ +bool not_(bool x); + +// xor_ +template +constexpr bool xor_(Args... args); + +// Relational ================================= + +// eq +template +constexpr bool eq(double first, double second, Args... rest); + +// geq +template +constexpr bool geq(double first, double second, Args... rest); + +// gt +template +constexpr bool gt(double first, double second, Args... rest); + +// leq +template +constexpr bool leq(double first, double second, Args... rest); + +// lt +template +constexpr bool lt(double first, double second, Args... rest); + +// neq +bool neq(double x, double y); + +// Trigonometry =============================== + +// cot, coth, acot, acoth +double cot(double x); +double coth(double x); +double acot(double x); +double acoth(double x); + +// csc, csch, acsc, acsch +double csc(double x); +double csch(double x); +double acsc(double x); +double acsch(double x); + +// sec, sech, asec, asech +double sec(double x); +double sech(double x); +double asec(double x); +double asech(double x); + +// Other functions ============================ + +// factorial +double factorial(double x); + +// max +template +constexpr double max(double first, double second, Args... rest); + +// min +template +constexpr double min(double first, double second, Args... rest); + +// piecewise +constexpr double piecewise(double otherwise); + +constexpr double piecewise(double value, bool condition, double otherwise); + +template +constexpr double piecewise(double value, bool condition, Args... rest); + +// quotient +double quotient(double numer, double denom); + +} // namespace sbmlmath + +// Arithmetic (variadic templates) ============ + +// plus +template +constexpr double sbmlmath::plus(Args... args) +{ + return (0.0 + ... + args); +} + +// times +template +constexpr double sbmlmath::times(Args... args) +{ + return (1.0 * ... * args); +} + +// Logical (variadic templates) ================= + +// and_ +template +constexpr bool sbmlmath::and_(Args... args) +{ + return (true && ... && args); +} + +// or_ +template +constexpr bool sbmlmath::or_(Args... args) +{ + return (false || ... || args); +} + +// xor_ +template +constexpr bool sbmlmath::xor_(Args... args) +{ + return (false ^ ... ^ args); +} + +// Relational (variadic templates) ============== + +// eq +template +constexpr bool sbmlmath::eq(double first, double second, Args... rest) +{ + if constexpr (sizeof...(rest) > 0) + { + return (first == second) && sbmlmath::eq(second, rest...); + } + return first == second; +} + +// geq +template +constexpr bool sbmlmath::geq(double first, double second, Args... rest) +{ + if constexpr (sizeof...(rest) > 0) + { + return (first >= second) && sbmlmath::geq(second, rest...); + } + return first >= second; +} + +// gt +template +constexpr bool sbmlmath::gt(double first, double second, Args... rest) +{ + if constexpr (sizeof...(rest) > 0) + { + return (first > second) && sbmlmath::gt(second, rest...); + } + return first > second; +} + +// leq +template +constexpr bool sbmlmath::leq(double first, double second, Args... rest) +{ + if constexpr (sizeof...(rest) > 0) + { + return (first <= second) && sbmlmath::leq(second, rest...); + } + return first <= second; +} + +// lt +template +constexpr bool sbmlmath::lt(double first, double second, Args... rest) +{ + if constexpr (sizeof...(rest) > 0) + { + return (first < second) && sbmlmath::lt(second, rest...); + } + return first < second; +} + +// Other (variadic templates) =================== + +// max +template +constexpr double sbmlmath::max(double first, double second, Args... rest) +{ + if constexpr (sizeof...(rest) > 0) + { + return std::fmax(first, sbmlmath::max(second, rest...)); + } + return std::fmax(first, second); +} + +// min +template +constexpr double sbmlmath::min(double first, double second, Args... rest) +{ + if constexpr (sizeof...(rest) > 0) + { + return std::fmin(first, sbmlmath::min(second, rest...)); + } + return std::fmin(first, second); +} + +// piecewise +constexpr double sbmlmath::piecewise(double otherwise) +{ + return otherwise; +} + +constexpr double sbmlmath::piecewise(double value, bool condition, double otherwise) +{ + return condition ? value : otherwise; +} + +template +constexpr double sbmlmath::piecewise(double value, bool condition, Args... rest) +{ + return condition ? value : sbmlmath::piecewise(rest...); +} + +#endif // SBML_MATH_HPP_ diff --git a/src/fortests/SbmlTestHelpers.cpp b/src/fortests/SbmlTestHelpers.cpp new file mode 100644 index 0000000..a01ab3f --- /dev/null +++ b/src/fortests/SbmlTestHelpers.cpp @@ -0,0 +1,143 @@ + +#include +#include +#include +#include + +#include "AbstractOdeSystem.hpp" +#include "OdeSolution.hpp" +#include "OutputFileHandler.hpp" + +#include "SbmlTestHelpers.hpp" + +void sbmltesthelpers::ExportCsv(const std::string& rFilename, + OdeSolution& rOdeSolution, + AbstractOdeSystem& rOdeSystem, + const std::vector >* pParamsPerStep) +{ + OutputFileHandler handler(""); + out_stream file = handler.OpenOutputFile(rFilename); + + // Times + const std::vector& time_data = rOdeSolution.rGetTimes(); + + // State variables + const std::vector& svar_names = rOdeSystem.rGetStateVariableNames(); + const std::vector >& svar_data = rOdeSolution.rGetSolutions(); + + // Derived quantities + rOdeSolution.CalculateDerivedQuantitiesAndParameters(&rOdeSystem); + const std::vector& dq_names = rOdeSystem.rGetDerivedQuantityNames(); + const std::vector >& dq_data = rOdeSolution.rGetDerivedQuantities(&rOdeSystem); + + // Parameters + const std::vector& param_names = rOdeSystem.rGetParameterNames(); + const std::vector& param_data = rOdeSolution.rGetParameters(&rOdeSystem); + + // Sanity checks + if (time_data.empty()) + { + throw std::invalid_argument("OdeSolution contains no time points."); + } + + if (svar_data.empty() && dq_data.empty()) + { + throw std::invalid_argument("OdeSolution contains no state variables or derived quantities."); + } + + if (!svar_data.empty() && (svar_data.size() != time_data.size())) + { + throw std::length_error("Number of state variable data rows do not match time points."); + } + + if (!dq_data.empty() && (dq_data.size() != time_data.size())) + { + throw std::length_error("Number of derived quantity data rows do not match time points."); + } + + if ((svar_data.empty() && !svar_names.empty()) || (!svar_data.empty() && svar_data[0].size() != svar_names.size())) + { + throw std::length_error("Number of state variable names do not match data."); + } + + if ((dq_data.empty() && !dq_names.empty()) || (!dq_data.empty() && dq_data[0].size() != dq_names.size())) + { + throw std::length_error("Number of derived quantity names do not match data."); + } + + if ((param_data.empty() && !param_names.empty()) || (!param_data.empty() && param_data.size() != param_names.size())) + { + throw std::length_error("Number of parameter names do not match data."); + } + + // Write column headings + (*file) << "time"; + if (!svar_names.empty()) + { + for (unsigned i = 0; i < svar_names.size(); i++) + { + (*file) << "," << svar_names[i]; + } + } + if (!dq_names.empty()) + { + for (unsigned i = 0; i < dq_names.size(); i++) + { + (*file) << "," << dq_names[i]; + } + } + if (!param_names.empty()) + { + for (unsigned i = 0; i < param_names.size(); i++) + { + (*file) << "," << param_names[i]; + } + } + (*file) << std::endl + << std::flush; + + // Write data + for (unsigned i = 0; i < time_data.size(); i++) + { + (*file) << time_data[i]; + if (!svar_data.empty()) + { + for (unsigned j = 0; j < svar_data[i].size(); j++) + { + (*file) << "," << svar_data[i][j]; + } + } + if (!dq_data.empty()) + { + for (unsigned j = 0; j < dq_data[i].size(); j++) + { + (*file) << "," << dq_data[i][j]; + } + } + if (pParamsPerStep != nullptr && i < pParamsPerStep->size()) + { + // Time-resolved parameters (e.g. a parameter changed by an event). + for (unsigned j = 0; j < (*pParamsPerStep)[i].size(); j++) + { + (*file) << "," << (*pParamsPerStep)[i][j]; + } + } + else if (!param_data.empty()) + { + for (unsigned j = 0; j < param_data.size(); j++) + { + (*file) << "," << param_data[j]; + } + } + (*file) << std::endl + << std::flush; + } + file->close(); +} + +std::string sbmltesthelpers::ToString(double value, unsigned precision) +{ + std::ostringstream oss; + oss << std::fixed << std::setprecision(precision) << value; + return oss.str(); +} diff --git a/src/fortests/SbmlTestHelpers.hpp b/src/fortests/SbmlTestHelpers.hpp new file mode 100644 index 0000000..6485aae --- /dev/null +++ b/src/fortests/SbmlTestHelpers.hpp @@ -0,0 +1,195 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#ifndef SBML_TEST_HELPERS_HPP_ +#define SBML_TEST_HELPERS_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AbstractOdeSystem.hpp" +#include "OdeSolution.hpp" + +namespace sbmltesthelpers +{ +/** Export results to a CSV file. + * + * The first column is time, followed by the state variables, derived quantities and + * parameters. + * + * @param rFilename The name of the file to create. + * @param rOdeSolution The OdeSolution containing the results. + * @param rOdeSystem The ODE system (for variable names and derived quantities). + * @param pParamsPerStep Optional time-resolved parameter values (one row per time step, + * as recorded by SbmlTestOdeSolution). When null the parameters are written from the + * system's single current snapshot, repeated on every row. + */ +void ExportCsv(const std::string& rFilename, + OdeSolution& rOdeSolution, + AbstractOdeSystem& rOdeSystem, + const std::vector >* pParamsPerStep = nullptr); + +/** Calculate the maximum of a vector of doubles. + * @param vec The vector of doubles. + * @return The maximum value. + */ +inline double Max(const std::vector& vec); + +/** Calculate the mean of a vector of doubles. + * @param vec The vector of doubles. + * @return The mean value. + */ +inline double Mean(const std::vector& vec); + +/** Calculate the minimum of a vector of doubles. + * @param vec The vector of doubles. + * @return The minimum value. + */ +inline double Min(const std::vector& vec); + +/** Calculate the standard deviation of a vector of doubles. + * @param vec The vector of doubles. + * @return The standard deviation value. + */ +inline double Stdev(const std::vector& vec); + +/** Convert a double to a string. + * @param value The double value. + * @return The string representation. + */ +std::string ToString(double value, unsigned precision = 9); + +/** Calculate the qth quantile of a vector of doubles. + * @param vec The vector of doubles, assumed to be sorted. + * @param q The quantile to calculate (between 0 and 1). + * @return The qth quantile value. + */ +inline double Quantile(const std::vector& vec, double q); + +/** Calculate the variance of a vector of doubles. + * @param vec The vector of doubles. + * @return The variance value. + */ +inline double Variance(const std::vector& vec); +} // namespace sbmltesthelpers + +// max +inline double sbmltesthelpers::Max(const std::vector& vec) +{ + if (vec.empty()) + { + throw std::invalid_argument("Cannot calculate maximum of an empty vector."); + } + return *std::max_element(vec.begin(), vec.end()); +} + +// mean +inline double sbmltesthelpers::Mean(const std::vector& vec) +{ + if (vec.empty()) + { + throw std::invalid_argument("Cannot calculate mean of an empty vector."); + } + return std::accumulate(vec.begin(), vec.end(), 0.0) / vec.size(); +} + +// min +inline double sbmltesthelpers::Min(const std::vector& vec) +{ + if (vec.empty()) + { + throw std::invalid_argument("Cannot calculate minimum of an empty vector."); + } + return *std::min_element(vec.begin(), vec.end()); +} + +// stdev +inline double sbmltesthelpers::Stdev(const std::vector& vec) +{ + return std::sqrt(sbmltesthelpers::Variance(vec)); +} + +// quantile +inline double sbmltesthelpers::Quantile(const std::vector& vec, double q) +{ + if (vec.empty()) + { + throw std::invalid_argument("Cannot calculate quantile of an empty vector."); + } + + if (q < 0.0 || q > 1.0) + { + throw std::out_of_range("Quantile must be between 0.0 and 1.0"); + } + + std::vector sorted_vec = vec; + std::sort(sorted_vec.begin(), sorted_vec.end()); + + size_t n = sorted_vec.size(); + size_t index = static_cast(q * (n - 1)); + + // Even number of elements + if (n % 2 == 0) + { + return (sorted_vec[index] + sorted_vec[index - 1]) / 2.0; + } + + // Odd number of elements + return sorted_vec[index]; +} + +// variance +inline double sbmltesthelpers::Variance(const std::vector& vec) +{ + if (vec.size() < 2) + { + throw std::invalid_argument("Variance requires at least two data points."); + } + double mean_val = sbmltesthelpers::Mean(vec); + double accum = 0.0; + for (double val : vec) + { + accum += (val - mean_val) * (val - mean_val); + } + return accum / (vec.size() - 1); +} + +#endif // SBML_TEST_HELPERS_HPP_ diff --git a/src/fortests/SbmlTestOdeSolution.cpp b/src/fortests/SbmlTestOdeSolution.cpp new file mode 100644 index 0000000..7162743 --- /dev/null +++ b/src/fortests/SbmlTestOdeSolution.cpp @@ -0,0 +1,97 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#include "SbmlTestOdeSolution.hpp" + +void SbmlTestOdeSolution::RecordPoint(double time, const std::vector& rY, AbstractOdeSystem* pSystem) +{ + if (rGetTimes().empty()) + { + SetOdeSystemInformation(pSystem->GetSystemInformation()); + } + + // Snapshot the system's current parameters for this point. + std::vector params(pSystem->GetNumberOfParameters()); + for (unsigned i = 0; i < params.size(); ++i) + { + params[i] = pSystem->GetParameter(i); + } + + rGetTimes().push_back(time); + rGetSolutions().push_back(rY); + mParametersPerStep.push_back(params); + + SetNumberOfTimeSteps(rGetTimes().size()); +} + +std::vector SbmlTestOdeSolution::GetParameterSeries(const std::string& rName, AbstractOdeSystem* pSystem) const +{ + unsigned index = pSystem->GetParameterIndex(rName); + std::vector series(mParametersPerStep.size()); + for (unsigned i = 0; i < series.size(); ++i) + { + series[i] = mParametersPerStep[i][index]; + } + return series; +} + +std::vector SbmlTestOdeSolution::GetDerivedQuantitySeries(const std::string& rName, AbstractOdeSystem* pSystem) const +{ + unsigned index = pSystem->GetSystemInformation()->GetDerivedQuantityIndex(rName); + + // Save the system's current parameters so they can be restored afterwards. + std::vector saved(pSystem->GetNumberOfParameters()); + for (unsigned p = 0; p < saved.size(); ++p) + { + saved[p] = pSystem->GetParameter(p); + } + + std::vector series(rGetTimes().size()); + for (unsigned i = 0; i < series.size(); ++i) + { + // Restore this step's parameters so parameter-dependent derived quantities are time-resolved. + for (unsigned p = 0; p < mParametersPerStep[i].size(); ++p) + { + pSystem->SetParameter(p, mParametersPerStep[i][p]); + } + series[i] = pSystem->ComputeDerivedQuantities(rGetTimes()[i], rGetSolutions()[i])[index]; + } + + for (unsigned p = 0; p < saved.size(); ++p) + { + pSystem->SetParameter(p, saved[p]); + } + return series; +} diff --git a/src/fortests/SbmlTestOdeSolution.hpp b/src/fortests/SbmlTestOdeSolution.hpp new file mode 100644 index 0000000..ba1b8e7 --- /dev/null +++ b/src/fortests/SbmlTestOdeSolution.hpp @@ -0,0 +1,104 @@ +/* + +Copyright (c) 2005-2025, 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. + +*/ + +#ifndef SBML_TEST_ODE_SOLUTION_HPP_ +#define SBML_TEST_ODE_SOLUTION_HPP_ + +#include +#include + +#include "AbstractOdeSystem.hpp" +#include "OdeSolution.hpp" + +/** + * An OdeSolution that additionally records the model parameters at each time step. + * + * Chaste's OdeSolution stores only a single parameter snapshot - GetVariableAtIndex + * returns mParameters[i] with no time index, and the header notes it "assumes that + * mParameters is constant through time". A parameter changed by an event (e.g. a + * compartment resized by an event assignment) is therefore reported with its final + * value at every time step. + * + * An SBML model is solved one sample-grid point at a time, stopping at each event; the + * parameters are constant between events and change (via AdjustParameters) at them. + * RecordPoint stores the system's current parameters alongside each grid point, so + * GetParameterSeries can return a genuinely time-resolved series. + */ +class SbmlTestOdeSolution : public OdeSolution +{ +private: + /** Parameter values for each stored time step. */ + std::vector > mParametersPerStep; + +public: + /** + * Append a single solution point, recording the system's current parameter values for it. + * Building the solution one grid point at a time (rather than one segment at a time) keeps + * the output on the requested sample grid even when an event fires between grid points. + * + * @param time the time of the point + * @param rY the state variables at this point + * @param pSystem the ODE system, queried for its current parameter values + */ + void RecordPoint(double time, const std::vector& rY, AbstractOdeSystem* pSystem); + + /** + * @param rName the parameter name + * @param pSystem the ODE system, used to resolve the parameter's index + * @return the parameter's value at each stored time step + */ + std::vector GetParameterSeries(const std::string& rName, AbstractOdeSystem* pSystem) const; + + /** + * Evaluate a derived quantity at every stored step with that step's recorded parameters + * restored first. A derived quantity that depends on an event-modified parameter (e.g. the + * amount conversion amt__X = X * compartment for a boundary species X changed by an event) + * is otherwise computed with the parameter's final value at every point, losing the time + * resolution. Restoring the per-step parameters fixes this. + * + * @param rName the derived quantity name + * @param pSystem the ODE system, used to compute the derived quantities + * @return the derived quantity's value at each stored time step + */ + std::vector GetDerivedQuantitySeries(const std::string& rName, AbstractOdeSystem* pSystem) const; + + /** @return the parameter values recorded for each stored time step. */ + const std::vector >& rGetParametersPerStep() const + { + return mParametersPerStep; + } +}; + +#endif // SBML_TEST_ODE_SOLUTION_HPP_ From adaaab4bd7a8f6f81856e31d1d48156bb1323c7b Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Mon, 29 Jun 2026 19:49:55 +0100 Subject: [PATCH 35/51] #3 Add python bindings guide and example --- .github/workflows/test-force-example.yml | 57 +++++++++ .github/workflows/test-python-project.yml | 2 +- README.md | 75 ++++++++++++ examples/my_force/MyForce.cpp | 48 ++++++++ examples/my_force/MyForce.hpp | 68 +++++++++++ examples/my_force/README.md | 124 ++++++++++++++++++++ examples/my_force/run_my_force.py | 64 ++++++++++ scripts/{install.sh => bindings_install.sh} | 0 8 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test-force-example.yml create mode 100644 examples/my_force/MyForce.cpp create mode 100644 examples/my_force/MyForce.hpp create mode 100644 examples/my_force/README.md create mode 100644 examples/my_force/run_my_force.py rename scripts/{install.sh => bindings_install.sh} (100%) diff --git a/.github/workflows/test-force-example.yml b/.github/workflows/test-force-example.yml new file mode 100644 index 0000000..286fea9 --- /dev/null +++ b/.github/workflows/test-force-example.yml @@ -0,0 +1,57 @@ +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/develop + options: --user root + + env: + CHASTE_BUILD_DIR: /tmp/build + CHASTE_TEST_OUTPUT: /tmp/testoutput + PROJECT_DIR: /tmp/myproject + + steps: + - name: Checkout template + uses: actions/checkout@v7 + + - name: Mark Chaste source as safe for git + run: 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 cell_based component and Python bindings, defaults otherwise, yes to confirm + printf '\ny\n\n\n\ny\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 's/^source_includes:/source_includes:\n - MyForce.hpp/' dynamic/config.yaml + sed -i 's/^ classes:/ classes:\n - name: MyForce/' 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-python-project.yml b/.github/workflows/test-python-project.yml index 0a37457..faabc24 100644 --- a/.github/workflows/test-python-project.yml +++ b/.github/workflows/test-python-project.yml @@ -47,7 +47,7 @@ jobs: - name: Install run: | - ${{ env.PROJECT_DIR }}/scripts/install.sh + ${{ env.PROJECT_DIR }}/scripts/bindings_install.sh - name: Test Python bindings run: | diff --git a/README.md b/README.md index a8d13a4..3c366c4 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,78 @@ 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. + +## 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). + +### 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`). + +> **Class names for templated classes.** A templated class is wrapped once per +> dimension, with the dimensions appended to the name. For example a class templated over +> `` becomes `YourClass2` / `YourClass3`, and one templated over +> `` becomes `YourClass2_2` / `YourClass3_3` (matching PyChaste's +> own `OffLatticeSimulation2_2`, `GeneralisedLinearSpringForce2_2`, etc.). 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. 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..d0250ce --- /dev/null +++ b/examples/my_force/README.md @@ -0,0 +1,124 @@ +# 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 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.hpp + - MyForce.hpp + ``` + +* add the class under the `all` module's `classes:` + + ```yaml + modules: + - name: all + source_locations: + - src/ + classes: + - name: Hello + - name: MyForce + ``` + +Because `MyForce` is templated over ``, it is wrapped once per dimension and +exposed in Python as `MyForce2` (2D) and `MyForce3` (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 MyForce2 + +# Build a small node-based cell population. +generator = chaste.mesh.HoneycombMeshGenerator(5, 5) +mesh = chaste.mesh.NodesOnlyMesh2() +mesh.ConstructNodesWithoutMesh(generator.GetMesh(), 1.5) + +transit_type = chaste.cell_based.TransitCellProliferativeType() +cell_generator = chaste.cell_based.CellsGeneratorUniformCellCycleModel_2() +cells = cell_generator.GenerateBasicRandom(mesh.GetNumNodes(), transit_type) +cell_population = chaste.cell_based.NodeBasedCellPopulation2(mesh, cells) + +# Run an off-lattice simulation using a standard force and our custom force. +simulator = chaste.cell_based.OffLatticeSimulation2_2(cell_population) +simulator.SetOutputDirectory("Python/MyForce") +simulator.SetEndTime(1.0) +simulator.AddForce(chaste.cell_based.GeneralisedLinearSpringForce2_2()) +simulator.AddForce(myproject.MyForce2(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.MyForce2` 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). + +## 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..9a57c85 --- /dev/null +++ b/examples/my_force/run_my_force.py @@ -0,0 +1,64 @@ +"""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, so MyForce<2> is exposed as MyForce2. +# If this name is wrong for your build, run: +# print([n for n in dir(myproject) if "MyForce" in n]) +MyForce2 = myproject.MyForce2 + + +def main(): + # Choose where results are written. + chaste.core.OutputFileHandler("Python/MyForce") + + # Build a small node-based cell population on a honeycomb mesh. + generator = chaste.mesh.HoneycombMeshGenerator(5, 5) + generating_mesh = generator.GetMesh() + + mesh = chaste.mesh.NodesOnlyMesh2() + mesh.ConstructNodesWithoutMesh(generating_mesh, 1.5) + + transit_type = chaste.cell_based.TransitCellProliferativeType() + cell_generator = chaste.cell_based.CellsGeneratorUniformCellCycleModel_2() + cells = cell_generator.GenerateBasicRandom(mesh.GetNumNodes(), transit_type) + + cell_population = chaste.cell_based.NodeBasedCellPopulation2(mesh, cells) + + # Set up an off-lattice simulation. + simulator = chaste.cell_based.OffLatticeSimulation2_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.GeneralisedLinearSpringForce2_2() + simulator.AddForce(spring_force) + + # ...and our custom force from the project bindings pushes every cell in +x. + my_force = MyForce2(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/install.sh b/scripts/bindings_install.sh similarity index 100% rename from scripts/install.sh rename to scripts/bindings_install.sh From 894f9365cac91ea27e2e073a3aa14d2f6e4d041e Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Tue, 30 Jun 2026 08:22:31 +0100 Subject: [PATCH 36/51] #3 Fix pychaste bindings inheritance --- .github/workflows/test-force-example.yml | 2 ++ README.md | 24 +++++++++++---- examples/my_force/README.md | 37 +++++++++++++++++------- examples/my_force/run_my_force.py | 23 ++++++++------- scripts/bindings_install.sh | 5 +++- 5 files changed, 65 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test-force-example.yml b/.github/workflows/test-force-example.yml index 286fea9..a21c74c 100644 --- a/.github/workflows/test-force-example.yml +++ b/.github/workflows/test-force-example.yml @@ -43,6 +43,8 @@ jobs: cp examples/my_force/MyForce.hpp examples/my_force/MyForce.cpp src/ sed -i 's/^source_includes:/source_includes:\n - MyForce.hpp/' dynamic/config.yaml sed -i 's/^ classes:/ classes:\n - name: MyForce/' dynamic/config.yaml + # Import PyChaste's module so MyForce can inherit AbstractForce across modules + sed -i 's/^ - name: all/ - name: all\n imports:\n - chaste._pychaste_all/' dynamic/config.yaml - name: Configure run: ${{ env.PROJECT_DIR }}/scripts/configure.sh diff --git a/README.md b/README.md index 3c366c4..f4aa866 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,26 @@ To expose a new class, add it to `src/`, then list it in `dynamic/config.yaml`: 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), also import PyChaste's compiled module under the `all` +module's `imports:` so cppwg can link the inheritance across modules: + +```yaml +modules: + - name: all + imports: + - chaste._pychaste_all + classes: + - name: YourClass +``` + > **Class names for templated classes.** A templated class is wrapped once per -> dimension, with the dimensions appended to the name. For example a class templated over -> `` becomes `YourClass2` / `YourClass3`, and one templated over -> `` becomes `YourClass2_2` / `YourClass3_3` (matching PyChaste's -> own `OffLatticeSimulation2_2`, `GeneralisedLinearSpringForce2_2`, etc.). If you are -> unsure of a generated name, run `print([n for n in dir(myproject) if "YourClass" in n])`. +> 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. diff --git a/examples/my_force/README.md b/examples/my_force/README.md index d0250ce..dec9285 100644 --- a/examples/my_force/README.md +++ b/examples/my_force/README.md @@ -37,11 +37,15 @@ Tell cppwg to wrap the new class by editing `dynamic/config.yaml`: - MyForce.hpp ``` -* add the class under the `all` module's `classes:` +* under the `all` module, add the class to `classes:`, and tell cppwg that the + base class `AbstractForce` is wrapped in PyChaste by importing PyChaste's + compiled module under `imports:` ```yaml modules: - name: all + imports: + - chaste._pychaste_all source_locations: - src/ classes: @@ -49,8 +53,18 @@ Tell cppwg to wrap the new class by editing `dynamic/config.yaml`: - name: MyForce ``` + The `imports` line is what makes the cross-module inheritance work: `MyForce` + subclasses `AbstractForce`, which is wrapped in PyChaste (a different module). + cppwg references `AbstractForce` as an external base and imports + `chaste._pychaste_all` so that base type is registered before `MyForce`. + Without it, `OffLatticeSimulation.AddForce(my_force)` would reject the force + because Python would not recognise `MyForce` as an `AbstractForce`. + + > This requires a version of [cppwg](https://github.com/Chaste/cppwg) with + > cross-module inheritance support (the `imports` config key). + Because `MyForce` is templated over ``, it is wrapped once per dimension and -exposed in Python as `MyForce2` (2D) and `MyForce3` (3D). +exposed in Python as `MyForce_2` (2D) and `MyForce_3` (3D). ## 3. Compile and install the bindings @@ -81,24 +95,27 @@ import chaste.mesh chaste.init() -import myproject # provides MyForce2 +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.NodesOnlyMesh2() +mesh = chaste.mesh.NodesOnlyMesh_2() mesh.ConstructNodesWithoutMesh(generator.GetMesh(), 1.5) transit_type = chaste.cell_based.TransitCellProliferativeType() -cell_generator = chaste.cell_based.CellsGeneratorUniformCellCycleModel_2() +cell_generator = chaste.cell_based.CellsGenerator["UniformCellCycleModel", "2"]() cells = cell_generator.GenerateBasicRandom(mesh.GetNumNodes(), transit_type) -cell_population = chaste.cell_based.NodeBasedCellPopulation2(mesh, cells) +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.OffLatticeSimulation2_2(cell_population) +simulator = chaste.cell_based.OffLatticeSimulation_2_2(cell_population) simulator.SetOutputDirectory("Python/MyForce") simulator.SetEndTime(1.0) -simulator.AddForce(chaste.cell_based.GeneralisedLinearSpringForce2_2()) -simulator.AddForce(myproject.MyForce2(1.0)) # <-- our new force, from C++ +simulator.AddForce(chaste.cell_based.GeneralisedLinearSpringForce_2_2()) +simulator.AddForce(myproject.MyForce_2(1.0)) # <-- our new force, from C++ simulator.Solve() ``` @@ -112,7 +129,7 @@ You should see the simulation run to completion and print the number of cells. T 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.MyForce2` is not found, list the generated names with +> 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). diff --git a/examples/my_force/run_my_force.py b/examples/my_force/run_my_force.py index 9a57c85..3577213 100644 --- a/examples/my_force/run_my_force.py +++ b/examples/my_force/run_my_force.py @@ -18,41 +18,44 @@ # The project bindings, which provide MyForce. Replace with your project name. import myproject -# A templated class is wrapped once per dimension, so MyForce<2> is exposed as MyForce2. -# If this name is wrong for your build, run: -# print([n for n in dir(myproject) if "MyForce" in n]) -MyForce2 = myproject.MyForce2 +# 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.NodesOnlyMesh2() + mesh = chaste.mesh.NodesOnlyMesh_2() mesh.ConstructNodesWithoutMesh(generating_mesh, 1.5) transit_type = chaste.cell_based.TransitCellProliferativeType() - cell_generator = chaste.cell_based.CellsGeneratorUniformCellCycleModel_2() + cell_generator = chaste.cell_based.CellsGenerator["UniformCellCycleModel", "2"]() cells = cell_generator.GenerateBasicRandom(mesh.GetNumNodes(), transit_type) - cell_population = chaste.cell_based.NodeBasedCellPopulation2(mesh, cells) + cell_population = chaste.cell_based.NodeBasedCellPopulation_2(mesh, cells) # Set up an off-lattice simulation. - simulator = chaste.cell_based.OffLatticeSimulation2_2(cell_population) + 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.GeneralisedLinearSpringForce2_2() + 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 = MyForce2(1.0) + my_force = MyForce_2(1.0) simulator.AddForce(my_force) simulator.Solve() diff --git a/scripts/bindings_install.sh b/scripts/bindings_install.sh index 8c03271..3debc4c 100755 --- a/scripts/bindings_install.sh +++ b/scripts/bindings_install.sh @@ -38,8 +38,11 @@ if [[ ! -d "${pychaste_pkg}" ]]; then fi # Create the virtualenv if it does not already exist. +# Use --system-site-packages so the venv can see PyChaste's native runtime +# dependencies (petsc4py, mpi4py, vtk), which are provided by the system Python +# and are not pip-installable here. venv_dir="${PROJECT_ROOT}/.virtualenv" -python3 -m venv "${venv_dir}" +python3 -m venv --system-site-packages "${venv_dir}" # Install pychaste first (it is a dependency of the project bindings). "${venv_dir}/bin/pip" install "${pychaste_pkg}" From 3e90cfbd9bbf52fdec1695650a0345621085ec38 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Tue, 30 Jun 2026 10:04:33 +0100 Subject: [PATCH 37/51] #3 Update cross-package bindings inheritance --- .github/workflows/test-force-example.yml | 5 +++-- README.md | 7 +++++-- examples/my_force/README.md | 18 +++++++++++------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-force-example.yml b/.github/workflows/test-force-example.yml index a21c74c..a6c2417 100644 --- a/.github/workflows/test-force-example.yml +++ b/.github/workflows/test-force-example.yml @@ -43,8 +43,9 @@ jobs: cp examples/my_force/MyForce.hpp examples/my_force/MyForce.cpp src/ sed -i 's/^source_includes:/source_includes:\n - MyForce.hpp/' dynamic/config.yaml sed -i 's/^ classes:/ classes:\n - name: MyForce/' dynamic/config.yaml - # Import PyChaste's module so MyForce can inherit AbstractForce across modules - sed -i 's/^ - name: all/ - name: all\n imports:\n - chaste._pychaste_all/' dynamic/config.yaml + # Import PyChaste's module and declare AbstractForce as an external base + # so MyForce can inherit AbstractForce across modules + sed -i 's/^ - name: all/ - name: all\n imports:\n - chaste._pychaste_all\n external_bases:\n - AbstractForce/' dynamic/config.yaml - name: Configure run: ${{ env.PROJECT_DIR }}/scripts/configure.sh diff --git a/README.md b/README.md index f4aa866..dd811da 100644 --- a/README.md +++ b/README.md @@ -74,14 +74,17 @@ To expose a new class, add it to `src/`, then list it in `dynamic/config.yaml`: 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), also import PyChaste's compiled module under the `all` -module's `imports:` so cppwg can link the inheritance across modules: +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 ``` diff --git a/examples/my_force/README.md b/examples/my_force/README.md index dec9285..dae71c9 100644 --- a/examples/my_force/README.md +++ b/examples/my_force/README.md @@ -38,14 +38,16 @@ Tell cppwg to wrap the new class by editing `dynamic/config.yaml`: ``` * under the `all` module, add the class to `classes:`, and tell cppwg that the - base class `AbstractForce` is wrapped in PyChaste by importing PyChaste's - compiled module under `imports:` + base class `AbstractForce` is wrapped in PyChaste: import PyChaste's compiled + module under `imports:` and list the base class under `external_bases:` ```yaml modules: - name: all imports: - chaste._pychaste_all + external_bases: + - AbstractForce source_locations: - src/ classes: @@ -53,15 +55,17 @@ Tell cppwg to wrap the new class by editing `dynamic/config.yaml`: - name: MyForce ``` - The `imports` line is what makes the cross-module inheritance work: `MyForce` - subclasses `AbstractForce`, which is wrapped in PyChaste (a different module). - cppwg references `AbstractForce` as an external base and imports + 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 it, `OffLatticeSimulation.AddForce(my_force)` would reject the force + Without this, `OffLatticeSimulation.AddForce(my_force)` would reject the force because Python would not recognise `MyForce` as an `AbstractForce`. > This requires a version of [cppwg](https://github.com/Chaste/cppwg) with - > cross-module inheritance support (the `imports` config key). + > cross-module inheritance support (the `imports` and `external_bases` config + > keys). Because `MyForce` is templated over ``, it is wrapped once per dimension and exposed in Python as `MyForce_2` (2D) and `MyForce_3` (3D). From 25b4b8992a41b34f7fea3338de2660ff46e10187 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Tue, 30 Jun 2026 11:22:53 +0100 Subject: [PATCH 38/51] #3 Use cmake flto setting --- dynamic/CMakeLists.txt | 4 +++- dynamic/config.yaml | 2 -- setup_project.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dynamic/CMakeLists.txt b/dynamic/CMakeLists.txt index 537d8b0..1f7dccf 100644 --- a/dynamic/CMakeLists.txt +++ b/dynamic/CMakeLists.txt @@ -114,7 +114,9 @@ target_link_libraries(_template_project_all PRIVATE target_include_directories(_template_project_all PRIVATE ${PROJECT_INCLUDE_DIRS}) -target_compile_options(_template_project_all PRIVATE -flto=auto -Wno-unused-local-typedefs) +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) diff --git a/dynamic/config.yaml b/dynamic/config.yaml index 0bba4fd..3fdee7d 100644 --- a/dynamic/config.yaml +++ b/dynamic/config.yaml @@ -50,8 +50,6 @@ template_substitutions: replacement: [[2], [3]] - signature: replacement: [[2], [3]] - - signature: - replacement: [[2, 2], [3, 3]] - signature: replacement: [[2, 2], [3, 3]] - signature: diff --git a/setup_project.py b/setup_project.py index 53ee85d..ac3e7c9 100644 --- a/setup_project.py +++ b/setup_project.py @@ -169,7 +169,7 @@ def setup(settings: Settings) -> None: # Abort if the project has already been configured. if is_setup(settings): print_banner( - "ERROR: This Chaste user project has already been setup.", + "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.", From 4b35e12022125815f30993f51356aabfad2bfcb6 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Tue, 30 Jun 2026 13:26:50 +0100 Subject: [PATCH 39/51] #3 Update workflow prompts and dependencies --- .github/workflows/test-force-example.yml | 4 ++-- .github/workflows/test-project.yml | 4 ++-- .github/workflows/test-python-project.yml | 4 ++-- .github/workflows/test-sbml-project.yml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-force-example.yml b/.github/workflows/test-force-example.yml index a6c2417..b41b10d 100644 --- a/.github/workflows/test-force-example.yml +++ b/.github/workflows/test-force-example.yml @@ -34,8 +34,8 @@ jobs: run: | cp -r . ${{ env.PROJECT_DIR }} cd ${{ env.PROJECT_DIR }} - # Yes to cell_based component and Python bindings, defaults otherwise, yes to confirm - printf '\ny\n\n\n\ny\ny\n' | python3 setup_project.py + # Yes to cell_based component and Python bindings, no to SBML, defaults otherwise, yes to confirm + printf '\ny\n\n\n\ny\nn\ny\n' | python3 setup_project.py - name: Add MyForce to the project bindings run: | diff --git a/.github/workflows/test-project.yml b/.github/workflows/test-project.yml index 8402c3b..0c5679e 100644 --- a/.github/workflows/test-project.yml +++ b/.github/workflows/test-project.yml @@ -34,8 +34,8 @@ jobs: run: | cp -r . ${{ env.PROJECT_DIR }} cd ${{ env.PROJECT_DIR }} - # Use prompt defaults, yes to final confirmation - printf '\n\n\n\n\nn\ny\n' | python3 setup_project.py + # 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 diff --git a/.github/workflows/test-python-project.yml b/.github/workflows/test-python-project.yml index faabc24..0dc629e 100644 --- a/.github/workflows/test-python-project.yml +++ b/.github/workflows/test-python-project.yml @@ -34,8 +34,8 @@ jobs: run: | cp -r . ${{ env.PROJECT_DIR }} cd ${{ env.PROJECT_DIR }} - # Yes to python bindings prompt and final confirmation prompt - printf '\n\n\n\n\ny\ny\n' | python3 setup_project.py + # 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: | diff --git a/.github/workflows/test-sbml-project.yml b/.github/workflows/test-sbml-project.yml index eb97927..b70ad86 100644 --- a/.github/workflows/test-sbml-project.yml +++ b/.github/workflows/test-sbml-project.yml @@ -30,8 +30,8 @@ jobs: - name: Mark Chaste source as safe for git run: git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" - - name: Install clang-format - run: apt-get update && apt-get install -y clang-format + - name: Install clang-format and curl + run: apt-get update && apt-get install -y clang-format curl - name: Create test project run: | From 061cf3ab293b4dfd19308593ed08f64edea41aa9 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Tue, 30 Jun 2026 14:29:42 +0100 Subject: [PATCH 40/51] #3 Include Goldbeter example --- .github/workflows/test-sbml-project.yml | 5 +- examples/goldbeter_1991/Goldbeter1991.xml | 464 ++++++++++++++++++++++ examples/goldbeter_1991/README.md | 15 +- 3 files changed, 479 insertions(+), 5 deletions(-) create mode 100644 examples/goldbeter_1991/Goldbeter1991.xml diff --git a/.github/workflows/test-sbml-project.yml b/.github/workflows/test-sbml-project.yml index b70ad86..b65d0de 100644 --- a/.github/workflows/test-sbml-project.yml +++ b/.github/workflows/test-sbml-project.yml @@ -46,8 +46,9 @@ jobs: - name: Import the Goldbeter 1991 model run: | cd ${{ env.PROJECT_DIR }} - curl -L "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000003?filename=BIOMD0000000003_url.xml" \ - -o Goldbeter1991.xml + # 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_codegen_sbml Goldbeter1991.xml --model-type srn --output-dir src/ cp examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp test/ echo "TestGoldbeter1991SbmlSrnModel.hpp" >> test/ContinuousTestPack.txt 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 index 9dbeffe..f152957 100644 --- a/examples/goldbeter_1991/README.md +++ b/examples/goldbeter_1991/README.md @@ -19,10 +19,19 @@ Run every command below from your project's root directory. source .virtualenv/bin/activate ``` -## 2. Download the SBML model and give it a clean name +## 2. Get the SBML model with a clean name -The generated C++ class names come from the **file name**, so download the model and -rename it to something C++-friendly. Here we use `Goldbeter1991`: +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 From 24c616f4d2230abb0d63ade07f11a473169bf109 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Tue, 30 Jun 2026 16:22:21 +0100 Subject: [PATCH 41/51] #3 Switch CI to chaste/base --- .github/workflows/test-force-example.yml | 8 +++++--- .github/workflows/test-project.yml | 8 +++++--- .github/workflows/test-python-project.yml | 8 +++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test-force-example.yml b/.github/workflows/test-force-example.yml index a6c2417..a0a50d4 100644 --- a/.github/workflows/test-force-example.yml +++ b/.github/workflows/test-force-example.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest container: - image: chaste/develop + image: chaste/base options: --user root env: @@ -27,8 +27,10 @@ jobs: - name: Checkout template uses: actions/checkout@v7 - - name: Mark Chaste source as safe for git - run: git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" + - 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: | diff --git a/.github/workflows/test-project.yml b/.github/workflows/test-project.yml index 8402c3b..699c38e 100644 --- a/.github/workflows/test-project.yml +++ b/.github/workflows/test-project.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest container: - image: chaste/develop + image: chaste/base options: --user root env: @@ -27,8 +27,10 @@ jobs: - name: Checkout template uses: actions/checkout@v7 - - name: Mark Chaste source as safe for git - run: git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" + - 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: | diff --git a/.github/workflows/test-python-project.yml b/.github/workflows/test-python-project.yml index faabc24..c481894 100644 --- a/.github/workflows/test-python-project.yml +++ b/.github/workflows/test-python-project.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest container: - image: chaste/develop + image: chaste/base options: --user root env: @@ -27,8 +27,10 @@ jobs: - name: Checkout template uses: actions/checkout@v7 - - name: Mark Chaste source as safe for git - run: git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" + - 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: | From a2dc66ab2ee36cda6cd94c287a0e58f27650fe56 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Tue, 30 Jun 2026 17:16:46 +0100 Subject: [PATCH 42/51] #3 Fix Goldbeter1991 example --- .github/workflows/test-sbml-project.yml | 8 ++-- examples/goldbeter_1991/README.md | 45 ++++++++++++------- .../TestGoldbeter1991SbmlSrnModel.hpp | 43 +++++++++++------- test/ContinuousTestPack.txt | 2 +- 4 files changed, 61 insertions(+), 37 deletions(-) diff --git a/.github/workflows/test-sbml-project.yml b/.github/workflows/test-sbml-project.yml index b65d0de..998f4da 100644 --- a/.github/workflows/test-sbml-project.yml +++ b/.github/workflows/test-sbml-project.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest container: - image: chaste/develop + image: chaste/base options: --user root env: @@ -27,8 +27,10 @@ jobs: - name: Checkout template uses: actions/checkout@v7 - - name: Mark Chaste source as safe for git - run: git config --global --add safe.directory "${CHASTE_SOURCE_DIR}" + - 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 diff --git a/examples/goldbeter_1991/README.md b/examples/goldbeter_1991/README.md index f152957..c56e0d4 100644 --- a/examples/goldbeter_1991/README.md +++ b/examples/goldbeter_1991/README.md @@ -67,10 +67,11 @@ active cyclin protease (`X`). #include "AbstractCellBasedTestSuite.hpp" #include "Cell.hpp" -#include "NoCellCycleModel.hpp" +#include "CellPropertyRegistry.hpp" +#include "FixedG1GenerationalCellCycleModel.hpp" #include "SimulationTime.hpp" #include "SmartPointers.hpp" -#include "StemCellProliferativeType.hpp" +#include "TransitCellProliferativeType.hpp" #include "WildTypeCellMutationState.hpp" // The header generated from Goldbeter1991.xml in step 3. @@ -84,33 +85,43 @@ class TestGoldbeter1991SbmlSrnModel : public AbstractCellBasedTestSuite public: void TestSteadyStateSimulation() { - // Integrate to t = 1000 in 1000 steps (AbstractCellBasedTestSuite has set the start time to 0). + // 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(); - p_simulation_time->SetEndTimeAndNumberOfTimeSteps(1000.0, 1000); - - // Create the SRN model from the imported SBML model and attach it to a cell. + 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(); - MAKE_PTR(WildTypeCellMutationState, p_state); - MAKE_PTR(StemCellProliferativeType, p_stem_type); - NoCellCycleModel* p_cc_model = new NoCellCycleModel(); - - CellPtr p_cell(new Cell(p_state, p_cc_model, p_srn_model)); - p_cell->SetCellProliferativeType(p_stem_type); + 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, advancing the SRN model each step. + // Step the simulation to the end time. while (!p_simulation_time->IsFinished()) { p_simulation_time->IncrementTimeOneStep(); - p_srn_model->SimulateToCurrentTime(); + if (p_cell->ReadyToDivide()) + { + p_cell->Divide(); + } } // Check the steady state of the mitotic oscillator. - TS_ASSERT_DELTA(p_srn_model->GetStateVariable("C"), 0.5470, 1e-2); - TS_ASSERT_DELTA(p_srn_model->GetStateVariable("M"), 0.2936, 1e-2); - TS_ASSERT_DELTA(p_srn_model->GetStateVariable("X"), 0.0067, 1e-3); + 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); } }; diff --git a/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp b/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp index a3d8592..9029202 100644 --- a/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp +++ b/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp @@ -5,10 +5,11 @@ #include "AbstractCellBasedTestSuite.hpp" #include "Cell.hpp" -#include "NoCellCycleModel.hpp" +#include "CellPropertyRegistry.hpp" +#include "FixedG1GenerationalCellCycleModel.hpp" #include "SimulationTime.hpp" #include "SmartPointers.hpp" -#include "StemCellProliferativeType.hpp" +#include "TransitCellProliferativeType.hpp" #include "WildTypeCellMutationState.hpp" // The header generated from Goldbeter1991.xml by chaste_codegen_sbml. @@ -22,33 +23,43 @@ class TestGoldbeter1991SbmlSrnModel : public AbstractCellBasedTestSuite public: void TestSteadyStateSimulation() { - // Integrate to t = 1000 in 1000 steps (AbstractCellBasedTestSuite has set the start time to 0). + // 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(); - p_simulation_time->SetEndTimeAndNumberOfTimeSteps(1000.0, 1000); + 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 the SRN model from the imported SBML model and attach it to a cell. - Goldbeter1991SbmlSrnModel* p_srn_model = new Goldbeter1991SbmlSrnModel(); + // 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()); - MAKE_PTR(WildTypeCellMutationState, p_state); - MAKE_PTR(StemCellProliferativeType, p_stem_type); - NoCellCycleModel* p_cc_model = new NoCellCycleModel(); + FixedG1GenerationalCellCycleModel* p_cell_model = new FixedG1GenerationalCellCycleModel(); + Goldbeter1991SbmlSrnModel* p_srn_model = new Goldbeter1991SbmlSrnModel(); - CellPtr p_cell(new Cell(p_state, p_cc_model, p_srn_model)); - p_cell->SetCellProliferativeType(p_stem_type); + 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, advancing the SRN model each step. + // Step the simulation to the end time. while (!p_simulation_time->IsFinished()) { p_simulation_time->IncrementTimeOneStep(); - p_srn_model->SimulateToCurrentTime(); + if (p_cell->ReadyToDivide()) + { + p_cell->Divide(); + } } // Check the steady state of the mitotic oscillator. - TS_ASSERT_DELTA(p_srn_model->GetStateVariable("C"), 0.5470, 1e-2); - TS_ASSERT_DELTA(p_srn_model->GetStateVariable("M"), 0.2936, 1e-2); - TS_ASSERT_DELTA(p_srn_model->GetStateVariable("X"), 0.0067, 1e-3); + 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); } }; 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 From 753d18e9ccf20b96e3c0f1658c6756f5cdabb164 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 3 Jul 2026 13:52:54 +0100 Subject: [PATCH 43/51] #3 Remove wrapper generation guard --- dynamic/CMakeLists.txt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/dynamic/CMakeLists.txt b/dynamic/CMakeLists.txt index 1f7dccf..b4a9e39 100644 --- a/dynamic/CMakeLists.txt +++ b/dynamic/CMakeLists.txt @@ -74,13 +74,11 @@ set(WRAPPER_GENERATION_COMMAND --std c++17 ) -# Generate wrappers if not already present -if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/wrappers") - execute_process( - COMMAND ${WRAPPER_GENERATION_COMMAND} - COMMAND_ERROR_IS_FATAL ANY - ) -endif() +# Generate wrappers +execute_process( + COMMAND ${WRAPPER_GENERATION_COMMAND} + COMMAND_ERROR_IS_FATAL ANY +) # Target for manually regenerating wrappers add_custom_target(template_project_wrappers From 52b0f965846d95f3529648c71edd870a85388f6e Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 3 Jul 2026 14:55:21 +0100 Subject: [PATCH 44/51] #3 Update wrapping troubleshooting docs --- README.md | 44 +++++++++++++++++++++++++++++++++++++ dynamic/CMakeLists.txt | 7 ++++++ examples/my_force/README.md | 5 ++++- scripts/common.sh | 7 +----- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index dd811da..1a0739b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,22 @@ your project from Python. For a complete, worked example — writing a new C++ ` 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: @@ -99,3 +115,31 @@ modules: 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 index b4a9e39..cd40779 100644 --- a/dynamic/CMakeLists.txt +++ b/dynamic/CMakeLists.txt @@ -101,6 +101,13 @@ file(GLOB_RECURSE WRAPPER_SOURCES ${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 diff --git a/examples/my_force/README.md b/examples/my_force/README.md index dae71c9..ffd4cff 100644 --- a/examples/my_force/README.md +++ b/examples/my_force/README.md @@ -5,7 +5,7 @@ project's Python bindings, and then uses it in a [PyChaste](https://chaste.githu simulation driven from Python. It assumes you have already created your project from this template and answered **yes** to -the Python bindings prompt in `setup_project.py`, so that `dynamic/config.yaml`, +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 @@ -137,6 +137,9 @@ confirming that your new C++ class is callable from Python. > `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. diff --git a/scripts/common.sh b/scripts/common.sh index a3e23e9..7555d2c 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -96,10 +96,5 @@ safe_rm() { echo "Error: refusing to remove unsafe path '${path}'." >&2 exit 1 fi - if [[ -t 0 ]]; then - # Prompt for confirmation if in an interactive shell. - rm -rI "${path}" - else - rm -rf "${path}" - fi + rm -rf "${path}" } From 8f8294ada0318fe5ca4480fb99ad52e8bfa92b9e Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 3 Jul 2026 15:04:08 +0100 Subject: [PATCH 45/51] #3 Update force example --- examples/my_force/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/my_force/README.md b/examples/my_force/README.md index ffd4cff..8d867c6 100644 --- a/examples/my_force/README.md +++ b/examples/my_force/README.md @@ -33,7 +33,7 @@ Tell cppwg to wrap the new class by editing `dynamic/config.yaml`: ```yaml source_includes: - SmartPointers.hpp - - Hello.hpp + - Hello_ProjectName.hpp - MyForce.hpp ``` @@ -51,7 +51,7 @@ Tell cppwg to wrap the new class by editing `dynamic/config.yaml`: source_locations: - src/ classes: - - name: Hello + - name: Hello_ProjectName - name: MyForce ``` From 091e0a7c4df2ef0fef681f1bc528a981ec9b65fe Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 3 Jul 2026 20:03:14 +0100 Subject: [PATCH 46/51] #3 Update force example --- .github/workflows/test-force-example.yml | 12 ++++++----- examples/my_force/README.md | 27 ++++++++++++------------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test-force-example.yml b/.github/workflows/test-force-example.yml index a0a50d4..8a5a8b2 100644 --- a/.github/workflows/test-force-example.yml +++ b/.github/workflows/test-force-example.yml @@ -37,17 +37,19 @@ jobs: cp -r . ${{ env.PROJECT_DIR }} cd ${{ env.PROJECT_DIR }} # Yes to cell_based component and Python bindings, defaults otherwise, yes to confirm - printf '\ny\n\n\n\ny\ny\n' | python3 setup_project.py + printf '\ny\ny\n\n\ny\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 's/^source_includes:/source_includes:\n - MyForce.hpp/' dynamic/config.yaml - sed -i 's/^ classes:/ classes:\n - name: MyForce/' dynamic/config.yaml + 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 - # so MyForce can inherit AbstractForce across modules - sed -i 's/^ - name: all/ - name: all\n imports:\n - chaste._pychaste_all\n external_bases:\n - AbstractForce/' dynamic/config.yaml + 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 diff --git a/examples/my_force/README.md b/examples/my_force/README.md index 8d867c6..7bda8fd 100644 --- a/examples/my_force/README.md +++ b/examples/my_force/README.md @@ -37,23 +37,23 @@ Tell cppwg to wrap the new class by editing `dynamic/config.yaml`: - MyForce.hpp ``` -* under the `all` module, add the class to `classes:`, and tell cppwg that the - base class `AbstractForce` is wrapped in PyChaste: import PyChaste's compiled - module under `imports:` and list the base class under `external_bases:` +* 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: - - chaste._pychaste_all - external_bases: - - AbstractForce + imports: #<-- new + - chaste._pychaste_all #<-- new + external_bases: #<-- new + - AbstractForce #<-- new source_locations: - src/ classes: - name: Hello_ProjectName - - name: MyForce - ``` + - name: MyForce #<-- new +``` This is what makes the cross-module inheritance work: `MyForce` subclasses `AbstractForce`, which is wrapped in PyChaste (a different package). Listing @@ -63,12 +63,11 @@ Tell cppwg to wrap the new class by editing `dynamic/config.yaml`: Without this, `OffLatticeSimulation.AddForce(my_force)` would reject the force because Python would not recognise `MyForce` as an `AbstractForce`. - > This requires a version of [cppwg](https://github.com/Chaste/cppwg) with - > cross-module inheritance support (the `imports` and `external_bases` config - > keys). + > 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). +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 From 82b9612b93f9738ce5cab5da05f36c2e26da4ab8 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 3 Jul 2026 20:18:42 +0100 Subject: [PATCH 47/51] #3 Create virtualenv during python setup --- setup_project.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/setup_project.py b/setup_project.py index ac3e7c9..1352d6f 100644 --- a/setup_project.py +++ b/setup_project.py @@ -37,6 +37,7 @@ import os import re import shutil +import subprocess class Settings: @@ -159,6 +160,41 @@ def print_banner(*lines: str) -> None: print(border) +def create_virtualenv(settings: Settings) -> None: + """Create the project virtualenv for the Python bindings. + + Creates .virtualenv/ with --system-site-packages so it can see PyChaste's native + runtime dependencies (petsc4py, mpi4py, vtk) provided by the system Python. On any + failure this is non-fatal: it prints the manual command so the user can create it + themselves. The compiled bindings are installed into this virtualenv later by + scripts/bindings_install.sh, once the project has been built. + """ + venv_dir = os.path.join(settings.PROJECT_ROOT, ".virtualenv") + try: + subprocess.run(["python3", "-m", "venv", "--system-site-packages", venv_dir], 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 {venv_dir}") + return + + # Warn (non-fatal) if PyChaste's native runtime dependencies are not visible to the + # venv. They 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. + venv_python = os.path.join(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 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) @@ -256,6 +292,9 @@ def setup(settings: Settings) -> None: # 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). + create_virtualenv(settings) else: # Remove the Python binding template files shutil.rmtree(os.path.join(settings.PROJECT_ROOT, "dynamic")) @@ -276,6 +315,7 @@ def setup(settings: Settings) -> None: 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/.") From 7f2ebac998c24ecb539ce559a0b7621ecd966067 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 3 Jul 2026 21:20:42 +0100 Subject: [PATCH 48/51] #3 Add create_venv.sh --- scripts/bindings_install.sh | 18 ++++++------- scripts/common.sh | 16 +++++++----- scripts/create_venv.sh | 24 +++++++++++++++++ scripts/sbml_install.sh | 27 ++++++++------------ setup_project.py | 51 ++++++++++++++++++++++--------------- 5 files changed, 81 insertions(+), 55 deletions(-) create mode 100755 scripts/create_venv.sh diff --git a/scripts/bindings_install.sh b/scripts/bindings_install.sh index 3debc4c..7da7848 100755 --- a/scripts/bindings_install.sh +++ b/scripts/bindings_install.sh @@ -37,18 +37,16 @@ if [[ ! -d "${pychaste_pkg}" ]]; then exit 1 fi -# Create the virtualenv if it does not already exist. -# Use --system-site-packages so the venv can see PyChaste's native runtime -# dependencies (petsc4py, mpi4py, vtk), which are provided by the system Python -# and are not pip-installable here. -venv_dir="${PROJECT_ROOT}/.virtualenv" -python3 -m venv --system-site-packages "${venv_dir}" +# 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}" +"${VENV_DIR}/bin/pip" install "${pychaste_pkg}" # Install the project Python bindings. -"${venv_dir}/bin/pip" install "${project_pkg}" +"${VENV_DIR}/bin/pip" install "${project_pkg}" -echo "Installed Python bindings to '${venv_dir}'." -echo "Activate with: source '${venv_dir}/bin/activate'" +echo "Installed Python bindings to '${VENV_DIR}'." +echo "Activate with: source '${VENV_DIR}/bin/activate'" diff --git a/scripts/common.sh b/scripts/common.sh index 7555d2c..acc54d6 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -6,7 +6,13 @@ 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" \ @@ -19,12 +25,8 @@ if [[ -z "${CHASTE_SOURCE_DIR:-}" ]]; then break fi done - if [[ -z "${CHASTE_SOURCE_DIR:-}" ]]; then - echo "Error: could not find Chaste source directory." >&2 - echo "Set CHASTE_SOURCE_DIR to the location of your Chaste source." >&2 - exit 1 - fi fi +CHASTE_SOURCE_DIR="${CHASTE_SOURCE_DIR:-}" CHASTE_PROJECTS_DIR="${CHASTE_SOURCE_DIR}/projects" @@ -53,9 +55,9 @@ else BUILD_PROJECT_PYTHON_BINDINGS=OFF fi -# Set the number of parallel jobs for building and testing. +# Set the number of parallel jobs for building and testing if ! [[ "${NCORES:-}" =~ ^[1-9][0-9]*$ ]]; then - NCORES="$(nproc)" + 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. 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/sbml_install.sh b/scripts/sbml_install.sh index eacb584..762d115 100755 --- a/scripts/sbml_install.sh +++ b/scripts/sbml_install.sh @@ -9,17 +9,9 @@ if [[ $# -ne 0 ]]; then exit 1 fi -# The project root is the parent of this script's directory. +# Import common variables and helpers. script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd -- "${script_dir}/.." && pwd)" - -# 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 -} +source "${script_dir}/common.sh" require_command python3 @@ -28,15 +20,16 @@ if ! command -v clang-format >/dev/null 2>&1; then echo "Warning: clang-format is not on PATH; chaste_codegen_sbml needs it to format generated code." >&2 fi -# Create the project virtualenv (idempotent; shared with Python bindings if both are set up). -venv_dir="${PROJECT_ROOT}/.virtualenv" -python3 -m venv "${venv_dir}" +# 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" +"${VENV_DIR}/bin/pip" install --upgrade pip +"${VENV_DIR}/bin/pip" install "git+https://github.com/Chaste/chaste-codegen-sbml@develop" echo "" -echo "Installed chaste-codegen-sbml into '${venv_dir}'." -echo "Activate the virtualenv with: source '${venv_dir}/bin/activate'" +echo "Installed chaste-codegen-sbml into '${VENV_DIR}'." +echo "Activate the virtualenv with: source '${VENV_DIR}/bin/activate'" echo "Then convert an SBML model with: chaste_codegen_sbml --help" diff --git a/setup_project.py b/setup_project.py index 63aa7de..60db8c9 100644 --- a/setup_project.py +++ b/setup_project.py @@ -106,6 +106,11 @@ def __init__(self) -> None: # 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") + # SBML support files vendored from chaste-codegen-sbml (kept if SBML opted in, deleted otherwise). # Their names are kept unchanged: generated model code #includes and subclasses them by name. self.SBML_SUPPORT_FILES = [ @@ -180,29 +185,31 @@ def print_banner(*lines: str) -> None: print(border) -def create_virtualenv(settings: Settings) -> None: - """Create the project virtualenv for the Python bindings. +def create_virtualenv(settings: Settings) -> bool: + """Create the project virtualenv, shared by the Python bindings and SBML paths. - Creates .virtualenv/ with --system-site-packages so it can see PyChaste's native - runtime dependencies (petsc4py, mpi4py, vtk) provided by the system Python. On any - failure this is non-fatal: it prints the manual command so the user can create it - themselves. The compiled bindings are installed into this virtualenv later by - scripts/bindings_install.sh, once the project has been built. + 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 ). """ - venv_dir = os.path.join(settings.PROJECT_ROOT, ".virtualenv") try: - subprocess.run(["python3", "-m", "venv", "--system-site-packages", venv_dir], check=True) + 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 {venv_dir}") - return + print(f" python3 -m venv --system-site-packages {settings.VENV_DIR}") + return False + return True + - # Warn (non-fatal) if PyChaste's native runtime dependencies are not visible to the - # venv. They 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. - venv_python = os.path.join(venv_dir, "bin", "python") +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") @@ -218,19 +225,20 @@ def create_virtualenv(settings: Settings) -> None: def install_sbml_codegen(settings: Settings) -> None: """Create the project virtualenv and install chaste-codegen-sbml into it. - Runs scripts/sbml_install.sh. On any failure this is non-fatal: it prints the - manual commands so the user can finish the install themselves. + Creates the shared virtualenv via create_virtualenv(), then runs scripts/sbml_install.sh + to install the code generator. 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: - venv_dir = os.path.join(settings.PROJECT_ROOT, ".virtualenv") print("") print(f"WARNING: could not install chaste-codegen-sbml automatically ({error}).") print("Install it manually with:") - print(f" python3 -m venv {venv_dir}") print( - f" {os.path.join(venv_dir, 'bin', 'pip')} install " + f" {os.path.join(settings.VENV_DIR, 'bin', 'pip')} install " "'git+https://github.com/Chaste/chaste-codegen-sbml@develop'" ) @@ -341,7 +349,8 @@ def setup(settings: Settings) -> None: 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). - create_virtualenv(settings) + 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")) From a13463b0d95d024bd700bcbf4a520539fc1c5aa3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:36:44 +0000 Subject: [PATCH 49/51] Fix missing CHASTE_SOURCE_DIR env var in workflow files --- .github/workflows/test-force-example.yml | 1 + .github/workflows/test-project.yml | 1 + .github/workflows/test-python-project.yml | 1 + .github/workflows/test-sbml-project.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/test-force-example.yml b/.github/workflows/test-force-example.yml index dc47926..037f842 100644 --- a/.github/workflows/test-force-example.yml +++ b/.github/workflows/test-force-example.yml @@ -19,6 +19,7 @@ jobs: options: --user root env: + CHASTE_SOURCE_DIR: /tmp/Chaste CHASTE_BUILD_DIR: /tmp/build CHASTE_TEST_OUTPUT: /tmp/testoutput PROJECT_DIR: /tmp/myproject diff --git a/.github/workflows/test-project.yml b/.github/workflows/test-project.yml index 5259c70..1d881e0 100644 --- a/.github/workflows/test-project.yml +++ b/.github/workflows/test-project.yml @@ -19,6 +19,7 @@ jobs: options: --user root env: + CHASTE_SOURCE_DIR: /tmp/Chaste CHASTE_BUILD_DIR: /tmp/build CHASTE_TEST_OUTPUT: /tmp/testoutput PROJECT_DIR: /tmp/myproject diff --git a/.github/workflows/test-python-project.yml b/.github/workflows/test-python-project.yml index dca014f..461eed6 100644 --- a/.github/workflows/test-python-project.yml +++ b/.github/workflows/test-python-project.yml @@ -19,6 +19,7 @@ jobs: options: --user root env: + CHASTE_SOURCE_DIR: /tmp/Chaste CHASTE_BUILD_DIR: /tmp/build CHASTE_TEST_OUTPUT: /tmp/testoutput PROJECT_DIR: /tmp/myproject diff --git a/.github/workflows/test-sbml-project.yml b/.github/workflows/test-sbml-project.yml index 998f4da..03df4d0 100644 --- a/.github/workflows/test-sbml-project.yml +++ b/.github/workflows/test-sbml-project.yml @@ -19,6 +19,7 @@ jobs: options: --user root env: + CHASTE_SOURCE_DIR: /tmp/Chaste CHASTE_BUILD_DIR: /tmp/build CHASTE_TEST_OUTPUT: /tmp/testoutput PROJECT_DIR: /tmp/myproject From ac1716c343839ace707a4ee177e04519274974b0 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Sat, 4 Jul 2026 13:07:46 +0100 Subject: [PATCH 50/51] #3 Copy SBML base classes from the chaste-sbml package - Stop vendoring the SBML base classes in src/; sbml_install.sh now copies them from the installed chaste-sbml package via 'chaste-sbml copy-base-classes'. - Drop the vendored-file handling from setup_project.py. - Use the chaste-sbml command and its generate subcommand in the workflow, README, and Goldbeter example. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/test-sbml-project.yml | 2 +- README.md | 9 +- examples/goldbeter_1991/README.md | 4 +- .../TestGoldbeter1991SbmlSrnModel.hpp | 2 +- scripts/sbml_install.sh | 11 +- setup_project.py | 52 +-- src/AbstractSbmlCellCycleModel.cpp | 165 ---------- src/AbstractSbmlCellCycleModel.hpp | 165 ---------- src/AbstractSbmlOdeSystem.cpp | 148 --------- src/AbstractSbmlOdeSystem.hpp | 229 ------------- src/AbstractSbmlSrnModel.cpp | 124 ------- src/AbstractSbmlSrnModel.hpp | 138 -------- src/SbmlEventType.hpp | 10 - src/SbmlMath.cpp | 190 ----------- src/SbmlMath.hpp | 310 ------------------ src/fortests/SbmlTestHelpers.cpp | 143 -------- src/fortests/SbmlTestHelpers.hpp | 195 ----------- src/fortests/SbmlTestOdeSolution.cpp | 97 ------ src/fortests/SbmlTestOdeSolution.hpp | 104 ------ 19 files changed, 33 insertions(+), 2065 deletions(-) delete mode 100644 src/AbstractSbmlCellCycleModel.cpp delete mode 100644 src/AbstractSbmlCellCycleModel.hpp delete mode 100644 src/AbstractSbmlOdeSystem.cpp delete mode 100644 src/AbstractSbmlOdeSystem.hpp delete mode 100644 src/AbstractSbmlSrnModel.cpp delete mode 100644 src/AbstractSbmlSrnModel.hpp delete mode 100644 src/SbmlEventType.hpp delete mode 100644 src/SbmlMath.cpp delete mode 100644 src/SbmlMath.hpp delete mode 100644 src/fortests/SbmlTestHelpers.cpp delete mode 100644 src/fortests/SbmlTestHelpers.hpp delete mode 100644 src/fortests/SbmlTestOdeSolution.cpp delete mode 100644 src/fortests/SbmlTestOdeSolution.hpp diff --git a/.github/workflows/test-sbml-project.yml b/.github/workflows/test-sbml-project.yml index 03df4d0..ca1834b 100644 --- a/.github/workflows/test-sbml-project.yml +++ b/.github/workflows/test-sbml-project.yml @@ -52,7 +52,7 @@ jobs: # 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_codegen_sbml Goldbeter1991.xml --model-type srn --output-dir src/ + .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 diff --git a/README.md b/README.md index 47397ae..581a31f 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,13 @@ Do you want to create an SBML user project? source .virtualenv/bin/activate ``` -(If you ever need to (re)install the generator by hand, run `scripts/sbml_install.sh`.) +(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_codegen_sbml my_model.xml --model-type srn --output-dir src/ +chaste-sbml generate my_model.xml --model-type srn --output-dir src/ ``` * `--model-type` is one of `generic`, `srn` (sub-cellular reaction network), or @@ -60,7 +61,9 @@ scripts/test.sh # run the project's tests ### The SBML base classes -These live in `src/` and are required by the generated code: +`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 | | --- | --- | diff --git a/examples/goldbeter_1991/README.md b/examples/goldbeter_1991/README.md index c56e0d4..2fdddec 100644 --- a/examples/goldbeter_1991/README.md +++ b/examples/goldbeter_1991/README.md @@ -9,7 +9,7 @@ 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-codegen-sbml` is installed in `.virtualenv/`. +* `chaste-sbml` is installed in `.virtualenv/`. Run every command below from your project's root directory. @@ -42,7 +42,7 @@ curl -L "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000003?filename Goldbeter 1991 is a sub-cellular reaction network, so use `--model-type srn`: ```sh -chaste_codegen_sbml Goldbeter1991.xml --model-type srn --output-dir src/ +chaste-sbml generate Goldbeter1991.xml --model-type srn --output-dir src/ ``` This generates four files in `src/`: diff --git a/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp b/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp index 9029202..cdad0c7 100644 --- a/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp +++ b/examples/goldbeter_1991/TestGoldbeter1991SbmlSrnModel.hpp @@ -12,7 +12,7 @@ #include "TransitCellProliferativeType.hpp" #include "WildTypeCellMutationState.hpp" -// The header generated from Goldbeter1991.xml by chaste_codegen_sbml. +// The header generated from Goldbeter1991.xml by chaste-sbml. #include "Goldbeter1991SbmlSrnModel.hpp" // This is a serial test. diff --git a/scripts/sbml_install.sh b/scripts/sbml_install.sh index 762d115..ce05584 100755 --- a/scripts/sbml_install.sh +++ b/scripts/sbml_install.sh @@ -17,7 +17,7 @@ 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_codegen_sbml needs it to format generated code." >&2 + 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 @@ -29,7 +29,12 @@ fi "${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 chaste-codegen-sbml into '${VENV_DIR}'." +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_codegen_sbml --help" +echo "Then convert an SBML model with: chaste-sbml --help" diff --git a/setup_project.py b/setup_project.py index 60db8c9..f0e9117 100644 --- a/setup_project.py +++ b/setup_project.py @@ -111,24 +111,8 @@ def __init__(self) -> None: self.VENV_DIR = os.path.join(self.PROJECT_ROOT, ".virtualenv") self.CREATE_VENV_SCRIPT = os.path.join(self.PROJECT_ROOT, "scripts", "create_venv.sh") - # SBML support files vendored from chaste-codegen-sbml (kept if SBML opted in, deleted otherwise). - # Their names are kept unchanged: generated model code #includes and subclasses them by name. - self.SBML_SUPPORT_FILES = [ - os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlOdeSystem.hpp"), - os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlOdeSystem.cpp"), - os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlSrnModel.hpp"), - os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlSrnModel.cpp"), - os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlCellCycleModel.hpp"), - os.path.join(self.PROJECT_ROOT, "src", "AbstractSbmlCellCycleModel.cpp"), - os.path.join(self.PROJECT_ROOT, "src", "SbmlEventType.hpp"), - os.path.join(self.PROJECT_ROOT, "src", "SbmlMath.hpp"), - os.path.join(self.PROJECT_ROOT, "src", "SbmlMath.cpp"), - ] - - # SBML test-helper directory (removed alongside SBML_SUPPORT_FILES if SBML is declined). - self.SBML_FORTESTS_DIR = os.path.join(self.PROJECT_ROOT, "src", "fortests") - - # The script that creates the project virtualenv and installs the SBML code generator. + # 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") @@ -223,24 +207,26 @@ def warn_missing_pychaste_deps(settings: Settings) -> None: def install_sbml_codegen(settings: Settings) -> None: - """Create the project virtualenv and install chaste-codegen-sbml into it. + """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. On any failure this is non-fatal: it prints the manual - commands so the user can finish the install themselves. + 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 install chaste-codegen-sbml automatically ({error}).") - print("Install it manually with:") - print( - f" {os.path.join(settings.VENV_DIR, 'bin', 'pip')} install " - "'git+https://github.com/Chaste/chaste-codegen-sbml@develop'" - ) + 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: @@ -356,15 +342,9 @@ def setup(settings: Settings) -> None: shutil.rmtree(os.path.join(settings.PROJECT_ROOT, "dynamic")) shutil.rmtree(os.path.join(settings.PROJECT_ROOT, "src", "py")) - # Set up or remove SBML support + # Set up SBML support: install the code generator and copy the base classes into src/. if sbml: - # Keep the vendored SBML files (unchanged) and install the code generator into the venv. install_sbml_codegen(settings) - else: - # Remove the vendored SBML support files and test helpers. - for file in settings.SBML_SUPPORT_FILES: - os.remove(file) - shutil.rmtree(settings.SBML_FORTESTS_DIR) # Summarise the changes that were made print("") @@ -386,10 +366,8 @@ def setup(settings: Settings) -> None: print("* Removed Python bindings template files in dynamic/ and src/py/.") if sbml: - print("* Kept the SBML base classes in src/ and installed chaste-codegen-sbml into .virtualenv.") + 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.") - else: - print("* Removed the SBML base classes from src/.") def main() -> None: diff --git a/src/AbstractSbmlCellCycleModel.cpp b/src/AbstractSbmlCellCycleModel.cpp deleted file mode 100644 index e7bc3f7..0000000 --- a/src/AbstractSbmlCellCycleModel.cpp +++ /dev/null @@ -1,165 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#include "AbstractOdeBasedCellCycleModel.hpp" -#include "BackwardEulerIvpOdeSolver.hpp" -#include "CellCycleModelOdeSolver.hpp" -#include "CvodeAdaptor.hpp" -#include "SbmlEventType.hpp" -#include "StemCellProliferativeType.hpp" -#include "TransitCellProliferativeType.hpp" - -#include "AbstractSbmlCellCycleModel.hpp" - -AbstractSbmlCellCycleModel::AbstractSbmlCellCycleModel(boost::shared_ptr pOdeSolver) - : AbstractOdeBasedCellCycleModel(SimulationTime::Instance()->GetTime(), pOdeSolver) -{ - if (!mpOdeSolver) - { -#ifdef CHASTE_CVODE - // Default to CVODE where available - mpOdeSolver = CellCycleModelOdeSolver::Instance(); - mpOdeSolver->Initialise(); - mpOdeSolver->SetMaxSteps(10000); // Safe defaults - mpOdeSolver->SetTolerances(1e-6, 1e-8); - // CVODE needs to be instructed to check for stopping events - mpOdeSolver->CheckForStoppingEvents(); -#else - // Default to Chaste Runge-Kutta solver where CVODE is not available - mpOdeSolver = CellCycleModelOdeSolver::Instance(); - mpOdeSolver->Initialise(); - this->SetDt(0.0001); // Safe default -#endif // CHASTE_CVODE - } - - assert(mpOdeSolver->IsSetUp()); -} - -AbstractSbmlCellCycleModel::AbstractSbmlCellCycleModel(const AbstractSbmlCellCycleModel& rModel) - : AbstractOdeBasedCellCycleModel(rModel) -{ - /* - * Set each member variable of the new Cell Cycle model that inherits - * its value from the parent. - * - * Note 1: some of the new Cell Cycle model's member variables - * will already have been correctly initialized in its constructor. - * - * Note 2: one or more of the new Cell Cycle model's member variables - * may be set/overwritten as soon as InitialiseDaughterCell() is called on - * the new Cell Cycle model. - * - * Note 3: Only set the variables defined in this class. Variables defined - * in parent classes will be defined there. - */ -} - -AbstractSbmlCellCycleModel::~AbstractSbmlCellCycleModel() -{ -} - -void AbstractSbmlCellCycleModel::AdjustOdeParameters(double currentTime) -{ - static_cast(mpOdeSystem)->AdjustParameters(currentTime); -} - -bool AbstractSbmlCellCycleModel::CanCellTerminallyDifferentiate() -{ - return false; -} - -double AbstractSbmlCellCycleModel::GetAverageTransitCellCycleTime() -{ - // A default value, should be overridden in subclasses - return 1.25; -} - -double AbstractSbmlCellCycleModel::GetAverageStemCellCycleTime() -{ - // A default value, should be overridden in subclasses - return 1.25; -} - -double AbstractSbmlCellCycleModel::GetStateVariable(const std::string& rName) -{ - assert(mpOdeSystem != nullptr); - return mpOdeSystem->GetStateVariable(rName); -} - -void AbstractSbmlCellCycleModel::OutputCellCycleModelParameters(out_stream& rParamsFile) -{ - // No new parameters to output, so just call method on direct parent class - AbstractOdeBasedCellCycleModel::OutputCellCycleModelParameters(rParamsFile); -} - -bool AbstractSbmlCellCycleModel::ReadyToDivide() -{ - if (!mReadyToDivide) - { - bool was_ready_to_divide = mReadyToDivide; - double previous_divide_time = mDivideTime; - - // Solves ODE to current time and update cell division flag and time - bool stopping_event_occurred = AbstractOdeBasedCellCycleModel::ReadyToDivide(); - - if (stopping_event_occurred) - { - // Reset division flag and time if stopping event is not cell division - if (!static_cast(mpOdeSystem)->HasEventOccurred(SbmlEventType::CELL_DIVISION)) - { - mReadyToDivide = was_ready_to_divide; - mDivideTime = previous_divide_time; - } - } - } - return mReadyToDivide; -} - -void AbstractSbmlCellCycleModel::ResetForDivision() -{ - assert(mReadyToDivide); - AbstractOdeBasedCellCycleModel::ResetForDivision(); - - assert(mpOdeSystem != nullptr); - static_cast(mpOdeSystem)->ResetEventsOccurred(); -} - -// Register class with Boost serialization -#include "SerializationExportWrapperForCpp.hpp" -CHASTE_CLASS_EXPORT(AbstractSbmlCellCycleModel) - -// Register the CellCycleModel classes with Boost serialization -#include "CellCycleModelOdeSolverExportWrapper.hpp" -EXPORT_CELL_CYCLE_MODEL_ODE_SOLVER(AbstractSbmlCellCycleModel) diff --git a/src/AbstractSbmlCellCycleModel.hpp b/src/AbstractSbmlCellCycleModel.hpp deleted file mode 100644 index b5b078b..0000000 --- a/src/AbstractSbmlCellCycleModel.hpp +++ /dev/null @@ -1,165 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#ifndef ABSTRACT_SBML_CELL_CYCLE_MODEL_HPP_ -#define ABSTRACT_SBML_CELL_CYCLE_MODEL_HPP_ - -#include - -#include "AbstractCellCycleModelOdeSolver.hpp" -#include "AbstractOdeBasedCellCycleModel.hpp" -#include "AbstractSbmlOdeSystem.hpp" -#include "ChasteSerialization.hpp" -#include "ClassIsAbstract.hpp" - -/** - * A base class for cell cycle models generated from SBML - */ - -class AbstractSbmlCellCycleModel : public AbstractOdeBasedCellCycleModel -{ -private: - /** Needed for serialization. */ - friend class boost::serialization::access; - /** - * Archive / unarchive the AbstractSbmlCellCycleModel. - * - * @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); - } - -protected: - /** - * Protected copy-constructor for use by CreateCellCycleModel(). - * - * The only way to copy an instance of a subclass of AbstractCellCycleModel is - * by calling CreateCellCycleModel(), which ensures that the instance is copied - * correctly. - * - * This copy-constructor helps subclasses of AbstractCellCycleModel to - * ensure that all their members are copied over correctly. It is primarily - * used during cell division to set member variables for a daughter cell. - * Note that the cell-cycle model of the parent cell will have run ResetForDivision() - * just before calling CreateCellCycleModel(), so performing an exact copy of the - * parent cell's cell-cycle model is suitable behaviour. Any further initialisation - * specific to the daughter cell can be completed via InitialiseDaughterCell(). - * - * @param rModel the cell-cycle model to copy. - */ - AbstractSbmlCellCycleModel(const AbstractSbmlCellCycleModel& rModel); - -public: - /** - * Default constructor calls base class. - * - * @param pOdeSolver An optional pointer to a cell-cycle model ODE solver object (allows the use of different ODE solvers) - */ - AbstractSbmlCellCycleModel(boost::shared_ptr pOdeSolver = boost::shared_ptr()); - - /** - * Destructor. - */ - virtual ~AbstractSbmlCellCycleModel(); - - /** - * Adjust any ODE parameters needed before solving until currentTime. - * - * @param currentTime the time up to which the system will be solved. - */ - void AdjustOdeParameters(double currentTime); - - /** - * Overridden CanCellTerminallyDifferentiate() method. - * @return whether cell can terminally differentiate - */ - bool CanCellTerminallyDifferentiate(); - - /** - * Overridden GetAverageStemCellCycleTime() method. - * @return time - */ - double GetAverageStemCellCycleTime(); - - /** - * Overridden GetAverageTransitCellCycleTime() method. - * @return time - */ - double GetAverageTransitCellCycleTime(); - - /** - * @return the value of a given state variable. - * - * @param rName the name of the state variable - */ - double GetStateVariable(const std::string& rName); - - /** - * Outputs cell-cycle model parameters to file. - * - * @param rParamsFile the file stream to which the parameters are output - */ - void OutputCellCycleModelParameters(out_stream& rParamsFile); - - /** - * See AbstractCellCycleModel::ResetForDivision() - * - * @return whether the cell is ready to divide (enter M phase). - */ - bool ReadyToDivide() override; - - /** - * Each cell-cycle model must be able to be reset 'after' a cell division. - * - * Actually, this method is called from Cell::Divide() to - * reset the cell cycle just before the daughter cell is created. - * CreateCellCycleModel() can then clone our state to generate a - * cell-cycle model instance for the daughter cell. - */ - void ResetForDivision() override; -}; - -// Register abstract class with Boost serialization -CLASS_IS_ABSTRACT(AbstractSbmlCellCycleModel) - -// Register the CellCycleModel classes with Boost serialization -#include "CellCycleModelOdeSolverExportWrapper.hpp" -EXPORT_CELL_CYCLE_MODEL_ODE_SOLVER(AbstractSbmlCellCycleModel) - -#endif // ABSTRACT_SBML_CELL_CYCLE_MODEL_HPP_ diff --git a/src/AbstractSbmlOdeSystem.cpp b/src/AbstractSbmlOdeSystem.cpp deleted file mode 100644 index 152cea3..0000000 --- a/src/AbstractSbmlOdeSystem.cpp +++ /dev/null @@ -1,148 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#include "ChasteSerialization.hpp" -#include "SbmlEventType.hpp" - -#include "AbstractSbmlOdeSystem.hpp" - -AbstractSbmlOdeSystem::AbstractSbmlOdeSystem(unsigned numberOfStateVariables, unsigned numberOfParameters, unsigned numberOfEvents) - : AbstractOdeSystem(numberOfStateVariables), - mNumberOfParameters(numberOfParameters), - mNumberOfEvents(numberOfEvents) -{ - if (mNumberOfEvents > 0) - { - mEventType.resize(mNumberOfEvents, SbmlEventType::UNKNOWN); - - mEventSatisfied.resize(mNumberOfEvents, true); // Prevent events from triggering at the start - mEventClampActive.resize(mNumberOfEvents, true); // Re-derived at each Solve segment start - mEventTriggered.resize(mNumberOfEvents, false); - - if (mNumberOfStateVariables > 0) - { - mEventAdjustedStateVars.resize(mNumberOfStateVariables, false); - mEventAdjustedStateValues.resize(mNumberOfStateVariables, 0.0); - mEventAdjustedStatePriority.resize(mNumberOfStateVariables, 0.0); - } - - if (mNumberOfParameters > 0) - { - mEventAdjustedParameters.resize(mNumberOfParameters, false); - mEventAdjustedParameterValues.resize(mNumberOfParameters, 0.0); - mEventAdjustedParameterPriority.resize(mNumberOfParameters, 0.0); - } - } -} - -AbstractSbmlOdeSystem::~AbstractSbmlOdeSystem() -{ -} - -void AbstractSbmlOdeSystem::AdjustParameters(double time) -{ - for (unsigned i = 0; i < mEventAdjustedParameters.size(); ++i) - { - if (mEventAdjustedParameters[i]) - { - SetParameter(i, mEventAdjustedParameterValues[i]); - } - } - - for (unsigned i = 0; i < mEventAdjustedStateVars.size(); ++i) - { - if (mEventAdjustedStateVars[i]) - { - SetStateVariable(i, mEventAdjustedStateValues[i]); - mEventAdjustedStateVars[i] = false; - } - } -} - -double AbstractSbmlOdeSystem::CalculateRootFunction(double time, const std::vector& rY) -{ - return ProcessModelEvents(time, rY); -} - -bool AbstractSbmlOdeSystem::CalculateStoppingEvent(double time, const std::vector& rY) -{ - // Clear all event flags before a fresh BackwardEuler evaluation. ProcessModelEvents - // no longer resets these itself (so CVODE bisection doesn't erase a stored halving), - // so we must do it here to avoid stale flags from prior detections causing false - // positives on the initial-condition check at the start of the next Solve() call. - std::fill(mEventTriggered.begin(), mEventTriggered.end(), false); - std::fill(mEventAdjustedStateVars.begin(), mEventAdjustedStateVars.end(), false); - std::fill(mEventAdjustedParameters.begin(), mEventAdjustedParameters.end(), false); - - ProcessModelEvents(time, rY); - - // Freeze the clamp set for this Solve segment: clamp exactly those events whose trigger - // is active here (carried over from a previous segment) so CVODE reports no spurious root - // at the initial condition. ProcessModelEvents then clears each flag the instant its - // trigger first goes false, leaving the clamp stable across CVODE's in-step root - // bracketing so events localize at the true crossing rather than the step endpoint. - mEventClampActive = mEventSatisfied; - - for (unsigned i = 0; i < mEventTriggered.size(); ++i) - { - if (mEventTriggered[i]) return true; - } - return false; -} - -bool AbstractSbmlOdeSystem::HasEventOccurred(SbmlEventType eventType) -{ - for (unsigned i = 0; i < mEventTriggered.size(); ++i) - { - if (mEventTriggered[i] && mEventType[i] == eventType) - { - return true; - } - } - return false; -} - -void AbstractSbmlOdeSystem::ResetEventsOccurred() -{ - std::fill(mEventTriggered.begin(), mEventTriggered.end(), false); -} - -void AbstractSbmlOdeSystem::RunModelRules(double time, const std::vector& rY) -{ - UpdateStateVariables(time, rY); - UpdateParameters(time); - RunAssignmentRules(time); - RunReactions(time); -} diff --git a/src/AbstractSbmlOdeSystem.hpp b/src/AbstractSbmlOdeSystem.hpp deleted file mode 100644 index 7476e14..0000000 --- a/src/AbstractSbmlOdeSystem.hpp +++ /dev/null @@ -1,229 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#ifndef ABSTRACT_SBML_ODE_SYSTEM_HPP_ -#define ABSTRACT_SBML_ODE_SYSTEM_HPP_ - -#include - -#include "AbstractOdeSystem.hpp" -#include "ChasteSerialization.hpp" -#include "ClassIsAbstract.hpp" -#include "SbmlEventType.hpp" - -/** - * Abstract SBML ODE System class. - * - * Sets up variables and functions for an ODE system imported from SBML. - * - * Instances can store event state internally in the mEvent* vectors - the - * vectors may be empty if the model does not have any events defined. - */ -class AbstractSbmlOdeSystem : public AbstractOdeSystem -{ -private: - /** Needed for serialization. */ - friend class boost::serialization::access; - /** - * Archive / unarchive the AbstractSbmlOdeSystem. - * - * @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 & mNumberOfParameters; - archive & mNumberOfEvents; - archive & mEventSatisfied; - archive & mEventClampActive; - archive & mEventTriggered; - archive & mEventType; - archive & mEventAdjustedStateVars; - archive & mEventAdjustedStateValues; - archive & mEventAdjustedStatePriority; - archive & mEventAdjustedParameters; - archive & mEventAdjustedParameterValues; - archive & mEventAdjustedParameterPriority; - } - - /** - * Process the events in the model. - * - * @param time The current time - * @param rY The current state variables - * - * @return How close we are to the time of the next event - */ - virtual double ProcessModelEvents(double time, const std::vector& rY) = 0; - - /** - * Run the assignment rules to update state. - * - * @param time The current time - * @param rY The current state variables - */ - virtual void RunAssignmentRules(double time) = 0; - - /** - * Run the initial assignments to set initial state. - * - * @param time The current time - */ - virtual void RunInitialAssignments(double time) = 0; - - /** - * Run the reactions to update state. - * - * @param time The current time - * @param rY The current state variables - */ - virtual void RunReactions(double time) = 0; - - /** - * Update variable parameters from current ODE system parameter settings. - * - * @param time The current time - */ - virtual void UpdateParameters(double time) = 0; - - /** - * Update state variables from the given ODE system state. - * - * @param time The current time - * @param rStateVariables The state variables to use - */ - virtual void UpdateStateVariables(double time, const std::vector& rStateVariables) = 0; - -protected: - /** The number of parameters in the model */ - unsigned mNumberOfParameters; - - /** The number of events in the model */ - unsigned mNumberOfEvents; - - // Event handling - std::vector mEventSatisfied; - /** - * Per-event clamp flag used only by the CVODE root function. Frozen at the start of each - * Solve segment (a snapshot of mEventSatisfied in CalculateStoppingEvent) and cleared - * permanently the instant an event's trigger first goes false (the event re-arms). While - * set, the event's root distance is forced large-negative so CVODE reports no spurious - * root for a trigger already active at the initial condition. Being monotonic within a - * segment (only ever cleared, never re-set) it stays constant across CVODE's in-step root - * bracketing, so events localize at the true crossing instead of the step endpoint, while - * an event that re-arms and re-fires within the same segment is still detected. - */ - std::vector mEventClampActive; - std::vector mEventTriggered; - std::vector mEventType; - std::vector mEventAdjustedStateVars; - std::vector mEventAdjustedStateValues; - std::vector mEventAdjustedStatePriority; - std::vector mEventAdjustedParameters; - std::vector mEventAdjustedParameterValues; - std::vector mEventAdjustedParameterPriority; - - /** - * Run the equations governing the model to update state. - * - * @param time The current time - * @param rY The current state variables - */ - void RunModelRules(double time, const std::vector& rY); - -public: - /** - * Constructor. - * - * @param numberOfStateVariables The number of state variables in the model - * @param numberOfParameters The number of parameters in the model - * @param numberOfEvents The number of events in the model - */ - AbstractSbmlOdeSystem(unsigned numberOfStateVariables, unsigned numberOfParameters, unsigned numberOfEvents); - - /** - * Destructor. - */ - virtual ~AbstractSbmlOdeSystem(); - - /** - * Adjust parameters and state variables after a stopping event - * - * @param time The current time - */ - void AdjustParameters(double time); - - /** - * Calculate whether the conditions to trigger an event have been met - * (Used by CVODE solver to find exact stopping position) - * - * @param time The current time - * @param rY The current state variables - * - * @return How close we are to the root of the stopping condition - */ - double CalculateRootFunction(double time, const std::vector& rY) override; - - /** - * Calculate whether the conditions to trigger an event have been met - * - * @param time The current time - * @param rY The current state variables - * - * @return True if conditions for an event are met, false otherwise - */ - bool CalculateStoppingEvent(double time, const std::vector& rY) override; - - /** - * Check if a specific type of event has occurred. - * - * @param eventType The type of event to check - * - * @return True if the type of event has occurred, false otherwise - */ - bool HasEventOccurred(SbmlEventType eventType); - - /** - * Reset the flags that indicate which events have been triggered. - */ - void ResetEventsOccurred(); -}; - -// Register abstract class with Boost serialization -CLASS_IS_ABSTRACT(AbstractSbmlOdeSystem) - -#endif // ABSTRACT_SBML_ODE_SYSTEM_HPP_ diff --git a/src/AbstractSbmlSrnModel.cpp b/src/AbstractSbmlSrnModel.cpp deleted file mode 100644 index e08427c..0000000 --- a/src/AbstractSbmlSrnModel.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#include "AbstractOdeSrnModel.hpp" -#include "AbstractSbmlOdeSystem.hpp" -#include "CellCycleModelOdeSolver.hpp" -#include "ChasteSerialization.hpp" -#include "CvodeAdaptor.hpp" -#include "RungeKutta4IvpOdeSolver.hpp" - -#include "AbstractSbmlSrnModel.hpp" - -AbstractSbmlSrnModel::AbstractSbmlSrnModel(unsigned stateSize, boost::shared_ptr pOdeSolver) - : AbstractOdeSrnModel(stateSize, pOdeSolver) -{ - if (mpOdeSolver == boost::shared_ptr()) - { -#ifdef CHASTE_CVODE - // Default to CVODE where available - mpOdeSolver = CellCycleModelOdeSolver::Instance(); - mpOdeSolver->Initialise(); - mpOdeSolver->SetMaxSteps(10000); // Safe default - mpOdeSolver->SetTolerances(1e-6, 1e-8); - // CVODE needs to be instructed to check for stopping events - mpOdeSolver->CheckForStoppingEvents(); -#else - // Default to Chaste Runge-Kutta solver where CVODE is not available - mpOdeSolver = CellCycleModelOdeSolver::Instance(); - mpOdeSolver->Initialise(); - this->SetDt(0.0001); // Safe default -#endif // CHASTE_CVODE - } - - assert(mpOdeSolver->IsSetUp()); -} - -AbstractSbmlSrnModel::AbstractSbmlSrnModel(const AbstractSbmlSrnModel& rModel) - : AbstractOdeSrnModel(rModel) -{ - /* - * Set each member variable of the new SRN model that inherits - * its value from the parent. - * - * Note 1: some of the new SRN model's member variables - * will already have been correctly initialized in its constructor. - * - * Note 2: one or more of the new SRN model's member variables - * may be set/overwritten as soon as InitialiseDaughterCell() is called on - * the new SRN model. - * - * Note 3: Only set the variables defined in this class. Variables defined - * in parent classes will be defined there. - */ -} - -AbstractSbmlSrnModel::~AbstractSbmlSrnModel() -{ -} - -double AbstractSbmlSrnModel::GetStateVariable(const std::string& rName) -{ - assert(mpOdeSystem != nullptr); - return mpOdeSystem->GetStateVariable(rName); -} - -void AbstractSbmlSrnModel::Initialise(AbstractSbmlOdeSystem* pOdeSystem) -{ - AbstractOdeSrnModel::Initialise(pOdeSystem); -} - -void AbstractSbmlSrnModel::OutputSrnModelParameters(out_stream& rParamsFile) -{ - // No new parameters to output, so just call method on direct parent class - AbstractOdeSrnModel::OutputSrnModelParameters(rParamsFile); -} - -void AbstractSbmlSrnModel::SimulateToCurrentTime() -{ - assert(mpOdeSystem != NULL); - assert(mpCell != NULL); - - // Run the ODE simulation as needed - AbstractOdeSrnModel::SimulateToCurrentTime(); -} - -// Register class with Boost serialization -#include "SerializationExportWrapperForCpp.hpp" -CHASTE_CLASS_EXPORT(AbstractSbmlSrnModel) - -// Register the CellCycleModel classes with Boost serialization -#include "CellCycleModelOdeSolverExportWrapper.hpp" -EXPORT_CELL_CYCLE_MODEL_ODE_SOLVER(AbstractSbmlSrnModel) diff --git a/src/AbstractSbmlSrnModel.hpp b/src/AbstractSbmlSrnModel.hpp deleted file mode 100644 index db0dd18..0000000 --- a/src/AbstractSbmlSrnModel.hpp +++ /dev/null @@ -1,138 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#ifndef ABSTRACT_SBML_SRN_MODEL_HPP_ -#define ABSTRACT_SBML_SRN_MODEL_HPP_ - -#include - -#include "AbstractOdeSrnModel.hpp" -#include "AbstractSbmlOdeSystem.hpp" -#include "ChasteSerialization.hpp" -#include "ClassIsAbstract.hpp" - -/** - * A base class for SRN models generated from SBML - */ - -class AbstractSbmlSrnModel : public AbstractOdeSrnModel -{ -private: - /** Needed for serialization. */ - friend class boost::serialization::access; - /** - * Archive / unarchive the AbstractSbmlSrnModel. - * - * @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); - } - -protected: - /** - * Protected copy-constructor for use by CreateSrnModel(). - * - * The only way to copy an instance of a subclass of AbstractCellCycleModel is - * by calling CreateSrnModel(), which ensures that the instance is copied - * correctly. - * - * This copy-constructor helps subclasses of AbstractSrnModel to - * ensure that all their members are copied over correctly. It is primarily - * used during cell division to set member variables for a daughter cell. - * Note that the SRN model of the parent cell will have run ResetForDivision() - * just before calling CreateSrnModel(), so performing an exact copy of the - * parent cell's SRN model is suitable behaviour. Any further initialisation - * specific to the daughter cell can be completed via InitialiseDaughterCell(). - * - * @param rModel the SRN model to copy. - */ - AbstractSbmlSrnModel(const AbstractSbmlSrnModel& rModel); - - using AbstractOdeSrnModel::Initialise; - /** - * Overridden Initialise() method to set up the ODE system. - * - * @param pOdeSystem pointer to an ODE system - */ - void Initialise(AbstractSbmlOdeSystem* pOdeSystem); - -public: - /** - * Constructor. - * - * @param stateSize The number of state variables in the ODE system. - * @param pOdeSolver An optional pointer to a cell-cycle model ODE solver - * object (allows the use of different ODE solvers) - */ - AbstractSbmlSrnModel(unsigned stateSize, - boost::shared_ptr pOdeSolver = boost::shared_ptr()); - - /** - * Destructor. - */ - virtual ~AbstractSbmlSrnModel(); - - /** - * @return the value of a given state variable. - * - * @param rName the name of the state variable - */ - double GetStateVariable(const std::string& rName); - - /** - * Outputs SRN model parameters to file. - * - * @param rParamsFile the file stream to which the parameters are output - */ - void OutputSrnModelParameters(out_stream& rParamsFile); - - /** - * Overridden SimulateToCurrentTime() method for custom behaviour - */ - void SimulateToCurrentTime(); -}; - -// Register abstract class with Boost serialization -CLASS_IS_ABSTRACT(AbstractSbmlSrnModel) - -// Register the CellCycleModel classes with Boost serialization -#include "CellCycleModelOdeSolverExportWrapper.hpp" -EXPORT_CELL_CYCLE_MODEL_ODE_SOLVER(AbstractSbmlSrnModel) - -#endif // ABSTRACT_SBML_SRN_MODEL_HPP_ diff --git a/src/SbmlEventType.hpp b/src/SbmlEventType.hpp deleted file mode 100644 index e2b39b5..0000000 --- a/src/SbmlEventType.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef SBML_EVENT_TYPE_HPP_ -#define SBML_EVENT_TYPE_HPP_ - -enum class SbmlEventType -{ - CELL_DIVISION, - UNKNOWN -}; - -#endif // SBML_EVENT_TYPE_HPP_ diff --git a/src/SbmlMath.cpp b/src/SbmlMath.cpp deleted file mode 100644 index 4a05778..0000000 --- a/src/SbmlMath.cpp +++ /dev/null @@ -1,190 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#include - -#include "SbmlMath.hpp" - -// Arithmetic =================================== - -// divide -double sbmlmath::divide(double x, double y) -{ - return x / y; -} - -// minus -double sbmlmath::minus(double x, double y) -{ - return x - y; -} - -// plus -// times - -// Logs and exponents =========================== - -// log -double sbmlmath::log(double x) -{ - return std::log(x); -} - -double sbmlmath::log(double b, double x) -{ - return std::log(x) / std::log(b); -} - -// root -double sbmlmath::root(double n, double x) -{ - return std::pow(x, 1.0 / n); -} - -// sqr -double sbmlmath::sqr(double x) -{ - return x * x; -} - -// Logical ====================================== - -// and_ -// or_ - -// not_ -bool sbmlmath::not_(bool x) -{ - return !x; -} - -// xor_ - -// Relational =================================== - -// eq -// geq -// gt -// leq -// lt - -// neq -bool sbmlmath::neq(double x, double y) -{ - return x != y; -} - -// Trigonometry ================================= - -// cot, coth, acot, acoth -double sbmlmath::cot(double x) -{ - return 1.0 / std::tan(x); -} - -double sbmlmath::coth(double x) -{ - return 1.0 / std::tanh(x); -} - -double sbmlmath::acot(double x) -{ - return std::atan(1.0 / x); -} - -double sbmlmath::acoth(double x) -{ - return std::atanh(1.0 / x); -} - -// csc, csch, acsc, acsch -double sbmlmath::csc(double x) -{ - return 1.0 / std::sin(x); -} - -double sbmlmath::csch(double x) -{ - return 1.0 / std::sinh(x); -} - -double sbmlmath::acsc(double x) -{ - return std::asin(1.0 / x); -} - -double sbmlmath::acsch(double x) -{ - return std::asinh(1.0 / x); -} - -// sec, sech, asec, asech -double sbmlmath::sec(double x) -{ - return 1.0 / std::cos(x); -} - -double sbmlmath::sech(double x) -{ - return 1.0 / std::cosh(x); -} - -double sbmlmath::asec(double x) -{ - return std::acos(1.0 / x); -} - -double sbmlmath::asech(double x) -{ - return std::acosh(1.0 / x); -} - -// Other functions ============================== - -// factorial -double sbmlmath::factorial(double x) -{ - return std::tgamma(x + 1.0); -} - -// max -// min -// piecewise - -// quotient -double sbmlmath::quotient(double numer, double denom) -{ - return std::trunc(numer / denom); -} diff --git a/src/SbmlMath.hpp b/src/SbmlMath.hpp deleted file mode 100644 index 28422c8..0000000 --- a/src/SbmlMath.hpp +++ /dev/null @@ -1,310 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#ifndef SBML_MATH_HPP_ -#define SBML_MATH_HPP_ - -/** - * SBML math functions. - */ - -#include - -namespace sbmlmath -{ -// Constants ================================== - -// SBML Level 3 recommended avogadro value: -// https://sbml.org/documents/specifications/level-3/ -inline constexpr double AVOGADRO = 6.02214179E23; - -// Note: Avogadro value has been updated in the most recent SI Brochure. -// Bureau International des Poids et Mesures (2019): -// The International System of Units (SI), 9th edition - -// Arithmetic ================================= - -// divide -double divide(double x, double y); - -// minus -double minus(double x, double y); - -// plus -template -constexpr double plus(Args... args); - -// times -template -constexpr double times(Args... args); - -// Logs and exponents ========================= - -// log -double log(double x); -double log(double b, double x); - -// root -double root(double n, double x); - -// sqr -double sqr(double x); - -// Logical ==================================== - -// and_ -template -constexpr bool and_(Args... args); - -// or_ -template -constexpr bool or_(Args... args); - -// not_ -bool not_(bool x); - -// xor_ -template -constexpr bool xor_(Args... args); - -// Relational ================================= - -// eq -template -constexpr bool eq(double first, double second, Args... rest); - -// geq -template -constexpr bool geq(double first, double second, Args... rest); - -// gt -template -constexpr bool gt(double first, double second, Args... rest); - -// leq -template -constexpr bool leq(double first, double second, Args... rest); - -// lt -template -constexpr bool lt(double first, double second, Args... rest); - -// neq -bool neq(double x, double y); - -// Trigonometry =============================== - -// cot, coth, acot, acoth -double cot(double x); -double coth(double x); -double acot(double x); -double acoth(double x); - -// csc, csch, acsc, acsch -double csc(double x); -double csch(double x); -double acsc(double x); -double acsch(double x); - -// sec, sech, asec, asech -double sec(double x); -double sech(double x); -double asec(double x); -double asech(double x); - -// Other functions ============================ - -// factorial -double factorial(double x); - -// max -template -constexpr double max(double first, double second, Args... rest); - -// min -template -constexpr double min(double first, double second, Args... rest); - -// piecewise -constexpr double piecewise(double otherwise); - -constexpr double piecewise(double value, bool condition, double otherwise); - -template -constexpr double piecewise(double value, bool condition, Args... rest); - -// quotient -double quotient(double numer, double denom); - -} // namespace sbmlmath - -// Arithmetic (variadic templates) ============ - -// plus -template -constexpr double sbmlmath::plus(Args... args) -{ - return (0.0 + ... + args); -} - -// times -template -constexpr double sbmlmath::times(Args... args) -{ - return (1.0 * ... * args); -} - -// Logical (variadic templates) ================= - -// and_ -template -constexpr bool sbmlmath::and_(Args... args) -{ - return (true && ... && args); -} - -// or_ -template -constexpr bool sbmlmath::or_(Args... args) -{ - return (false || ... || args); -} - -// xor_ -template -constexpr bool sbmlmath::xor_(Args... args) -{ - return (false ^ ... ^ args); -} - -// Relational (variadic templates) ============== - -// eq -template -constexpr bool sbmlmath::eq(double first, double second, Args... rest) -{ - if constexpr (sizeof...(rest) > 0) - { - return (first == second) && sbmlmath::eq(second, rest...); - } - return first == second; -} - -// geq -template -constexpr bool sbmlmath::geq(double first, double second, Args... rest) -{ - if constexpr (sizeof...(rest) > 0) - { - return (first >= second) && sbmlmath::geq(second, rest...); - } - return first >= second; -} - -// gt -template -constexpr bool sbmlmath::gt(double first, double second, Args... rest) -{ - if constexpr (sizeof...(rest) > 0) - { - return (first > second) && sbmlmath::gt(second, rest...); - } - return first > second; -} - -// leq -template -constexpr bool sbmlmath::leq(double first, double second, Args... rest) -{ - if constexpr (sizeof...(rest) > 0) - { - return (first <= second) && sbmlmath::leq(second, rest...); - } - return first <= second; -} - -// lt -template -constexpr bool sbmlmath::lt(double first, double second, Args... rest) -{ - if constexpr (sizeof...(rest) > 0) - { - return (first < second) && sbmlmath::lt(second, rest...); - } - return first < second; -} - -// Other (variadic templates) =================== - -// max -template -constexpr double sbmlmath::max(double first, double second, Args... rest) -{ - if constexpr (sizeof...(rest) > 0) - { - return std::fmax(first, sbmlmath::max(second, rest...)); - } - return std::fmax(first, second); -} - -// min -template -constexpr double sbmlmath::min(double first, double second, Args... rest) -{ - if constexpr (sizeof...(rest) > 0) - { - return std::fmin(first, sbmlmath::min(second, rest...)); - } - return std::fmin(first, second); -} - -// piecewise -constexpr double sbmlmath::piecewise(double otherwise) -{ - return otherwise; -} - -constexpr double sbmlmath::piecewise(double value, bool condition, double otherwise) -{ - return condition ? value : otherwise; -} - -template -constexpr double sbmlmath::piecewise(double value, bool condition, Args... rest) -{ - return condition ? value : sbmlmath::piecewise(rest...); -} - -#endif // SBML_MATH_HPP_ diff --git a/src/fortests/SbmlTestHelpers.cpp b/src/fortests/SbmlTestHelpers.cpp deleted file mode 100644 index a01ab3f..0000000 --- a/src/fortests/SbmlTestHelpers.cpp +++ /dev/null @@ -1,143 +0,0 @@ - -#include -#include -#include -#include - -#include "AbstractOdeSystem.hpp" -#include "OdeSolution.hpp" -#include "OutputFileHandler.hpp" - -#include "SbmlTestHelpers.hpp" - -void sbmltesthelpers::ExportCsv(const std::string& rFilename, - OdeSolution& rOdeSolution, - AbstractOdeSystem& rOdeSystem, - const std::vector >* pParamsPerStep) -{ - OutputFileHandler handler(""); - out_stream file = handler.OpenOutputFile(rFilename); - - // Times - const std::vector& time_data = rOdeSolution.rGetTimes(); - - // State variables - const std::vector& svar_names = rOdeSystem.rGetStateVariableNames(); - const std::vector >& svar_data = rOdeSolution.rGetSolutions(); - - // Derived quantities - rOdeSolution.CalculateDerivedQuantitiesAndParameters(&rOdeSystem); - const std::vector& dq_names = rOdeSystem.rGetDerivedQuantityNames(); - const std::vector >& dq_data = rOdeSolution.rGetDerivedQuantities(&rOdeSystem); - - // Parameters - const std::vector& param_names = rOdeSystem.rGetParameterNames(); - const std::vector& param_data = rOdeSolution.rGetParameters(&rOdeSystem); - - // Sanity checks - if (time_data.empty()) - { - throw std::invalid_argument("OdeSolution contains no time points."); - } - - if (svar_data.empty() && dq_data.empty()) - { - throw std::invalid_argument("OdeSolution contains no state variables or derived quantities."); - } - - if (!svar_data.empty() && (svar_data.size() != time_data.size())) - { - throw std::length_error("Number of state variable data rows do not match time points."); - } - - if (!dq_data.empty() && (dq_data.size() != time_data.size())) - { - throw std::length_error("Number of derived quantity data rows do not match time points."); - } - - if ((svar_data.empty() && !svar_names.empty()) || (!svar_data.empty() && svar_data[0].size() != svar_names.size())) - { - throw std::length_error("Number of state variable names do not match data."); - } - - if ((dq_data.empty() && !dq_names.empty()) || (!dq_data.empty() && dq_data[0].size() != dq_names.size())) - { - throw std::length_error("Number of derived quantity names do not match data."); - } - - if ((param_data.empty() && !param_names.empty()) || (!param_data.empty() && param_data.size() != param_names.size())) - { - throw std::length_error("Number of parameter names do not match data."); - } - - // Write column headings - (*file) << "time"; - if (!svar_names.empty()) - { - for (unsigned i = 0; i < svar_names.size(); i++) - { - (*file) << "," << svar_names[i]; - } - } - if (!dq_names.empty()) - { - for (unsigned i = 0; i < dq_names.size(); i++) - { - (*file) << "," << dq_names[i]; - } - } - if (!param_names.empty()) - { - for (unsigned i = 0; i < param_names.size(); i++) - { - (*file) << "," << param_names[i]; - } - } - (*file) << std::endl - << std::flush; - - // Write data - for (unsigned i = 0; i < time_data.size(); i++) - { - (*file) << time_data[i]; - if (!svar_data.empty()) - { - for (unsigned j = 0; j < svar_data[i].size(); j++) - { - (*file) << "," << svar_data[i][j]; - } - } - if (!dq_data.empty()) - { - for (unsigned j = 0; j < dq_data[i].size(); j++) - { - (*file) << "," << dq_data[i][j]; - } - } - if (pParamsPerStep != nullptr && i < pParamsPerStep->size()) - { - // Time-resolved parameters (e.g. a parameter changed by an event). - for (unsigned j = 0; j < (*pParamsPerStep)[i].size(); j++) - { - (*file) << "," << (*pParamsPerStep)[i][j]; - } - } - else if (!param_data.empty()) - { - for (unsigned j = 0; j < param_data.size(); j++) - { - (*file) << "," << param_data[j]; - } - } - (*file) << std::endl - << std::flush; - } - file->close(); -} - -std::string sbmltesthelpers::ToString(double value, unsigned precision) -{ - std::ostringstream oss; - oss << std::fixed << std::setprecision(precision) << value; - return oss.str(); -} diff --git a/src/fortests/SbmlTestHelpers.hpp b/src/fortests/SbmlTestHelpers.hpp deleted file mode 100644 index 6485aae..0000000 --- a/src/fortests/SbmlTestHelpers.hpp +++ /dev/null @@ -1,195 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#ifndef SBML_TEST_HELPERS_HPP_ -#define SBML_TEST_HELPERS_HPP_ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "AbstractOdeSystem.hpp" -#include "OdeSolution.hpp" - -namespace sbmltesthelpers -{ -/** Export results to a CSV file. - * - * The first column is time, followed by the state variables, derived quantities and - * parameters. - * - * @param rFilename The name of the file to create. - * @param rOdeSolution The OdeSolution containing the results. - * @param rOdeSystem The ODE system (for variable names and derived quantities). - * @param pParamsPerStep Optional time-resolved parameter values (one row per time step, - * as recorded by SbmlTestOdeSolution). When null the parameters are written from the - * system's single current snapshot, repeated on every row. - */ -void ExportCsv(const std::string& rFilename, - OdeSolution& rOdeSolution, - AbstractOdeSystem& rOdeSystem, - const std::vector >* pParamsPerStep = nullptr); - -/** Calculate the maximum of a vector of doubles. - * @param vec The vector of doubles. - * @return The maximum value. - */ -inline double Max(const std::vector& vec); - -/** Calculate the mean of a vector of doubles. - * @param vec The vector of doubles. - * @return The mean value. - */ -inline double Mean(const std::vector& vec); - -/** Calculate the minimum of a vector of doubles. - * @param vec The vector of doubles. - * @return The minimum value. - */ -inline double Min(const std::vector& vec); - -/** Calculate the standard deviation of a vector of doubles. - * @param vec The vector of doubles. - * @return The standard deviation value. - */ -inline double Stdev(const std::vector& vec); - -/** Convert a double to a string. - * @param value The double value. - * @return The string representation. - */ -std::string ToString(double value, unsigned precision = 9); - -/** Calculate the qth quantile of a vector of doubles. - * @param vec The vector of doubles, assumed to be sorted. - * @param q The quantile to calculate (between 0 and 1). - * @return The qth quantile value. - */ -inline double Quantile(const std::vector& vec, double q); - -/** Calculate the variance of a vector of doubles. - * @param vec The vector of doubles. - * @return The variance value. - */ -inline double Variance(const std::vector& vec); -} // namespace sbmltesthelpers - -// max -inline double sbmltesthelpers::Max(const std::vector& vec) -{ - if (vec.empty()) - { - throw std::invalid_argument("Cannot calculate maximum of an empty vector."); - } - return *std::max_element(vec.begin(), vec.end()); -} - -// mean -inline double sbmltesthelpers::Mean(const std::vector& vec) -{ - if (vec.empty()) - { - throw std::invalid_argument("Cannot calculate mean of an empty vector."); - } - return std::accumulate(vec.begin(), vec.end(), 0.0) / vec.size(); -} - -// min -inline double sbmltesthelpers::Min(const std::vector& vec) -{ - if (vec.empty()) - { - throw std::invalid_argument("Cannot calculate minimum of an empty vector."); - } - return *std::min_element(vec.begin(), vec.end()); -} - -// stdev -inline double sbmltesthelpers::Stdev(const std::vector& vec) -{ - return std::sqrt(sbmltesthelpers::Variance(vec)); -} - -// quantile -inline double sbmltesthelpers::Quantile(const std::vector& vec, double q) -{ - if (vec.empty()) - { - throw std::invalid_argument("Cannot calculate quantile of an empty vector."); - } - - if (q < 0.0 || q > 1.0) - { - throw std::out_of_range("Quantile must be between 0.0 and 1.0"); - } - - std::vector sorted_vec = vec; - std::sort(sorted_vec.begin(), sorted_vec.end()); - - size_t n = sorted_vec.size(); - size_t index = static_cast(q * (n - 1)); - - // Even number of elements - if (n % 2 == 0) - { - return (sorted_vec[index] + sorted_vec[index - 1]) / 2.0; - } - - // Odd number of elements - return sorted_vec[index]; -} - -// variance -inline double sbmltesthelpers::Variance(const std::vector& vec) -{ - if (vec.size() < 2) - { - throw std::invalid_argument("Variance requires at least two data points."); - } - double mean_val = sbmltesthelpers::Mean(vec); - double accum = 0.0; - for (double val : vec) - { - accum += (val - mean_val) * (val - mean_val); - } - return accum / (vec.size() - 1); -} - -#endif // SBML_TEST_HELPERS_HPP_ diff --git a/src/fortests/SbmlTestOdeSolution.cpp b/src/fortests/SbmlTestOdeSolution.cpp deleted file mode 100644 index 7162743..0000000 --- a/src/fortests/SbmlTestOdeSolution.cpp +++ /dev/null @@ -1,97 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#include "SbmlTestOdeSolution.hpp" - -void SbmlTestOdeSolution::RecordPoint(double time, const std::vector& rY, AbstractOdeSystem* pSystem) -{ - if (rGetTimes().empty()) - { - SetOdeSystemInformation(pSystem->GetSystemInformation()); - } - - // Snapshot the system's current parameters for this point. - std::vector params(pSystem->GetNumberOfParameters()); - for (unsigned i = 0; i < params.size(); ++i) - { - params[i] = pSystem->GetParameter(i); - } - - rGetTimes().push_back(time); - rGetSolutions().push_back(rY); - mParametersPerStep.push_back(params); - - SetNumberOfTimeSteps(rGetTimes().size()); -} - -std::vector SbmlTestOdeSolution::GetParameterSeries(const std::string& rName, AbstractOdeSystem* pSystem) const -{ - unsigned index = pSystem->GetParameterIndex(rName); - std::vector series(mParametersPerStep.size()); - for (unsigned i = 0; i < series.size(); ++i) - { - series[i] = mParametersPerStep[i][index]; - } - return series; -} - -std::vector SbmlTestOdeSolution::GetDerivedQuantitySeries(const std::string& rName, AbstractOdeSystem* pSystem) const -{ - unsigned index = pSystem->GetSystemInformation()->GetDerivedQuantityIndex(rName); - - // Save the system's current parameters so they can be restored afterwards. - std::vector saved(pSystem->GetNumberOfParameters()); - for (unsigned p = 0; p < saved.size(); ++p) - { - saved[p] = pSystem->GetParameter(p); - } - - std::vector series(rGetTimes().size()); - for (unsigned i = 0; i < series.size(); ++i) - { - // Restore this step's parameters so parameter-dependent derived quantities are time-resolved. - for (unsigned p = 0; p < mParametersPerStep[i].size(); ++p) - { - pSystem->SetParameter(p, mParametersPerStep[i][p]); - } - series[i] = pSystem->ComputeDerivedQuantities(rGetTimes()[i], rGetSolutions()[i])[index]; - } - - for (unsigned p = 0; p < saved.size(); ++p) - { - pSystem->SetParameter(p, saved[p]); - } - return series; -} diff --git a/src/fortests/SbmlTestOdeSolution.hpp b/src/fortests/SbmlTestOdeSolution.hpp deleted file mode 100644 index ba1b8e7..0000000 --- a/src/fortests/SbmlTestOdeSolution.hpp +++ /dev/null @@ -1,104 +0,0 @@ -/* - -Copyright (c) 2005-2025, 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. - -*/ - -#ifndef SBML_TEST_ODE_SOLUTION_HPP_ -#define SBML_TEST_ODE_SOLUTION_HPP_ - -#include -#include - -#include "AbstractOdeSystem.hpp" -#include "OdeSolution.hpp" - -/** - * An OdeSolution that additionally records the model parameters at each time step. - * - * Chaste's OdeSolution stores only a single parameter snapshot - GetVariableAtIndex - * returns mParameters[i] with no time index, and the header notes it "assumes that - * mParameters is constant through time". A parameter changed by an event (e.g. a - * compartment resized by an event assignment) is therefore reported with its final - * value at every time step. - * - * An SBML model is solved one sample-grid point at a time, stopping at each event; the - * parameters are constant between events and change (via AdjustParameters) at them. - * RecordPoint stores the system's current parameters alongside each grid point, so - * GetParameterSeries can return a genuinely time-resolved series. - */ -class SbmlTestOdeSolution : public OdeSolution -{ -private: - /** Parameter values for each stored time step. */ - std::vector > mParametersPerStep; - -public: - /** - * Append a single solution point, recording the system's current parameter values for it. - * Building the solution one grid point at a time (rather than one segment at a time) keeps - * the output on the requested sample grid even when an event fires between grid points. - * - * @param time the time of the point - * @param rY the state variables at this point - * @param pSystem the ODE system, queried for its current parameter values - */ - void RecordPoint(double time, const std::vector& rY, AbstractOdeSystem* pSystem); - - /** - * @param rName the parameter name - * @param pSystem the ODE system, used to resolve the parameter's index - * @return the parameter's value at each stored time step - */ - std::vector GetParameterSeries(const std::string& rName, AbstractOdeSystem* pSystem) const; - - /** - * Evaluate a derived quantity at every stored step with that step's recorded parameters - * restored first. A derived quantity that depends on an event-modified parameter (e.g. the - * amount conversion amt__X = X * compartment for a boundary species X changed by an event) - * is otherwise computed with the parameter's final value at every point, losing the time - * resolution. Restoring the per-step parameters fixes this. - * - * @param rName the derived quantity name - * @param pSystem the ODE system, used to compute the derived quantities - * @return the derived quantity's value at each stored time step - */ - std::vector GetDerivedQuantitySeries(const std::string& rName, AbstractOdeSystem* pSystem) const; - - /** @return the parameter values recorded for each stored time step. */ - const std::vector >& rGetParametersPerStep() const - { - return mParametersPerStep; - } -}; - -#endif // SBML_TEST_ODE_SOLUTION_HPP_ From 202d8cd59ad344600d637d254d1a691114f6af07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 13:38:39 +0000 Subject: [PATCH 51/51] Fix review comments in workflow and manifest --- .github/workflows/test-force-example.yml | 4 ++-- src/py/MANIFEST.in | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-force-example.yml b/.github/workflows/test-force-example.yml index 037f842..c4687c9 100644 --- a/.github/workflows/test-force-example.yml +++ b/.github/workflows/test-force-example.yml @@ -37,8 +37,8 @@ jobs: run: | cp -r . ${{ env.PROJECT_DIR }} cd ${{ env.PROJECT_DIR }} - # Yes to cell_based component and Python bindings, no to SBML, defaults otherwise, yes to confirm - printf '\ny\n\n\n\ny\nn\ny\n' | python3 setup_project.py + # 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: | diff --git a/src/py/MANIFEST.in b/src/py/MANIFEST.in index d0cf51e..8dc9c7f 100644 --- a/src/py/MANIFEST.in +++ b/src/py/MANIFEST.in @@ -1 +1 @@ -recursive-include template_project/ *.so +recursive-include template_project *.so