diff --git a/.github/workflows/build_binaries.yml b/.github/workflows/build_binaries.yml index cd0e5b943..0186d20e2 100644 --- a/.github/workflows/build_binaries.yml +++ b/.github/workflows/build_binaries.yml @@ -12,10 +12,10 @@ jobs: strategy: matrix: projects: [ - {short_name: "physiboss-tutorial", project: "physiboss-tutorial", name: "PhysiBoSS tutorial", binary: "project", extra_run: "make Compile_MaBoSS PHYSICELL_CPP=g++-11"}, - {short_name: "physiboss-tutorial-invasion", project: "physiboss-tutorial-invasion", name: "PhysiBoSS Cancer Invasion", binary: "invasion_model", extra_run: "make Compile_MaBoSS PHYSICELL_CPP=g++-11"}, - {short_name: "physiboss-cell-lines", project: "physiboss-cell-lines-sample", name: "PhysiBoSS Cell Lines", binary: "PhysiBoSS_Cell_Lines", extra_run: "make Compile_MaBoSS PHYSICELL_CPP=g++-11"}, - {short_name: "template_BM", project: "template_BM", name: "PhysiBoSS Template", binary: "project", extra_run: "make Compile_MaBoSS PHYSICELL_CPP=g++-11"}, + {short_name: "physiboss-tutorial", project: "physiboss-tutorial", name: "PhysiBoSS tutorial", binary: "project", extra_run: "make Compile_MaBoSS PHYSICELL_CPP=g++-12"}, + {short_name: "physiboss-tutorial-invasion", project: "physiboss-tutorial-invasion", name: "PhysiBoSS Cancer Invasion", binary: "invasion_model", extra_run: "make Compile_MaBoSS PHYSICELL_CPP=g++-12"}, + {short_name: "physiboss-cell-lines", project: "physiboss-cell-lines-sample", name: "PhysiBoSS Cell Lines", binary: "PhysiBoSS_Cell_Lines", extra_run: "make Compile_MaBoSS PHYSICELL_CPP=g++-12"}, + {short_name: "template_BM", project: "template_BM", name: "PhysiBoSS Template", binary: "project", extra_run: "make Compile_MaBoSS PHYSICELL_CPP=g++-12"}, {short_name: "template", project: "template", name: "PhysiCell Template", binary: "project", extra_run: ""}, {short_name: "rules", project: "rules-sample", name: "PhysiCell Rules", binary: "project", extra_run: ""}, {short_name: "physimess", project: "physimess-sample", name: "PhysiMeSS", binary: "project", extra_run: ""}, @@ -23,7 +23,7 @@ jobs: {short_name: "episode", project: "episode-sample", name: "PhysiCell Episode", binary: "project", extra_run: ""}, ] - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -127,17 +127,17 @@ jobs: {short_name: "episode", project: "episode-sample", name: "PhysiCell Episode", binary: "project", extra_run: ""}, ] - runs-on: macos-13 + runs-on: macos-15-intel steps: - uses: actions/checkout@v4 - name: Install dependencies run : | - if ! brew list gcc@13 &>/dev/null; then - brew install gcc@13 + if ! brew list gcc@14 &>/dev/null; then + brew install gcc@14 else - echo "gcc@13 is already installed." + echo "gcc@14 is already installed." fi - name: Build ${{ matrix.projects.name }} project @@ -146,15 +146,15 @@ jobs: make ${{ matrix.projects.project }} make clean ${{ matrix.projects.extra_run }} - make PHYSICELL_CPP=g++-13 static - cp ${{ matrix.projects.binary }} ${{ matrix.projects.binary }}_macos13 + make PHYSICELL_CPP=g++-14 static + cp ${{ matrix.projects.binary }} ${{ matrix.projects.short_name }}_macos_intel - name: Caching produced project binary uses: actions/cache@v4 with: path: | - ${{ github.workspace }}/${{ matrix.projects.binary }}_macos13 - key: ${{ runner.os }}-macos13-${{ github.run_id }} + ${{ github.workspace }}/${{ matrix.projects.short_name }}_macos_intel + key: ${{ runner.os }}-macos-intel-${{ github.run_id }} - name: Look at the generated binary run: | @@ -184,10 +184,10 @@ jobs: - name: Install dependencies run : | - if ! brew list gcc@13 &>/dev/null; then - brew install gcc@13 + if ! brew list gcc@14 &>/dev/null; then + brew install gcc@14 else - echo "gcc@13 is already installed." + echo "gcc@14 is already installed." fi - name: Build ${{ matrix.projects.name }} project @@ -196,15 +196,15 @@ jobs: make ${{ matrix.projects.project }} make clean ${{ matrix.projects.extra_run }} - make PHYSICELL_CPP=g++-13 static - cp ${{ matrix.projects.binary }} ${{ matrix.projects.binary }}_macosm1 + make PHYSICELL_CPP=g++-14 static + cp ${{ matrix.projects.binary }} ${{ matrix.projects.short_name }}_macos_arm - name: Caching produced project binary uses: actions/cache@v4 with: path: | - ${{ github.workspace }}/${{ matrix.projects.binary }}_macosm1 - key: ${{ runner.os }}-macosm1-${{ github.run_id }} + ${{ github.workspace }}/${{ matrix.projects.short_name }}_macos_arm + key: ${{ runner.os }}-macos-arm-${{ github.run_id }} - name: Look at the generated binary run: | @@ -227,7 +227,7 @@ jobs: {short_name: "episode", project: "episode-sample", name: "PhysiCell Episode", binary: "project", extra_run: ""}, ] - runs-on: macos-13 + runs-on: macos-15-intel needs: [macos_step0a, macos_step0b] steps: @@ -237,20 +237,19 @@ jobs: uses: actions/cache@v4 with: path: | - ${{ github.workspace }}/${{ matrix.projects.binary }}_macosm1 - key: ${{ runner.os }}-macosm1-${{ github.run_id }} + ${{ github.workspace }}/${{ matrix.projects.short_name }}_macos_arm + key: ${{ runner.os }}-macos-arm-${{ github.run_id }} - name: Caching produced project binary uses: actions/cache@v4 with: path: | - ${{ github.workspace }}/${{ matrix.projects.binary }}_macos13 - key: ${{ runner.os }}-macos13-${{ github.run_id }} + ${{ github.workspace }}/${{ matrix.projects.short_name }}_macos_intel + key: ${{ runner.os }}-macos-intel-${{ github.run_id }} - name: Creating universal binary run: | - lipo -create -output ${{ matrix.projects.binary }} ${{ matrix.projects.binary }}_macos13 ${{ matrix.projects.binary }}_macosm1 - + lipo -create -output ${{ matrix.projects.binary }} ${{ matrix.projects.short_name }}_macos_arm ${{ matrix.projects.short_name }}_macos_intel - name: Checking universal binary run: | lipo -archs ${{ matrix.projects.binary }} diff --git a/.github/workflows/build_binaries_cmake.yml b/.github/workflows/build_binaries_cmake.yml new file mode 100644 index 000000000..b124c78ab --- /dev/null +++ b/.github/workflows/build_binaries_cmake.yml @@ -0,0 +1,77 @@ +# This starter workflow is for a CMake project running on multiple platforms. There is a different starter workflow if you just want a single platform. +# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-single-platform.yml +name: Build binaries CMake + +on: + push: + pull_request: + release: + types: [published] + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. + fail-fast: false + + # Set up a matrix to run the following 3 configurations: + # 1. + # 2. + # 3. + # + # To add more build types (Release, Debug, RelWithDebInfo, etc.) customize the build_type list. + matrix: + include: + - os: windows-latest + c_compiler: cl + cpp_compiler: cl + build_physiboss: "ON" + - os: ubuntu-latest + c_compiler: gcc + cpp_compiler: g++ + build_physiboss: "ON" + - os: ubuntu-latest + c_compiler: clang + cpp_compiler: clang++ + build_physiboss: "ON" + - os: macos-latest + c_compiler: gcc-15 + cpp_compiler: g++-15 + build_physiboss: "ON" + # - os: macos-latest + # c_compiler: cc + # cpp_compiler: c++ + # build_physiboss: "ON" + + steps: + - uses: actions/checkout@v4 + + - name: Set reusable strings + # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. + id: strings + shell: bash + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + + - name: Install dependencies (Linux) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libomp-dev + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: > + cmake -B ${{ steps.strings.outputs.build-output-dir }} + -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} + -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} + -DCMAKE_BUILD_TYPE=Release + -DPHYSICELL_BUILD_PHYSIBOSS=${{ matrix.build_physiboss }} + -S ${{ github.workspace }} + + - name: Build + # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config Release diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index badff60f1..8bc74eb6c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,8 +13,8 @@ jobs: matrix: os: [ {name: "Ubuntu", os: "ubuntu-latest", shell: "bash", compiler: "g++"}, - {name: "MacOS 13", os: "macos-13", shell: "bash", compiler: "g++-12"}, - {name: "MacOS 14 (M1)", os: "macos-14", shell: "bash", compiler: "g++-13"}, + {name: "MacOS 15", os: "macos-15-intel", shell: "bash", compiler: "g++-14"}, + {name: "MacOS 14 (M1)", os: "macos-14", shell: "bash", compiler: "g++-14"}, {name: "Windows", os: "windows-latest", shell: "msys2", compiler: "g++"}, ] projects: [ @@ -24,14 +24,15 @@ jobs: {project: "physimess-sample", name: "PhysiMeSS Sample", binary: "project", config: "config/Fibre_Degradation/mymodel_matrix_degradation.xml", max_time: 120, output_folder: ""}, {project: "physiboss-tutorial", name: "PhysiBoSS Tutorial", binary: "project", config: "config/cell_cycle/PhysiCell_settings.xml", max_time: 300, output_folder: "output"}, {project: "worm-sample", name: "PhysiCell worm", binary: "worm", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: "output"}, - {project: "virus-macrophage-sample", name: "Virus Macrophage", binary: "virus-sample", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: "output"}, - {project: "mechano-sample", name: "PhysiCell Mechano", binary: "project", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: "output"}, + {project: "virus-macrophage-sample", name: "Virus Macrophage", binary: "virus-sample", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, + {project: "mechano-sample", name: "PhysiCell Mechano", binary: "project", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, {project: "cancer-biorobots-sample", name: "PhysiCell Cancer Biorobots", binary: "cancer_biorobots", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, {project: "biorobots-sample", name: "PhysiCell Biorobots", binary: "biorobots", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, {project: "celltypes3-sample", name: "PhysiCell Celltypes3", binary: "celltypes3", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, {project: "custom-division-sample", name: "PhysiCell custom division", binary: "project", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, {project: "interaction-sample", name: "PhysiCell interactions", binary: "interaction_demo", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, {project: "pred-prey-farmer", name: "PhysiCell prey predator", binary: "pred_prey", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, + {project: "dfba_unit_test", name: "PhysiCelldFBA Unit Test", binary: "dfba-unit-test", config: "config/Unit_test_mass_conversion.xml", max_time: 20, output_folder: ""}, {project: "rules-sample", name: "PhysiCell rules sample", binary: "rules_sample", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, {project: "asymmetric-division-sample", name: "PhysiCell asymmetric division", binary: "project", config: "config/PhysiCell_settings.xml", max_time: 120, output_folder: ""}, {project: "episode-sample", name: "PhysiCell Episode", binary: "project", max_time: 120, config: "config/PhysiCell_settings.xml", output_folder: ""}, @@ -48,11 +49,18 @@ jobs: steps: - uses: actions/checkout@v4 + - if: matrix.os.name == 'Ubuntu' + run: sudo apt-get update && sudo apt-get install -y liblapack-dev libbz2-dev + + - if: runner.os == 'macOS' + run: brew install lapack bzip2 + + - if: matrix.os.name == 'Windows' uses: msys2/setup-msys2@v2 with: update: true - install: mingw-w64-x86_64-binutils mingw-w64-x86_64-gcc mingw-w64-x86_64-headers-git mingw-w64-x86_64-gcc-libs mingw-w64-x86_64-libwinpthread-git mingw-w64-x86_64-lapack mingw-w64-x86_64-openblas mingw-w64-x86_64-libxml2 mingw-w64-x86_64-bzip2 mingw-w64-x86_64-python mingw-w64-x86_64-python-zstandard mingw-w64-x86_64-python-cffi make bison flex mingw-w64-x86_64-ca-certificates mingw-w64-x86_64-diffutils + install: mingw-w64-x86_64-binutils mingw-w64-x86_64-gcc mingw-w64-x86_64-headers-git mingw-w64-x86_64-gcc-libs mingw-w64-x86_64-libwinpthread-git mingw-w64-x86_64-lapack mingw-w64-x86_64-openblas mingw-w64-x86_64-libxml2 mingw-w64-x86_64-bzip2 mingw-w64-x86_64-python mingw-w64-x86_64-python-zstandard mingw-w64-x86_64-python-cffi make bison flex mingw-w64-x86_64-ca-certificates diffutils - name: Build ${{ matrix.projects.name }} project run: | diff --git a/.github/workflows/tests_cmake.yml b/.github/workflows/tests_cmake.yml new file mode 100644 index 000000000..24105183f --- /dev/null +++ b/.github/workflows/tests_cmake.yml @@ -0,0 +1,87 @@ +name: Tests CMake + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + tests: + name: Testing ${{ matrix.projects.name }} on ${{ matrix.platform.os }} + runs-on: ${{ matrix.platform.os }} + + strategy: + # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. + fail-fast: false + + # Set up a matrix to run cross-product of projects and OS/compiler combinations + matrix: + projects: [ + {"use_physiboss": false, cwd: "build/sample_projects/template", name: "PhysiCell Template", binary: "template", max_time: 120, config: "config/PhysiCell_settings.xml", validation_output_folder: "output_template"}, + {"use_physiboss": true, cwd: "build/sample_projects_intracellular/boolean/template_BM", name: "PhysiBoSS Template", binary: "template_BM", max_time: 120, config: "config/PhysiCell_settings.xml", validation_output_folder: "output_template_BM"}, + {"use_physiboss": true, cwd: "build/sample_projects_intracellular/boolean/physiboss_cell_lines", name: "PhysiBoSS Cell Lines", binary: "physiboss_cell_lines", max_time: 120, config: "config/PhysiCell_settings.xml", validation_output_folder: "output_physiboss-cell-lines-sample"}, + {"use_physiboss": false, cwd: "build/sample_projects/physimess", name: "PhysiMeSS Sample", binary: "physimess", config: "config/Fibre_Degradation/mymodel_matrix_degradation.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": true, cwd: "build/sample_projects_intracellular/boolean/tutorial", name: "PhysiBoSS Tutorial", binary: "tutorial", config: "config/cell_cycle/PhysiCell_settings.xml", max_time: 300, validation_output_folder: "output_physiboss-tutorial"}, + {"use_physiboss": false, cwd: "build/sample_projects/worm", name: "PhysiCell worm", binary: "worm", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: "output_worm-sample"}, + {"use_physiboss": false, cwd: "build/sample_projects/virus_macrophage", name: "Virus Macrophage", binary: "virus_macrophage", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/mechano", name: "PhysiCell Mechano", binary: "mechano", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/cancer_biorobots", name: "PhysiCell Cancer Biorobots", binary: "cancer_biorobots", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/biorobots", name: "PhysiCell Biorobots", binary: "biorobots", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/celltypes3", name: "PhysiCell Celltypes3", binary: "celltypes3", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/custom_division", name: "PhysiCell custom division", binary: "custom_division", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/interactions", name: "PhysiCell interactions", binary: "interactions", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/pred_prey_farmer", name: "PhysiCell prey predator", binary: "pred_prey_farmer", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/rules_sample", name: "PhysiCell rules sample", binary: "rules_sample", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/asymmetric_division", name: "PhysiCell asymmetric division", binary: "asymmetric_division", config: "config/PhysiCell_settings.xml", max_time: 120, validation_output_folder: ""}, + {"use_physiboss": false, cwd: "build/sample_projects/episode", name: "PhysiCell Episode", binary: "episode", max_time: 120, config: "config/PhysiCell_settings.xml", validation_output_folder: ""}, + ] + platform: [ + {"os": "windows-latest", "c_compiler": "cl", "cpp_compiler": "cl", "build_physiboss": "ON"}, + {"os": "ubuntu-latest", "c_compiler": "gcc", "cpp_compiler": "g++", "build_physiboss": "ON"}, + {"os": "ubuntu-latest", "c_compiler": "clang", "cpp_compiler": "clang++", "build_physiboss": "ON"}, + {"os": "macos-latest", "c_compiler": "gcc-15", "cpp_compiler": "g++-15", "build_physiboss": "ON"}, + # {"os": "macos-latest", "c_compiler": "cc", "cpp_compiler": "c++", "build_physiboss": "ON"}, + ] + + steps: + - uses: actions/checkout@v4 + + - name: Set reusable strings + # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. + id: strings + shell: bash + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + + - name: Install dependencies (Linux) + if: matrix.platform.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libomp-dev + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: > + cmake -B ${{ steps.strings.outputs.build-output-dir }} + -DCMAKE_CXX_COMPILER=${{ matrix.platform.cpp_compiler }} + -DCMAKE_C_COMPILER=${{ matrix.platform.c_compiler }} + -DCMAKE_BUILD_TYPE=Release + -DPHYSICELL_BUILD_PHYSIBOSS=${{ matrix.platform.build_physiboss }} + -S ${{ github.workspace }} + + - name: Build + # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config Release --target ${{ matrix.projects.binary }} + + - name: Run ${{ matrix.projects.name }} project + run: | + python ${{ github.workspace }}/beta/test_run_sample.py ${{ matrix.platform.os == 'windows-latest' && 'Release/' || '' }}${{ matrix.projects.binary }} ${{ matrix.projects.config }} ${{ matrix.projects.max_time }} + working-directory: ${{ matrix.projects.cwd }} + + - name: Check ${{ matrix.projects.name }} project simulation results + if: matrix.projects.validation_output_folder != '' + run: | + python ${{ github.workspace }}/beta/test_diff_svg.py output ${{ github.workspace }}/tests/cases/${{ matrix.projects.validation_output_folder }} + working-directory: ${{ matrix.projects.cwd }} + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 765ccdc8c..0a3b0a27f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,85 @@ *.asv ._.DS_Store .DS_Store -*.exe *.o addons/libRoadrunner/roadrunner/ addons/libRoadrunner/roadrunner-win64-vs14-cp35m.zip addons/PhysiBoSS/MaBoSS/ addons/PhysiBoSS/libMaBoSS-*.tar.gz -biorobots -cancer_immune_3D +build/ config/PhysiCell_settings-backup.xml heterogeneity initial.svg -interaction_demo Makefile-backup + +*.exe + +addons/dFBA/ext/* +addons/dFBA/test/*.o +addons/dFBA/test/test_* + +venv/ +.venv pmb_debug.log -project studio_debug.log user_projects/* !user_projects/empty.txt Studio.zip /studio + +output/*.svg +output/*.xml +output/*.mat +output/*.txt +output/cells.csv + +**/.ipynb_checkpoints/ + +# executables +/project +/biorobots +/cancer_biorobots +/cancer_immune_3D +/celltypes3 +/heterogeneity +/interaction_demo +/pred_prey +/virus-sample +/worm +/*.exe + +# generated header-dependency files (-MMD) +*.d + +# boolean intracellular project executables +/invasion_model +/PhysiBoSS_Cell_Lines + +# fba intracellular project executables +/cancer_metabolism +/ecoli-dfba + +# ode intracellular project executables +/ode_energy + +# test project executables +/time_tests +/unit_tests +/test_custom_DCs +/test_voxel_values +__pycache__/ +*.py[cod] + +# dFBA standalone test binaries and generated artifacts +addons/dFBA/test/test_sbml +addons/dFBA/test/test_clp +addons/dFBA/test/test_fba +addons/dFBA/test/test_pfba +addons/dFBA/test/bench_solve +addons/dFBA/test/bench_pfba +addons/dFBA/test/bench_warmstart +addons/dFBA/test/*.o +addons/dFBA/test/pfba_dump.tsv + +# compiler-generated header dependency files (-MMD) +*.d diff --git a/BioFVM/BioFVM_microenvironment.cpp b/BioFVM/BioFVM_microenvironment.cpp index e18c9ff43..84c975ed6 100644 --- a/BioFVM/BioFVM_microenvironment.cpp +++ b/BioFVM/BioFVM_microenvironment.cpp @@ -47,6 +47,7 @@ */ #include "BioFVM_microenvironment.h" +#include "BioFVM_utilities.h" #include "BioFVM_solvers.h" #include "BioFVM_vector.h" #include @@ -272,12 +273,12 @@ void Microenvironment::apply_dirichlet_conditions( void ) { /* #pragma omp parallel for - for( unsigned int i=0 ; i < dirichlet_indices.size() ; i++ ) + for( long long i=0 ; i < dirichlet_indices.size() ; i++ ) { density_vector( dirichlet_indices[i] ) = dirichlet_value_vectors[i]; } */ // #pragma omp parallel for - for( unsigned int i=0 ; i < mesh.voxels.size() ;i++ ) + for( long long i=0 ; i < mesh.voxels.size() ;i++ ) { /* if( mesh.voxels[i].is_Dirichlet == true ) @@ -740,7 +741,7 @@ void Microenvironment::simulate_bulk_sources_and_sinks( double dt ) } #pragma omp parallel for - for( unsigned int i=0; i < mesh.voxels.size() ; i++ ) + for( long long i=0; i < mesh.voxels.size() ; i++ ) { bulk_supply_rate_function( this,i, &bulk_source_sink_solver_temp1[i] ); // temp1 = S bulk_supply_target_densities_function( this,i, &bulk_source_sink_solver_temp2[i]); // temp2 = T @@ -762,7 +763,7 @@ void Microenvironment::simulate_bulk_sources_and_sinks( double dt ) void Microenvironment::simulate_cell_sources_and_sinks( std::vector& basic_agent_list , double dt ) { #pragma omp parallel for - for( unsigned int i=0 ; i < basic_agent_list.size() ; i++ ) + for( long long i=0 ; i < basic_agent_list.size() ; i++ ) { basic_agent_list[i]->simulate_secretion_and_uptake( this , dt ); } @@ -787,7 +788,7 @@ void Microenvironment::update_rates( void ) { uptake_rates.assign( number_of_voxels() , zero ); } #pragma omp parallel for - for( unsigned int i=0 ; i < number_of_voxels() ; i++ ) + for( long long i=0 ; i < number_of_voxels() ; i++ ) { bulk_uptake_rate_function( this,i, &(uptake_rates[i]) ); bulk_supply_rate_function( this,i, &(supply_rates[i]) ); @@ -858,7 +859,7 @@ void Microenvironment::compute_all_gradient_vectors( void ) } #pragma omp parallel for - for( unsigned int k=0; k < mesh.z_coordinates.size() ; k++ ) + for( long long k=0; k < mesh.z_coordinates.size() ; k++ ) { for( unsigned int j=0; j < mesh.y_coordinates.size() ; j++ ) { @@ -904,7 +905,7 @@ void Microenvironment::compute_all_gradient_vectors( void ) } #pragma omp parallel for - for( unsigned int k=0; k < mesh.z_coordinates.size() ; k++ ) + for( long long k=0; k < mesh.z_coordinates.size() ; k++ ) { for( unsigned int i=0; i < mesh.x_coordinates.size() ; i++ ) { @@ -953,7 +954,7 @@ void Microenvironment::compute_all_gradient_vectors( void ) { return; } #pragma omp parallel for - for( unsigned int j=0; j < mesh.y_coordinates.size() ; j++ ) + for( long long j=0; j < mesh.y_coordinates.size() ; j++ ) { for( unsigned int i=0; i < mesh.x_coordinates.size() ; i++ ) { @@ -1467,6 +1468,7 @@ void load_initial_conditions_from_csv(std::string filename) // determine if header row exists std::string line; std::getline( file , line ); + trim_cr(line); char c = line.c_str()[0]; std::vector substrate_indices; bool header_provided = false; @@ -1515,6 +1517,7 @@ void load_initial_conditions_from_csv(std::string filename) file.close(); std::ifstream file(filename, std::ios::in); std::getline(file, line); + trim_cr(line); } std::cout << "Loading substrate initial conditions from CSV file " << filename << " ... " << std::endl; @@ -1522,6 +1525,7 @@ void load_initial_conditions_from_csv(std::string filename) while (std::getline(file, line)) { + trim_cr(line); get_row_from_substrate_initial_condition_csv(voxel_set, line, substrate_indices, header_provided); } diff --git a/BioFVM/BioFVM_solvers.cpp b/BioFVM/BioFVM_solvers.cpp index be2457be2..58fad982a 100644 --- a/BioFVM/BioFVM_solvers.cpp +++ b/BioFVM/BioFVM_solvers.cpp @@ -195,7 +195,7 @@ void diffusion_decay_solver__constant_coefficients_LOD_3D( Microenvironment& M, M.apply_dirichlet_conditions(); #pragma omp parallel for - for( unsigned int k=0; k < M.mesh.z_coordinates.size() ; k++ ) + for( long long k=0; k < M.mesh.z_coordinates.size() ; k++ ) { for( unsigned int j=0; j < M.mesh.y_coordinates.size() ; j++ ) { @@ -225,7 +225,7 @@ void diffusion_decay_solver__constant_coefficients_LOD_3D( Microenvironment& M, M.apply_dirichlet_conditions(); #pragma omp parallel for - for( unsigned int k=0; k < M.mesh.z_coordinates.size() ; k++ ) + for( long long k=0; k < M.mesh.z_coordinates.size() ; k++ ) { for( unsigned int i=0; i < M.mesh.x_coordinates.size() ; i++ ) { @@ -259,7 +259,7 @@ void diffusion_decay_solver__constant_coefficients_LOD_3D( Microenvironment& M, M.apply_dirichlet_conditions(); #pragma omp parallel for - for( unsigned int j=0; j < M.mesh.y_coordinates.size() ; j++ ) + for( long long j=0; j < M.mesh.y_coordinates.size() ; j++ ) { for( unsigned int i=0; i < M.mesh.x_coordinates.size() ; i++ ) @@ -386,7 +386,7 @@ void diffusion_decay_solver__constant_coefficients_LOD_2D( Microenvironment& M, // x-diffusion #pragma omp parallel for - for( unsigned int j=0; j < M.mesh.y_coordinates.size() ; j++ ) + for( long long j=0; j < M.mesh.y_coordinates.size() ; j++ ) { // Thomas solver, x-direction @@ -416,7 +416,7 @@ void diffusion_decay_solver__constant_coefficients_LOD_2D( Microenvironment& M, M.apply_dirichlet_conditions(); #pragma omp parallel for - for( unsigned int i=0; i < M.mesh.x_coordinates.size() ; i++ ) + for( long long i=0; i < M.mesh.x_coordinates.size() ; i++ ) { // Thomas solver, y-direction @@ -497,7 +497,7 @@ void diffusion_decay_explicit_uniform_rates( Microenvironment& M, double dt ) static vector constant4 = M.one - dt * M.decay_rates; #pragma omp parallel for - for( unsigned int i=0; i < (*(M.p_density_vectors)).size() ; i++ ) + for( long long i=0; i < (*(M.p_density_vectors)).size() ; i++ ) { unsigned int number_of_neighbors = M.mesh.connected_voxel_indices[i].size(); @@ -591,7 +591,7 @@ void diffusion_decay_solver__constant_coefficients_LOD_1D( Microenvironment& M, // x-diffusion #pragma omp parallel for - for( unsigned int j=0; j < M.mesh.y_coordinates.size() ; j++ ) + for( long long j=0; j < M.mesh.y_coordinates.size() ; j++ ) { // Thomas solver, x-direction diff --git a/BioFVM/BioFVM_utilities.h b/BioFVM/BioFVM_utilities.h index c5162d88a..fbaea7ace 100644 --- a/BioFVM/BioFVM_utilities.h +++ b/BioFVM/BioFVM_utilities.h @@ -78,9 +78,12 @@ void seed_random( void ); double uniform_random( void ); double compute_mean( std::vector& values ); -double compute_variance( std::vector& values, double mean ); -double compute_variance( std::vector& values ); - +double compute_variance( std::vector& values, double mean ); +double compute_variance( std::vector& values ); + +inline void trim_cr( std::string& line ) +{ if (!line.empty() && line.back() == '\r') { line.pop_back(); } } + }; #endif diff --git a/BioFVM/CMakeLists.txt b/BioFVM/CMakeLists.txt new file mode 100644 index 000000000..5f034d99e --- /dev/null +++ b/BioFVM/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(BioFVM OBJECT) + +file(GLOB SOURCES "*.cpp") +target_sources(BioFVM PRIVATE ${SOURCES}) + +find_package(OpenMP REQUIRED) +target_link_libraries(BioFVM PUBLIC OpenMP::OpenMP_CXX) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..ee3eafcba --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,65 @@ +cmake_minimum_required(VERSION 3.12) +project( + PhysiCell + LANGUAGES CXX + VERSION 1.14.2) + +# Set C++ standard and enable optimizations +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native") +set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) + +# Options to build addons +option(PHYSICELL_BUILD_PHYSIBOSS "Build PhysiBoSS addon" ON) +option(PHYSICELL_BUILD_LIBROADDRUNNER "Build libRoadrunner addon" OFF) +option(PHYSICELL_BUILD_DFBA "Build dFBA addon" OFF) + +# Main libraries and modules +add_subdirectory(BioFVM) +add_subdirectory(core) +add_subdirectory(modules) + +# Examples and sample projects +add_subdirectory(examples) +add_subdirectory(addons/PhysiMeSS) +add_subdirectory(sample_projects/asymmetric_division) +add_subdirectory(sample_projects/biorobots) +add_subdirectory(sample_projects/cancer_biorobots) +add_subdirectory(sample_projects/cancer_immune) +add_subdirectory(sample_projects/celltypes3) +add_subdirectory(sample_projects/custom_division) +add_subdirectory(sample_projects/episode) +add_subdirectory(sample_projects/heterogeneity) +add_subdirectory(sample_projects/immune_function) +add_subdirectory(sample_projects/interactions) +add_subdirectory(sample_projects/mechano) +add_subdirectory(sample_projects/physimess) +add_subdirectory(sample_projects/pred_prey_farmer) +add_subdirectory(sample_projects/rules_sample) +add_subdirectory(sample_projects/template) +add_subdirectory(sample_projects/virus_macrophage) +add_subdirectory(sample_projects/worm) + +# PhysiBoSS addon and related sample projects +if(PHYSICELL_BUILD_PHYSIBOSS) + add_subdirectory(addons/PhysiBoSS) + add_subdirectory(sample_projects_intracellular/boolean/cancer_invasion) + add_subdirectory(sample_projects_intracellular/boolean/physiboss_cell_lines) + add_subdirectory(sample_projects_intracellular/boolean/template_BM) + add_subdirectory(sample_projects_intracellular/boolean/tutorial) +endif() + +# libRoadrunner addon and related sample projects +if(PHYSICELL_BUILD_LIBROADDRUNNER) + add_subdirectory(addons/libRoadrunner) + add_subdirectory(sample_projects_intracellular/ode/ode_energy) +endif() + +# dFBA addon and related sample projects +if(PHYSICELL_BUILD_DFBA) + add_subdirectory(addons/dFBA) + add_subdirectory(sample_projects_intracellular/fba/cancer_metabolism) + # broken target - can not compile + # add_subdirectory(sample_projects_intracellular/fba/ecoli_acetic_switch) +endif() diff --git a/Makefile b/Makefile index 5d256d1c3..693ecd4f6 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,12 @@ else endif endif -COMPILE_COMMAND := $(CC) $(CFLAGS) +# -MMD emits a .d file beside each .o listing the headers it included; -MP adds +# a dummy target for each so that deleting or renaming a header does not break +# the build. The .d files are read back in at the bottom of this file. +DEPFLAGS := -MMD -MP + +COMPILE_COMMAND := $(CC) $(CFLAGS) $(DEPFLAGS) BioFVM_OBJECTS := BioFVM_vector.o BioFVM_mesh.o BioFVM_microenvironment.o BioFVM_solvers.o BioFVM_matlab.o \ BioFVM_utilities.o BioFVM_basic_agent.o BioFVM_MultiCellDS.o BioFVM_agent_container.o @@ -77,7 +82,7 @@ list-projects: @echo " asymmetric-division-sample immune-function-sample episode-sample" @echo "" @echo "Sample intracellular projects: template_BM ode-energy-sample physiboss-cell-lines-sample" - @echo " cancer-metabolism-sample physiboss-tutorial physiboss-tutorial-invasion" + @echo " cancer-metabolism-sample crossfeeding physiboss-tutorial physiboss-tutorial-invasion bacterial-colony metabolic_driven_motility" @echo "" template: @@ -273,12 +278,23 @@ physiboss-tutorial-invasion: ecoli-acetic-switch-sample: cp ./sample_projects_intracellular/fba/ecoli_acetic_switch/custom_modules/* ./custom_modules/ touch main.cpp && cp main.cpp main-backup.cpp - cp ./sample_projects_intracellular/fba/ecoli_acetic_switch/main_ecoli_acetic_switch.cpp ./main.cpp + cp ./sample_projects_intracellular/fba/ecoli_acetic_switch/main.cpp ./main.cpp cp Makefile Makefile-backup cp ./sample_projects_intracellular/fba/ecoli_acetic_switch/Makefile ./ cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml cp ./sample_projects_intracellular/fba/ecoli_acetic_switch/config/* ./config/ +dfba_unit_test: + cp ./sample_projects_intracellular/fba/dfba_unit_test/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/dfba_unit_test/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/dfba_unit_test/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/dfba_unit_test/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/dfba_unit_test/scripts/* ./scripts/ + cancer-metabolism-sample: cp ./sample_projects_intracellular/fba/cancer_metabolism/custom_modules/* ./custom_modules/ touch main.cpp && cp main.cpp main-backup.cpp @@ -287,6 +303,54 @@ cancer-metabolism-sample: cp ./sample_projects_intracellular/fba/cancer_metabolism/Makefile ./ cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml cp ./sample_projects_intracellular/fba/cancer_metabolism/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/cancer_metabolism/scripts/* ./scripts/ + +cancer-core-metabolism: + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/scripts/* ./scripts/ + +bacterial-colony: + cp ./sample_projects_intracellular/fba/bacterial_colony/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/bacterial_colony/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/bacterial_colony/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/bacterial_colony/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/bacterial_colony/scripts/* ./scripts/ + +crossfeeding: + cp ./sample_projects_intracellular/fba/crossfeeding/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/crossfeeding/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/crossfeeding/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp -r ./sample_projects_intracellular/fba/crossfeeding/config/* ./config/ + mkdir ./scripts/ + cp -r ./sample_projects_intracellular/fba/crossfeeding/scripts/* ./scripts/ + cp ./config/PhysiCell_settings_crossfeeding.xml ./config/PhysiCell_settings.xml + + +metabolic_driven_motility: + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/scripts/* ./scripts/ template_BM: cp ./sample_projects_intracellular/boolean/template_BM/custom_modules/* ./custom_modules/ @@ -435,6 +499,7 @@ reset: clean: rm -f *.o + rm -f *.d rm -f $(PROGRAM_NAME)* data-cleanup: @@ -472,20 +537,19 @@ untar: FRAMERATE := 24 OUTPUT := output +MAGICK_DENSITY := 96 +MAGICK_RESIZE_X := 1024 +MAGICK_RESIZE_Y := 1024 +MAGICK_RESIZE := $(MAGICK_RESIZE_X)x$(MAGICK_RESIZE_Y) + jpeg: - @magick identify -format "%h" $(OUTPUT)/initial.svg > __H.txt - @magick identify -format "%w" $(OUTPUT)/initial.svg > __W.txt - @expr 2 \* \( $$(grep . __H.txt) / 2 \) > __H1.txt - @expr 2 \* \( $$(grep . __W.txt) / 2 \) > __W1.txt - @echo "$$(grep . __W1.txt)!x$$(grep . __H1.txt)!" > __resize.txt - @magick mogrify -format jpg -resize $$(grep . __resize.txt) $(OUTPUT)/s*.svg - rm -f __H*.txt __W*.txt __resize.txt + magick mogrify -density $(MAGICK_DENSITY) -format jpg -resize $(MAGICK_RESIZE) $(OUTPUT)/s*.svg gif: - magick convert $(OUTPUT)/s*.svg $(OUTPUT)/out.gif + magick -density $(MAGICK_DENSITY) $(OUTPUT)/s*.svg -resize $(MAGICK_RESIZE) $(OUTPUT)/out.gif movie: - ffmpeg -r $(FRAMERATE) -f image2 -i $(OUTPUT)/snapshot%08d.jpg -vcodec libx264 -pix_fmt yuv420p -strict -2 -tune animation -crf 15 -acodec none $(OUTPUT)/out.mp4 + ffmpeg -r $(FRAMERATE) -f image2 -i $(OUTPUT)/snapshot%08d.jpg -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" -vcodec libx264 -pix_fmt yuv420p -strict -2 -tune animation -crf 15 -acodec none $(OUTPUT)/out.mp4 # upgrade rules @@ -560,3 +624,7 @@ unpack: list-user-projects: @echo "user projects::" @cd ./user_projects && ls -dt1 * | grep . | sed 's!empty.txt!!' + +# Keep this at the end of the file: each .d declares rules for its own .o, so +# including them any earlier would replace `all` as the default goal. +-include $(ALL_OBJECTS:.o=.d) diff --git a/Makefile-backup b/Makefile-backup new file mode 100644 index 000000000..cf4654420 --- /dev/null +++ b/Makefile-backup @@ -0,0 +1,621 @@ +VERSION := $(shell grep . VERSION.txt | cut -f1 -d:) +PROGRAM_NAME := project + +CC := g++ +# CC := g++-mp-7 # typical macports compiler name +# CC := g++-7 # typical homebrew compiler name + +# Check for environment definitions of compiler +# e.g., on CC = g++-7 on OSX +ifdef PHYSICELL_CPP + CC := $(PHYSICELL_CPP) +endif + +ARCH := native # best auto-tuning +# ARCH := core2 # a reasonably safe default for most CPUs since 2007 +# ARCH := corei7 +# ARCH := core-avx-i # i7 ivy bridge or newer +# ARCH := core-avx2 # i7 with Haswell or newer +# ARCH := silvermont +# ARCH := skylake-avx512 +# ARCH := nocona #64-bit pentium 4 or later + +# CFLAGS := -march=$(ARCH) -Ofast -s -fomit-frame-pointer -mfpmath=both -fopenmp -m64 -std=c++11 +CFLAGS := -march=$(ARCH) -O3 -fomit-frame-pointer -mfpmath=both -fopenmp -m64 -std=c++11 + +ifeq ($(OS),Windows_NT) +else + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Darwin) + UNAME_P := $(shell uname -p) + var := $(shell which $(CC) | xargs file) + ifeq ($(lastword $(var)),arm64) + CFLAGS := -march=$(ARCH) -O3 -fomit-frame-pointer -fopenmp -m64 -std=c++11 + endif + endif +endif + +COMPILE_COMMAND := $(CC) $(CFLAGS) + +BioFVM_OBJECTS := BioFVM_vector.o BioFVM_mesh.o BioFVM_microenvironment.o BioFVM_solvers.o BioFVM_matlab.o \ +BioFVM_utilities.o BioFVM_basic_agent.o BioFVM_MultiCellDS.o BioFVM_agent_container.o + +PhysiCell_core_OBJECTS := PhysiCell_phenotype.o PhysiCell_cell_container.o PhysiCell_standard_models.o \ +PhysiCell_cell.o PhysiCell_custom.o PhysiCell_utilities.o PhysiCell_constants.o PhysiCell_basic_signaling.o \ +PhysiCell_signal_behavior.o PhysiCell_rules.o + +PhysiCell_module_OBJECTS := PhysiCell_SVG.o PhysiCell_pathology.o PhysiCell_MultiCellDS.o PhysiCell_various_outputs.o \ +PhysiCell_pugixml.o PhysiCell_settings.o PhysiCell_geometry.o + +# put your custom objects here (they should be in the custom_modules directory) + +PhysiCell_custom_module_OBJECTS := .o + +pugixml_OBJECTS := pugixml.o + +PhysiCell_OBJECTS := $(BioFVM_OBJECTS) $(pugixml_OBJECTS) $(PhysiCell_core_OBJECTS) $(PhysiCell_module_OBJECTS) +ALL_OBJECTS := $(PhysiCell_OBJECTS) $(PhysiCell_custom_module_OBJECTS) + +EXAMPLES := ./examples/PhysiCell_test_mechanics_1.cpp ./examples/PhysiCell_test_mechanics_2.cpp \ + ./examples/PhysiCell_test_DCIS.cpp ./examples/PhysiCell_test_HDS.cpp \ + ./examples/PhysiCell_test_cell_cycle.cpp ./examples/PhysiCell_test_volume.cpp + +all: + make heterogeneity-sample + make + +name: + @echo "" + @echo "Executable name is" $(PROGRAM_NAME) + @echo "" + +# sample projects +list-projects: + @echo "Sample projects: template biorobots-sample cancer-biorobots-sample cancer-immune-sample" + @echo " celltypes3-sample heterogeneity-sample pred-prey-farmer virus-macrophage-sample" + @echo " worm-sample interaction-sample mechano-sample rules-sample physimess-sample custom-division-sample" + @echo " asymmetric-division-sample immune-function-sample episode-sample" + @echo "" + @echo "Sample intracellular projects: template_BM ode-energy-sample physiboss-cell-lines-sample" + @echo " cancer-metabolism-sample crossfeeding physiboss-tutorial physiboss-tutorial-invasion bacterial-colony metabolic_driven_motility" + @echo "" + +template: + cp -r ./sample_projects/template/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/template/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/template/Makefile . + cp -r ./sample_projects/template/config/* ./config + +# sample projects + +# ---- non-intracellular projects +biorobots-sample: + cp ./sample_projects/biorobots/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/biorobots/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/biorobots/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/biorobots/config/* ./config/ + +cancer-biorobots-sample: + cp ./sample_projects/cancer_biorobots/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/cancer_biorobots/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/cancer_biorobots/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/cancer_biorobots/config/* ./config/ + +cancer-immune-sample: + cp ./sample_projects/cancer_immune/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/cancer_immune/main-cancer_immune_3D.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/cancer_immune/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/cancer_immune/config/* ./config/ + +celltypes3-sample: + cp ./sample_projects/celltypes3/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/celltypes3/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/celltypes3/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/celltypes3/config/* ./config/ + +heterogeneity-sample: + cp ./sample_projects/heterogeneity/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/heterogeneity/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/heterogeneity/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/heterogeneity/config/* ./config/ + +pred-prey-farmer: + cp ./sample_projects/pred_prey_farmer/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/pred_prey_farmer/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/pred_prey_farmer/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/pred_prey_farmer/config/* ./config/ + +virus-macrophage-sample: + cp ./sample_projects/virus_macrophage/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/virus_macrophage/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/virus_macrophage/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/virus_macrophage/config/* ./config/ + +worm-sample: + cp ./sample_projects/worm/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/worm/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/worm/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/worm/config/* ./config/ + +interaction-sample: + cp ./sample_projects/interactions/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/interactions/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/interactions/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/interactions/config/* ./config/ + +mechano-sample: + cp ./sample_projects/mechano/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/mechano/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/mechano/Makefile . + cp ./sample_projects/mechano/config/* ./config/ + +rules-sample: + cp ./sample_projects/rules_sample/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/rules_sample/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/rules_sample/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects/rules_sample/config/* ./config/ + +physimess-sample: + cp ./sample_projects/physimess/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/physimess/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/physimess/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp -r ./sample_projects/physimess/config/* ./config/ + +custom-division-sample: + cp -r ./sample_projects/custom_division/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/custom_division/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/custom_division/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp -r ./sample_projects/custom_division/config/* ./config/ + +asymmetric-division-sample: + cp -r ./sample_projects/asymmetric_division/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/asymmetric_division/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/asymmetric_division/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp -r ./sample_projects/asymmetric_division/config/* ./config/ + +immune-function-sample: + cp -r ./sample_projects/immune_function/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/immune_function/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/immune_function/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp -r ./sample_projects/immune_function/config/* ./config/ + +episode-sample: + cp -r ./sample_projects/episode/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects/episode/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects/episode/Makefile . + cp -r ./sample_projects/episode/config/* ./config + +# ---- intracellular projects +ode-energy-sample: + cp ./sample_projects_intracellular/ode/ode_energy/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/ode/ode_energy/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/ode/ode_energy/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/ode/ode_energy/config/* ./config/ + +physiboss-cell-lines-sample: + cp ./sample_projects_intracellular/boolean/physiboss_cell_lines/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/boolean/physiboss_cell_lines/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/boolean/physiboss_cell_lines/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/boolean/physiboss_cell_lines/config/* ./config/ + +physiboss-tutorial: + cp ./sample_projects_intracellular/boolean/tutorial/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/boolean/tutorial/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/boolean/tutorial/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp -r ./sample_projects_intracellular/boolean/tutorial/config/* ./config/ + +physiboss-tutorial-invasion: + cp ./sample_projects_intracellular/boolean/cancer_invasion/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/boolean/cancer_invasion/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/boolean/cancer_invasion/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp -r ./sample_projects_intracellular/boolean/cancer_invasion/config/* ./config/ + +ecoli-acetic-switch-sample: + cp ./sample_projects_intracellular/fba/ecoli_acetic_switch/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/ecoli_acetic_switch/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/ecoli_acetic_switch/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/ecoli_acetic_switch/config/* ./config/ + +dfba_unit_test: + cp ./sample_projects_intracellular/fba/dfba_unit_test/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/dfba_unit_test/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/dfba_unit_test/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/dfba_unit_test/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/dfba_unit_test/scripts/* ./scripts/ + +cancer-metabolism-sample: + cp ./sample_projects_intracellular/fba/cancer_metabolism/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/cancer_metabolism/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/cancer_metabolism/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/cancer_metabolism/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/cancer_metabolism/scripts/* ./scripts/ + +cancer-core-metabolism: + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/cancer_core_metabolism/scripts/* ./scripts/ + +bacterial-colony: + cp ./sample_projects_intracellular/fba/bacterial_colony/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/bacterial_colony/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/bacterial_colony/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/bacterial_colony/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/bacterial_colony/scripts/* ./scripts/ + +crossfeeding: + cp ./sample_projects_intracellular/fba/crossfeeding/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/crossfeeding/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/crossfeeding/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp -r ./sample_projects_intracellular/fba/crossfeeding/config/* ./config/ + mkdir ./scripts/ + cp -r ./sample_projects_intracellular/fba/crossfeeding/scripts/* ./scripts/ + cp ./config/PhysiCell_settings_crossfeeding.xml ./config/PhysiCell_settings.xml + + +metabolic_driven_motility: + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/Makefile ./ + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/fba/metabolic_driven_motility/scripts/* ./scripts/ + +template_BM: + cp ./sample_projects_intracellular/boolean/template_BM/custom_modules/* ./custom_modules/ + touch main.cpp && cp main.cpp main-backup.cpp + cp ./sample_projects_intracellular/boolean/template_BM/main.cpp ./main.cpp + cp Makefile Makefile-backup + cp ./sample_projects_intracellular/boolean/template_BM/Makefile . + cp ./config/PhysiCell_settings.xml ./config/PhysiCell_settings-backup.xml + cp -r ./sample_projects_intracellular/boolean/template_BM/config/* ./config/ + mkdir ./scripts/ + cp ./sample_projects_intracellular/boolean/template_BM/scripts/* ./scripts/ + +# early examples for convergence testing + +physicell_test_mech1: $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_mechanics_1.cpp + $(COMPILE_COMMAND) -o test_mech1 $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_mechanics_1.cpp + +physicell_test_mech2: $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_mechanics_2.cpp + $(COMPILE_COMMAND) -o test_mech2 $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_mechanics_2.cpp + +physicell_test_DCIS: $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_DCIS.cpp + $(COMPILE_COMMAND) -o test_DCIS $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_DCIS.cpp + +physicell_test_HDS: $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_HDS.cpp + $(COMPILE_COMMAND) -o test_HDS $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_HDS.cpp + +physicell_test_cell_cycle: $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_cell_cycle.cpp + $(COMPILE_COMMAND) -o test_cycle $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_cell_cycle.cpp + +PhysiCell_test_volume: $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_volume.cpp + $(COMPILE_COMMAND) -o test_volume $(PhysiCell_OBJECTS) ./examples/PhysiCell_test_volume.cpp + +examples: $(PhysiCell_OBJECTS) + $(COMPILE_COMMAND) -o ./examples/test_mech1 ./examples/PhysiCell_test_mechanics_1.cpp $(PhysiCell_OBJECTS) + $(COMPILE_COMMAND) -o ./examples/test_mech2 ./examples/PhysiCell_test_mechanics_2.cpp $(PhysiCell_OBJECTS) + $(COMPILE_COMMAND) -o ./examples/test_DCIS ./examples/PhysiCell_test_DCIS.cpp $(PhysiCell_OBJECTS) + $(COMPILE_COMMAND) -o ./examples/test_HDS ./examples/PhysiCell_test_HDS.cpp $(PhysiCell_OBJECTS) + $(COMPILE_COMMAND) -o ./examples/test_cycle ./examples/PhysiCell_test_cell_cycle.cpp $(PhysiCell_OBJECTS) + $(COMPILE_COMMAND) -o ./examples/test_volume ./examples/PhysiCell_test_volume.cpp $(PhysiCell_OBJECTS) + +# PhysiCell core components + +PhysiCell_phenotype.o: ./core/PhysiCell_phenotype.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_phenotype.cpp + +PhysiCell_digital_cell_line.o: ./core/PhysiCell_digital_cell_line.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_digital_cell_line.cpp + +PhysiCell_cell.o: ./core/PhysiCell_cell.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_cell.cpp + +PhysiCell_cell_container.o: ./core/PhysiCell_cell_container.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_cell_container.cpp + +PhysiCell_standard_models.o: ./core/PhysiCell_standard_models.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_standard_models.cpp + +PhysiCell_utilities.o: ./core/PhysiCell_utilities.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_utilities.cpp + +PhysiCell_custom.o: ./core/PhysiCell_custom.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_custom.cpp + +PhysiCell_constants.o: ./core/PhysiCell_constants.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_constants.cpp + +PhysiCell_signal_behavior.o: ./core/PhysiCell_signal_behavior.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_signal_behavior.cpp + +PhysiCell_rules.o: ./core/PhysiCell_rules.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_rules.cpp + +# BioFVM core components (needed by PhysiCell) + +BioFVM_vector.o: ./BioFVM/BioFVM_vector.cpp + $(COMPILE_COMMAND) -c ./BioFVM/BioFVM_vector.cpp + +BioFVM_agent_container.o: ./BioFVM/BioFVM_agent_container.cpp + $(COMPILE_COMMAND) -c ./BioFVM/BioFVM_agent_container.cpp + +BioFVM_mesh.o: ./BioFVM/BioFVM_mesh.cpp + $(COMPILE_COMMAND) -c ./BioFVM/BioFVM_mesh.cpp + +BioFVM_microenvironment.o: ./BioFVM/BioFVM_microenvironment.cpp + $(COMPILE_COMMAND) -c ./BioFVM/BioFVM_microenvironment.cpp + +BioFVM_solvers.o: ./BioFVM/BioFVM_solvers.cpp + $(COMPILE_COMMAND) -c ./BioFVM/BioFVM_solvers.cpp + +BioFVM_utilities.o: ./BioFVM/BioFVM_utilities.cpp + $(COMPILE_COMMAND) -c ./BioFVM/BioFVM_utilities.cpp + +BioFVM_basic_agent.o: ./BioFVM/BioFVM_basic_agent.cpp + $(COMPILE_COMMAND) -c ./BioFVM/BioFVM_basic_agent.cpp + +BioFVM_matlab.o: ./BioFVM/BioFVM_matlab.cpp + $(COMPILE_COMMAND) -c ./BioFVM/BioFVM_matlab.cpp + +BioFVM_MultiCellDS.o: ./BioFVM/BioFVM_MultiCellDS.cpp + $(COMPILE_COMMAND) -c ./BioFVM/BioFVM_MultiCellDS.cpp + +pugixml.o: ./BioFVM/pugixml.cpp + $(COMPILE_COMMAND) -c ./BioFVM/pugixml.cpp + +# standard PhysiCell modules + +PhysiCell_SVG.o: ./modules/PhysiCell_SVG.cpp + $(COMPILE_COMMAND) -c ./modules/PhysiCell_SVG.cpp + +PhysiCell_pathology.o: ./modules/PhysiCell_pathology.cpp + $(COMPILE_COMMAND) -c ./modules/PhysiCell_pathology.cpp + +PhysiCell_MultiCellDS.o: ./modules/PhysiCell_MultiCellDS.cpp + $(COMPILE_COMMAND) -c ./modules/PhysiCell_MultiCellDS.cpp + +PhysiCell_various_outputs.o: ./modules/PhysiCell_various_outputs.cpp + $(COMPILE_COMMAND) -c ./modules/PhysiCell_various_outputs.cpp + +PhysiCell_pugixml.o: ./modules/PhysiCell_pugixml.cpp + $(COMPILE_COMMAND) -c ./modules/PhysiCell_pugixml.cpp + +PhysiCell_settings.o: ./modules/PhysiCell_settings.cpp + $(COMPILE_COMMAND) -c ./modules/PhysiCell_settings.cpp + +PhysiCell_basic_signaling.o: ./core/PhysiCell_basic_signaling.cpp + $(COMPILE_COMMAND) -c ./core/PhysiCell_basic_signaling.cpp + +PhysiCell_geometry.o: ./modules/PhysiCell_geometry.cpp + $(COMPILE_COMMAND) -c ./modules/PhysiCell_geometry.cpp + +# user-defined PhysiCell modules + +# cleanup + +reset: + rm -f *.cpp PhysiCell_cell.o + cp ./sample_projects/Makefile-default Makefile + rm -rf ./custom_modules/* + touch ./custom_modules/empty.txt + touch ALL_CITATIONS.txt + touch ./core/PhysiCell_cell.cpp + rm ALL_CITATIONS.txt + cp ./config/PhysiCell_settings-backup.xml ./config/PhysiCell_settings.xml + touch ./config/empty.csv + rm ./config/*.csv + +clean: + rm -f *.o + rm -f $(PROGRAM_NAME)* + +data-cleanup: + rm -rf ./output + mkdir ./output + touch ./output/empty.txt + +# archival + +checkpoint: + zip -r $$(date +%b_%d_%Y_%H%M).zip Makefile *.cpp *.h config/*.xml custom_modules/* + +zip: + zip -r latest.zip Makefile* *.cpp *.h BioFVM/* config/* core/* custom_modules/* matlab/* modules/* sample_projects/* + cp latest.zip $$(date +%b_%d_%Y_%H%M).zip + cp latest.zip VERSION_$(VERSION).zip + mv *.zip archives/ + +tar: + tar --ignore-failed-read -czf latest.tar Makefile* *.cpp *.h BioFVM/* config/* core/* custom_modules/* matlab/* modules/* sample_projects/* + cp latest.tar $$(date +%b_%d_%Y_%H%M).tar + cp latest.tar VERSION_$(VERSION).tar + mv *.tar archives/ + +unzip: + cp ./archives/latest.zip . + unzip latest.zip + +untar: + cp ./archives/latest.tar . + tar -xzf latest.tar + +# easier animation + +FRAMERATE := 24 +OUTPUT := output + +jpeg: + @magick identify -format "%h" $(OUTPUT)/initial.svg > __H.txt + @magick identify -format "%w" $(OUTPUT)/initial.svg > __W.txt + @expr 2 \* \( $$(grep . __H.txt) / 2 \) > __H1.txt + @expr 2 \* \( $$(grep . __W.txt) / 2 \) > __W1.txt + @echo "$$(grep . __W1.txt)!x$$(grep . __H1.txt)!" > __resize.txt + @magick mogrify -format jpg -resize $$(grep . __resize.txt) $(OUTPUT)/s*.svg + rm -f __H*.txt __W*.txt __resize.txt + +gif: + magick convert $(OUTPUT)/s*.svg $(OUTPUT)/out.gif + +movie: + ffmpeg -r $(FRAMERATE) -f image2 -i $(OUTPUT)/snapshot%08d.jpg -vcodec libx264 -pix_fmt yuv420p -strict -2 -tune animation -crf 15 -acodec none $(OUTPUT)/out.mp4 + +# upgrade rules + +SOURCE := PhysiCell_upgrade.zip +get-upgrade: + @echo $$(curl https://raw.githubusercontent.com/MathCancer/PhysiCell/master/VERSION.txt) > VER.txt + @echo https://github.com/MathCancer/PhysiCell/releases/download/$$(grep . VER.txt)/PhysiCell_V.$$(grep . VER.txt).zip > DL_FILE.txt + rm -f VER.txt + $$(curl -L $$(grep . DL_FILE.txt) --output PhysiCell_upgrade.zip) + rm -f DL_FILE.txt + +PhysiCell_upgrade.zip: + make get-upgrade + +upgrade: $(SOURCE) + unzip $(SOURCE) PhysiCell/VERSION.txt + mv -f PhysiCell/VERSION.txt . + unzip $(SOURCE) PhysiCell/core/* + cp -r PhysiCell/core/* core + unzip $(SOURCE) PhysiCell/modules/* + cp -r PhysiCell/modules/* modules + unzip $(SOURCE) PhysiCell/sample_projects/* + cp -r PhysiCell/sample_projects/* sample_projects + unzip $(SOURCE) PhysiCell/BioFVM/* + cp -r PhysiCell/BioFVM/* BioFVM + unzip $(SOURCE) PhysiCell/documentation/User_Guide.pdf + mv -f PhysiCell/documentation/User_Guide.pdf documentation + rm -f -r PhysiCell + rm -f $(SOURCE) + +# use: make save PROJ=your_project_name +PROJ := my_project + +save: + echo "Saving project as $(PROJ) ... " + mkdir -p ./user_projects + mkdir -p ./user_projects/$(PROJ) + mkdir -p ./user_projects/$(PROJ)/custom_modules + mkdir -p ./user_projects/$(PROJ)/config + cp main.cpp ./user_projects/$(PROJ) + cp Makefile ./user_projects/$(PROJ) + cp VERSION.txt ./user_projects/$(PROJ) + cp -r ./config/* ./user_projects/$(PROJ)/config + cp -r ./custom_modules/* ./user_projects/$(PROJ)/custom_modules + +load: + echo "Loading project from $(PROJ) ... " + cp ./user_projects/$(PROJ)/main.cpp . + cp ./user_projects/$(PROJ)/Makefile . + cp -r ./user_projects/$(PROJ)/config/* ./config/ + cp -r ./user_projects/$(PROJ)/custom_modules/* ./custom_modules/ + +pack: + @echo " " + @echo "Preparing project $(PROJ) for sharing ... " + @echo " " + cd ./user_projects && zip -r $(PROJ).zip $(PROJ) + @echo " " + @echo "Share ./user_projects/$(PROJ).zip ... " + @echo "Other users can unzip $(PROJ).zip in their ./user_projects, compile, and run." + @echo " " + +unpack: + @echo " " + @echo "Preparing shared project $(PROJ).zip for use ... " + @echo " " + cd ./user_projects && unzip $(PROJ).zip + @echo " " + @echo "Load this project via make load PROJ=$(PROJ) ... " + @echo " " + +list-user-projects: + @echo "user projects::" + @cd ./user_projects && ls -dt1 * | grep . | sed 's!empty.txt!!' diff --git a/addons/PhysiBoSS/CMakeLists.txt b/addons/PhysiBoSS/CMakeLists.txt new file mode 100644 index 000000000..e7ab48ca1 --- /dev/null +++ b/addons/PhysiBoSS/CMakeLists.txt @@ -0,0 +1,59 @@ +set(MABOSS_VERSION v2.6.5.3) + +function(create_physiboss_target MAX_NODES) + set(TARGET_NAME PhysiBoSS_${MAX_NODES}n) + add_library(${TARGET_NAME} OBJECT) + + target_sources( + ${TARGET_NAME} + PRIVATE src/maboss_intracellular.cpp src/maboss_network.cpp + ${PROJECT_SOURCE_DIR}/core/PhysiCell_cell.cpp + ${PROJECT_SOURCE_DIR}/modules/PhysiCell_MultiCellDS.cpp) + + if(NOT MAX_NODES EQUAL 64) + set(MABOSS_LIB_NAME MaBoSS_${MAX_NODES}n-static) + else() + set(MABOSS_LIB_NAME MaBoSS-static) + endif() + + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(MABOSS_ARCHIVE libMaBoSS-linux64.tar.gz) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR + STREQUAL "x86_64") + set(MABOSS_ARCHIVE libMaBoSS-osx64.tar.gz) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR + STREQUAL "arm64") + set(MABOSS_ARCHIVE libMaBoSS-macos-arm64.tar.gz) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + if (MSVC) + set(MABOSS_ARCHIVE libMaBoSS-win64-msvc.tar.gz) + else() + set(MABOSS_ARCHIVE libMaBoSS-win64.tar.gz) + endif() + else() + message(FATAL_ERROR "Unsupported OS: ${CMAKE_SYSTEM_NAME}") + endif() + + include(FetchContent) + FetchContent_Declare( + maboss + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + URL https://github.com/sysbio-curie/MaBoSS/releases/download/${MABOSS_VERSION}/${MABOSS_ARCHIVE} + ) + FetchContent_MakeAvailable(maboss) + + target_include_directories(${TARGET_NAME} + PUBLIC ${maboss_SOURCE_DIR}/engine/include) + target_link_directories(${TARGET_NAME} PUBLIC ${maboss_SOURCE_DIR}/engine/lib) + target_link_libraries(${TARGET_NAME} PUBLIC ${MABOSS_LIB_NAME}) + target_compile_definitions(${TARGET_NAME} PUBLIC MAXNODES=${MAX_NODES} + ADDON_PHYSIBOSS) + find_package(OpenMP REQUIRED) + target_link_libraries(${TARGET_NAME} PUBLIC OpenMP::OpenMP_CXX) +endfunction(create_physiboss_target) + +create_physiboss_target(64) +create_physiboss_target(128) +create_physiboss_target(256) + +add_library(PhysiBoSS ALIAS PhysiBoSS_64n) diff --git a/addons/PhysiBoSS/setup_libmaboss.py b/addons/PhysiBoSS/setup_libmaboss.py index 888dd2748..8fb348c56 100644 --- a/addons/PhysiBoSS/setup_libmaboss.py +++ b/addons/PhysiBoSS/setup_libmaboss.py @@ -20,7 +20,7 @@ # Assume Windows mb_file = "" url = "" - maboss_version = "v2.5.6" + maboss_version = "v2.6.5.3" if os_type.lower() == 'darwin': if "ARM64" in platform.uname().version: mb_file = "libMaBoSS-macos-arm64.tar.gz" diff --git a/addons/PhysiBoSS/src/maboss_intracellular.h b/addons/PhysiBoSS/src/maboss_intracellular.h index 06242a8c7..60fe87a44 100644 --- a/addons/PhysiBoSS/src/maboss_intracellular.h +++ b/addons/PhysiBoSS/src/maboss_intracellular.h @@ -127,6 +127,8 @@ class MaBoSSIntracellular : public PhysiCell::Intracellular { int validate_PhysiCell_tokens(PhysiCell::Phenotype& phenotype) {return 0; } int validate_SBML_species() {return 0; } int create_custom_data_for_SBML(PhysiCell::Phenotype& phenotype) {return 0; } + double get_flux_value(std::string name) {return 0.0;} + double get_growth_rate() {return 0.0;} }; diff --git a/addons/PhysiBoSS/src/maboss_network.h b/addons/PhysiBoSS/src/maboss_network.h index 5aace16d3..869106796 100644 --- a/addons/PhysiBoSS/src/maboss_network.h +++ b/addons/PhysiBoSS/src/maboss_network.h @@ -1,9 +1,7 @@ #ifndef _MaBoSS_Net_h_ #define _MaBoSS_Net_h_ -#include "StochasticSimulationEngine.h" -#include "BooleanNetwork.h" -#include "RunConfig.h" +#include "engines/StochasticSimulationEngine.h" #include "../../../core/PhysiCell_utilities.h" /** diff --git a/addons/PhysiMeSS/CMakeLists.txt b/addons/PhysiMeSS/CMakeLists.txt new file mode 100644 index 000000000..6d4052fbb --- /dev/null +++ b/addons/PhysiMeSS/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(PhysiMeSS OBJECT) + +file(GLOB SOURCES "*.cpp") +target_sources(PhysiMeSS PRIVATE ${SOURCES}) + +find_package(OpenMP REQUIRED) +target_link_libraries(PhysiMeSS PUBLIC OpenMP::OpenMP_CXX) diff --git a/addons/dFBA/CMakeLists.txt b/addons/dFBA/CMakeLists.txt new file mode 100644 index 000000000..f7dc3958f --- /dev/null +++ b/addons/dFBA/CMakeLists.txt @@ -0,0 +1,52 @@ +add_library(dFBA OBJECT) + +file(GLOB SOURCES "src/*.cpp") +target_sources( + dFBA PRIVATE ${SOURCES} ${PROJECT_SOURCE_DIR}/core/PhysiCell_cell.cpp + ${PROJECT_SOURCE_DIR}/modules/PhysiCell_MultiCellDS.cpp) + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(DFBA_ARCHIVE_KIND linux64) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(DFBA_ARCHIVE_KIND osx64) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(DFBA_ARCHIVE_KIND win64) +else() + message(FATAL_ERROR "Unsupported OS: ${CMAKE_SYSTEM_NAME}") +endif() + +find_package(ZLIB REQUIRED) +find_package(BZip2 REQUIRED) +find_package(LAPACK REQUIRED) +find_package(LibXml2 REQUIRED) + +include(FetchContent) +FetchContent_Declare( + coin + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + URL https://maboss.curie.fr/pub/Clp-1.17.6-${DFBA_ARCHIVE_KIND}.zip) +FetchContent_MakeAvailable(coin) + +FetchContent_Declare( + libsbml + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + URL https://maboss.curie.fr/pub/libSBML-5.19.0-${DFBA_ARCHIVE_KIND}.zip) +FetchContent_MakeAvailable(libsbml) + +target_include_directories(dFBA PUBLIC ${libsbml_SOURCE_DIR}/include + ${coin_SOURCE_DIR}/include) +target_link_directories(dFBA PUBLIC ${libsbml_SOURCE_DIR}/lib + ${coin_SOURCE_DIR}/lib) +target_link_libraries( + dFBA + PUBLIC Clp-static + CoinUtils-static + sbml-static + BZip2::BZip2 + LAPACK::LAPACK + ZLIB::ZLIB + LibXml2::LibXml2) +target_compile_definitions(dFBA PUBLIC ADDON_PHYSIDFBA) + +find_package(OpenMP REQUIRED) +target_link_libraries(dFBA PUBLIC OpenMP::OpenMP_CXX) diff --git a/output/empty.txt b/addons/dFBA/ext/.gitkeep similarity index 100% rename from output/empty.txt rename to addons/dFBA/ext/.gitkeep diff --git a/addons/dFBA/scripts/dfba_config.yaml b/addons/dFBA/scripts/dfba_config.yaml new file mode 100644 index 000000000..5990a8651 --- /dev/null +++ b/addons/dFBA/scripts/dfba_config.yaml @@ -0,0 +1,119 @@ +# dFBA Configuration File +# This YAML file configures individual models, their SBML files, exchanges, and cell parameters +# +# Structure: +# substrates: List of substrate configurations for the microenvironment +# Each substrate contains: +# - name: Substrate name (must match substrate names in exchanges) +# - diffusion_coefficient: Diffusion coefficient (micron^2/min) +# - decay_rate: Decay rate (1/min) +# - initial_condition: Initial concentration (mM, optional, defaults to 0.0) +# +# models: Dictionary of model configurations, keyed by model name +# Each model contains: +# - sbml_path: Path to SBML file (relative to --sbml-folder or absolute) +# - exchanges: List of exchange reaction configurations +# - growth_model: Growth model parameters (optional, uses hardcoded defaults if not specified) +# - settings: Intracellular settings (optional, uses hardcoded defaults if not specified) +# - secondary_objective: Optional second optimisation stage, e.g. pFBA (omitted entirely if absent) +# - death_model: Death model configuration (optional, uses hardcoded defaults if not specified) +# + +# Substrate configuration +# Define physical properties for each substrate in the microenvironment +substrates: + - name: "glucose" + diffusion_coefficient: 50000.0 # micron^2/min + decay_rate: 0.0 # 1/min + initial_condition: 0.0 # mM (optional, defaults to 0.0) + + - name: "acetate" + diffusion_coefficient: 50000.0 + decay_rate: 0.0 + initial_condition: 0.0 + + - name: "oxygen" + diffusion_coefficient: 100000.0 + decay_rate: 0.1 + initial_condition: 0.0 + + - name: "CO2" + diffusion_coefficient: 100000.0 + decay_rate: 0.0 + initial_condition: 0.0 + +models: + # Model 1: iML1515 + iML1515: + # Path to SBML file (can be relative to --sbml-folder or absolute) + sbml_path: "iML1515.xml" + + # Exchange reactions configuration + # Each exchange defines how a substrate is transported between the cell and microenvironment + exchanges: + - substrate: "glucose" # Substrate name (must match microenvironment variable) + fba_flux: "R_EX_glc__D_e" # Exchange reaction ID from SBML model + Km: 0.001 # Michaelis-Menten constant (mM) + Vmax: 8.0 # Maximum velocity (fmol/pg DW cell/min) + + - substrate: "acetate" + fba_flux: "R_EX_ac_e" + Km: 0.001 + Vmax: 8.0 + + - substrate: "oxygen" + fba_flux: "R_EX_o2_e" + Km: 0.002 + Vmax: 18.0 + + - substrate: "CO2" + fba_flux: "R_EX_co2_e" + Km: 0.001 + Vmax: 0.0 + + # Growth model parameters (optional - will use hardcoded defaults if not specified) + growth_model: + cell_density: 1.04 # Cell density (g/ml) + reference_volume: 1.3 # Reference volume for cell division (pg) + nuclear_volume: 0.21 # Nuclear volume (pg) + max_growth_rate: 0.86 # Maximum growth rate (1/min) + objective_reaction: "R_Biomass" # Objective reaction ID from SBML model + + # Intracellular settings (optional - will use hardcoded defaults if not specified) + settings: + intracellular_dt: 0.01 # Intracellular time step (min) + + # Secondary objective (optional - omit for standard FBA, which is the default). + # Runs a second optimisation after the objective has been maximised, to pick + # one flux distribution among the equally optimal ones. Without it the solver + # returns an arbitrary vertex, and in dFBA that vertex is what sets the + # exchange rates seen by BioFVM. + # secondary_objective: + # enabled: true + # type: parsimonious # only type implemented: minimise total flux + # fraction_of_optimum: 1.0 # growth floor as a fraction of the optimum, in (0, 1] + + # Death model (optional - will use hardcoded defaults if not specified) + death_model: + enabled: false + death_type: necrosis # death type string + death_trigger_flux: 0.0 # 1/min + death_flux_threshold: 0.0 # dimensionless + death_rate_increase: 1.67e-5 # rate increase + + # Example: Add more models here + # model2: + # sbml_path: "model2.xml" + # exchanges: + # - substrate: "glucose" + # fba_flux: "R_EX_glc__D_e" + # Km: 0.001 + # Vmax: 10.0 + # growth_model: + # cell_density: 1.05 + # reference_volume: 2500 + # max_growth_rate: 0.90 + # objective_reaction: "R_Biomass" + + + diff --git a/addons/dFBA/scripts/dfba_config_crossfeeding.yaml b/addons/dFBA/scripts/dfba_config_crossfeeding.yaml new file mode 100644 index 000000000..da251fcc5 --- /dev/null +++ b/addons/dFBA/scripts/dfba_config_crossfeeding.yaml @@ -0,0 +1,124 @@ +# dFBA Configuration File +# This YAML file configures both metabolic models (C. beijerinckii and M. barkeri), +# their SBML files, exchanges, and cell parameters for the crossfeeding simulation +# +# Structure: +# substrates: List of substrate configurations for the microenvironment +# Each substrate contains: +# - name: Substrate name (must match substrate names in exchanges) +# - diffusion_coefficient: Diffusion coefficient (micron^2/min) +# - decay_rate: Decay rate (1/min) +# - initial_condition: Initial concentration (mM, optional, defaults to 0.0) +# +# models: Dictionary of model configurations, keyed by model name +# Each model contains: +# - sbml_path: Path to SBML file (relative to --sbml-folder or absolute) +# - exchanges: List of exchange reaction configurations +# - growth_model: Growth model parameters (optional, uses hardcoded defaults if not specified) +# - settings: Intracellular settings (optional, uses hardcoded defaults if not specified) +# - death_model: Death model configuration (optional, uses hardcoded defaults if not specified) +# + +# Substrate configuration +# Define physical properties for each substrate in the microenvironment +substrates: + - name: "glucose" + diffusion_coefficient: 48000.0 # micron^2/min + decay_rate: 0.0 # 1/min + initial_condition: 300.0 # mM + + - name: "methane" + diffusion_coefficient: 5233.0 # micron^2/min + decay_rate: 0.0 # 1/min + initial_condition: 0.0 # mM + + - name: "CO2" + diffusion_coefficient: 48000.0 # micron^2/min + decay_rate: 0.0 # 1/min + initial_condition: 300.0 # mM + + - name: "H2" + diffusion_coefficient: 5233.0 # micron^2/min + decay_rate: 0.0 # 1/min + initial_condition: 100.0 # mM + +models: + # Model 1: C. beijerinckii (iCB925) + # Clostridium beijerinckii - glucose fermenter that produces H2 + c_beijerinckii: + # Path to SBML file (can be relative to --sbml-folder or absolute) + sbml_path: "C_beijerinckii_model_iCB925_trimmed.xml" + + # Exchange reactions configuration + # Each exchange defines how a substrate is transported between the cell and microenvironment + exchanges: + - substrate: "glucose" # Substrate name (must match microenvironment variable) + fba_flux: "R_EX_glc_D_e" # Exchange reaction ID from SBML model + Km: 0.002 # Michaelis-Menten constant (mM) + Vmax: 0.5 # Maximum velocity (mol/(L·min)) + + - substrate: "H2" + fba_flux: "R_EX_h2_e" + Km: 0.002 # mM + Vmax: 180.0 # mol/(L·min) + + # Growth model parameters + growth_model: + cell_density: 1.04 # Cell density (g/ml) + reference_volume: 1.3 # Reference volume for cell division (micron^3) + max_growth_rate: 48.0 # Maximum growth rate (1/h) + objective_reaction: "R_biomass" # Objective reaction ID from SBML model + + # Intracellular settings + settings: + intracellular_dt: 6 # Intracellular time step (min) + + # Death model configuration + death_model: + enabled: true + death_type: "necrosis" # death type string + death_trigger_flux: 0.0 # 1/min + death_flux_threshold: 0.0 # dimensionless + death_rate_increase: 1.67e-5 # rate increase + + # Model 2: M. barkeri (iMG746) + # Methanosarcina barkeri - methanogen that consumes H2 and CO2 to produce methane + m_barkeri: + # Path to SBML file (can be relative to --sbml-folder or absolute) + sbml_path: "M_barkeri_model_iMG746_trimmed.xml" + + # Exchange reactions configuration + # Each exchange defines how a substrate is transported between the cell and microenvironment + exchanges: + - substrate: "CO2" # Substrate name (must match microenvironment variable) + fba_flux: "R_EX_co2_e" # Exchange reaction ID from SBML model + Km: 0.02 + Vmax: 8.0 + + - substrate: "H2" + fba_flux: "R_EX_h2_e" + Km: 0.002 + Vmax: 18.0 + + - substrate: "methane" + fba_flux: "R_EX_ch4_e" + Km: 0.8 + Vmax: 10.0 + + # Growth model parameters + growth_model: + cell_density: 1.04 # Cell density (g/ml) + reference_volume: 1.8 # Reference volume for cell division (micron^3) + max_growth_rate: 48.0 # Maximum growth rate (1/h) + objective_reaction: "R_Mb_biomass_65" # Objective reaction ID from SBML model + + # Intracellular settings (optional - will use hardcoded defaults if not specified) + # Note: No settings specified in XML, so using defaults + + # Death model configuration + death_model: + enabled: false + death_type: "" # death type string (empty in XML) + death_trigger_flux: 0.0 # 1/min + death_flux_threshold: 0.0 # dimensionless + death_rate_increase: 0.0 # rate increase \ No newline at end of file diff --git a/addons/dFBA/src/FBA_metabolite.cpp b/addons/dFBA/src/FBA_metabolite.cpp deleted file mode 100755 index 0d48d4df9..000000000 --- a/addons/dFBA/src/FBA_metabolite.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Metabolite.cpp - * - * Created on: 13 jun. 2019 - * Author: mponce - */ - - -#include "FBA_metabolite.h" - -#include -#include -#include - - - -FBA_metabolite::FBA_metabolite(std::string id) { - this->id = id; -} - -FBA_metabolite::~FBA_metabolite() { - -} - -const std::string& FBA_metabolite::getId() const { - return this->id; -} - -void FBA_metabolite::setName(std::string value) { - this->name = value; -} - -const std::string& FBA_metabolite::getName() const { - return this->name; -} - - diff --git a/addons/dFBA/src/FBA_model.cpp b/addons/dFBA/src/FBA_model.cpp deleted file mode 100755 index 86d196897..000000000 --- a/addons/dFBA/src/FBA_model.cpp +++ /dev/null @@ -1,400 +0,0 @@ -/* - * flux_balance_model.cpp - * - * Created on: 11 jun. 2019 - * Author: mponce - */ - -#include "FBA_model.h" - - -/* Default model used to initialize the initial cell */ -FBA_model FBA_default_model; - - -FBA_model::FBA_model() -{ - this->id = "None"; - //this->lp_model = nullptr; - this->is_initialized = false; -} - -FBA_model::~FBA_model() { -for(FBA_reaction* rxn: this->reactions) - delete rxn; - - for(FBA_metabolite* met: this->metabolites) - delete met; - -// delete handler; -// delete lp_model; - -} - -const ClpSimplex* FBA_model::getLpModel() const -{ - return &this->lp_model; -} - -const int FBA_model::getNumReactions() -{ - - return this->reactions.size(); -} - -const int FBA_model::getNumMetabolites() -{ - return this->metabolites.size(); -} - -bool FBA_model::hasMetabolite(std::string mId) -{ - std::map::iterator itr; - itr = this->metaboliteIndexer.find(mId); - return itr != this->metaboliteIndexer.end(); -} - -void FBA_model::addMetabolite(FBA_metabolite* met) -{ - if (!this->hasMetabolite( met->getId() )) - { - this->metabolites.push_back(met); - this->metaboliteIndexer[met->getId()] = this->metabolites.size() - 1; - } - -} - -const FBA_metabolite* FBA_model::getMetabolite(std::string mId) -{ - if (this->hasMetabolite(mId)) - { - int idx = this->metaboliteIndexer[mId]; - return this->metabolites[idx]; - } - return nullptr; -} - -const std::vector FBA_model::getListOfMetabolites() const -{ - return this->metabolites; -} - -bool FBA_model::hasReaction(std::string rId) -{ - std::map::iterator it; - it = this->reactionsIndexer.find(rId); - - return it != this->reactionsIndexer.end(); -} - -FBA_reaction* FBA_model::getReaction(std::string rId) -{ - if (this->hasReaction(rId)) - { - int idx = this->reactionsIndexer[rId]; - FBA_reaction* rxn = this->reactions[idx]; - return rxn; - } - return nullptr; -} - - -float FBA_model::getReactionUpperBound(std::string rId) -{ - FBA_reaction* rxn = this->getReaction(rId); - return rxn->getUpperBound(); -} -void FBA_model::setReactionUpperBound(std::string rId, float upperBound) -{ - FBA_reaction* rxn = this->getReaction(rId); - if (rxn) - { - rxn->setUpperBound(upperBound); - int colIdx = this->reactionsIndexer[rId]; - this->lp_model.setColumnUpper(colIdx, upperBound); - } -} - -float FBA_model::getReactionLowerBound(std::string rId) -{ - FBA_reaction* rxn = this->getReaction(rId); - return rxn->getLowerBound(); -} -void FBA_model::setReactionLowerBound(std::string rId, float lowerBound) -{ - FBA_reaction* rxn = this->getReaction(rId); - if (rxn) - { - rxn->setLowerBound(lowerBound); - int colIdx = this->reactionsIndexer[rId]; - this->lp_model.setColumnLower(colIdx, lowerBound); - } - -} - - -void FBA_model::addReaction(FBA_reaction* rxn) -{ - if (!this->hasReaction( rxn->getId() )) - { - this->reactions.push_back(rxn); - this->reactionsIndexer[rxn->getId()] = this->reactions.size() - 1; - } -} - -const int FBA_model::getReactionIndex(std::string rId) -{ - if (this->hasReaction(rId)) - return this->reactionsIndexer[rId]; - else - return -1; -} - - -const std::vector FBA_model::getListOfReactions() const -{ - return this->reactions; -} - - -std::vector FBA_model::getListOfBoundaryReactions() -{ - std::vector listOfBoundarys; - for(FBA_reaction* reaction: this->reactions) - { - if (reaction->getNumberOfMetabolites() == 1) - { - listOfBoundarys.push_back(reaction); - } - } - return listOfBoundarys; -} - - -std::vector FBA_model::getListOfBoundaryReactionIds() -{ - std::vector listOfBoundaryIds; - for(FBA_reaction* reaction: this->reactions) - { - if (reaction->getNumberOfMetabolites() == 1) - { - listOfBoundaryIds.push_back(reaction->getId()); - } - } - return listOfBoundaryIds; -} - -void FBA_model::readSBMLModel(const char* sbmlFileName) -{ - SBMLReader reader; - SBMLDocument* document = reader.readSBML(sbmlFileName); - Model* model = document->getModel(); - - ListOfSpecies* listOfSpecies = model->getListOfSpecies(); - ListOfReactions* listOfFBA_reactions = model->getListOfReactions(); - ListOfParameters* listOfParameters = model->getListOfParameters(); - - this->id = model->getId(); - - for (unsigned int i = 0; i < model->getNumSpecies(); i++) - { - Species* species = listOfSpecies->get(i); - // Skipping boundary metabolites - if ( species->getBoundaryCondition() ) - continue; - - FBA_metabolite* metabolite = new FBA_metabolite(species->getId()); - metabolite->setName(species->getName()); - this->addMetabolite(metabolite); - } - - for(unsigned int i = 0; i < model->getNumReactions(); i++) - { - Reaction* sbml_reaction = listOfFBA_reactions->get(i); - - FBA_reaction* reaction = new FBA_reaction(sbml_reaction->getId()); - reaction->setName(sbml_reaction->getName()); - - FbcReactionPlugin* rxnFbc = static_cast (sbml_reaction->getPlugin("fbc")); - if ( rxnFbc ) - { - // Getting reaction's upper and lower bounds - const std::string lbId = rxnFbc->getLowerFluxBound(); - double lb = listOfParameters->get(lbId)->getValue(); - reaction->setLowerBound(lb); - - const std::string ubId = rxnFbc->getUpperFluxBound(); - double ub = listOfParameters->get(ubId)->getValue(); - reaction->setUpperBound(ub); - } - int numReactans = sbml_reaction->getNumReactants(); - for(int j = 0; j < numReactans; j++) - { - SpeciesReference* sbml_species = sbml_reaction->getReactant(j); - double stoich_coef = -1. * sbml_species->getStoichiometry(); - - if ( !this->hasMetabolite(sbml_species->getSpecies()) ) - { - FBA_metabolite* metabolite = new FBA_metabolite(sbml_species->getSpecies()); - metabolite->setName(sbml_species->getName()); - this->addMetabolite(metabolite); - } - const FBA_metabolite* metabolite = this->getMetabolite(sbml_species->getSpecies()); - if (metabolite != nullptr) - reaction->addMetabolite(metabolite, stoich_coef); - else - std::cout << "ERROR: FBA_metabolite " << sbml_species->getSpecies() << " not found" << std::endl; - } - - int numProducts = sbml_reaction->getNumProducts(); - for(int j = 0; j < numProducts; j++) - { - SpeciesReference* sbml_species = sbml_reaction->getProduct(j); - double stoich_coef = sbml_species->getStoichiometry(); - - if ( !this->hasMetabolite(sbml_species->getSpecies()) ) - { - FBA_metabolite* metabolite = new FBA_metabolite(sbml_species->getSpecies()); - metabolite->setName(sbml_species->getName()); - addMetabolite(metabolite); - } - const FBA_metabolite* metabolite = this->getMetabolite(sbml_species->getSpecies()); - if (metabolite != nullptr) - reaction->addMetabolite(metabolite, stoich_coef); - else - std::cout << "ERROR: FBA_metabolite " << sbml_species->getSpecies() << " not found" << std::endl; - } - this->addReaction(reaction); - } - - // The following code is intended to extract the objective function from the sbml using - // the FbcModelPlugin; then the coefficients are assigned to the corresponding reactions - FbcModelPlugin* mplugin = static_cast(model->getPlugin("fbc")); - ListOfObjectives* listOfObjectives = mplugin->getListOfObjectives(); - Objective* objective = mplugin->getObjective(listOfObjectives->getActiveObjective()); - ListOfFluxObjectives* listOfFluxObjectives = objective->getListOfFluxObjectives(); - - for(unsigned int i=0; i getNumFluxObjectives(); i++) - { - FluxObjective* fluxObjective = listOfFluxObjectives->get(i); - std::string rId = fluxObjective->getReaction(); - double objectiveCoefficient = fluxObjective->getCoefficient(); - FBA_reaction* reaction = this->getReaction(rId); - reaction->setObjectiveCoefficient(objectiveCoefficient); - } - - delete document; -} - -void FBA_model::initLpModel() -{ - - int n_rows = this->getNumMetabolites(); - int n_cols = this->getNumReactions(); - - //this->lp_model = new ClpSimplex(); - handler = new CoinMessageHandler(nullptr); - - handler->setLogLevel(0); - lp_model.passInMessageHandler(handler); - - CoinPackedMatrix matrix; - matrix.setDimensions(n_rows, 0); - - double* row_lb = new double[n_rows]; //the row lower bounds - double* row_ub = new double[n_rows]; //the row upper bounds - double* col_lb = new double[n_cols]; //the column lower bounds - double* col_ub = new double[n_cols]; //the column upper bounds - double* objective = new double[n_cols]; //the objective coefficients - - for(int i=0; i< n_rows; i++) - { - row_lb[i] = 0; - row_ub[i] = 0; - } - for(FBA_reaction* reaction: this->reactions) - { - int col_idx = this->reactionsIndexer[reaction->getId()]; - col_lb[col_idx] = reaction->getLowerBound(); - col_ub[col_idx] = reaction->getUpperBound(); - objective[col_idx] = reaction->getObjectiveCoefficient(); - - const std::map metabolites = reaction->getMetabolites(); - CoinPackedVector col; - for(auto it=metabolites.begin(); it!=metabolites.end(); it++) - { - const FBA_metabolite* metabolite = it->first; - double stoich_coeff = it->second; - int row_idx = this->metaboliteIndexer[metabolite->getId()]; - col.insert(row_idx, stoich_coeff); - } - matrix.appendCol(col); - } - - this->lp_model.loadProblem(matrix, col_lb, col_ub, objective, row_lb, row_ub); - this->lp_model.setOptimizationDirection(-1); - - delete col_lb; - delete col_ub; - delete row_lb; - delete row_ub; - delete objective; - - this->is_initialized = true; -} - -void FBA_model::initFBAmodel(const char* sbmlFileName) -{ - this->readSBMLModel(sbmlFileName); - this->initLpModel(); - -} - -void FBA_model::writeLp(const char *filename) -{ - this->lp_model.writeMps(filename); -} - -void FBA_model::runFBA() -{ - std::cout << "Running FBA... "; - this->lp_model.primal(); - if ( lp_model.isProvenOptimal() ) - { - double * columnPrimal = this->lp_model.primalColumnSolution(); - std::cout << "Optimal solution found!" << std::endl; - for(FBA_reaction* reaction: this->reactions) - { - int idx = this->reactionsIndexer[reaction->getId()]; - double v = columnPrimal[idx]; - reaction->setFluxValue(v); - } - } - else - { - for(FBA_reaction* reaction: this->reactions) - { reaction->setFluxValue(0.0); } - std::cout << "Primal infeasible" << std::endl; - } - -} - -bool FBA_model::getSolutionStatus() -{ - if (this->is_initialized) - return this->lp_model.isProvenOptimal(); - else - return false; -} - -float FBA_model::getObjectiveValue() -{ - assert(this->is_initialized); - if (this->lp_model.isProvenOptimal()) - return this->lp_model.getObjValue(); - else - std::cout << "WARNING: Primal infeasible" << std::endl; - return 0; -} - - diff --git a/addons/dFBA/src/FBA_model.h b/addons/dFBA/src/FBA_model.h deleted file mode 100755 index c45ce2013..000000000 --- a/addons/dFBA/src/FBA_model.h +++ /dev/null @@ -1,141 +0,0 @@ -/** - * \brief Wrapper class - * - * - * Created on 06/11/2019 - * M. Ponce-de-Leon, Barcelona Supercomputing Center - */ - -#ifndef __FBA_model_h__ -#define __FBA_model_h__ - -#include -#include -#include - -#include -#include - -#include -#include -#include - - -#include "FBA_metabolite.h" -#include "FBA_reaction.h" - -LIBSBML_CPP_NAMESPACE_USE - -class FBA_model -{ -private: - /** \brief Constraint-Based Model Class to perform FBA*/ - - std::string id; - - /** \brief vector of reaction objects*/ - std::vector metabolites; - - /** \brief map between metabolites' ids and metabolites' references **/ - std::map metaboliteIndexer; - - /** \brief vector of reaction objects*/ - std::vector reactions; - - /** \brief map between reaction IDs and reaction references */ - std::map< std::string, int> reactionsIndexer; - - /** \brief Coin CLP simplex model to encode the FBA problem**/ - ClpSimplex lp_model; - - CoinMessageHandler* handler; - - bool is_initialized = false; - -public: - - /** \brief Constructor */ - FBA_model(); - - /** \brief Destructor */ - ~FBA_model(); - - /** \brief Check if there is a metaboltie with a given ID*/ - bool hasMetabolite(std::string mId); - - /** \brief a metabolite pointer using a string Id*/ - const FBA_metabolite* getMetabolite(std::string mId); - - /** \brief Add new metabolite to the model*/ - void addMetabolite(FBA_metabolite* met); - - - /** \brief Check if there is a reaction with a given ID*/ - bool hasReaction(std::string rId); - - /** \brief Get a reaction pointer using string ID*/ - FBA_reaction* getReaction(std::string rId); - - /** \brief Add new reaction to the model*/ - void addReaction(FBA_reaction* rxn); - - /** \brief Get the integer index of a reaction*/ - const int getReactionIndex(std::string rId); - - /** \brief Get the upper bound of a reactions*/ - float getReactionUpperBound(std::string rId); - - /** \brief Set the upper bound of a reactions*/ - void setReactionUpperBound(std::string rId, float upperBound); - - /** \brief Get the upper bound of a reactions*/ - float getReactionLowerBound(std::string rId); - - /** \brief Set the lower bound of a reactions*/ - void setReactionLowerBound(std::string rId, float lowerBound); - - /** \brief Get the number of model reactions*/ - const int getNumReactions(); - - /** \brief Get the number of model metabolites*/ - const int getNumMetabolites(); - - /** \brief Get a metabolite pointer using s string Id*/ - const std::vector getListOfMetabolites() const; - - /** \brief Get the list of reaction pointers*/ - const std::vector getListOfReactions() const; - - /** \brief Get the list of reaction pointers*/ - std::vector getListOfBoundaryReactions(); - - /** \brief Get the list of IDs of the boundary reactions*/ - std::vector getListOfBoundaryReactionIds(); - - /** \brief Get the ClpSimplex model */ - const ClpSimplex* getLpModel() const; - - /** \brief Parse and read a metabolic model from a SBML file*/ - void readSBMLModel(const char* sbmlFileName); - - /** \brief Get the ClpSimplex model */ - void initLpModel(); - - void initFBAmodel(const char* sbmlFileName); - - /** \brief Write LP problem in MPS format */ - void writeLp(const char *filename); - - /** \brief Run FBA using primal method */ - void runFBA(); - - /** \brief Get solution status */ - bool getSolutionStatus(); - - /** \brief Get objective value */ - float getObjectiveValue(); -}; - - - -#endif diff --git a/addons/dFBA/src/FBA_reaction.cpp b/addons/dFBA/src/FBA_reaction.cpp deleted file mode 100755 index 551fff82d..000000000 --- a/addons/dFBA/src/FBA_reaction.cpp +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Reaction.cpp - * - * Created on: 13 jun. 2019 - * Author: mponce - */ - -#include "FBA_reaction.h" - -#include -#include -#include - -#include "FBA_metabolite.h" - - - -FBA_reaction::FBA_reaction(std::string id) -{ - this->id = id; - this->name = ""; - this->lowerBound = 0; - this->upperBound = 1000; - this->objectiveCoefficient = 0; - this->fluxValue = 0; -} - -FBA_reaction::~FBA_reaction() -{ - -} - -const std::string & FBA_reaction::getId() const -{ - return this->id; -} - -void FBA_reaction::setName(std::string name) -{ - this->name = name; -} - -const std::string & FBA_reaction::getName() const -{ - return this->name; -} - -void FBA_reaction::setLowerBound(double lowerBound) -{ - this->lowerBound = lowerBound; -} - -double FBA_reaction::getLowerBound() -{ - return this->lowerBound; -} - -void FBA_reaction::setUpperBound(double upperBound) -{ - this->upperBound = upperBound; -} - -double FBA_reaction::getUpperBound() -{ - return this->upperBound; -} - -void FBA_reaction::setObjectiveCoefficient(double ojectiveCoefficient) -{ - this->objectiveCoefficient = ojectiveCoefficient; -} - -double FBA_reaction::getObjectiveCoefficient() -{ - return this->objectiveCoefficient; -} - -void FBA_reaction::setFluxValue(double fluxValue) -{ - this->fluxValue = fluxValue; -} - -double FBA_reaction::getFluxValue() -{ - return this->fluxValue; -} - -int FBA_reaction::getNumberOfMetabolites() -{ - return this->metabolites.size(); -} - -const std::map < const FBA_metabolite *, double >&FBA_reaction::getMetabolites() const -{ - return this->metabolites; -} - -bool FBA_reaction::reversible() -{ - return lowerBound < 0; -} - -bool FBA_reaction::hasMetabolite(std::string mId) -{ - std::map < std::string, const FBA_metabolite *>::iterator it; - it = this->idMetaboliteMap.find(mId); - bool hasMetabolite = (it != this->idMetaboliteMap.end()); - return hasMetabolite; -} - - -void FBA_reaction::addMetabolite(const FBA_metabolite * met, double stoich) -{ - if (this->hasMetabolite(met->getId())) - { - this->metabolites[met] += stoich; - } - else - { - this->idMetaboliteMap[met->getId()] = met; - this->metabolites[met] = stoich; - } -} - -std::vector < std::string > FBA_reaction::getReactants() -{ - std::vector < std::string > reactants; - for (auto itr = this->metabolites.begin(); - itr != this->metabolites.end(); ++itr) - { - - const FBA_metabolite *met = itr->first; - double sotich = itr->second; - if (sotich < 0) - { - std::string mId = met->getId(); - reactants.push_back(mId); - } - } - - return reactants; -} - -std::vector < std::string > FBA_reaction::getProducts() -{ - std::vector < std::string > products; - for (auto itr = this->metabolites.begin(); - itr != this->metabolites.end(); ++itr) - { - - const FBA_metabolite *met = itr->first; - double sotich = itr->second; - if (sotich > 0) - { - std::string mId = met->getId(); - products.push_back(mId); - } - } - return products; -} - -double FBA_reaction::getStoichCoefficient(std::string mId) -{ - double coefficient = 0; - if (hasMetabolite(mId)) - { - const FBA_metabolite *met = this->idMetaboliteMap[mId]; - coefficient = this->metabolites[met]; - } - return coefficient; -} - -std::string FBA_reaction::getReactionString() -{ - std::vector < std::string > compounds; - std::string reactionString = ""; - - compounds = this->getReactants(); - for (unsigned int i = 0; i < compounds.size(); i++) - { - std::string & mId = compounds[i]; - const FBA_metabolite *met = this->idMetaboliteMap[mId]; - // Change the sign (-) of the coeeficient for printing - float coeff = -1. * this->getStoichCoefficient(mId); - if (int (coeff) == coeff) - reactionString += std::to_string(int (coeff)); - else - reactionString += std::to_string(coeff); - - //reactionString += " " + met->getName(); - reactionString += " " + met->getId(); - - if (i < compounds.size() - 1) - reactionString += " + "; - } - if (reversible()) - reactionString += " <==> "; - else - reactionString += " --> "; - - compounds = getProducts(); - for (unsigned int i = 0; i < compounds.size(); i++) - { - const std::string & mId = compounds[i]; - const FBA_metabolite *met = this->idMetaboliteMap[mId]; - float coeff = getStoichCoefficient(mId); - if (int (coeff) == coeff) - reactionString += std::to_string(int (coeff)); - else - reactionString += std::to_string(coeff); - - reactionString += " " + met->getId(); - - if (i < compounds.size() - 1) - reactionString += " + "; - } - return reactionString; -} diff --git a/addons/dFBA/src/FBA_reaction.h b/addons/dFBA/src/FBA_reaction.h deleted file mode 100755 index fd4951f2f..000000000 --- a/addons/dFBA/src/FBA_reaction.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Reaction.h - * - * Created on: 13 jun. 2019 - * Author: mponce - */ - -#ifndef SRC_REACTION_H_ -#define SRC_REACTION_H_ - -#include -#include -#include - -#include "FBA_metabolite.h" - -class FBA_reaction -{ - private: - std::string id; - std::string name; - - double lowerBound; - double upperBound; - double objectiveCoefficient; - double fluxValue; - - std::map metabolites; - std::map idMetaboliteMap; - - public: - FBA_reaction(std::string id); - ~FBA_reaction(); - - const std::string& getId() const; - - void setName(std::string name); - const std::string& getName() const; - - int getNumberOfMetabolites(); - - void setLowerBound(double lowerBound); - double getLowerBound(); - - void setUpperBound(double upperBound); - double getUpperBound(); - - void setObjectiveCoefficient(double ojectiveCoefficient); - double getObjectiveCoefficient(); - - void setFluxValue(double flux_value); - double getFluxValue(); - - bool reversible(); - bool hasMetabolite(std::string mId); - void addMetabolite(const FBA_metabolite* met, double stoich); - - std::vector getReactants(); - std::vector getProducts(); - double getStoichCoefficient(std::string mId); - - std::string getReactionString(); - - const std::map& getMetabolites() const; -}; - - - -#endif /* SRC_REACTION_H_ */ diff --git a/addons/dFBA/src/dfba_Metabolite.cpp b/addons/dFBA/src/dfba_Metabolite.cpp new file mode 100755 index 000000000..9524b6c8a --- /dev/null +++ b/addons/dFBA/src/dfba_Metabolite.cpp @@ -0,0 +1,42 @@ +/* + * Metabolite.cpp + * + * Created on: 13 jun. 2019 + * Author: mponce + */ + + +#include "dfba_Metabolite.h" + +#include +#include +#include + + + +dFBAMetabolite::dFBAMetabolite(std::string id) { + this->id = id; +} + +dFBAMetabolite::~dFBAMetabolite() { + +} + +dFBAMetabolite::dFBAMetabolite(const dFBAMetabolite& copy) { + this->id = copy.id; + this->name = copy.name; +} + +const std::string& dFBAMetabolite::getId() const { + return this->id; +} + +void dFBAMetabolite::setName(std::string value) { + this->name = value; +} + +const std::string& dFBAMetabolite::getName() const { + return this->name; +} + + diff --git a/addons/dFBA/src/FBA_metabolite.h b/addons/dFBA/src/dfba_Metabolite.h similarity index 73% rename from addons/dFBA/src/FBA_metabolite.h rename to addons/dFBA/src/dfba_Metabolite.h index 157d901d8..95a2d97dc 100755 --- a/addons/dFBA/src/FBA_metabolite.h +++ b/addons/dFBA/src/dfba_Metabolite.h @@ -12,15 +12,18 @@ #include #include -class FBA_metabolite +class dFBAMetabolite { private: std::string id; std::string name; public: - FBA_metabolite(std::string id); - ~FBA_metabolite(); + dFBAMetabolite(std::string id); + ~dFBAMetabolite(); + + /** \brief Copy */ + dFBAMetabolite(const dFBAMetabolite& copy); const std::string& getId() const; diff --git a/addons/dFBA/src/dfba_Model.cpp b/addons/dFBA/src/dfba_Model.cpp new file mode 100755 index 000000000..a18e96bfe --- /dev/null +++ b/addons/dFBA/src/dfba_Model.cpp @@ -0,0 +1,1138 @@ +/* + dFBAintracellular.cpp + * + * Created on: 11 jun. 2019 + * Author: mponce + */ + +#include "dfba_Model.h" +#include "dfba_Reaction.h" // Include the full definition + +#include +#include +#include + +/* Default dFBAModel used to initialize the initial cell */ +dFBAModel default_dFBAModel; + + +dFBAModel::dFBAModel() +{ + this->id = "none"; + this->is_initialized = false; + this->handler = NULL; + this->solution.status = "none"; +} + +dFBAModel::~dFBAModel() { + for(dFBAReaction* rxn: this->reactions) + delete rxn; + + for(dFBAMetabolite* met: this->metabolites) + delete met; + + if (this->handler != nullptr) + delete handler; + + if (this->pfba_handler != nullptr) + delete pfba_handler; +} + +dFBAModel::dFBAModel(const dFBAModel& copy) { + // Copy primitive members + this->id = copy.id; + this->is_initialized = copy.is_initialized; + this->track_flux_map = copy.track_flux_map; + this->pfba_enabled = copy.pfba_enabled; + this->pfba_fraction_of_optimum = copy.pfba_fraction_of_optimum; + + // Deep copy metabolites + for (auto original_metabolite : copy.metabolites) { + this->metabolites.push_back(new dFBAMetabolite(*original_metabolite)); + } + + // Deep copy reactions + for (auto original_reaction : copy.reactions) { + this->reactions.push_back(new dFBAReaction(*original_reaction)); + } + + // Copy maps + this->metaboliteIndexer = copy.metaboliteIndexer; + this->reactionsIndexer = copy.reactionsIndexer; + + // Copy solution (assuming it can be copied by value) + this->solution = copy.solution; + + // Handle ClpSimplex problem copying (depends on how `ClpSimplex` needs to be cloned) + if (copy.is_initialized) { + this->initProblem(); + if (this->pfba_enabled) this->buildParsimoniousProblem(); + } + + // Copy message handler + if (copy.handler != nullptr) { + this->handler = new CoinMessageHandler(*copy.handler); + } else { + this->handler = nullptr; + } +} + + + +dFBAModel& dFBAModel::operator=(const dFBAModel& other) { + if (this == &other) { + return *this; // Handle self-assignment + } + + // Clean up existing resources to prevent memory leaks + for (auto met : metabolites) { + delete met; + } + metabolites.clear(); + + for (auto rxn : reactions) { + delete rxn; + } + reactions.clear(); + + if (handler != nullptr) { + delete handler; + } + + // Copy primitive members + this->id = other.id; + this->is_initialized = other.is_initialized; + this->track_flux_map = other.track_flux_map; + this->pfba_enabled = other.pfba_enabled; + this->pfba_fraction_of_optimum = other.pfba_fraction_of_optimum; + + // Deep copy metabolites + for (auto original_metabolite : other.metabolites) { + this->metabolites.push_back(new dFBAMetabolite(*original_metabolite)); + } + + // Deep copy reactions + for (auto original_reaction : other.reactions) { + this->reactions.push_back(new dFBAReaction(*original_reaction)); + } + + // Copy other maps + this->metaboliteIndexer = other.metaboliteIndexer; + this->reactionsIndexer = other.reactionsIndexer; + + // Copy solution (assuming it can be copied by value) + this->solution = other.solution; + + // Handle ClpSimplex problem copying + if (other.is_initialized) { + this->initProblem(); + this->is_initialized = true; + if (this->pfba_enabled) this->buildParsimoniousProblem(); + } + + // Copy message handler + if (other.handler != nullptr) { + this->handler = new CoinMessageHandler(*other.handler); + } else { + this->handler = nullptr; + } + + return *this; +} + + + +void dFBAModel::clear(){ + + this->id = "none"; + this->is_initialized = false; + this->track_flux_map = false; + this->pfba_enabled = false; + this->pfba_fraction_of_optimum = 1.0; + this->pfba_built = false; + this->pfba_problem = ClpSimplex(); + this->pfba_handler = NULL; + this->pfba_pos_col.clear(); + this->pfba_neg_col.clear(); + this->reactions.clear(); + this->metabolites.clear(); + this->reactionsIndexer.clear(); + this->metaboliteIndexer.clear(); + this->solution.clear(); + this->problem = ClpSimplex(); + this->handler = NULL; + + return; +} + +const ClpSimplex* dFBAModel::getLpModel() const +{ + return &this->problem; +} + +const int dFBAModel::getNumReactions() +{ + return this->reactions.size(); +} + +const int dFBAModel::getNumMetabolites() +{ + return this->metabolites.size(); +} + +bool dFBAModel::hasMetabolite(const std::string& id) const +{ + return this->metaboliteIndexer.find(id) != this->metaboliteIndexer.end(); +} + +dFBAMetabolite* dFBAModel::addMetabolite(const std::string& id) +{ + // Check if metabolite already exists by its ID + auto it = this->metaboliteIndexer.find(id); + if (it == this->metaboliteIndexer.end()) { + // If metabolite does not exist, create it and add it to the model + dFBAMetabolite* newMet = new dFBAMetabolite(id); + this->metabolites.push_back(newMet); + this->metaboliteIndexer[id] = this->metabolites.size() - 1; + return newMet; + } else { + // If metabolite already exists, return the existing one + return this->metabolites[it->second]; + } +} + +dFBAMetabolite* dFBAModel::getMetabolite(const std::string& mId) const { + auto it = this->metaboliteIndexer.find(mId); + if (it != this->metaboliteIndexer.end()) { + int idx = it->second; + return this->metabolites[idx]; + } + return nullptr; // Return nullptr if metabolite doesn't exist +} + + + +const std::vector dFBAModel::getListOfMetabolites() const +{ + return this->metabolites; +} + +bool dFBAModel::hasReaction(std::string rId) +{ + std::map::iterator it; + it = this->reactionsIndexer.find(rId); + + return it != this->reactionsIndexer.end(); +} + +dFBAReaction* dFBAModel::getReaction(std::string rId) +{ + if (this->hasReaction(rId)) + { + int idx = this->reactionsIndexer[rId]; + dFBAReaction* rxn = this->reactions[idx]; + return rxn; + } + return nullptr; +} + + +double dFBAModel::getReactionUpperBound(std::string rId) +{ + dFBAReaction* rxn = this->getReaction(rId); + if (rxn) + return rxn->getUpperBound(); + else { + std::cerr << "Error: Reaction " << rId << " not found in getReactionUpperBound" << std::endl; + return 0.0; + } +} + +void dFBAModel::setReactionUpperBound(std::string rId, double upperBound) +{ + dFBAReaction* rxn = this->getReaction(rId); + if (rxn) + { + rxn->setUpperBound(upperBound); + int colIdx = this->reactionsIndexer[rId]; + this->problem.setColumnUpper(colIdx, upperBound); + } + else{ + std::cerr << "Reaction with ID " << rId << " not found in the model." << std::endl; + } +} + +double dFBAModel::getReactionLowerBound(std::string rId) +{ + dFBAReaction* rxn = this->getReaction(rId); + if (rxn) + return rxn->getLowerBound(); + else { + std::cerr << "Error: Reaction " << rId << " not found in getReactionLowerBound" << std::endl; + return 0.0; + } +} + +void dFBAModel::setReactionLowerBound(std::string rId, double lowerBound) +{ + dFBAReaction* rxn = this->getReaction(rId); + if (rxn) + { + rxn->setLowerBound(lowerBound); + int colIdx = this->reactionsIndexer[rId]; + this->problem.setColumnLower(colIdx, lowerBound); + } + else{ + std::cerr << "Reaction with ID " << rId << " not found in the model." << std::endl; + } + +} + +void dFBAModel::addReaction(dFBAReaction* rxn) +{ + if (this->is_initialized) { + std::cerr << "CRITICAL ERROR: Cannot add reactions after model initialization." << std::endl; + exit(-1); + } + if (!this->hasReaction( rxn->getId() )) + { + this->reactions.push_back(rxn); + this->reactionsIndexer[rxn->getId()] = this->reactions.size() - 1; + } +} + +const int dFBAModel::getReactionIndex(std::string rId) +{ + if (this->hasReaction(rId)) + return this->reactionsIndexer[rId]; + else + return -1; +} + +const std::vector dFBAModel::getListOfReactions() const +{ + return this->reactions; +} + +std::vector dFBAModel::getListOfBoundaryReactions() +{ + std::vector listOfBoundarys; + for(dFBAReaction* dFBAReaction: this->reactions) + { + if (dFBAReaction->getNumberOfMetabolites() == 1) + { + listOfBoundarys.push_back(dFBAReaction); + } + } + return listOfBoundarys; +} + +std::vector dFBAModel::getListOfBoundaryReactionIds() +{ + std::vector listOfBoundaryIds; + for(dFBAReaction* dFBAReaction: this->reactions) + { + if (dFBAReaction->getNumberOfMetabolites() == 1) + { + listOfBoundaryIds.push_back(dFBAReaction->getId()); + } + } + return listOfBoundaryIds; +} + +void dFBAModel::readSBMLModel(const char* sbmlFileName) +{ + SBMLReader reader; + SBMLDocument* document = reader.readSBML(sbmlFileName); + + // Check if the document was successfully read + if (document == nullptr) { + std::cerr << "Error: SBMLDocument is null for " << sbmlFileName << "\n"; + return; + } + + // Optional: run consistency checks (can add more warnings) + // document->checkConsistency(); + + const unsigned nFatal = document->getNumErrors(LIBSBML_SEV_FATAL); + const unsigned nError = document->getNumErrors(LIBSBML_SEV_ERROR); + const unsigned nWarn = document->getNumErrors(LIBSBML_SEV_WARNING); + + if (nFatal + nError > 0) { + std::cerr << "Failed to read SBML: " << sbmlFileName << "\n"; + document->printErrors(); // show why it failed + delete document; + return; + } + + // Non-fatal: show warnings but continue + if (nWarn) { + std::cerr << "Validation warning(s): " << nWarn << "\n"; + document->printErrors(); + } + + Model* model = document->getModel(); + + // Check if the model was successfully retrieved + if (model == nullptr) { + std::cerr << "Error: Model is null in SBML file: " << sbmlFileName << std::endl; + delete document; + return; + } + + ListOfSpecies* listOfSpecies = model->getListOfSpecies(); + ListOfReactions* listOfReactions = model->getListOfReactions(); + ListOfParameters* listOfParameters = model->getListOfParameters(); + + this->id = model->getId(); + + for (unsigned int i = 0; i < model->getNumSpecies(); i++) + { + Species* species = listOfSpecies->get(i); + // Skipping boundary dFBAMetabolites + if ( species->getBoundaryCondition() ) + continue; + + dFBAMetabolite* metabolite = new dFBAMetabolite(species->getId()); + metabolite->setName(species->getName()); + this->addMetabolite(metabolite->getId()); + } + + for(unsigned int i = 0; i < model->getNumReactions(); i++) + { + Reaction* sbml_reaction = listOfReactions->get(i); + + dFBAReaction* reaction = new dFBAReaction(sbml_reaction->getId()); + // std::cout << "Adding reaction with ID: " << reaction->getId() << std::endl; + reaction->setName(sbml_reaction->getName()); + + FbcReactionPlugin* rxnFbc = static_cast (sbml_reaction->getPlugin("fbc")); + if ( rxnFbc ) + { + // Getting dFBAReaction's upper and lower bounds + std::string lbId = rxnFbc->getLowerFluxBound(); + Parameter* lbParam = listOfParameters->get(lbId); + if (lbParam) { + double lb = lbParam->getValue(); + reaction->setLowerBound(lb); + } + else { + std::cout << "WARNING: Lower bound parameter " << lbId << " not found for reaction " << sbml_reaction->getId() << std::endl; + } + + std::string ubId = rxnFbc->getUpperFluxBound(); + Parameter* ubParam = listOfParameters->get(ubId); + if (ubParam) { + double ub = ubParam->getValue(); + reaction->setUpperBound(ub); + } + else { + std::cout << "WARNING: Upper bound parameter " << ubId << " not found for reaction " << sbml_reaction->getId() << std::endl; + } + } + else { + std::cout << "WARNING: No FBC plugin found for reaction " << sbml_reaction->getId() << std::endl; + } + int numReactans = sbml_reaction->getNumReactants(); + for(int j = 0; j < numReactans; j++) + { + SpeciesReference* sbml_species = sbml_reaction->getReactant(j); + double stoich_coef = -1. * sbml_species->getStoichiometry(); + + if ( !this->hasMetabolite(sbml_species->getSpecies()) ) + { + dFBAMetabolite* metabolite = new dFBAMetabolite(sbml_species->getSpecies()); + metabolite->setName(sbml_species->getName()); + this->addMetabolite(metabolite->getId()); + } + + const dFBAMetabolite* metabolite = this->getMetabolite(sbml_species->getSpecies()); + if (metabolite != nullptr) + reaction->addMetabolite(metabolite->getId(), stoich_coef); + else + std::cout << "ERROR: dFBAMetabolite " << sbml_species->getSpecies() << " not found" << std::endl; + } + + int numProducts = sbml_reaction->getNumProducts(); + for(int j = 0; j < numProducts; j++) + { + SpeciesReference* sbml_species = sbml_reaction->getProduct(j); + double stoich_coef = sbml_species->getStoichiometry(); + + if ( !this->hasMetabolite(sbml_species->getSpecies()) ) + { + dFBAMetabolite* metabolite = new dFBAMetabolite(sbml_species->getSpecies()); + metabolite->setName(sbml_species->getName()); + this->addMetabolite(metabolite->getId()); + } + const dFBAMetabolite* metabolite = this->getMetabolite(sbml_species->getSpecies()); + if (metabolite != nullptr) + reaction->addMetabolite(metabolite->getId(), stoich_coef); + else + std::cout << "ERROR: dFBAMetabolite " << sbml_species->getSpecies() << " not found" << std::endl; + } + this->addReaction(reaction); + } + + // The following code is intended to extract the objective function from the sbml using + // the FbcdFBAModelPlugin; then the coefficients are assigned to the corresponding dFBAReactions + FbcModelPlugin* mplugin = static_cast(model->getPlugin("fbc")); + if (mplugin) { + ListOfObjectives* listOfObjectives = mplugin->getListOfObjectives(); + Objective* objective = mplugin->getObjective(listOfObjectives->getActiveObjective()); + ListOfFluxObjectives* listOfFluxObjectives = objective->getListOfFluxObjectives(); + + for (unsigned int i = 0; i < listOfFluxObjectives->getNumFluxObjectives(); i++) + { + FluxObjective* fluxObjective = listOfFluxObjectives->get(i); + std::string rId = fluxObjective->getReaction(); + double objectiveCoefficient = fluxObjective->getCoefficient(); + dFBAReaction* dFBAReaction = this->getReaction(rId); + if (dFBAReaction) { + dFBAReaction->setObjectiveCoefficient(objectiveCoefficient); + } else { + std::cerr << "ERROR: dFBAReaction " << rId << " not found" << std::endl; + } + } + } + + delete document; +} + +void dFBAModel::initProblem() +{ + int n_rows = this->getNumMetabolites(); + int n_cols = this->getNumReactions(); + + // LP column i must be reaction i. Three places rely on it: the bounds and + // objective below are written at the indexer's position, the matrix columns + // are appended in vector order, and optimize() reads fluxes as + // columnPrimal[i] for reactions[i]. addReaction() keeps the two in step, so + // this only fires if something reorders or removes reactions without + // rebuilding the indexer, which would otherwise assign fluxes to the wrong + // reactions without any error. + for (size_t i = 0; i < this->reactions.size(); ++i) + { + std::map::const_iterator it = this->reactionsIndexer.find(this->reactions[i]->getId()); + if (it == this->reactionsIndexer.end() || it->second != static_cast(i)) + { + std::cerr << "ERROR: dFBAModel reaction order is inconsistent with its indexer at position " + << i << " (reaction '" << this->reactions[i]->getId() << "', indexer says " + << (it == this->reactionsIndexer.end() ? -1 : it->second) + << "). LP columns would no longer match reactions." << std::endl; + std::exit(1); + } + } + + this->handler = new CoinMessageHandler(nullptr); + // std::cout << "Initilizing LP problem n=" << n_rows << std::endl; + this->handler->setLogLevel(0); + this->problem.passInMessageHandler(this->handler); + + CoinPackedMatrix matrix; + matrix.setDimensions(n_rows, 0); + + double* row_lb = new double[n_rows]; //the row lower bounds + double* row_ub = new double[n_rows]; //the row upper bounds + double* col_lb = new double[n_cols]; //the column lower bounds + double* col_ub = new double[n_cols]; //the column upper bounds + double* objective = new double[n_cols]; //the objective coefficients + + for(int i=0; i< n_rows; i++) + { + row_lb[i] = 0; + row_ub[i] = 0; + } + for(dFBAReaction* rxn: this->reactions) + { + int col_idx = this->reactionsIndexer[rxn->getId()]; + //std::cout << "Adding reaction " << rxn->getId() << " with index: " << this->reactionsIndexer[rxn->getId()] << " to the LP problem" << std::endl; + col_lb[col_idx] = rxn->getLowerBound(); + col_ub[col_idx] = rxn->getUpperBound(); + objective[col_idx] = rxn->getObjectiveCoefficient(); + + const std::map& metabolites = rxn->getMetabolites(); + + CoinPackedVector col; + for (auto it = metabolites.begin(); it != metabolites.end(); ++it) { + const std::string& metId = it->first; // Get metabolite ID + double stoich_coeff = it->second; + + // Use the metabolite ID to get the index from metaboliteIndexer + auto idx_it = this->metaboliteIndexer.find(metId); + if (idx_it != this->metaboliteIndexer.end()) { + int row_idx = idx_it->second; + col.insert(row_idx, stoich_coeff); + } + } + matrix.appendCol(col); + } + + this->problem.loadProblem(matrix, col_lb, col_ub, objective, row_lb, row_ub); + this->problem.setOptimizationDirection(-1); + + this->problem.setPerturbation(50); // 50 is a standard default for perturbation. It means that the perturbation is always applied. 100 is the default value (automatic) + + delete[] col_lb; + delete[] col_ub; + delete[] row_lb; + delete[] row_ub; + delete[] objective; + + this->problem.setPrimalTolerance(1e-7); + this->problem.setDualTolerance(1e-7); + this->problem.scaling(3); + this->problem.setMaximumIterations(10000); + this->problem.setLogLevel(0); + this->problem.initialSolve(); + + this->is_initialized = true; +} + +void dFBAModel::initModel(const char* sbmlFileName) +{ + this->readSBMLModel(sbmlFileName); + std::cout << "SBML model correctly loaded: " << sbmlFileName << std::endl; + this->initProblem(); +} + +void dFBAModel::writeProblem(const char *filename) +{ + this->problem.writeLp(filename); +} + +dFBASolution dFBAModel::optimize() +{ + + this->problem.dual(); +/* // If Dual fails, try Primal (more robust from scratch) --> if numerical issue occurs, try primal to make sure the cell is really dead + if (this->problem.status() != 0) { + this->problem.primal(); + } */ + bool isOptimal = problem.isProvenOptimal(); + + solution.fluxes.clear(); + + // Reset the second-stage fields here, for EVERY outcome. They used to be + // set only inside the optimal branch, so an infeasible or unknown solve + // left whatever the previous call had put there -- a cell that failed + // stage one still reported secondary_status "optimal" and a stale + // total_absolute_flux from an earlier timestep. + solution.secondary_status = "none"; + solution.total_absolute_flux = 0.0; + + std::string status; + switch (this->problem.status()) { + case 0: + status = "optimal"; + break; + case 1: + case 4: + status = "infeasible"; + break; + default: + status = "unknown"; + std::cerr << "Solver returned unknown status code: " << this->problem.status() << std::endl; + break; + } + + if (isOptimal) + { + const double *columnPrimal = this->problem.getColSolution(); + + double fopt = problem.getObjValue(); + + // Column i of the LP is reactions[i]: addReaction() appends to both the + // vector and the indexer together, so the index equals the position and + // the string lookup this used to do was redundant. + // + // The string-keyed flux map is only populated on request. Building it + // here meant clearing and re-inserting one node per reaction on every + // solve -- for a cell solving every intracellular_dt, that was the + // majority of the cost of optimize() and nothing in the addon reads it. + const int n_rxn = (int)this->reactions.size(); + for(int i = 0; i < n_rxn; i++) + this->reactions[i]->setFluxValue(columnPrimal[i]); + + if (this->track_flux_map) + { + for(int i = 0; i < n_rxn; i++) + solution.fluxes[this->reactions[i]->getId()] = columnPrimal[i]; + } + + solution.objective_value = fopt; + solution.status = status; + + // The second optimisation stage, if one is configured, runs HERE -- + // inside optimize() -- and not in the caller. + // + // The reason is that the choice of flux distribution among equally + // optimal ones is a property of how this model is asked to be solved, + // not a step a caller should have to orchestrate. dFBAIntracellular, + // a custom_optimization function and the standalone tests all just call + // optimize() and get back the distribution the model was configured to + // produce. Nothing outside dFBAModel needs to know that two LPs were + // solved, which also means pFBA cannot be silently skipped by a code + // path that forgot to ask for it. + // + // What "consolidated" means precisely: + // objective_value stage one -- the growth rate. Never stage two's, + // which is a total flux. + // fluxes / reactions stage two, when it succeeded. + // secondary_status what the second stage did, so the caller CAN + // tell, even though it does not need to. + if (this->pfba_enabled) + { + this->solveParsimoniousStage(fopt); + + // total_absolute_flux always describes the distribution actually + // being returned, whether that came from stage two or -- when stage + // two failed -- from stage one. It is not "stage two's objective", + // which would be meaningless on the failure path. + solution.total_absolute_flux = this->getTotalAbsoluteFlux(); + + if (this->track_flux_map) + { + solution.fluxes.clear(); + for (int i = 0; i < n_rxn; i++) + solution.fluxes[this->reactions[i]->getId()] + = this->reactions[i]->getFluxValue(); + } + } + + // Debugging info + //std::cout << "Optimal solution found: Objective = " << fopt << "\n"; + } + else if (status == "infeasible") + { + solution.status = status; + + //std::cerr << "FBA optimization infeasible. Cell should die.\n"; + for (auto &reaction : this->reactions) + { + reaction->setFluxValue(0.0); + if (this->track_flux_map) + solution.fluxes[reaction->getId()] = 0.0; + } + this->solution.objective_value = 0.0; + } + else + { + solution.status = status; + + std::cerr << "ERROR: FBA optimization failed with unknown status!\n"; + std::cerr << "Reaction bounds at failure:\n"; + for (auto &reaction : this->reactions) + { + int idx = this->reactionsIndexer[reaction->getId()]; + double lb = reaction->getLowerBound(); + double ub = reaction->getUpperBound(); + std::cerr << reaction->getId() << ": [" << lb << ", " << ub << "]\n"; + reaction->setFluxValue(0.0); + } + } + + return solution; +} + + +bool dFBAModel::getSolutionStatus() +{ + if (this->is_initialized) + return this->problem.isProvenOptimal(); + else + return false; +} + +double dFBAModel::getObjectiveValue() +{ + assert(this->is_initialized); + if (this->problem.isProvenOptimal()) + return this->problem.getObjValue(); + else + std::cout << "WARNING: Primal infeasible" << std::endl; + return 0; +} + +bool dFBAModel::isInitialized(){ + return this->is_initialized; +} + +void dFBAModel::setObjectiveCoefficient(std::string rId, double coefficient) +{ + dFBAReaction* rxn = this->getReaction(rId); + if (rxn) + { + rxn->setObjectiveCoefficient(coefficient); + if (this->is_initialized) + { + int colIdx = this->reactionsIndexer[rId]; + this->problem.setObjectiveCoefficient(colIdx, coefficient); + } + } + else + { + std::cerr << "Reaction with ID " << rId << " not found in setObjectiveCoefficient." << std::endl; + } +} + +void dFBAModel::clearObjective() +{ + for(dFBAReaction* rxn: this->reactions) + { + rxn->setObjectiveCoefficient(0.0); + if (this->is_initialized) + { + int colIdx = this->reactionsIndexer[rxn->getId()]; + this->problem.setObjectiveCoefficient(colIdx, 0.0); + } + } +} + +void dFBAModel::deepCopy(const dFBAModel& source) { + // Clean up existing resources + for (auto met : metabolites) delete met; + metabolites.clear(); + for (auto rxn : reactions) delete rxn; + reactions.clear(); + if (handler != nullptr) { delete handler; handler = nullptr; } + + // Copy primitive members + id = source.id; + is_initialized = source.is_initialized; + track_flux_map = source.track_flux_map; + pfba_enabled = source.pfba_enabled; + pfba_fraction_of_optimum = source.pfba_fraction_of_optimum; + + // Deep copy metabolites + for (auto original_metabolite : source.metabolites) + metabolites.push_back(new dFBAMetabolite(*original_metabolite)); + + // Deep copy reactions + for (auto original_reaction : source.reactions) + reactions.push_back(new dFBAReaction(*original_reaction)); + + // Copy indexers and solution + metaboliteIndexer = source.metaboliteIndexer; + reactionsIndexer = source.reactionsIndexer; + solution = source.solution; + + // Copy saved objective state + original_objective_reaction = source.original_objective_reaction; + original_objective_coefficients = source.original_objective_coefficients; + + // Reinitialize LP problem if source was initialized + if (source.is_initialized) + { + initProblem(); + if (pfba_enabled) buildParsimoniousProblem(); + } + + // Copy message handler + if (source.handler != nullptr) + handler = new CoinMessageHandler(*source.handler); +} + +void dFBAModel::saveObjectiveState() { + original_objective_coefficients.clear(); + for (const auto* reaction : reactions) { + if (reaction->getObjectiveCoefficient() != 0.0) { + original_objective_coefficients[reaction->getId()] = reaction->getObjectiveCoefficient(); + } + } +} + +void dFBAModel::restoreObjectiveState() { + clearObjective(); + for (const auto& pair : original_objective_coefficients) { + setObjectiveCoefficient(pair.first, pair.second); + } +} + + +bool dFBAModel::getTrackFluxMap() const +{ + return this->track_flux_map; +} + +void dFBAModel::setTrackFluxMap(bool value) +{ + this->track_flux_map = value; +} + +std::map dFBAModel::getFluxDistribution() const +{ + // Built on demand from the reactions, which always hold the last solution. + // Prefer this to dFBASolution::getFluxes(), which is only filled when flux + // tracking is enabled. + std::map out; + for (dFBAReaction* rxn : this->reactions) + out[rxn->getId()] = rxn->getFluxValue(); + return out; +} + +// ===================================================================== +// Parsimonious FBA +// +// Stage one maximises the objective, as before. Stage two holds the +// objective at (a fraction of) that optimum and minimises the sum of +// absolute fluxes, which picks the enzyme-parsimonious distribution out of +// the set of equally optimal ones. +// +// |v| is not linear for a reversible reaction, so each reversible internal +// reaction i gets two non-negative columns and one row: +// +// v_i - p_i + m_i = 0, p_i >= 0, m_i >= 0 +// +// and the objective minimises sum(p_i + m_i) over those, plus v_i directly +// for irreversible internal reactions, which are non-negative already. +// +// Exchange reactions are excluded from both the splitting and the objective. +// They are auxiliary constructs coupling the model to the microenvironment, +// not enzymatic steps, so charging them an enzyme cost would be wrong; and +// leaving them unsplit means the flux a cell reports to BioFVM is still read +// from the same single column. +// ===================================================================== + +void dFBAModel::enableParsimonious(double fraction_of_optimum) +{ + this->pfba_enabled = true; + this->pfba_fraction_of_optimum = fraction_of_optimum; + if (this->is_initialized) + this->buildParsimoniousProblem(); +} + +void dFBAModel::buildParsimoniousProblem() +{ + const int n_rxn = (int)this->reactions.size(); + const int n_met = (int)this->metabolites.size(); + + this->pfba_pos_col.assign(n_rxn, -1); + this->pfba_neg_col.assign(n_rxn, -1); + + // Which reactions take part: internal (more than one metabolite), and of + // those, the reversible ones need splitting. + std::vector internal(n_rxn, false); + int n_split = 0; + for (int i = 0; i < n_rxn; i++) + { + internal[i] = this->reactions[i]->getNumberOfMetabolites() > 1; + if (internal[i] && this->reactions[i]->getLowerBound() < 0.0) + n_split++; + } + + const int n_cols = n_rxn + 2 * n_split; + const int n_rows = n_met + n_split; + + CoinPackedMatrix matrix; + matrix.setDimensions(n_rows, 0); + + std::vector col_lb(n_cols), col_ub(n_cols), obj(n_cols, 0.0); + std::vector row_lb(n_rows, 0.0), row_ub(n_rows, 0.0); + + // original columns, carrying the stoichiometry unchanged + int next_col = n_rxn, next_row = n_met; + for (int i = 0; i < n_rxn; i++) + { + dFBAReaction* rxn = this->reactions[i]; + col_lb[i] = rxn->getLowerBound(); + col_ub[i] = rxn->getUpperBound(); + + CoinPackedVector col; + const std::map& mets = rxn->getMetabolites(); + for (std::map::const_iterator it = mets.begin(); + it != mets.end(); ++it) + { + std::map::iterator idx = this->metaboliteIndexer.find(it->first); + if (idx != this->metaboliteIndexer.end()) + col.insert(idx->second, it->second); + } + + if (internal[i]) + { + if (rxn->getLowerBound() < 0.0) + { + // v_i appears in its own linking row with coefficient +1 + col.insert(next_row, 1.0); + this->pfba_pos_col[i] = next_col; + this->pfba_neg_col[i] = next_col + 1; + next_col += 2; + next_row++; + } + else + { + // already non-negative: minimise it directly + obj[i] = 1.0; + } + } + matrix.appendCol(col); + } + + // the split columns, appended in the same order + for (int i = 0; i < n_rxn; i++) + { + if (this->pfba_pos_col[i] < 0) continue; + const int row = n_met + (this->pfba_pos_col[i] - n_rxn) / 2; + + CoinPackedVector pos; pos.insert(row, -1.0); // v - p + m = 0 + CoinPackedVector neg; neg.insert(row, 1.0); + matrix.appendCol(pos); + matrix.appendCol(neg); + + const double big = std::max(std::fabs(this->reactions[i]->getLowerBound()), + std::fabs(this->reactions[i]->getUpperBound())); + col_lb[this->pfba_pos_col[i]] = 0.0; col_ub[this->pfba_pos_col[i]] = big; + col_lb[this->pfba_neg_col[i]] = 0.0; col_ub[this->pfba_neg_col[i]] = big; + obj[this->pfba_pos_col[i]] = 1.0; + obj[this->pfba_neg_col[i]] = 1.0; + } + + if (this->pfba_handler == nullptr) + { + this->pfba_handler = new CoinMessageHandler(nullptr); + this->pfba_handler->setLogLevel(0); + } + this->pfba_problem.passInMessageHandler(this->pfba_handler); + this->pfba_problem.loadProblem(matrix, col_lb.data(), col_ub.data(), obj.data(), + row_lb.data(), row_ub.data()); + this->pfba_problem.setOptimizationDirection(1); // minimise + this->pfba_problem.setPerturbation(50); + this->pfba_problem.setPrimalTolerance(1e-7); + this->pfba_problem.setDualTolerance(1e-7); + this->pfba_problem.scaling(3); + this->pfba_problem.setMaximumIterations(10000); + this->pfba_problem.setLogLevel(0); + this->pfba_problem.initialSolve(); + this->pfba_built = true; +} + +bool dFBAModel::solveParsimoniousStage(double stage_one_objective) +{ + if (!this->pfba_enabled) { this->solution.secondary_status = "none"; return false; } + if (!this->pfba_built) this->buildParsimoniousProblem(); + + const int n_rxn = (int)this->reactions.size(); + + // Mirror the current bounds, which update_dfba_inputs rewrites every step. + for (int i = 0; i < n_rxn; i++) + { + this->pfba_problem.setColumnLower(i, this->reactions[i]->getLowerBound()); + this->pfba_problem.setColumnUpper(i, this->reactions[i]->getUpperBound()); + } + + // Hold the objective at its stage-one value. The objective is a single + // reaction here, so this is a bound rather than an extra row. Both signs + // are handled: a maximised objective is floored, never capped below. + std::vector obj_cols; + for (int i = 0; i < n_rxn; i++) + if (this->reactions[i]->getObjectiveCoefficient() != 0.0) obj_cols.push_back(i); + + const double target = this->pfba_fraction_of_optimum * stage_one_objective; + for (size_t k = 0; k < obj_cols.size(); k++) + { + const int i = obj_cols[k]; + const double ub = this->reactions[i]->getUpperBound(); + // floor at the target but never above the reaction's own upper bound, + // which would make the problem trivially infeasible on rounding + this->pfba_problem.setColumnLower(i, std::min(target, ub)); + } + + // Primal simplex by default, and the choice barely matters. + // + // Theory favours the dual: between timesteps only this problem's bounds + // move -- the mirrored Michaelis-Menten bounds and the objective floor that + // tracks the stage-one optimum -- and a bound change leaves the previous + // basis dual-feasible, which is the textbook dual-simplex case. Measured + // over 1000 consecutive steps, 11 repetitions, median ms per stage-two + // solve (addons/dFBA/test, `make bench_pfba`): + // + // primal dual spread within one mode + // E.coli core 0.121 0.137 0.088 - 0.531 + // MCF7 core 0.438 0.462 0.343 - 0.964 + // + // Primal is ahead on the median of both models, but by less than the + // run-to-run spread, so this is not a measured win for either simplex -- + // only evidence that neither is a trap here. Both return the same flux + // distribution to the last digit, so correctness does not turn on it. + // Set DFBA_PFBA_DUAL=1 to switch, and re-measure on a genome-scale model + // before assuming this still holds: that is where a real difference, if + // there is one, would show up. + static const bool force_dual = (getenv("DFBA_PFBA_DUAL") != NULL + && std::string(getenv("DFBA_PFBA_DUAL")) == "1"); + if (force_dual) this->pfba_problem.dual(); + else this->pfba_problem.primal(); + + // Retry once from a clean basis before giving up. + // + // This problem is warm-started across timesteps, and after many re-solves + // with moving bounds Clp can fail with status 4 -- "stopped due to errors", + // a numerical failure in the factorization -- on a problem that is + // perfectly well posed. Measured on MCF7 core: 3 of 81 bound combinations + // failed when one model was reused across all of them, and 0 of 81 when + // each was given a fresh model. Same bounds, same fraction_of_optimum, so + // the cause is accumulated basis state, not the model or the constraint. + // + // Discarding the basis and re-solving is the standard remedy. It costs a + // cold solve, but only on the rare failure, and the alternative is a cell + // silently keeping its degenerate stage-one fluxes -- which is exactly the + // cell most in need of pFBA, since these failures cluster where the flux + // distribution is worst conditioned. + if (!this->pfba_problem.isProvenOptimal()) + { + this->pfba_problem.allSlackBasis(true); + if (force_dual) this->pfba_problem.dual(); + else this->pfba_problem.primal(); + } + + if (!this->pfba_problem.isProvenOptimal()) + { + // Still no. The stage-one fluxes are left untouched on purpose: they + // are still optimal, so the cell carries on with them. The status is + // kept so the failure is visible to whoever is debugging. + this->solution.secondary_status = + (this->pfba_problem.status() == 1) ? "infeasible" : "unknown"; + + // Warned ONCE per process, not once per cell per timestep. This runs + // inside the OpenMP-parallel per-cell update, where routine output is + // forbidden -- a per-occurrence message would be both a race and a + // flood. The same once-only-under-critical idiom is used for undeclared + // custom_data names in the sample projects. + static bool warned = false; + #pragma omp critical(dfba_pfba_stage_two_warn) + { + if (!warned) + { + warned = true; + std::cout << "WARNING: the parsimonious second stage did not solve (" + << this->solution.secondary_status << ")." << std::endl + << " The cell keeps its stage-one fluxes, which are" + << " still optimal, and growth is unaffected." << std::endl + << " This is a solver outcome, not cell death." + << " Reported once per run; dFBASolution::secondary_status" + << " carries it per cell." << std::endl; + } + } + return false; + } + + // The original columns already hold the unified flux: the linking row + // v - p + m = 0 forces column i to equal p - m at every feasible point, so + // there is nothing to subtract here. Reading v directly is also immune to + // the degenerate case where perturbation leaves both p and m slightly + // nonzero, which an explicit p - m would inherit. + const double* primal = this->pfba_problem.getColSolution(); + for (int i = 0; i < n_rxn; i++) + this->reactions[i]->setFluxValue(primal[i]); + + this->solution.secondary_status = "optimal"; + return true; +} + +double dFBAModel::getTotalAbsoluteFlux() const +{ + double total = 0.0; + for (size_t i = 0; i < this->reactions.size(); i++) + if (this->reactions[i]->getNumberOfMetabolites() > 1) + total += std::fabs(this->reactions[i]->getFluxValue()); + return total; +} diff --git a/addons/dFBA/src/dfba_Model.h b/addons/dFBA/src/dfba_Model.h new file mode 100755 index 000000000..4fb07ce73 --- /dev/null +++ b/addons/dFBA/src/dfba_Model.h @@ -0,0 +1,299 @@ +/** + * \brief Wrapper class + * + * + * Created on 06/11/2019 + * M. Ponce-de-Leon, Barcelona Supercomputing Center + */ + +#ifndef __FBA_model_h__ +#define __FBA_model_h__ + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "dfba_Metabolite.h" +#include "dfba_Solution.h" + +LIBSBML_CPP_NAMESPACE_USE + +// Forward declaration of dFBAReaction +class dFBAReaction; + +class dFBAModel +{ + private: + /** \brief Model ID */ + std::string id; + + /** \brief vector of metabolite objects*/ + std::vector metabolites; + + /** \brief Saved objective reaction ID for restore */ + std::string original_objective_reaction; + + /** \brief Saved objective coefficients for restore */ + std::map original_objective_coefficients; + + /** \brief map between metabolites' ids and metabolites' references **/ + std::map metaboliteIndexer; + + /** \brief vector of reaction objects*/ + std::vector reactions; + + /** \brief map between reaction IDs and reaction references */ + std::map< std::string, int> reactionsIndexer; + + /** \brief solution */ + dFBASolution solution; + + /** \brief Coin CLP simplex model to encode the FBA problem**/ + ClpSimplex problem; + + CoinMessageHandler* handler; + + bool is_initialized = false; + + /** + * \brief whether optimize() also fills dFBASolution::fluxes. + * + * Off by default. Populating that string-keyed map costs one map + * insertion per reaction on every solve, which dominated optimize() + * for cells solving at every intracellular step. Nothing in the addon + * reads it; use getFluxDistribution() instead, which builds the same + * map on demand from the reactions. + */ + bool track_flux_map = false; + + // ---------------- parsimonious FBA (second stage) ---------------- + // + // pFBA answers the alternative-optima problem: standard FBA fixes the + // objective value but not the flux distribution, and in dFBA the flux + // distribution is what couples the cell to BioFVM. The second stage + // pins the objective at its optimum and then minimises total flux, + // which selects the enzyme-parsimonious solution among the equally + // optimal ones. + // + // It is held as a SEPARATE ClpSimplex rather than by reformulating the + // existing one, so that a run with pFBA disabled solves exactly the + // problem it solved before: byte-identical results, no silent change + // to anyone's existing simulations. + + /** \brief true once the second-stage problem has been built */ + bool pfba_enabled = false; + + /** \brief objective must stay at least this fraction of its optimum */ + double pfba_fraction_of_optimum = 1.0; + + /** \brief the second-stage LP: original columns plus split columns */ + ClpSimplex pfba_problem; + CoinMessageHandler* pfba_handler = nullptr; + bool pfba_built = false; + + /** + * \brief for each reaction, its two split columns in the second-stage + * problem, or -1 when the reaction is not split. + * + * Only reversible INTERNAL reactions are split. Exchange reactions are + * excluded deliberately: they are auxiliary constructs linking the model + * to BioFVM rather than enzymatic steps, they carry no gene and no + * enzyme cost, and leaving them unsplit keeps the flux-to-net_export + * mapping reading the same single column it always did. + */ + std::vector pfba_pos_col; + std::vector pfba_neg_col; + + /** \brief build the second-stage problem from the current model */ + void buildParsimoniousProblem(); + + /** + * \brief Run the second stage: minimise total flux with the objective + * held at stage_one_objective. + * + * Private, and optimize() is the only caller. There is deliberately no + * second dFBASolution: this writes the parsimonious fluxes onto the + * reactions and sets solution.secondary_status in place, so there is + * only ever one solution object and it can only mean one thing. An + * earlier version returned its own solution whose objective_value was a + * total flux rather than a growth rate, which was a trap for any caller + * that read it as growth. + * + * \return true if the second stage solved. On false the stage-one + * fluxes are left in place -- they are still optimal -- and + * secondary_status records why. A solver failure here is not a + * biological outcome and must never be read as cell death. + */ + bool solveParsimoniousStage(double stage_one_objective); + + public: + + /** \brief Constructor */ + dFBAModel(); + + /** \brief Destructor */ + ~dFBAModel(); + + /** \brief Copy */ + dFBAModel(const dFBAModel& copy); + + /** \brief new assignment operator */ + dFBAModel& operator=(const dFBAModel& other); + + /** \brief Clear function */ + void clear(); + + /** \brief Check if the model is initialized*/ + bool isInitialized(); + + /** \brief Check if there is a metaboltie with a given ID*/ + bool hasMetabolite(const std::string& id) const; + + /** \brief a metabolite pointer using a string Id*/ + dFBAMetabolite* getMetabolite(const std::string& mId) const; + + /** \brief Add new metabolite to the model*/ + dFBAMetabolite* addMetabolite(const std::string& id); + + + /** \brief Check if there is a reaction with a given ID*/ + bool hasReaction(std::string rId); + + /** \brief Get a reaction pointer using string ID*/ + dFBAReaction* getReaction(std::string rId); + + /** \brief Add new reaction to the model*/ + void addReaction(dFBAReaction* rxn); + + /** \brief Get the integer index of a reaction*/ + const int getReactionIndex(std::string rId); + + /** \brief Get the upper bound of a reactions*/ + double getReactionUpperBound(std::string rId); + + /** \brief Set the upper bound of a reactions*/ + void setReactionUpperBound(std::string rId, double upperBound); + + /** \brief Get the upper bound of a reactions*/ + double getReactionLowerBound(std::string rId); + + /** \brief Set the lower bound of a reactions*/ + void setReactionLowerBound(std::string rId, double lowerBound); + + /** \brief Get the number of model reactions*/ + const int getNumReactions(); + + /** \brief Get the number of model metabolites*/ + const int getNumMetabolites(); + + /** \brief Get a metabolite pointer using s string Id*/ + const std::vector getListOfMetabolites() const; + + /** \brief Get the list of reaction pointers*/ + const std::vector getListOfReactions() const; + + /** \brief Get the list of reaction pointers*/ + std::vector getListOfBoundaryReactions(); + + /** \brief Get the list of IDs of the boundary reactions*/ + std::vector getListOfBoundaryReactionIds(); + + /** \brief Get the ClpSimplex model */ + const ClpSimplex* getLpModel() const; + + /** \brief Parse and read a metabolic model from a SBML file*/ + void readSBMLModel(const char* sbmlFileName); + + /** \brief Initialize the ClpSimplex model */ + void initProblem(); + + /** \brief Initialize the CBM model and the LP problem */ + void initModel(const char* sbmlFileName); + + /** \brief Write problem in LP format */ + void writeProblem(const char *filename); + + /** + * \brief Solve the model and return its flux distribution. + * + * This is the ONE entry point callers need. If a second optimisation + * stage has been configured with enableParsimonious(), optimize() + * performs both stages and returns a consolidated solution; nothing + * outside this class has to know, or can forget, that it happened. + * + * The consolidated solution: + * + * status stage one's. A cell lives or dies on whether the + * FBA problem itself was feasible. + * objective_value stage one's -- the growth rate. Stage two + * minimises total flux, so its objective is NOT a + * growth rate and never lands here. + * fluxes, and the flux value on every reaction + * stage two's, when it succeeded; otherwise stage + * one's, which are still optimal. + * secondary_status "none" when no second stage is configured, else + * what it did. + * total_absolute_flux total |flux| over the internal reactions of + * the distribution actually being returned -- stage + * two's when it solved, stage one's when it did + * not. Never "stage two's objective", which would + * be meaningless on the failure path. + * + * With no second stage configured this solves exactly the problem it + * always solved, and returns bit-identical results. + */ + dFBASolution optimize(); + + /** \brief Get solution status */ + std::string getId() { return this->id; }; + + /** \brief Get solution status */ + bool getSolutionStatus(); + + /** \brief Get objective value */ + double getObjectiveValue(); + + dFBASolution getSolution(){ return this->solution; } + + /** \brief Flux of every reaction from the last solve, built on demand */ + std::map getFluxDistribution() const; + + /** \brief Whether optimize() also fills dFBASolution::fluxes (default false) */ + bool getTrackFluxMap() const; + void setTrackFluxMap(bool value); + + // ================ parsimonious FBA ================ + + /** + * \brief Enable the parsimonious second stage. + * + * \param fraction_of_optimum the objective is held at or above this + * fraction of its stage-one optimum while total flux is + * minimised. 1.0 asks for the exact optimal face. + */ + void enableParsimonious(double fraction_of_optimum = 1.0); + bool isParsimoniousEnabled() const { return this->pfba_enabled; } + double getParsimoniousFraction() const { return this->pfba_fraction_of_optimum; } + + /** \brief Sum of absolute fluxes over the reactions pFBA minimises */ + double getTotalAbsoluteFlux() const; + + void setObjectiveCoefficient(std::string rId, double coefficient); + void clearObjective(); + + // Deep copy from another model (cleans up existing data first) + void deepCopy(const dFBAModel& source); + + // Save/restore objective function state + void saveObjectiveState(); + void restoreObjectiveState(); +}; + +#endif diff --git a/addons/dFBA/src/dfba_Reaction.cpp b/addons/dFBA/src/dfba_Reaction.cpp new file mode 100755 index 000000000..4e248b09e --- /dev/null +++ b/addons/dFBA/src/dfba_Reaction.cpp @@ -0,0 +1,214 @@ +/* + * dfba_Reaction.cpp + * + * Created on: 13 jun. 2019 + * Author: mponce + */ + +#include "dfba_Reaction.h" +#include "dfba_Model.h" // Include the full definition + +dFBAReaction::dFBAReaction(std::string id) +{ + this->id = id; + this->name = ""; + this->lowerBound = 0; + this->upperBound = 1000; + this->objectiveCoefficient = 0; + this->fluxValue = 0; +} + +dFBAReaction::~dFBAReaction() { +} + + + +dFBAReaction::dFBAReaction(const dFBAReaction& copy) { + // Copy primitive members + this->id = copy.id; + this->name = copy.name; + this->lowerBound = copy.lowerBound; + this->upperBound = copy.upperBound; + this->objectiveCoefficient = copy.objectiveCoefficient; + this->fluxValue = copy.fluxValue; + this->metabolites = copy.metabolites; +} + + + + +const std::string & dFBAReaction::getId() const +{ + return this->id; +} + +void dFBAReaction::setName(std::string name) +{ + this->name = name; +} + +const std::string & dFBAReaction::getName() const +{ + return this->name; +} + +void dFBAReaction::setLowerBound(double lowerBound) +{ + this->lowerBound = lowerBound; +} + +double dFBAReaction::getLowerBound() +{ + return this->lowerBound; +} + +void dFBAReaction::setUpperBound(double upperBound) +{ + this->upperBound = upperBound; +} + +double dFBAReaction::getUpperBound() +{ + return this->upperBound; +} + +void dFBAReaction::setObjectiveCoefficient(double ojectiveCoefficient) +{ + this->objectiveCoefficient = ojectiveCoefficient; +} + +double dFBAReaction::getObjectiveCoefficient() const +{ + return this->objectiveCoefficient; +} + +void dFBAReaction::setFluxValue(double fluxValue) +{ + this->fluxValue = fluxValue; +} + +double dFBAReaction::getFluxValue() +{ + return this->fluxValue; +} + +int dFBAReaction::getNumberOfMetabolites() const { + return this->metabolites.size(); +} + +const std::map& dFBAReaction::getMetabolites() const { + return this->metabolites; +} + +bool dFBAReaction::reversible() const +{ + return lowerBound < 0; +} + +bool dFBAReaction::hasMetabolite(const std::string& mId) const +{ + return this->metabolites.find(mId) != this->metabolites.end(); +} + +void dFBAReaction::addMetabolite(const std::string& mId, double stoich) { + auto it = metabolites.find(mId); + if (it != metabolites.end()) { + // If the metabolite is already involved, just update the stoichiometric coefficient + it->second += stoich; + } else { + // Otherwise, add a new metabolite with its stoichiometric coefficient + metabolites[mId] = stoich; + } +} + + +std::vector dFBAReaction::getReactants() const +{ + std::vector reactants; + + for (const auto& metabolite : this->metabolites) + { + const std::string& mId = metabolite.first; + double stoich = metabolite.second; + + if (stoich < 0) // Reactants have negative stoichiometric coefficients + { + reactants.push_back(mId); + } + } + + return reactants; +} + + +std::vector dFBAReaction::getProducts() const +{ + std::vector products; + + for (const auto& metabolite : this->metabolites) + { + const std::string& mId = metabolite.first; + double stoich = metabolite.second; + + if (stoich > 0) // Products have positive stoichiometric coefficients + { + products.push_back(mId); + } + } + + return products; +} + + +double dFBAReaction::getStoichCoefficient(const std::string& mId) const +{ + // Check if the metabolite exists in the map + auto it = this->metabolites.find(mId); + if (it != this->metabolites.end()) + { + return it->second; // Return the stoichiometric coefficient if found + } + + return 0.0; // Return 0 if metabolite is not found +} + +std::string dFBAReaction::getReactionString(const dFBAModel& model) const { + std::vector compounds; + std::string reactionString = ""; + + // Get reactants + compounds = this->getReactants(); + for (unsigned int i = 0; i < compounds.size(); i++) { + const std::string& mId = compounds[i]; + const dFBAMetabolite* met = model.getMetabolite(mId); // Accessing the actual metabolite + + // Update reaction string using the metabolite details + if (met) { + double coeff = -1.0 * this->getStoichCoefficient(mId); + reactionString += std::to_string(coeff) + " " + met->getId(); + if (i < compounds.size() - 1) { + reactionString += " + "; + } + } + } + + reactionString += reversible() ? " <==> " : " --> "; + + // Get products (similar approach) + compounds = getProducts(); + for (unsigned int i = 0; i < compounds.size(); i++) { + const std::string& mId = compounds[i]; + const dFBAMetabolite* met = model.getMetabolite(mId); + + if (met) { + double coeff = this->getStoichCoefficient(mId); + reactionString += std::to_string(coeff) + " " + met->getId(); + if (i < compounds.size() - 1) { + reactionString += " + "; + } + } + } + + return reactionString; +} + diff --git a/addons/dFBA/src/dfba_Reaction.h b/addons/dFBA/src/dfba_Reaction.h new file mode 100755 index 000000000..fd90bb849 --- /dev/null +++ b/addons/dFBA/src/dfba_Reaction.h @@ -0,0 +1,79 @@ +/* + * dfba_Reaction.h + * + * Created on: jun. 2022 + * Author: mponce + */ + +#ifndef SRC_REACTION_H_ +#define SRC_REACTION_H_ + +#include +#include +#include +#include + +#include "dfba_Metabolite.h" + +// Forward declaration of dFBAModel +class dFBAModel; + +class dFBAReaction +{ + private: + std::string id; + std::string name; + + double lowerBound; + double upperBound; + double objectiveCoefficient; + double fluxValue; + + // Map of metabolite IDs to stoichiometric coefficients + std::map metabolites; + + public: + /** \brief Constructor */ + dFBAReaction(std::string id); + + /** \brief Destructor */ + ~dFBAReaction(); + + /** \brief Copy */ + dFBAReaction(const dFBAReaction& copy); + + const std::string& getId() const; + + void setName(std::string name); + const std::string& getName() const; + + int getNumberOfMetabolites() const; + + void setLowerBound(double lowerBound); + double getLowerBound(); + + void setUpperBound(double upperBound); + double getUpperBound(); + + void setObjectiveCoefficient(double ojectiveCoefficient); + double getObjectiveCoefficient() const; + + void setFluxValue(double flux_value); + double getFluxValue(); + + bool reversible() const; + bool hasMetabolite(const std::string& mId) const; + void addMetabolite(const std::string& mId, double stoich); + + std::vector getReactants() const; + std::vector getProducts() const; + double getStoichCoefficient(const std::string& mId) const; + + std::string getReactionString(const dFBAModel& model) const; + + const std::map& getMetabolites() const; +}; + + + +#endif /* SRC_REACTION_H_ */ diff --git a/addons/dFBA/src/dfba_Solution.cpp b/addons/dFBA/src/dfba_Solution.cpp new file mode 100644 index 000000000..a46b2d803 --- /dev/null +++ b/addons/dFBA/src/dfba_Solution.cpp @@ -0,0 +1,44 @@ +/* + * dfba_Reaction.cpp + * + * Created on: jun. 2022 + * Author: mponce + */ + +#include "dfba_Solution.h" + +#include +#include +#include + + +dFBASolution::dFBASolution() +{ + this->objective_value = 0; + this->status = "none"; + this->secondary_status = "none"; + this->total_absolute_flux = 0; +} + +dFBASolution::dFBASolution(double objective_value, std::string status, std::map fluxes) +{ + this->objective_value = objective_value; + this->status = status; + this->fluxes = fluxes; + this->secondary_status = "none"; + this->total_absolute_flux = 0; +} + +dFBASolution::~dFBASolution() +{ + +} +void dFBASolution::clear() +{ + this->objective_value = 0; + this->status = "none"; + this->fluxes.clear(); + this->reduced_costs.clear(); + this->secondary_status = "none"; + this->total_absolute_flux = 0; +} diff --git a/addons/dFBA/src/dfba_Solution.h b/addons/dFBA/src/dfba_Solution.h new file mode 100644 index 000000000..2310f9ef3 --- /dev/null +++ b/addons/dFBA/src/dfba_Solution.h @@ -0,0 +1,58 @@ +/* + * dfba_Reaction.h + * + * Created on: jun. 2022 + * Author: mponce + */ + +#ifndef SRC_SOLUTION_H_ +#define SRC_SOLUTION_H_ + +#include +#include +#include + + +class dFBASolution +{ + private: + /** \brief Constraint-Based Model Class that contains a solution*/ + + + public: + + double objective_value; + std::string status; + std::map fluxes; + std::map reduced_costs; + + /** + * \brief outcome of the optional second optimisation stage. + * + * "none" when no second stage is configured -- the overwhelmingly + * common case, and what every pre-pFBA configuration reports. + * "optimal" when it ran and selected a flux distribution. + * "infeasible" / "unknown" when it ran and failed, in which case the + * fluxes are still the stage-one ones and still optimal. + * + * objective_value ALWAYS comes from stage one. Stage two minimises + * total flux, so reporting its objective here would hand callers a sum + * of fluxes where they expect growth. The second stage's own objective + * is in total_absolute_flux instead. + */ + std::string secondary_status; + double total_absolute_flux; + + dFBASolution(); + dFBASolution(double objective_value, std::string status, std::map fluxes); + ~dFBASolution(); + + void clear(); + + double getObjectiveValue(){ return this->objective_value; } + std::string getStatus(){ return this->status; }; + std::map getFluxes(){ return fluxes; }; + +}; + +#endif \ No newline at end of file diff --git a/addons/dFBA/src/dfba_intracellular.cpp b/addons/dFBA/src/dfba_intracellular.cpp index 415f75b6b..00c7deec9 100644 --- a/addons/dFBA/src/dfba_intracellular.cpp +++ b/addons/dFBA/src/dfba_intracellular.cpp @@ -1,79 +1,225 @@ +#include "dfba_Reaction.h" +#include "dfba_Model.h" #include "dfba_intracellular.h" + #include #include +#include +#include +#include +#include -dFBAIntracellular::dFBAIntracellular() : Intracellular() + +namespace PhysiCelldFBA { + +// --------------------------------------------------------------------------- +// Per-voxel uptake demand, used to share a voxel's substrate between the cells in it. +// +// Each cell's uptake bound is capped by what its voxel holds, but several cells +// can share one diffusion voxel, and BioFVM applies the sum of their net export +// rates. Capping every cell against the whole voxel therefore lets n cells draw +// n times its content and drives the concentration negative. The cap is made +// proportional instead: every cell is scaled by +// +// min(1, content / sum over live cells in the voxel of their request), +// +// so the voxel's total uptake over one dFBA window cannot exceed its content. +// Cells that cannot take the substrate (Vmax = 0) request nothing and so do not +// dilute the others. With one cell this reduces to the previous per-cell cap. +// +// The table is shared by all cells, so it is built once per simulated time by +// the first cell to need it, under a named critical section, and is read-only +// afterwards. No cell can write its own rates before it has passed the check +// at the top of update_dfba_inputs, so the build sees a consistent snapshot. +// +// It assumes every live cell updates at every step, i.e. intracellular_dt equals +// the diffusion dt. If it does not, cells that are not updating still draw on +// their persisted rates, which the request sum does not include. +// --------------------------------------------------------------------------- +namespace { + +// Dry weight of a cell in grams, from its current volume. +inline double dry_mass_in_grams(const dFBAIntracellular& model, const PhysiCell::Phenotype& phenotype) { - this->intracellular_type = "dfba"; + const double solid_volume = phenotype.volume.total * (1.0 - phenotype.volume.fluid_fraction); // um^3 + return solid_volume * model.cell_density * 1e-12; // um^3 * pg/um^3 -> pg -> g } -dFBAIntracellular::dFBAIntracellular(pugi::xml_node& node) +// Michaelis-Menten uptake bound, mmol / gDW / h, for a non-negative concentration in mM. +inline double michaelis_menten_uptake_rate(const ExchangeFluxData& exchange, double concentration) { - intracellular_type = "dfba"; - this->initialize_intracellular_from_pugixml(node); + return (exchange.Vmax.value * concentration) / (exchange.Km.value + concentration); } -dFBAIntracellular::dFBAIntracellular(dFBAIntracellular* copy) +// Amount that rate would remove over one dFBA window for a cell of the given dry weight, in mmol. +inline double uptake_over_window_mmol(const dFBAIntracellular& model, double rate_mmol_per_gdw_per_h, double dry_mass_g) { - intracellular_type = copy->intracellular_type; - sbml_filename = copy->sbml_filename; - parameters = copy->parameters; - // model = copy->model; - model.readSBMLModel(copy->sbml_filename.c_str()); - model.initLpModel(); - model.runFBA(); + return rate_mmol_per_gdw_per_h * model.dfba_time_step * hours_to_minutes * dry_mass_g; } - -void dFBAIntracellular::initialize_intracellular_from_pugixml(pugi::xml_node& node) +struct VoxelUptakeDemand { + std::vector mmol; // [voxel * n_densities + density_index] + std::size_t n_densities = 0; + std::atomic built_at{-1.0}; // simulated time the table describes +}; - pugi::xml_node node_sbml = node.child( "sbml_filename" ); +VoxelUptakeDemand g_voxel_demand; - if ( node_sbml ) - { - this->sbml_filename = PhysiCell::xml_get_my_string_value (node_sbml); - - } - else +// Caller must hold the named critical section. +void rebuild_voxel_demand() +{ + const std::size_t n_voxels = BioFVM::microenvironment.number_of_voxels(); + const std::size_t n_dens = BioFVM::microenvironment.number_of_densities(); + g_voxel_demand.n_densities = n_dens; + g_voxel_demand.mmol.assign(n_voxels * n_dens, 0.0); + + for (PhysiCell::Cell* cell : *PhysiCell::all_cells) { - std::cout << "Error: attempted get sbml_filename but not foun. " << std::endl; - std::cout << "Please double-check your exchange nodes in the config file." << std::endl; - std::cout << std::endl; - exit(-1); + if (cell == NULL || cell->is_out_of_domain || cell->phenotype.death.dead) + continue; + dFBAIntracellular* model = dynamic_cast(cell->phenotype.intracellular); + if (model == NULL) + continue; + const int voxel = cell->get_current_voxel_index(); + if (voxel < 0 || static_cast(voxel) >= n_voxels) + continue; + + const std::vector& density = BioFVM::microenvironment.nearest_density_vector(voxel); + const double dry_mass = dry_mass_in_grams(*model, cell->phenotype); + for (const auto& entry : model->substrate_exchanges) + { + const ExchangeFluxData& exchange = entry.second; + const double concentration = std::max(density[exchange.density_index], 0.0); + g_voxel_demand.mmol[voxel * n_dens + exchange.density_index] += + uptake_over_window_mmol(*model, michaelis_menten_uptake_rate(exchange, concentration), dry_mass); + } } - - pugi::xml_node node_exchange = node.child( "exchange" ); +} + +} // namespace + +dFBAIntracellular::dFBAIntracellular() : Intracellular() +{ + intracellular_type = "dfba"; + objective_reaction = ""; + sbml_filename = ""; + use_secondary_objective = false; + secondary_objective_type = "parsimonious"; + secondary_fraction_of_optimum = 1.0; + cell_density = 0.0; + reference_volume = 0.0; + max_growth_rate = 0.0; + current_growth_rate = 0.0; + next_dfba_run = 0.0; + use_metabolic_death = true; + death_type = ""; + death_trigger_flux = ""; + death_flux_threshold = 0.0; + death_rate_increase = 0.0; + substrate_exchanges.clear(); + flag_for_death = false; + dfba_time_step = PhysiCell::diffusion_dt; // Default value +} + +dFBAIntracellular::dFBAIntracellular(pugi::xml_node& node) +{ + intracellular_type = "dfba"; + objective_reaction = ""; + sbml_filename = ""; + use_secondary_objective = false; + secondary_objective_type = "parsimonious"; + secondary_fraction_of_optimum = 1.0; + cell_density = 0.0; + reference_volume = 0.0; + max_growth_rate = 0.0; + current_growth_rate = 0.0; + next_dfba_run = 0.0; + use_metabolic_death = true; + death_type = ""; + death_trigger_flux = ""; + death_flux_threshold = 0.0; + death_rate_increase = 0.0; + substrate_exchanges.clear(); + flag_for_death = false; + sbml_model.clear(); + is_initialized = false; + dfba_time_step = PhysiCell::diffusion_dt; // Default value + this->initialize_intracellular_from_pugixml(node); +} + +dFBAIntracellular::dFBAIntracellular(const dFBAIntracellular& copy) : Intracellular() { + intracellular_type = copy.intracellular_type; + objective_reaction = copy.objective_reaction; + sbml_filename = copy.sbml_filename; + use_secondary_objective = copy.use_secondary_objective; + secondary_objective_type = copy.secondary_objective_type; + secondary_fraction_of_optimum = copy.secondary_fraction_of_optimum; + cell_density = copy.cell_density; + reference_volume = copy.reference_volume; + max_growth_rate = copy.max_growth_rate; + current_growth_rate = copy.current_growth_rate; + next_dfba_run = copy.next_dfba_run; + dfba_time_step = copy.dfba_time_step; + + // Deep copy the model instead of shallow copy + sbml_model.deepCopy(copy.sbml_model); + + substrate_exchanges = copy.substrate_exchanges; + + is_initialized = copy.is_initialized; + // Copy death-related parameters + use_metabolic_death = copy.use_metabolic_death; + death_type = copy.death_type; + death_trigger_flux = copy.death_trigger_flux; + death_flux_threshold = copy.death_flux_threshold; + death_rate_increase = copy.death_rate_increase; + flag_for_death = copy.flag_for_death; +} + + +int dFBAIntracellular::parse_transport_model(pugi::xml_node& node) +{ + ExchangeFluxData exchange_flux; + KineticParam Km; + KineticParam Vmax; - while( node_exchange ) + pugi::xml_node node_exchange = node.child( "exchange" ); + int num_exchanges = 0; + while( node_exchange ) { - exchange_data ex_struct; - kinetic_parm Km; - kinetic_parm Vmax; - - string density_name = node_exchange.attribute( "substrate" ).value(); int density_index = microenvironment.find_density_index( density_name ); - std::cout << "Parsing " << density_name << std::endl; + + if (density_index == -1) + { + std::cout << "Error: attempted to set secretion/uptake/export for \""; + std::cout << density_name << "\", which was not found in the microenvironment." << std::endl; + std::cout << "Please double-check your substrate name in the config file." << std::endl; + std::cout << std::endl; + exit(-1); + } + std::string actual_name = microenvironment.density_names[ density_index ]; // error check if( std::strcmp( density_name.c_str() , actual_name.c_str() ) != 0 ) { - std::cout << "Error: attempted to set secretion/uptake/export for \"" - << density_name << "\", which was not found in the microenvironment." << std::endl - << " Please double-check your substrate name in the config file." << std::endl << std::endl; + std::cout << "Error: attempted to set secretion/uptake/export for \""; + std::cout << density_name << "\", which was not found in the microenvironment." << std::endl; + std::cout << "Please double-check your substrate name in the config file." << std::endl; + std::cout << std::endl; exit(-1); } pugi::xml_node node_fba_flux = node_exchange.child( "fba_flux" ); if( node_fba_flux ) { - ex_struct.fba_flux_id = PhysiCell::xml_get_my_string_value(node_fba_flux); + exchange_flux.fba_flux_id = PhysiCell::xml_get_my_string_value(node_fba_flux); } else { std::cout << "Error: attempted get fba_flux node for "; - std::cout << ex_struct.density_name << "\", but not found." << std::endl; + std::cout << exchange_flux.density_name << "\", but not found." << std::endl; std::cout << "Please double-check your exchange nodes in the config file." << std::endl; std::cout << std::endl; exit(-1); @@ -83,8 +229,9 @@ void dFBAIntracellular::initialize_intracellular_from_pugixml(pugi::xml_node& no if( node_Km ) { Km.name = "Km"; - Km.untis = node_Km.attribute("units").value(); + Km.units = node_Km.attribute("units").value(); Km.value = PhysiCell::xml_get_my_double_value(node_Km); + assert(Km.value > 0); } else { @@ -99,8 +246,9 @@ void dFBAIntracellular::initialize_intracellular_from_pugixml(pugi::xml_node& no if( node_Vmax ) { Vmax.name = "Vmax"; - Vmax.untis = node_Vmax.attribute("units").value(); + Vmax.units = node_Vmax.attribute("units").value(); Vmax.value = PhysiCell::xml_get_my_double_value(node_Vmax); + assert(Vmax.value >= 0); } else { @@ -111,102 +259,758 @@ void dFBAIntracellular::initialize_intracellular_from_pugixml(pugi::xml_node& no exit(-1); } - ex_struct.density_name = density_name; - ex_struct.density_index = density_index; - ex_struct.Km = Km; - ex_struct.Vmax = Vmax; + exchange_flux.density_name = density_name; + exchange_flux.density_index = density_index; + exchange_flux.Km = Km; + exchange_flux.Vmax = Vmax; - this->substrate_exchanges[density_name] = ex_struct; + this->substrate_exchanges[density_name] = exchange_flux; + num_exchanges++; node_exchange = node_exchange.next_sibling( "exchange" ); } + return num_exchanges; +} + +void dFBAIntracellular::parse_growth_model(pugi::xml_node& parent) +{ + + pugi::xml_node node = parent.child( "cell_density" ); + if ( node ) + { + this->cell_density = PhysiCell::xml_get_my_double_value(node); + } + else + { + std::cout << "Error: attempted to read cell_density attribute." << std::endl; + std::cout << "Please double-check your growth model in the XML setting." << std::endl; + std::cout << std::endl; + exit(-1); + } + + node = parent.child( "reference_volume" ); + if ( node ) + { + this->reference_volume = PhysiCell::xml_get_my_double_value(node); + } + else + { + std::cout << "Error: attempted to read reference_volume attribute." << std::endl; + std::cout << "Please double-check your reference_volume in the XML setting." << std::endl; + std::cout << "This value is used to determine the reference volume for cell division." << std::endl; + std::cout << "Ensure consistency with the total cell volume defined in the cell phenotype." << std::endl; + std::cout << std::endl; + exit(-1); + } + - std::cout << "Loaing SBML model from: " << this->sbml_filename << std::endl; - this->model.readSBMLModel(this->sbml_filename.c_str()); - this->model.initLpModel(); - this->model.runFBA(); + node = parent.child( "max_growth_rate" ); + if ( node ) + { + this->max_growth_rate = PhysiCell::xml_get_my_double_value(node); + assert(this->max_growth_rate >= 0); + } + else + { + std::cout << "Error: attempted to read max_growth_rate attribute." << std::endl; + std::cout << "Please double-check your growth model in the XML setting." << std::endl; + std::cout << std::endl; + exit(-1); + } + + node = parent.child( "objective_reaction" ); + if ( node ) + { + this->objective_reaction = PhysiCell::xml_get_my_string_value(node); + } + else + { + std::cout << "Error: attempted to read objective_reaction attribute." << std::endl; + std::cout << "Please double-check your growth model in the XML setting." << std::endl; + std::cout << std::endl; + exit(-1); + } + } -void dFBAIntracellular::dFBAIntracellular::start() -{ - // return 0; +void dFBAIntracellular::parse_death_model(pugi::xml_node& parent){ + + + // If death_model is specified, parse its parameters + this->use_metabolic_death = true; + + // Parse death_type (apoptosis or necrosis) + pugi::xml_node node = parent.child("death_type"); + if (node){ + this->death_type = PhysiCell::xml_get_my_string_value(node); + assert(this->death_type == "apoptosis" || this->death_type == "necrosis"); + } else { + std::cout << "Warning: death_type not specified. Defaulting to apoptosis." << std::endl; + this->death_type = "apoptosis"; + } + + // Parse monitored_flux (optional) + node = parent.child("death_trigger_flux"); + if (node){ + this->death_trigger_flux = PhysiCell::xml_get_my_string_value(node); + } else { + std::cout << "Warning: monitored_flux not specified. Metabolic-dependent death disabled." << std::endl; + this->death_trigger_flux = ""; + return; + } + + // Parse flux_threshold (optional, default provided) + node = parent.child("death_flux_threshold"); + if (node){ + this->death_flux_threshold = PhysiCell::xml_get_my_double_value(node); + } else { + std::cout << "Warning: flux_threshold not specified. Using default 1e-6." << std::endl; + this->death_flux_threshold = 1e-6; + } + + // Parse death_rate (optional, default provided) + node = parent.child("death_rate_increase"); + if (node){ + this->death_rate_increase = PhysiCell::xml_get_my_double_value(node); + } else { + std::cout << "Warning: death_rate not specified. Using default 0.01." << std::endl; + this->death_rate_increase = 0.01; + } + + std::cout << "Metabolic-dependent death enabled: " << this->death_type << ", monitored flux: " << this->death_trigger_flux << ", threshold: " << this->death_flux_threshold << ", rate: " << this->death_rate_increase << std::endl; } -bool dFBAIntracellular::dFBAIntracellular::need_update() +void dFBAIntracellular::initialize_intracellular_from_pugixml(pugi::xml_node& node) { - return PhysiCell::PhysiCell_globals.current_time >= this->next_model_run; + + // Setting all the rest to default values : Nothing should be kept from the existing intracellular object (NO INHERITANCE) + objective_reaction = ""; + cell_density = 0.0; + max_growth_rate = 0.0; + current_growth_rate = 0.0; + use_secondary_objective = false; + secondary_objective_type = "parsimonious"; + secondary_fraction_of_optimum = 1.0; + use_metabolic_death = false; + death_type = ""; + death_trigger_flux = ""; + death_flux_threshold = 0.0; + death_rate_increase = 0.0; + substrate_exchanges.clear(); + flag_for_death = false; + sbml_model.clear(); + is_initialized = false; + + std::cout << "===================================================" << std::endl; + std::cout << "Parssing dFBA intracellular model" << std::endl; + + // Parse dFBA sbml_filename and time step from XML settings + pugi::xml_node node_settings = node.child("settings"); + if (node_settings) + { + pugi::xml_node node_intracellular_dt = node_settings.child("intracellular_dt"); + pugi::xml_node node_time_step = node_settings.child("time_step"); + if (node_intracellular_dt) + { + dfba_time_step = PhysiCell::xml_get_my_double_value(node_intracellular_dt); + } else if (node_time_step) + { + std::cout << "[Warning] The setting 'time_step' is deprecated. Please use 'intracellular_dt' instead." << std::endl; + dfba_time_step = PhysiCell::xml_get_my_double_value(node_time_step); + } else + { + std::cout << "[Warning] No intracellular_dt or time_step specified. Using default value: " << PhysiCell::diffusion_dt << std::endl; + dfba_time_step = PhysiCell::diffusion_dt; // Default value + } + // Getting sbml file name for reading the model + pugi::xml_node node_sbml = node_settings.child( "sbml_filename" ); + if ( node_sbml ) + { + + this->sbml_filename = PhysiCell::xml_get_my_string_value (node_sbml); + } + else + { + std::cout << "Error: attempted to read sbml_filename path but not found." << std::endl; + std::cout << PhysiCell::xml_get_my_string_value(node_sbml) << std::endl; + std::cout << "Please double-check your node in the XML setting." << std::endl; + std::cout << std::endl; + exit(-1); + } + } else + { + std::cout << "Error: No settings block found in the XML" << std::endl; + std::cout << "Please double-check your intracellular node in the XML setting." << std::endl; + std::cout << "intracellular.settings node in the XML setting should include and " << std::endl; + std::cout << std::endl; + exit(-1); + } + + // parsing the transport model + pugi::xml_node node_transport_model = node.child( "transport_model" ); + if ( node_transport_model ) + { + int num_exchanges = parse_transport_model(node_transport_model); + if (num_exchanges == 0){ + std::cout << "Error, dFBA model must have at least one exchange flux." << std::endl; + std::cout << "Please double-check your exchange nodes in the XML setting file." << std::endl; + std::cout << std::endl; + exit(-1); + } + } + else + { + std::cout << "Error: attempted to parse transport_model but not found." << std::endl; + std::cout << "Please double-check intracellular model nodes in the XML setting file." << std::endl; + std::cout << std::endl; + exit(-1); + } + + + // parsing the transport model + pugi::xml_node node_growth_model = node.child( "growth_model" ); + if ( node_growth_model ) + { + parse_growth_model(node_growth_model); + } + else + { + std::cout << "Error: attempted to parse growth_model but not found." << std::endl; + std::cout << "Please double-check intracellular model nodes in the XML setting file." << std::endl; + std::cout << std::endl; + exit(-1); + } + + // parsing the optional second optimisation stage + pugi::xml_node node_secondary = node.child( "secondary_objective" ); + if ( node_secondary ) + parse_secondary_objective(node_secondary); + + // parsing the death model + pugi::xml_node node_death_model = node.child( "death_model" ); + if ( node_death_model ) + { + bool death_enabled = node_death_model.attribute("enabled").as_bool(); + if (!death_enabled){ + std::cout << "Death model disabled. Using default behavior (no metabolic-dependent death)." << std::endl; + this->use_metabolic_death = false; + } + else{ + parse_death_model(node_death_model); + } + } + else + { + std::cout << "Death model not specified. Using default behavior (no metabolic-dependent death)." << std::endl; + this->use_metabolic_death = false; + } + + + // ============================================================================================== + + std::cout << "Loading SBML model from: " << this->sbml_filename << std::endl; + this->sbml_model.initModel(this->sbml_filename.c_str()); + + map::iterator it; + for(it = this->substrate_exchanges.begin(); it != this->substrate_exchanges.end(); it++) + { + std::string substrate_name = it->first; + ExchangeFluxData ex_strut = it->second; + // TODO + // check this ex_strut.density_index is a defined density at Microenviroment + dFBAReaction* rxn = this->sbml_model.getReaction(ex_strut.fba_flux_id); + if( rxn == nullptr ){ + std::cout << "Error: exchange reaction not found in model: " << ex_strut.fba_flux_id << " for substrate " << substrate_name << std::endl; + exit(-1); + } + } + dFBAReaction* growth_rxn = this->sbml_model.getReaction(this->objective_reaction); + assert( growth_rxn != nullptr ); + this->sbml_model.setReactionUpperBound(this->objective_reaction, this->max_growth_rate); + + if(this->use_metabolic_death && !this->death_trigger_flux.empty()){ + dFBAReaction* death_rxn = this->sbml_model.getReaction(this->death_trigger_flux); + if(death_rxn == nullptr){ + std::cout << "ERROR: Specified death_trigger_flux (" << this->death_trigger_flux << ") does not exist in the SBML model." << std::endl; + exit(-1); + } + else{ + this->sbml_model.setReactionLowerBound(this->death_trigger_flux, this->death_flux_threshold); + } + } + + if (this->use_secondary_objective) + { + // only meaningful once the LP exists, so this happens after initModel + this->sbml_model.enableParsimonious(this->secondary_fraction_of_optimum); + std::cout << "Secondary objective enabled: " << this->secondary_objective_type + << ", fraction_of_optimum = " << this->secondary_fraction_of_optimum + << std::endl; + } + + // Save original objective state after model is loaded + this->sbml_model.saveObjectiveState(); + + std::cout << "Done!" << std::endl; + std::cout << "===================================================" << std::endl; } -void dFBAIntracellular::update(PhysiCell::Cell* pCell, PhysiCell::Phenotype& phenotype, double dt) +void dFBAIntracellular::start() { - - map::iterator it; + this->next_dfba_run = PhysiCell::PhysiCell_globals.current_time + dfba_time_step; + /* + for (const auto& reaction : this->sbml_model.getListOfReactions()) + { + if (reaction != nullptr) { + std::cout << "Reaction: " << this->sbml_model.getReactionIndex(reaction->getId()) + << " Reaction id: " << reaction->getId() + << " Reaction string: " << reaction->getReactionString(this->sbml_model) << std::endl; + } + } + */ + +} + + +void dFBAIntracellular::update_dfba_inputs( PhysiCell::Cell* pCell, PhysiCell::Phenotype& phenotype, double dt ){ + + // HeLa cell mass 2.3 ng + // cell volume fL (um³) + // mM: mmol / L = 10⁻³ mol / 1e¹⁵ um³ = 10⁻¹⁵mol / um³ = amol / um³ + // cell_density --> 1.04 g/ml = 1.04 ug/nL = 1.04 ng / pL = 1.04 pg / fL = pg / um³ + // cell.mass.total (ng) = cell.volume (um³) * cell.density (pg / um³) = pg + // cell_density 1.04 pg/um³ + // cell volume ~ 2500 (um³) * ~ 1.04 pg / (um³) + // gDW cell (cell.volume.total * cell_density) = mass.total + // 2500 (um³) * 1.04 (pg/um³) =~ 2600 (pg) ~= 2.3 ng + // cell.mass.solid = cell.mass.total * (1-fluid_frac) = 780 pg + + // Cell Volume + double current_volume = phenotype.volume.total; + // dry weight in grams (solid volume * cell_density, pg -> g); same helper as the voxel demand + double mass_scaling = dry_mass_in_grams(*this, phenotype); + + // r = (3V / 4π))^1/3 (um) + double radius = cbrt((3.0 * current_volume) / (4.0 * PI)); + // um * um = um² + double cell_surface = 4 * PI * pow(radius, 2); + + // The voxel demand table describes the whole population at this simulated + // time. Build it on first use; the unguarded atomic read keeps the common + // path to one load, and the check is repeated inside the critical section. + const double now = PhysiCell::PhysiCell_globals.current_time; + if (g_voxel_demand.built_at.load() != now) + { + #pragma omp critical(dfba_voxel_demand) + { + if (g_voxel_demand.built_at.load() != now) + { + rebuild_voxel_demand(); + g_voxel_demand.built_at.store(now); + } + } + } + const int voxel_index = pCell->get_current_voxel_index(); + + // Setp 1 - Update exchange fluxes lower + // Standard FBA fluxes units: mmol / gDW cell / h + // Km: mM = mmol/L + // Vmax: mmol/g DW cell/min + + double dV = microenvironment.voxels(pCell->get_current_voxel_index()).volume; + + std::vector density_vector = pCell->nearest_density_vector(); + + // Only declare the iterator ONCE per function + map::iterator it; for(it = this->substrate_exchanges.begin(); it != this->substrate_exchanges.end(); it++) { + std::string substrate_name = it->first; - exchange_data ex_strut = it->second; + ExchangeFluxData ex_strut = it->second; - double Vmax = ex_strut.Vmax.value; - double Km = ex_strut.Km.value; - // geting the amount of substrate - double substrate_conc = pCell->nearest_density_vector()[ex_strut.density_index]; - // useing irreversible Michaelis Menten kinetics to estimate the flux bound - double flux_bound = (Vmax * substrate_conc) / (Km + substrate_conc); // should be calculated from density + double substrate_conc = density_vector[ex_strut.density_index]; + substrate_conc = max(substrate_conc, 0.0); // mM + // Irreversible Michaelis-Menten kinetics gives the flux bound, mmol / gDWcell / hours. + double uptake_rate = michaelis_menten_uptake_rate(ex_strut, substrate_conc); + + // What this bound would remove from the voxel over one dFBA window (mmol), + // i.e. what BioFVM will apply once the rate is turned into a net export rate. + double total_uptake = uptake_over_window_mmol(*this, uptake_rate, mass_scaling); + + // Total substrate held by the voxel. substrate_conc is mmol/L and dV is + // um^3; 1 L = 1e15 um^3, so mmol = mM * um^3 / 1e15. This must be a + // division: it was a multiplication, which made the cap larger than the + // request by ~1e30 so it never engaged. + double total_substrate = substrate_conc * dV / liter_micron_cubes_conversion; // mmol + + // Share the voxel between the cells that draw on it. The table sum already + // contains this cell's own request, so with one cell this is the plain cap. + const double epsilon = 1e-18; + if (total_uptake > 0.0) { + const double voxel_demand = + g_voxel_demand.mmol[static_cast(voxel_index) * g_voxel_demand.n_densities + ex_strut.density_index]; + if (voxel_demand > total_substrate + epsilon) { + uptake_rate *= total_substrate / voxel_demand; + } + } + // Change sign to use as lower bound of the exchange flux - flux_bound *= -1; + double exchange_flux_lb = -1 * uptake_rate; + // std::cout << "Substrate: " << substrate_name << " Density: " << substrate_conc << " Vmax: " << Vmax << " Km: " << Km << " Max Uptake rate (mmol/gDW/h): " << uptake_rate << " Exchange flux LB: " << exchange_flux_lb << std::endl; // Updateing the lower bound of the corresponding exchange flux + this->sbml_model.setReactionLowerBound(ex_strut.fba_flux_id, exchange_flux_lb); - std::cout << " - [" << substrate_name << "] = " << substrate_conc; - std::cout << " ==> " << ex_strut.fba_flux_id << " = " << flux_bound << std::endl; + } +} - this->model.setReactionLowerBound(ex_strut.fba_flux_id, flux_bound); +void dFBAIntracellular::update(){ + // Check if we should skip standard biomass optimization + if (this->skip_standard_optimization) { + this->skip_standard_optimization = false; // Reset for next round + return; + } + // Only run dFBA if current_time >= next_dfba_run + dFBASolution solution = this->sbml_model.optimize(); + //next_dfba_run = PhysiCell::PhysiCell_globals.current_time + dfba_time_step; + if (solution.status == "optimal"){ + // One call, whatever the model is configured to do. When a second + // optimisation stage is configured, optimize() has already run it and + // the fluxes below are the parsimonious ones; objective_value is the + // stage-one growth rate either way. See dFBAModel::optimize(). + this->current_growth_rate = solution.getObjectiveValue(); + this->flag_for_death = false; + } + else if(solution.status == "unknown"){ + std::cout << "ERROR: Unknown status for the FBA problem!" << std::endl; + exit(1); } - this->model.runFBA(); - // this->update_phenotype_parameters(phenotype); - // return 0; + else { + //std::cout << "I'm dead from the metabolic point of view" << std::endl; + this->flag_for_death = true; + this->current_growth_rate = 0.0; + } + } -int dFBAIntracellular::update_phenotype_parameters(PhysiCell::Phenotype& phenotype) +void dFBAIntracellular::update_dfba_outputs(PhysiCell::Cell* pCell, PhysiCell::Phenotype& phenotype, double dt ) { - /* - Required steps for the update + // Metabolic-dependent death check + //std::cout << "Cell ready to die? --> " << this->flag_for_death << std::endl; + //std::cout << "Death parameters: " << this->use_metabolic_death << " " << this->death_trigger_flux << " " << this->death_flux_threshold << " " << this->death_rate_increase << std::endl; + // Recalculate per cell (not static) to avoid stale indices + int nApoptosis = phenotype.death.find_death_model_index(PhysiCell::PhysiCell_constants::apoptosis_death_model ); + int nNecrosis = phenotype.death.find_death_model_index(PhysiCell::PhysiCell_constants::necrosis_death_model ); - 1- rescale exchange fluxes from the dfba model - 2- update the net_export_rates using the rescaled exchanges - 3- remove the internalized substrates if needed - 4- update the cell volumne using the growth rate from FBA - 5- if no-growth check non-growth associated mantainanace to update apoptotic rate - */ + if(this->flag_for_death && this->use_metabolic_death) + { + if(this->death_type == "apoptosis") + { + pCell->phenotype.death.rates[nApoptosis] += this->death_rate_increase; + } + else if(this->death_type == "necrosis") + { + pCell->phenotype.death.rates[nNecrosis] += this->death_rate_increase; + } + } + else if(this->use_metabolic_death && !this->flag_for_death) + { + if(this->death_type == "apoptosis") + { + pCell->phenotype.death.rates[nApoptosis] = 0.0; + } + else if(this->death_type == "necrosis") + { + pCell->phenotype.death.rates[nNecrosis] = 0.0; + } + } + + double fba_growth_rate = this->current_growth_rate; + + + if (fba_growth_rate > fba_epsilon_tolerance) { + // SCALE GROWTH RATE BY this->dfba_time_step + double growth_rate = fba_growth_rate * this->dfba_time_step * hours_to_minutes; // growth_rate 1/h * dt (h) - map::iterator it; + // exact solution (fallback to linear for very small x to avoid exp overhead) + double factor = (std::abs(growth_rate) < 1e-6) ? (1.0 + growth_rate) : std::exp(growth_rate); + double safe_factor = std::max(factor, 1e-12); + //double volume_increase_ratio = 1.0 + growth_rate; + phenotype.volume.multiply_by_ratio( safe_factor ); + pCell->set_total_volume( phenotype.volume.total ); + phenotype.geometry.update(pCell, phenotype, this->dfba_time_step); + } + + + // Cell Volume + double current_volume = phenotype.volume.total; + // unitless ~30% + double solid_fraction = 1.0 - phenotype.volume.fluid_fraction; + // um³ = um³ + double solid_volume = current_volume * solid_fraction; + // um³ * pg / um³ = pg + double cell_dry_weight = solid_volume * this->cell_density; + + // pg * 1e-12 = g + // pCell->custom_data["cell_dry_weight"] = cell_dry_weight; + cell_dry_weight *= 1e-12; + + + + + float current_growth_rate = this->get_growth_rate(); + // std::cout << "Current growth rate: " << current_growth_rate << std::endl; + // pCell->custom_data["growth_rate"] = current_growth_rate; + // float R_biomass_reaction = this->get_flux_value(this->objective_reaction); + // float biomass_ub = this->sbml_model.getReactionUpperBound("R_biomass_reaction"); + // float biomass_lb = this->sbml_model.getReactionLowerBound("R_biomass_reaction"); + // std::cout << "Biomass reaction flux value: " << R_biomass_reaction << endl; + // pCell->custom_data["R_biomass_reaction"] = R_biomass_reaction; + + + + + std::vector density_vector = pCell->nearest_density_vector(); + double dV = microenvironment.voxels(pCell->get_current_voxel_index()).volume; + // For each substrate exchange, scale net_export_rates by this->dfba_time_step + map::iterator it; for(it = this->substrate_exchanges.begin(); it != this->substrate_exchanges.end(); it++) { + // Retrive the exchange flux and its corresponding density std::string substrate_name = it->first; - exchange_data ex_strut = it->second; + ExchangeFluxData ex_strut = it->second; + + double Vmax = ex_strut.Vmax.value; + double Km = ex_strut.Km.value; + int density_index = ex_strut.density_index; + std::string density_name = ex_strut.density_name; std::string fba_flux_id = ex_strut.fba_flux_id; - FBA_reaction* exchange = this->model.getReaction(fba_flux_id); - double flux = exchange->getFluxValue(); + + double substrate_conc = density_vector[ex_strut.density_index]; // mM = mmol/L + double total_substrate = substrate_conc * (dV / 1e15); // mmol + + // pCell->custom_data[density_name + "_conc"] = substrate_conc; + // pCell->custom_data[density_name + "_total"] = total_substrate; + + + dFBAReaction* exchange_flux = this->sbml_model.getReaction(fba_flux_id); + double flux_value = exchange_flux->getFluxValue(); // mmol/gDW/h + // std::cout << "Exchange flux : " << fba_flux_id << " Flux value (mmol/gDW/h): " << flux_value << std::endl; - // how to rescale FBA exchanges into net_export_rates - float scaling = 1; - flux *= scaling; - phenotype.secretion.net_export_rates[ex_strut.density_index] = flux; + // pCell->custom_data[fba_flux_id] = flux_value; + + // Rescaling FBA exchanges flux into net_export_rates + // Net export rates are expressed in substance/time + // flux_value: mmol/gDW/h --> mmol/min + // net_export_rate (mmol/min) = flux_value / 60 * cell_dry_weight = mmol/min + //double net_export_rate = flux_value * cell_dry_weight * hours_to_minutes; // mmol/min + + double net_export_rate_mmol_per_min = flux_value * cell_dry_weight * hours_to_minutes; // mmol/min + + + /* + double substrate_conc = density_vector[ex_strut.density_index]; // mM = mmol/L + std::cout << " Cell Type: " << pCell->type_name << std::endl; + std::cout << "\tSubstrate: " << substrate_name << std::endl; + std::cout << "\tconcentration: " << substrate_conc << std::endl; + std::cout << "\tKm: " << Km << std::endl; + std::cout << "\tVmax: " << Vmax << std::endl; + std::cout << "\tTotal substrate: " << total_substrate << std::endl; + std::cout << "\tConsumption: " << substrate_consumption << std::endl; + std::cout << "\tNet export rate: " << net_export_rate << std::endl; + std::cout << "\tFBA Flux " << flux_value << std::endl; + std::cout << "\tFBA growth rate " << fba_growth_rate << std::endl; + if (substrate_name == "lactate" && flux_value > 1.0){ + } + */ + + // correct scaling that takes into account the volume units (liter to um³) in BioFVM + net_export_rate_mmol_per_min *= liter_micron_cubes_conversion; // BioFVM units are in mM = mmol / L whereas dV is in um³ = 1e-15 L + + // std::cout << "New net export rate for substrate " << substrate_name << ": " << net_export_rate_mmol_per_min << " mmol/min" << std::endl; + phenotype.secretion.net_export_rates[density_index] = net_export_rate_mmol_per_min; + + // phenotype.secretion.net_export_rates[density_index] = net_export_rate; + pCell->set_internal_uptake_constants(this->dfba_time_step); // Update internal uptake constants based on the new volume and rates + if (default_microenvironment_options.track_internalized_substrates_in_each_agent) + { + phenotype.molecular.internalized_total_substrates[density_index] = 0; + } + // print_model(pCell, dt, "./output"); } - float delta_vol = 1; - float growth_rate = 1; // - phenotype.volume.total; // FBA predicts growth rate mu - phenotype.volume.multiply_by_ratio(delta_vol); + return; +} + +dFBASolution dFBAIntracellular::optimize_for_objective(std::string reaction_id, double coefficient) { + // Temporarily switch objective + this->sbml_model.clearObjective(); + this->sbml_model.setObjectiveCoefficient(reaction_id, coefficient); + + // Run optimization + dFBASolution solution = this->sbml_model.optimize(); + + // Restore original objective immediately + this->sbml_model.restoreObjectiveState(); + + return solution; // Return full solution (includes status!) +} + - return 0; +double dFBAIntracellular::get_flux_value(std::string reaction_name) +{ + + auto it = this->sbml_model.getReaction(reaction_name); + if (it != nullptr){ + return it->getFluxValue(); + } + else{ + std::cout << "ERROR: Reaction " << reaction_name << " not found in the SBML model." << std::endl; + exit(-1); + } } +void dFBAIntracellular::print_model(PhysiCell::Cell* pCell, double current_time, std::string output_folder){ + // Create output filename with cell ID and timestamp + std::stringstream filename; + int cell_id = pCell->ID; + filename << output_folder << "/fba_model_cell_" << cell_id + << "_t_" << std::fixed << std::setprecision(2) << current_time << ".txt"; + + std::ofstream outfile(filename.str()); + if (!outfile.is_open()) { + std::cerr << "ERROR: Could not open file for writing: " << filename.str() << std::endl; + return; + } + + // Header with cell information + outfile << "=================================================================" << std::endl; + outfile << "FBA MODEL SUMMARY - Cell ID: " << cell_id << std::endl; + outfile << "Time: " << current_time << " min" << std::endl; + outfile << "SBML Model: " << this->sbml_filename << std::endl; + outfile << "=================================================================" << std::endl; + outfile << std::endl; + // Growth parameters + outfile << "--- GROWTH PARAMETERS ---" << std::endl; + outfile << "Objective Reaction: " << this->objective_reaction << std::endl; + outfile << "Current Growth Rate: " << this->current_growth_rate << " 1/h" << std::endl; + outfile << "Cell Density: " << this->cell_density << " pg/um³" << std::endl; + outfile << "Reference Volume: " << this->reference_volume << " um³" << std::endl; + outfile << "dFBA Time Step: " << this->dfba_time_step << " min" << std::endl; + outfile << std::endl; + // Substrate exchanges (transport model) + std::vector density_vector = pCell->nearest_density_vector(); + + outfile << "--- SUBSTRATE EXCHANGES ---" << std::endl; + outfile << "Substrate Name\tCurrent concentration\tFBA Flux ID\tKm (mM)\tVmax (mmol/gDW/h)" << std::endl; + for (const auto& exchange : this->substrate_exchanges) { + const ExchangeFluxData& ex = exchange.second; + double substrate_conc = density_vector[ex.density_index]; // mM = mmol/L + outfile << ex.density_name << "\t" + << substrate_conc << "\t" + << ex.fba_flux_id << "\t" + << ex.Km.value << "\t" + << ex.Vmax.value << std::endl; + } + outfile << std::endl; + + // Metabolites + outfile << "--- METABOLITES (" << this->sbml_model.getNumMetabolites() << " total) ---" << std::endl; + outfile << "ID\tName" << std::endl; + for (const auto& m : this->sbml_model.getListOfMetabolites()) { + outfile << m->getId() << "\t" << m->getName() << std::endl; + } + outfile << std::endl; + + // Reactions with detailed information + outfile << "--- REACTIONS (" << this->sbml_model.getNumReactions() << " total) ---" << std::endl; + outfile << "Reaction ID\tReaction Name\tLower Bound\tUpper Bound\tFlux Value\tReversible\tObjective Coeff\tReaction String" << std::endl; + + for (const auto& r : this->sbml_model.getListOfReactions()) { + outfile << r->getId() << "\t" + << r->getName() << "\t" + << r->getLowerBound() << "\t" + << r->getUpperBound() << "\t" + << r->getFluxValue() << "\t" + << (r->reversible() ? "Yes" : "No") << "\t" + << r->getObjectiveCoefficient() << "\t" + << r->getReactionString(this->sbml_model) << std::endl; + } + outfile << std::endl; + + // Boundary/Exchange reactions (for easier identification) + std::vector boundary_rxns = this->sbml_model.getListOfBoundaryReactions(); + if (!boundary_rxns.empty()) { + outfile << "--- BOUNDARY/EXCHANGE REACTIONS (" << boundary_rxns.size() << " total) ---" << std::endl; + outfile << "Reaction ID\tLower Bound\tUpper Bound\tFlux Value" << std::endl; + for (const auto& r : boundary_rxns) { + outfile << r->getId() << "\t" + << r->getLowerBound() << "\t" + << r->getUpperBound() << "\t" + << r->getFluxValue() << std::endl; + } + outfile << std::endl; + } + + outfile << "=================================================================" << std::endl; + outfile.close(); + + std::cout << "FBA model summary saved to: " << filename.str() << std::endl; +} + void dFBAIntracellular::save_dFBA(std::string path, std::string index) { -} \ No newline at end of file +} + + +void dFBAIntracellular::parse_secondary_objective(pugi::xml_node& parent) +{ + const bool enabled = parent.attribute("enabled").as_bool(true); + if (!enabled) + { + this->use_secondary_objective = false; + return; + } + + std::string type = parent.attribute("type").value(); + if (type.empty()) + type = "parsimonious"; + + if (type != "parsimonious") + { + std::cout << "Error: unknown secondary_objective type \"" << type << "\"." + << std::endl + << " The implemented type is \"parsimonious\"." << std::endl; + exit(-1); + } + + double fraction = 1.0; + pugi::xml_attribute attr = parent.attribute("fraction_of_optimum"); + if (attr) + fraction = attr.as_double(); + else + { + pugi::xml_node node_fraction = parent.child("fraction_of_optimum"); + if (node_fraction) + fraction = PhysiCell::xml_get_my_double_value(node_fraction); + } + + if (fraction <= 0.0 || fraction > 1.0) + { + std::cout << "Error: secondary_objective fraction_of_optimum must be in (0, 1], got " + << fraction << "." << std::endl; + exit(-1); + } + + this->use_secondary_objective = true; + this->secondary_objective_type = type; + this->secondary_fraction_of_optimum = fraction; +} + +} diff --git a/addons/dFBA/src/dfba_intracellular.h b/addons/dFBA/src/dfba_intracellular.h index 54800e1e4..d5684d8d4 100644 --- a/addons/dFBA/src/dfba_intracellular.h +++ b/addons/dFBA/src/dfba_intracellular.h @@ -7,99 +7,261 @@ #include #include // for setw +#include +#include +#include + #include "../../../core/PhysiCell.h" -#include "../../../BioFVM/BioFVM.h" +#include "../../../core/PhysiCell_phenotype.h" +#include "../../../core/PhysiCell_cell.h" +#include "../../../core/PhysiCell_utilities.h" #include "../../../modules/PhysiCell_pugixml.h" +#include "../../../core/PhysiCell_constants.h" + +#include "dfba_Model.h" +#include "dfba_Reaction.h" -#include "./FBA_model.h" using namespace std; +namespace PhysiCelldFBA { + +static std::string PhysiCelldFBA_Version = "0.0.1"; + +static double hours_to_minutes = 1.0 / 60.0; +static float PI = PhysiCell::PhysiCell_constants::pi; +static double fba_epsilon_tolerance = 1e-7; -struct kinetic_parm +struct KineticParam { string name; - string untis; + string units; float value; }; -struct exchange_data +struct ExchangeFluxData { string density_name; string fba_flux_id; int density_index; - kinetic_parm Km; - kinetic_parm Vmax; + KineticParam Km; + KineticParam Vmax; }; - class dFBAIntracellular : public PhysiCell::Intracellular { private: - std::string sbml_filename; - FBA_model model; + public: - std::map parameters; - std::map substrate_exchanges; + double dfba_time_step = PhysiCell::diffusion_dt; - double next_model_run = 0; + std::string sbml_filename; + std::string objective_reaction; + double reference_volume = 0.0; + double cell_density = 0.0; + double max_growth_rate = 0; + double current_growth_rate = 0; + double next_dfba_run = 0; + bool use_metabolic_death = true; + std::string death_type; + std::string death_trigger_flux; + double death_flux_threshold = 0.0; + double death_rate_increase = 0.0; + bool flag_for_death = false; + dFBAModel sbml_model; + bool is_initialized = false; - public: - + bool skip_standard_optimization = false; + + /** + * \brief Optional second optimisation stage, run after the objective has + * been maximised, to resolve alternative optima. + * + * Standard FBA fixes the objective value but not the flux distribution, + * and in dFBA the flux distribution is what couples the cell to BioFVM. + * When enabled, the second stage holds the objective at its optimum and + * selects one distribution among the equally optimal ones. + * + * Off unless configured, so an existing configuration behaves exactly as + * before. "parsimonious" is the only type implemented. + * + * These fields only record what was configured. They are applied once, by + * calling dFBAModel::enableParsimonious() after the model is loaded, and + * the second stage is then run by dFBAModel::optimize() itself -- not from + * here. update() and any custom_optimization function call optimize() and + * get the configured flux distribution back, with no stage handling of + * their own, so a code path cannot forget to apply pFBA. + */ + bool use_secondary_objective = false; + std::string secondary_objective_type = "parsimonious"; + double secondary_fraction_of_optimum = 1.0; + + double liter_micron_cubes_conversion = 1e15; // 1 liter = 1e15 micron^3 + + /** \brief map between density IDs and exchange reactions */ + std::map substrate_exchanges; dFBAIntracellular(); dFBAIntracellular(pugi::xml_node& node); - dFBAIntracellular(dFBAIntracellular* copy); - - // rwh: review this - Intracellular* clone() - { - dFBAIntracellular* clone = new dFBAIntracellular(this); - clone->sbml_filename = this->sbml_filename; - clone->substrate_exchanges = this->substrate_exchanges; - return static_cast(clone); + dFBAIntracellular(const dFBAIntracellular& copy); + + // ~Intracellular(); + + // rwh: review this + Intracellular* clone(){ + return static_cast(new dFBAIntracellular(*this)); } Intracellular* getIntracellularModel() - { + { return static_cast(this); } + // ================ generic ================ + // This function parse the xml cell definition void initialize_intracellular_from_pugixml(pugi::xml_node& node); - void start(); + // This function checks if it's time to update the model + bool need_update() { return std::abs(PhysiCell::PhysiCell_globals.current_time -this->next_dfba_run) <= 0.0001; } + + // This function deals with inheritance from mother to daughter cells + void inherit(PhysiCell::Cell* cell){ return;}; - bool need_update(); - - void update(){ }; + // Get value for model parameter + double get_parameter_value(std::string name) { return 0;} + + // Set value for model parameter + void set_parameter_value(std::string name, double value) { return; } - void update(PhysiCell::Cell* pCell, PhysiCell::Phenotype& phenotype, double dt); - void inherit(PhysiCell::Cell * cell) {} + std::string get_state(){ return ""; } - int update_phenotype_parameters(PhysiCell::Phenotype& phenotype); - bool has_variable(std::string name) { return true; } + void display(std::ostream& os){ return; } + + void start(); + + virtual void update(); + + void update(PhysiCell::Cell* cell, PhysiCell::Phenotype& phenotype, double dt){ + // STEP 1. + // date exchange fluxes lower bound using concentration values of the + // corresponding densities at the agent voxel + if (cell->phenotype.death.dead == false) + { + this->update_dfba_inputs(cell, phenotype, dt); + + // STEP 2: Run optimization (custom or standard) + if (cell->functions.custom_optimization != NULL) { + // Use custom optimization function if set + cell->functions.custom_optimization(cell, phenotype, dt); + } else { + // Standard biomass optimization + this->update(); + } + + // STEP 3. Update the cell volumne using the growth rate from FBA + // STEP 4. rescale exchange fluxes from the dfba model and use them to update the net_export_rates + // STEP 5. remove the internalized substrates if needed + this->update_dfba_outputs(cell, phenotype, dt); + + this->next_dfba_run = PhysiCell::PhysiCell_globals.current_time + dfba_time_step; + + // if (phenotype.volume.total >= 2 * this->reference_volume){ + // cell->flag_for_division(); + // } + double v = phenotype.volume.total; + double v_ref = this->reference_volume; + + // Only start checking after ~1.6 × reference volume + if (v >= 1.6 * v_ref) { + // normalized progress from 0 (at 1.6×) to 1 (at 2×) + double progress = (v - 1.6 * v_ref) / (0.4 * v_ref); + if (progress < 0.0) progress = 0.0; + if (progress > 1.0) progress = 1.0; + + // map progress to probability of dividing this timestep + // e.g. sigmoid or just linear: + double prob = progress; // 0 at 1.6×, 1 at 2× + + // stochastic check + if (PhysiCell::UniformRandom() < prob * dt) { + cell->flag_for_division(); + } + } + } + else + { + this->current_growth_rate = 0.0; + // update the cell volume using the standard volume update function + PhysiCell::standard_volume_update_function(cell, phenotype, dt); // to update volume during necrosis, during necrotitc swelling, volume increases until rupture + return ; + } + }; + + void update_dfba_inputs( PhysiCell::Cell* pCell, PhysiCell::Phenotype& phenotype, double dt ); + void update_dfba_outputs( PhysiCell::Cell* pCell, PhysiCell::Phenotype& phenotype, double dt ); + + dFBASolution optimize_for_objective(std::string reaction_id, double coefficient); + + // =============== dFBA specific functions =============== + + + int parse_transport_model(pugi::xml_node& node); + void parse_growth_model(pugi::xml_node& node); + void parse_death_model(pugi::xml_node& node); + void parse_secondary_objective(pugi::xml_node& node); + void initLpSolver(); + + void setObjectiveCoefficient(std::string reaction_id, double coefficient) { + this->sbml_model.setObjectiveCoefficient(reaction_id, coefficient); + } + + void clearObjective() { + this->sbml_model.clearObjective(); + } + + void restoreObjectiveState() { + this->sbml_model.restoreObjectiveState(); + } // libroadrunner specifics - int validate_PhysiCell_tokens(PhysiCell::Phenotype& phenotype){ return true;} - int validate_SBML_species(){ return true;} - int create_custom_data_for_SBML(PhysiCell::Phenotype& phenotype) {return 0; } - std::string get_state(){ return "none";} - double get_parameter_value(std::string name){ return -1.0; } - void set_parameter_value(std::string name, double value){ } + + // for now, define dummy methods for these in the abstract parent class + + // This function initialize the model, needs to be called on each cell once created + - // for now, define dummy methods for these in the abstract parent class - bool has_node(std::string name) { return false; } - bool get_boolean_variable_value(std::string name) { return false; } - void set_boolean_variable_value(std::string name, bool value) {} - void print_current_nodes() {} // static void save_PhysiBoSS(std::string path, std::string index); - void display(std::ostream&os) {} static void save_dFBA(std::string path, std::string index); -}; + void update_volume(PhysiCell::Cell* pCell, PhysiCell::Phenotype& phenotype, double growth_rate, double dt); + void standard_update_cell_volume(PhysiCell::Cell* pCell, PhysiCell::Phenotype& phenotype, double growth_rate, double dt); + + + + // ================ specific to "dFBA" ================ + + double get_flux_value(std::string name); + double get_growth_rate(){ return this->current_growth_rate; } + void print_model(PhysiCell::Cell* pCell, double current_time, std::string output_folder = "./output"); + + // ================ specific to "maboss" ================ + bool has_variable(std::string name) { return false; } + bool get_boolean_variable_value(std::string name) { return false; } + void set_boolean_variable_value(std::string name, bool value) { } + void print_current_nodes(){ } + + + // ================ specific to "roadrunner" ================ + int update_phenotype_parameters(PhysiCell::Phenotype& phenotype) {return 0; } + int validate_PhysiCell_tokens(PhysiCell::Phenotype& phenotype) {return 0; } + int validate_SBML_species() {return 0; } + int create_custom_data_for_SBML(PhysiCell::Phenotype& phenotype) {return 0; } + +}; +} #endif diff --git a/addons/dFBA/test/Makefile b/addons/dFBA/test/Makefile new file mode 100644 index 000000000..44a19da60 --- /dev/null +++ b/addons/dFBA/test/Makefile @@ -0,0 +1,147 @@ +CC := g++ + +# Allow override from environment +ifdef PHYSICELL_CPP + CC := $(PHYSICELL_CPP) +endif + +ARCH := native + +# For debugging with gdb just add: -g3 -ggdb -O3 +CFLAGS := -march=$(ARCH) -O3 -fomit-frame-pointer -mfpmath=both -fopenmp -m64 -std=c++11 + +# Dependencies +DEPS_DIR := ../ext +# Vendored libSBML and Clp come in with -isystem, not -I: they are never edited +# here, so -MMD should not record them as prerequisites, and -isystem also +# silences warnings originating inside them. ../src stays -I -- those headers +# are the ones that do change, and are exactly what the .d files must track. +DEPS_CFLAGS := -isystem $(DEPS_DIR)/libsbml/include -isystem $(DEPS_DIR)/coin-or/include -I../src +LD_PATH := -L$(DEPS_DIR)/libsbml/lib -L$(DEPS_DIR)/coin-or/lib + +# Runtime rpath (so you don’t need LD_LIBRARY_PATH) +RPATH := -Wl,-rpath,$(DEPS_DIR)/libsbml/lib -Wl,-rpath,$(DEPS_DIR)/coin-or/lib + +## "linux-x64", "linux-x86", "win64", "win32", "osx" +PKG_ARCH := linux-x64 + +# --- Link against the actual names installed by setup_fba.py --- +# Use the static archives explicitly (portable), or switch to -lClp -lCoinUtils to use shared libs. +LIBS := $(DEPS_DIR)/coin-or/lib/libClp.a $(DEPS_DIR)/coin-or/lib/libCoinUtils.a \ + -llapack -lsbml-static -lxml2 -lbz2 -lz + +# If you prefer shared libs, comment the two lines above and use this instead: +# LIBS := -lClp -lCoinUtils -llapack -lsbml -lxml2 -lbz2 -lz + +ifeq ($(OS),Windows_NT) + WIN_LD_FLAG := -LC:\msys64\mingw64\bin + ARCH := win64 +else + WIN_LD_FLAG := + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Darwin) + UNAME_P := $(shell uname -p) + var := $(shell which $(CC) | xargs file) + ifeq ($(lastword $(var)),arm64) + CFLAGS := -march=$(ARCH) -O3 -fomit-frame-pointer -fopenmp -m64 -std=c++11 + endif + endif +endif + +# Header dependency tracking. +# +# Without this, a .o is rebuilt only when its own .cpp changes. Editing a +# header -- say addons/dFBA/src/dfba_intracellular.h -- rebuilt nothing, so an +# incremental `make` produced a binary whose translation units disagreed about +# class layout: one object allocated a dFBAIntracellular of one size while +# another read its members at offsets from a different version of the header. +# The result was reads past the end of the object, a corrupted std::map, and a +# segfault that vanished on any clean rebuild and so looked like a latent bug +# in the source. -MMD writes a .d file of each object's header prerequisites +# and -MP keeps a removed header from breaking the build. +DEPFLAGS := -MMD -MP + +COMPILE_COMMAND := $(CC) $(CFLAGS) $(DEPS_CFLAGS) $(DEPFLAGS) + +SRC_DIR := "./" + +vpath %.o $(SRC_DIR) +vpath %.cpp $(SRC_DIR) +vpath %.h $(SRC_DIR) + +# Objects from ../src +ALL_OBJECTS := dfba_Metabolite.o dfba_Reaction.o dfba_Solution.o dfba_Model.o + +# Use a concrete header as the sentinel that setup_fba.py produced deps +LIBFBA_SENTINEL := $(DEPS_DIR)/coin-or/include/coin/CoinPackedMatrix.hpp + +.PHONY: all make_lib clean libFBA test_sbml test_clp test_fba test_pfba + +make_lib: libFBA $(ALL_OBJECTS) +all: make_lib test_sbml test_clp test_fba + +# --- Tests (link stage) --- +test_sbml: $(LIBFBA_SENTINEL) $(ALL_OBJECTS) ./main_test_sbml.cpp + $(COMPILE_COMMAND) -o $@ $(ALL_OBJECTS) main_test_sbml.cpp $(LD_PATH) $(WIN_LD_FLAG) $(RPATH) $(LIBS) + @echo created $@ + @echo + +test_clp: $(LIBFBA_SENTINEL) $(ALL_OBJECTS) ./main_test_clp.cpp + $(COMPILE_COMMAND) -o $@ $(ALL_OBJECTS) main_test_clp.cpp $(LD_PATH) $(WIN_LD_FLAG) $(RPATH) $(LIBS) + @echo created $@ + @echo + +test_fba: $(LIBFBA_SENTINEL) $(ALL_OBJECTS) ./main_test_fba.cpp + $(COMPILE_COMMAND) -o $@ $(ALL_OBJECTS) main_test_fba.cpp $(LD_PATH) $(WIN_LD_FLAG) $(RPATH) $(LIBS) + @echo created $@ + @echo + +test_pfba: $(LIBFBA_SENTINEL) $(ALL_OBJECTS) ./main_test_pfba.cpp ./dfba_test.h + $(COMPILE_COMMAND) -o $@ $(ALL_OBJECTS) main_test_pfba.cpp $(LD_PATH) $(WIN_LD_FLAG) $(RPATH) $(LIBS) + @echo created $@ + @echo + +# Stage-two warm-start benchmark. Backs the primal-vs-dual choice documented in +# dFBAModel::optimizeParsimonious(); run it with DFBA_PFBA_DUAL=1 to compare. +bench_pfba: $(LIBFBA_SENTINEL) $(ALL_OBJECTS) ./main_bench_pfba.cpp + $(COMPILE_COMMAND) -o $@ $(ALL_OBJECTS) main_bench_pfba.cpp $(LD_PATH) $(WIN_LD_FLAG) $(RPATH) $(LIBS) + @echo created $@ + @echo + +# Warm-start evidence: simplex iterations with a reused basis vs a rebuilt one. +bench_warmstart: $(LIBFBA_SENTINEL) $(ALL_OBJECTS) ./main_bench_warmstart.cpp + $(COMPILE_COMMAND) -o $@ $(ALL_OBJECTS) main_bench_warmstart.cpp $(LD_PATH) $(WIN_LD_FLAG) $(RPATH) $(LIBS) + @echo created $@ + @echo + +# --- Build dependencies once (creates headers and libs under ../ext) --- +libFBA: $(LIBFBA_SENTINEL) + +$(LIBFBA_SENTINEL): + python3 ../../../beta/setup_fba.py --pkg libsbml --arch $(PKG_ARCH) + python3 ../../../beta/setup_fba.py --pkg coin-or --arch $(PKG_ARCH) + +# --- Object files (compile stage; no link libs here) --- +dfba_intracellular.o: ../src/dfba_intracellular.cpp + $(COMPILE_COMMAND) -c $< -o $@ + +dfba_Model.o: ../src/dfba_Model.cpp + $(COMPILE_COMMAND) -c $< -o $@ + +dfba_Reaction.o: ../src/dfba_Reaction.cpp + $(COMPILE_COMMAND) -c $< -o $@ + +dfba_Metabolite.o: ../src/dfba_Metabolite.cpp + $(COMPILE_COMMAND) -c $< -o $@ + +dfba_Solution.o: ../src/dfba_Solution.cpp + $(COMPILE_COMMAND) -c $< -o $@ + +clean: + rm -f *.o + rm -f *.d + rm -f ./test_* + +# Pull in the header prerequisites recorded by -MMD on the previous build. +# Wrapped in wildcard so a first build, with no .d files yet, is not an error. +-include $(wildcard *.d) diff --git a/addons/dFBA/test/check_pfba_vs_cobrapy.py b/addons/dFBA/test/check_pfba_vs_cobrapy.py new file mode 100644 index 000000000..8953ffb44 --- /dev/null +++ b/addons/dFBA/test/check_pfba_vs_cobrapy.py @@ -0,0 +1,54 @@ +"""Compare the addon's pFBA against cobrapy's, on exactly the problem the addon solved. + +Reads pfba_dump.tsv, which carries every reaction's bounds and objective +coefficient, applies them to the same SBML in cobrapy, runs cobra's pfba, and +compares the total and the per-reaction fluxes. +""" +import sys +import numpy as np +from cobra.io import read_sbml_model +from cobra.flux_analysis import pfba + +noop = lambda x: x +F = {k: noop for k in ("F_GENE","F_GENE_REV","F_REACTION","F_REACTION_REV","F_SPECIE","F_SPECIE_REV")} + +meta, rows = {}, [] +for line in open(sys.argv[1] if len(sys.argv) > 1 else "pfba_dump.tsv"): + if line.startswith("#reaction"): continue + if line.startswith("#"): + k, v = line[1:].rstrip("\n").split("\t", 1); meta[k] = v; continue + f = line.rstrip("\n").split("\t") + rows.append((f[0], float(f[1]), float(f[2]), float(f[3]), float(f[4]))) + +m = read_sbml_model(meta["model"], f_replace=F); m.solver = "gurobi" +for rid, lb, ub, obj, _ in rows: # replicate the addon's exact problem + r = m.reactions.get_by_id(rid); r.lower_bound, r.upper_bound = lb, ub +m.objective = {m.reactions.get_by_id(rid): obj for rid, _, _, obj, _ in rows if obj != 0} + +sol = pfba(m) +internal = [r for r in m.reactions if len(r.metabolites) > 1] +tot_cobra = sum(abs(sol.fluxes[r.id]) for r in internal) +tot_addon = float(meta["total_abs_flux_pfba"]) + +print(f"model {meta['model']}") +print(f" growth addon {float(meta['growth']):.9g}") +print(f" total |flux| addon {tot_addon:.9g} cobrapy {tot_cobra:.9g} " + f"rel diff {abs(tot_addon-tot_cobra)/max(tot_cobra,1e-12):.3e}") + +d = np.array([abs(fx - sol.fluxes[rid]) for rid, _, _, _, fx in rows]) +print(f" per-reaction flux: max |diff| {d.max():.3e} over {len(d)} reactions, " + f"{(d > 1e-6).sum()} above 1e-6") +# Tolerance: 1e-5 relative. pFBA's optimal TOTAL is unique, but on a model where +# the second stage removes large futile cycles the optimum is very flat, and the +# two implementations impose the optimality constraint differently -- cobrapy adds +# a variable and a constraint, this adds a bound on the objective reaction. A +# difference at this scale is slack in one of the two, not a different answer. +# What must hold exactly is that growth is unchanged, which the C++ test asserts +# separately; a lower total at the SAME growth is a better solution, not a +# constraint violation. +rel = abs(tot_addon - tot_cobra) / max(tot_cobra, 1e-12) +ok = rel <= 1e-5 +print((" TOTAL MATCHES cobrapy" if ok else " TOTALS DISAGREE") + + (f" (addon is lower by {tot_cobra-tot_addon:.3e}; a lower total at equal growth" + " is a better solution)" if ok and tot_addon < tot_cobra - 1e-12 else "")) +sys.exit(0 if ok else 1) diff --git a/addons/dFBA/test/data/Ecoli_core.xml b/addons/dFBA/test/data/Ecoli_core.xml index 3484aff2a..f1b169359 100644 --- a/addons/dFBA/test/data/Ecoli_core.xml +++ b/addons/dFBA/test/data/Ecoli_core.xml @@ -3168,7 +3168,7 @@ - + diff --git a/addons/dFBA/test/test_problem.lp b/addons/dFBA/test/data/test_problem.lp similarity index 100% rename from addons/dFBA/test/test_problem.lp rename to addons/dFBA/test/data/test_problem.lp diff --git a/addons/dFBA/test/dfba_test.h b/addons/dFBA/test/dfba_test.h new file mode 100644 index 000000000..fb18e1ef0 --- /dev/null +++ b/addons/dFBA/test/dfba_test.h @@ -0,0 +1,97 @@ +/* + * dfba_test.h + * + * Minimal assertion helpers for the standalone dFBA tests. + * + * The pre-existing test programs in this directory print results and always + * exit 0, so they cannot fail a build. These macros record failures and + * DFBA_TEST_SUMMARY() returns a non-zero exit code when any check failed, so a + * test binary can actually gate a change. + * + * Deliberately header-only and dependency-free: AGENTS.md rules out pulling in + * a test framework. + */ + +#ifndef DFBA_TEST_H_ +#define DFBA_TEST_H_ + +#include +#include +#include +#include + +namespace dfba_test { + +static int checks_run = 0; +static int checks_failed = 0; +static std::string current_section = ""; + +inline void section(const std::string& name) +{ + current_section = name; + std::cout << "\n--- " << name << " ---" << std::endl; +} + +inline void report(bool ok, const std::string& what, const std::string& detail) +{ + checks_run++; + if (ok) + { + std::cout << " ok " << what << std::endl; + return; + } + checks_failed++; + std::cout << " FAIL " << what << std::endl; + if (!detail.empty()) + std::cout << " " << detail << std::endl; +} + +template +inline std::string to_str(const T& value) +{ + std::ostringstream os; + os << value; + return os.str(); +} + +inline int summary() +{ + std::cout << "\n=================================================" << std::endl; + std::cout << checks_run - checks_failed << "/" << checks_run << " checks passed"; + if (checks_failed > 0) + std::cout << " (" << checks_failed << " FAILED)"; + std::cout << std::endl; + std::cout << "=================================================" << std::endl; + return checks_failed == 0 ? 0 : 1; +} + +} // namespace dfba_test + +#define CHECK(cond, what) \ + dfba_test::report((cond), (what), "") + +/* Arguments are evaluated exactly once: test expressions may have side effects + (knockOutGene/restoreGene return a count and mutate the model). */ +#define CHECK_EQ(actual, expected, what) \ + do { \ + auto _a = (actual); \ + auto _e = (expected); \ + std::ostringstream _os; \ + _os << "expected " << _e << ", got " << _a; \ + dfba_test::report(_a == _e, (what), _os.str()); \ + } while (0) + +#define CHECK_NEAR(actual, expected, tol, what) \ + do { \ + auto _a = (actual); \ + auto _e = (expected); \ + auto _t = (tol); \ + std::ostringstream _os; \ + _os << "expected " << _e << " +/- " << _t << ", got " << _a; \ + dfba_test::report(std::fabs(_a - _e) <= _t, (what), _os.str()); \ + } while (0) + +#define DFBA_TEST_SECTION(name) dfba_test::section(name) +#define DFBA_TEST_SUMMARY() dfba_test::summary() + +#endif /* DFBA_TEST_H_ */ diff --git a/addons/dFBA/test/main_bench_pfba.cpp b/addons/dFBA/test/main_bench_pfba.cpp new file mode 100644 index 000000000..a53df522f --- /dev/null +++ b/addons/dFBA/test/main_bench_pfba.cpp @@ -0,0 +1,94 @@ +/* + * Stage-two warm-start benchmark: dual() or primal()? + * + * Between dFBA timesteps only the bounds of the stage-two problem move (the + * mirrored Michaelis-Menten bounds, and the objective floor that tracks the + * stage-one optimum). Theory says a bound change leaves the previous basis + * dual-feasible, which favours the dual simplex -- but that is an argument, not + * a measurement, so measure it. + * + * The simplex actually used is chosen inside dFBAModel::solveParsimoniousStage() + * from DFBA_PFBA_DUAL, so this program reads the SAME variable for its label. + * Do not introduce a second switch here: a benchmark whose label and whose + * subject disagree is worse than no benchmark. + * + * make bench_pfba + * ./bench_pfba data/Ecoli_core.xml # the default simplex + * DFBA_PFBA_DUAL=1 ./bench_pfba data/Ecoli_core.xml + */ +#include +#include +#include +#include +#include +#include + +#include "../src/dfba_Model.h" +#include "../src/dfba_Reaction.h" + +using namespace std::chrono; + +int main(int argc, char** argv) +{ + const char* path = (argc > 1) ? argv[1] : "data/Ecoli_core.xml"; + const char* glc = (argc > 2) ? argv[2] : "R_EX_glc__D_e"; + const char* o2 = (argc > 3) ? argv[3] : "R_EX_o2_e"; + const int N = 1000; + + const char* env = getenv("DFBA_PFBA_DUAL"); + const bool dual = (env != NULL && std::string(env) == "1"); + + // A trajectory of bounds, not random restarts: consecutive steps must be + // close together or there is no warm start to measure. + std::mt19937 rng(5); + std::uniform_real_distribution jitter(0.8, 1.2); + std::vector gl(N), ox(N); + for (int i = 0; i < N; i++) { gl[i] = -10.0 * jitter(rng); ox[i] = -20.0 * jitter(rng); } + + dFBAModel model; + model.readSBMLModel(path); + model.initProblem(); + model.enableParsimonious(1.0); + + // the same model without pFBA, walking the same trajectory, as the baseline + dFBAModel plain; + plain.readSBMLModel(path); + plain.initProblem(); + + double t_stage1 = 0.0, t_stage2 = 0.0, total_flux = 0.0; + int non_optimal = 0, counted = 0; + + for (int i = 0; i < N; i++) + { + model.setReactionLowerBound(glc, gl[i]); + model.setReactionLowerBound(o2, ox[i]); + + // optimize() now runs both stages, so stage two cannot be timed in + // isolation from here. Time the whole call against a pFBA-free model + // solving the same trajectory, and report the difference as stage two. + auto t0 = high_resolution_clock::now(); + dFBASolution both = model.optimize(); + const double t_both = duration(high_resolution_clock::now() - t0).count(); + + plain.setReactionLowerBound(glc, gl[i]); + plain.setReactionLowerBound(o2, ox[i]); + auto t1 = high_resolution_clock::now(); + dFBASolution one = plain.optimize(); + const double t_one = duration(high_resolution_clock::now() - t1).count(); + + if (both.status != "optimal" || one.status != "optimal") { non_optimal++; continue; } + if (both.secondary_status != "optimal") { non_optimal++; continue; } + + t_stage1 += t_one; + t_stage2 += (t_both - t_one > 0.0) ? (t_both - t_one) : 0.0; + total_flux += both.total_absolute_flux; + counted++; + } + + printf("%-28s %-7s stage1 %7.4f ms stage2 %7.4f ms sum(total|flux|) %.9g" + " solved %d/%d non-optimal %d\n", + path, dual ? "dual" : "primal", + t_stage1 / N * 1000.0, t_stage2 / N * 1000.0, + total_flux, counted, N, non_optimal); + return 0; +} diff --git a/addons/dFBA/test/main_bench_warmstart.cpp b/addons/dFBA/test/main_bench_warmstart.cpp new file mode 100644 index 000000000..63d0ad17a --- /dev/null +++ b/addons/dFBA/test/main_bench_warmstart.cpp @@ -0,0 +1,128 @@ +/* + * Does the addon warm-start its LP between dFBA timesteps? + * + * The question is not rhetorical: a per-cell ClpSimplex that is reused across + * timesteps keeps its basis, so when only the Michaelis-Menten bounds move the + * next solve should resume from the previous optimum and take a handful of + * simplex iterations instead of solving from scratch. If instead something + * rebuilt or re-scaled the problem each step, the basis would be discarded and + * every solve would pay full price -- which is what this measures. + * + * Simplex ITERATIONS are the primary evidence here, not wall time: they are a + * property of the solve, immune to CPU contention and to this machine's very + * noisy timings. Wall time is reported alongside as a sanity check. + * + * warm -- what the addon actually does: one model, move bounds, optimize() + * cold -- a fresh model per step, counting the iterations initProblem()'s + * initialSolve() spends getting there from an all-slack basis + * + * The cold timing includes building the LP, which is most of its cost; the + * iteration counts are the clean comparison. + * + * Both modes walk the SAME bound trajectory and must reach the same objective + * at every step; the program checks that and reports the largest disagreement, + * so a "speedup" that came from solving a different problem cannot pass. + * + * make bench_warmstart && ./bench_warmstart [model.xml] [glc_id] [o2_id] + */ +#include +#include +#include +#include +#include +#include + +#include "../src/dfba_Model.h" +#include "../src/dfba_Reaction.h" + +#include + +using namespace std::chrono; + +int main(int argc, char** argv) +{ + const char* path = (argc > 1) ? argv[1] : "data/Ecoli_core.xml"; + const char* glc = (argc > 2) ? argv[2] : "R_EX_glc__D_e"; + const char* o2 = (argc > 3) ? argv[3] : "R_EX_o2_e"; + const int N = 1000; + const int N_COLD = 50; // each cold step re-parses the SBML file + + // Consecutive timesteps, not independent restarts: the bounds have to move + // a little, the way local concentrations do, or there is nothing to reuse. + std::mt19937 rng(5); + std::uniform_real_distribution jitter(0.95, 1.05); + std::vector gl(N), ox(N); + double g = -10.0, o = -20.0; + for (int i = 0; i < N; i++) + { + g *= jitter(rng); o *= jitter(rng); + if (g < -20.0) g = -20.0; if (g > -1.0) g = -1.0; + if (o < -40.0) o = -40.0; if (o > -2.0) o = -2.0; + gl[i] = g; ox[i] = o; + } + + std::vector obj_warm(N, 0.0), obj_cold(N, 0.0); + double iter_warm = 0, iter_cold = 0, t_warm = 0, t_cold = 0; + int bad = 0; + + { // warm: one problem, reused -- the addon's own behaviour + dFBAModel m; m.readSBMLModel(path); m.initProblem(); + for (int i = 0; i < N; i++) + { + m.setReactionLowerBound(glc, gl[i]); + m.setReactionLowerBound(o2, ox[i]); + auto t0 = high_resolution_clock::now(); + dFBASolution s = m.optimize(); + t_warm += duration(high_resolution_clock::now() - t0).count(); + if (s.status != "optimal") { bad++; continue; } + obj_warm[i] = s.objective_value; + iter_warm += m.getLpModel()->numberIterations(); + } + } + + { // cold: a brand-new model per step, so there is no basis to inherit. + // + // The iterations that matter here are the ones initProblem() spends: + // it ends in initialSolve(), which solves the LP from an all-slack + // basis. Reading numberIterations() after a subsequent optimize() + // would report ~0 for BOTH modes -- the problem is already optimal by + // then -- which is exactly the trap that made the first version of + // this benchmark say warm and cold were identical. + // Only N_COLD steps: each one re-parses the SBML file, which dwarfs the + // solve. The per-solve averages stay comparable because both modes walk + // the same trajectory and this takes its prefix. + for (int i = 0; i < N_COLD; i++) + { + dFBAModel m; m.readSBMLModel(path); + // Set the bounds on the reaction objects, NOT through + // setReactionLowerBound(): that writes into this->problem, which + // does not exist until initProblem() builds it. initProblem() reads + // these values when it loads the matrix, so the LP is built with + // this step's bounds and then solved from an all-slack basis. + m.getReaction(glc)->setLowerBound(gl[i]); + m.getReaction(o2)->setLowerBound(ox[i]); + auto t0 = high_resolution_clock::now(); + m.initProblem(); // all-slack basis + initialSolve() + t_cold += duration(high_resolution_clock::now() - t0).count(); + iter_cold += m.getLpModel()->numberIterations(); + obj_cold[i] = m.getLpModel()->objectiveValue(); + if (!m.getLpModel()->isProvenOptimal()) bad++; + } + } + + double worst = 0.0; + for (int i = 0; i < N_COLD; i++) + worst = std::max(worst, std::fabs(obj_warm[i] - obj_cold[i])); + + printf("\n%s %d consecutive steps\n", path, N); + printf(" %-6s %12s %14s\n", "", "iters/solve", "ms/solve"); + printf(" %-6s %12.3f %14.4f\n", "warm", iter_warm / N, t_warm / N * 1000.0); + printf(" %-6s %12.3f %14.4f (n=%d, includes building the LP)\n", + "cold", iter_cold / N_COLD, t_cold / N_COLD * 1000.0, N_COLD); + const double w = iter_warm / N, c = iter_cold / N_COLD; + if (w > 0.0) printf(" cold needs %.0fx the simplex iterations of a warm re-solve\n", c / w); + else printf(" warm re-solves took ZERO simplex iterations across all %d steps\n", N); + printf(" worst objective disagreement warm vs cold: %.3e non-optimal: %d\n\n", + worst, bad); + return 0; +} diff --git a/addons/dFBA/test/testClp.cpp b/addons/dFBA/test/main_test_clp.cpp similarity index 92% rename from addons/dFBA/test/testClp.cpp rename to addons/dFBA/test/main_test_clp.cpp index 98c59c3cb..80f1107eb 100644 --- a/addons/dFBA/test/testClp.cpp +++ b/addons/dFBA/test/main_test_clp.cpp @@ -1,9 +1,9 @@ #include #include -#include -#include -#include +#include "../ext/coin-or/include/coin/CoinPackedMatrix.hpp" +#include "../ext/coin-or/include/coin/CoinPackedVector.hpp" +#include "../ext/coin-or/include/coin/ClpSimplex.hpp" using namespace std; @@ -72,7 +72,7 @@ int main() lp_model->loadProblem(matrix, col_lb, col_ub, objective, row_lb, row_ub); lp_model->setOptimizationDirection(z_sense); - const char* filename = "test/test_problem"; + const char* filename = "data/test_problem"; std::cout << "- Saving problem in lp format in " << filename << std::endl; lp_model->writeLp(filename, "lp"); diff --git a/addons/dFBA/test/main_test_fba.cpp b/addons/dFBA/test/main_test_fba.cpp new file mode 100644 index 000000000..6173d5ea5 --- /dev/null +++ b/addons/dFBA/test/main_test_fba.cpp @@ -0,0 +1,41 @@ +#include +#include + +#include "../src/dfba_Model.h" + +using namespace std; + +int main (int argc, const char *argv[]) +{ + + if (argc != 2) + { + cout << endl << "Usage: readSBML filename" << endl << endl; + return 1; + } + const char *sbml_fileame = argv[1]; + + dFBAModel *model = new dFBAModel(); + std::cout << "Reading SBML model from: " << sbml_fileame << " "; + model->readSBMLModel(sbml_fileame); + std::cout << "Ok!" << std::endl; + std::cout << "Model " << model->getId() << "has been correctly loaded" << std::endl; + std::cout << "Initializing LP model: "; + model->initProblem(); + std::cout << "Ok!" << std::endl; + model->setReactionLowerBound("R_EX_glc__D_e", -100.0); + model->setReactionLowerBound("R_EX_o2_e", 0.0); + + + std::cout << "Testing FBA: "; + dFBASolution solution = model->optimize(); + if ( solution.status == "optimal"){ + std::cout << "OPTIMAL SOLUTION FOUND" << std::endl; + solution = model->getSolution(); + float fopt = solution.getObjectiveValue(); + std::cout << "Objective value: " << fopt << std::endl; + } + + delete model; + return 0; +} diff --git a/addons/dFBA/test/main_test_pfba.cpp b/addons/dFBA/test/main_test_pfba.cpp new file mode 100644 index 000000000..e2a6be083 --- /dev/null +++ b/addons/dFBA/test/main_test_pfba.cpp @@ -0,0 +1,147 @@ +/* + * Parsimonious FBA: does the second stage preserve growth, reduce total flux, + * and agree with cobrapy? + * + * Usage: ./test_pfba [model.xml] (writes pfba_dump.tsv for the cobrapy check) + */ +#include +#include +#include +#include + +#include "../src/dfba_Model.h" +#include "../src/dfba_Reaction.h" +#include "dfba_test.h" + +int main(int argc, char** argv) +{ + const std::string f = (argc > 1) ? argv[1] : "data/Ecoli_core.xml"; + const double GLC = -10.0, O2 = -20.0; + + DFBA_TEST_SECTION("stage one is unchanged when pFBA is off"); + dFBAModel plain; + plain.readSBMLModel(f.c_str()); plain.initProblem(); + plain.setReactionLowerBound("R_EX_glc__D_e", GLC); + plain.setReactionLowerBound("R_EX_o2_e", O2); + dFBASolution s1 = plain.optimize(); + CHECK(s1.status == "optimal", "plain FBA solves"); + const double mu_plain = s1.objective_value; + const double tot_plain = plain.getTotalAbsoluteFlux(); + printf(" growth %.9g, total |flux| %.9g\n", mu_plain, tot_plain); + + DFBA_TEST_SECTION("pFBA preserves growth and reduces total flux"); + dFBAModel pf; + pf.readSBMLModel(f.c_str()); pf.initProblem(); + pf.enableParsimonious(1.0); + pf.setReactionLowerBound("R_EX_glc__D_e", GLC); + pf.setReactionLowerBound("R_EX_o2_e", O2); + // ONE call. The second stage is run by optimize() itself, so this is also + // the test that a caller cannot forget to apply pFBA. + dFBASolution a = pf.optimize(); + CHECK(a.status == "optimal", "stage one solves"); + CHECK(a.secondary_status == "optimal", "optimize() ran the second stage by itself"); + CHECK_NEAR(a.objective_value, mu_plain, 1e-9, + "objective_value is stage one's growth, not stage two's total flux"); + + double mu_after = 0.0; + for (auto r : pf.getListOfReactions()) + if (r->getObjectiveCoefficient() != 0.0) mu_after = r->getFluxValue(); + CHECK_NEAR(mu_after, mu_plain, 1e-6, "growth is preserved by stage two"); + + const double tot_pfba = pf.getTotalAbsoluteFlux(); + printf(" total |flux|: plain %.9g -> pFBA %.9g\n", tot_plain, tot_pfba); + CHECK(tot_pfba <= tot_plain + 1e-6, "total flux does not increase"); + if (tot_plain - tot_pfba > 1e-6) + printf(" stage two reduced total flux by %.3g%% -- the FBA optimum was degenerate\n", + 100.0 * (1.0 - tot_pfba / tot_plain)); + else + printf(" no reduction: the FBA optimum was already unique at these bounds\n"); + CHECK_NEAR(a.total_absolute_flux, tot_pfba, 1e-6, + "total_absolute_flux describes the distribution being returned"); + + // The unified reversible flux. Each split reaction's original column is + // constrained by v - p + m = 0, so Sum(p+m) from the LP can only equal + // Sum|v| read back off the reactions if every column really does hold + // p - m with at most one of the pair nonzero. This is what makes reading v + // directly correct, rather than subtracting p and m by hand. + CHECK_NEAR(pf.getTotalAbsoluteFlux(), tot_pfba, 1e-9, + "reversible fluxes are the unified p - m, consistent with Sum(p+m)"); + + DFBA_TEST_SECTION("pFBA is idempotent"); + dFBASolution c = pf.optimize(); + CHECK_NEAR(c.total_absolute_flux, a.total_absolute_flux, 1e-9, + "re-solving changes nothing"); + CHECK_NEAR(c.objective_value, a.objective_value, 1e-12, "growth is stable too"); + + DFBA_TEST_SECTION("a deep copy carries the pFBA setting and solves independently"); + dFBAModel clone; clone.deepCopy(pf); + CHECK(clone.isParsimoniousEnabled(), "clone has pFBA enabled"); + clone.setReactionLowerBound("R_EX_glc__D_e", GLC); + clone.setReactionLowerBound("R_EX_o2_e", O2); + dFBASolution d = clone.optimize(); + CHECK(d.secondary_status == "optimal", "clone's second stage ran"); + CHECK_NEAR(d.total_absolute_flux, a.total_absolute_flux, 1e-6, + "clone agrees with the original"); + CHECK_NEAR(d.objective_value, mu_plain, 1e-6, "clone's growth matches too"); + + DFBA_TEST_SECTION("disabled by default, and then nothing changes"); + dFBAModel off; off.readSBMLModel(f.c_str()); off.initProblem(); + CHECK(!off.isParsimoniousEnabled(), "pFBA is off unless asked for"); + off.setReactionLowerBound("R_EX_glc__D_e", GLC); + off.setReactionLowerBound("R_EX_o2_e", O2); + dFBASolution g = off.optimize(); + CHECK(g.secondary_status == "none", "no second stage is reported when none is configured"); + CHECK_NEAR(g.objective_value, mu_plain, 1e-12, + "a pFBA-free model still solves exactly the problem it always did"); + CHECK_NEAR(off.getTotalAbsoluteFlux(), tot_plain, 1e-9, + "and returns the same, unreduced flux distribution"); + + DFBA_TEST_SECTION("a reused model stays healthy across many bound changes"); + // Regression test for a real failure. The second-stage problem is + // warm-started across timesteps, and after many re-solves with moving + // bounds Clp returned status 4 -- "stopped due to errors" -- on problems + // that solve fine from a fresh model. Before the retry-from-clean-basis + // in solveParsimoniousStage(), 3 of these 81 combinations failed, and the + // affected cells silently kept stage-one fluxes 256x less parsimonious + // than the ones pFBA would have given them. + { + dFBAModel sweep; + sweep.readSBMLModel(f.c_str()); sweep.initProblem(); + sweep.enableParsimonious(1.0); + const double gs[] = {0.0, -1e-9, -1e-6, -1e-3, -0.01, -0.1, -1.0, -5.0, -10.0}; + const double os[] = {0.0, -1e-9, -1e-6, -1e-3, -0.01, -0.1, -1.0, -5.0, -20.0}; + int solved = 0, stage_two_failed = 0; + for (size_t i = 0; i < sizeof(gs)/sizeof(*gs); i++) + for (size_t j = 0; j < sizeof(os)/sizeof(*os); j++) + { + sweep.setReactionLowerBound("R_EX_glc__D_e", gs[i]); + sweep.setReactionLowerBound("R_EX_o2_e", os[j]); + dFBASolution s = sweep.optimize(); + if (s.status != "optimal") continue; + solved++; + if (s.secondary_status != "optimal") stage_two_failed++; + } + printf(" %d of %d feasible bound sets reached stage two\n", + solved - stage_two_failed, solved); + CHECK_EQ(stage_two_failed, 0, + "the second stage solves for every feasible bound set on a reused model"); + } + + // Dump the COMPLETE problem, not just the two bounds the test tried to set: + // the reaction ids differ between models (R_EX_glc__D_e vs R_EX_glc_e), so + // the only way the cobrapy check can replicate exactly what was solved is to + // be handed every bound. + FILE* out = fopen("pfba_dump.tsv", "w"); + fprintf(out, "#model\t%s\n", f.c_str()); + fprintf(out, "#growth\t%.17g\n#total_abs_flux_fba\t%.17g\n#total_abs_flux_pfba\t%.17g\n", + mu_plain, tot_plain, tot_pfba); + fprintf(out, "#reaction\tlb\tub\tobj\tflux_pfba\n"); + for (auto r : pf.getListOfReactions()) + fprintf(out, "%s\t%.17g\t%.17g\t%.17g\t%.17g\n", r->getId().c_str(), + r->getLowerBound(), r->getUpperBound(), + r->getObjectiveCoefficient(), r->getFluxValue()); + fclose(out); + printf("\nwrote pfba_dump.tsv for the cobrapy comparison\n"); + + return DFBA_TEST_SUMMARY(); +} diff --git a/addons/dFBA/test/testSBML.cpp b/addons/dFBA/test/main_test_sbml.cpp similarity index 96% rename from addons/dFBA/test/testSBML.cpp rename to addons/dFBA/test/main_test_sbml.cpp index 961229e54..73a230102 100644 --- a/addons/dFBA/test/testSBML.cpp +++ b/addons/dFBA/test/main_test_sbml.cpp @@ -30,7 +30,7 @@ int main (int argc, char* argv[]) cout << " Reading filename: " << filename << endl; cout << " Validation error(s): " << errors << endl; cout << endl; -// document->printErrors(cerr); + document->printErrors(cerr); Model* model = document->getModel(); int n_species = model->getNumSpecies(); diff --git a/addons/dFBA/test/testFBA.cpp b/addons/dFBA/test/testFBA.cpp deleted file mode 100644 index 0ba89e96d..000000000 --- a/addons/dFBA/test/testFBA.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include -#include - -#include "../src/FBA_model.h" - -using namespace std; - -int -main (int argc, const char *argv[]) -{ - - if (argc != 2) - { - cout << endl << "Usage: readSBML filename" << endl << endl; - return 1; - } - const char *sbml_fileame = argv[1]; - - FBA_model *model = new FBA_model(); - std::cout << "Reading SBML model from: " << sbml_fileame << " "; - model->readSBMLModel(sbml_fileame); - std::cout << "Ok!" << std::endl; - std::cout << "Initializing LP model: "; - model->initLpModel(); - std::cout << "Ok!" << std::endl; - - model->writeLp("iJO1366"); - - for(int i=1; i<10; i++){ - std::cout << "Running FBA: "; - - float bound = -1.0 * i; - model->setReactionLowerBound("R_EX_glc__D_e", bound); - - model->runFBA(); - std::cout << "Uppdating lower R_EX_glc__D_e bound: " << bound << std::endl; - - if (model->getSolutionStatus ()) - { - std::cout << "OPTIMAL SOLUTION FOUND" << std::endl; - float fopt = model->getObjectiveValue(); - std::cout << "Objective value: " << fopt << std::endl; - /* - for (Reaction* rxn :model->getListOfReactions ()) - { - double flux = rxn->getFluxValue(); - if ( std::abs(flux) < 1e-7 ) - continue; - std::cout << rxn->getId() << ": " << flux << std::endl; - } - */ - } - else - { - std::cout << "NO SOLUTION FOUND" << std::endl; - } - } - - delete model; - return 0; -} diff --git a/addons/libRoadrunner/CMakeLists.txt b/addons/libRoadrunner/CMakeLists.txt new file mode 100644 index 000000000..ad40f6ba4 --- /dev/null +++ b/addons/libRoadrunner/CMakeLists.txt @@ -0,0 +1,45 @@ +add_library(libRoadrunner OBJECT) + +file(GLOB SOURCES "src/*.cpp") +target_sources( + libRoadrunner + PRIVATE ${SOURCES} ${SOURCES} ${PROJECT_SOURCE_DIR}/core/PhysiCell_cell.cpp + ${PROJECT_SOURCE_DIR}/modules/PhysiCell_MultiCellDS.cpp) + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(LIBROADRUNNER_ARCHIVE_URL + https://sourceforge.net/projects/libroadrunner/files/libroadrunner-1.3.3/alpha-rhel-6.6/roadrunner-1.3.3-alpha.tar.gz/download + ) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL + "x86_64") + set(LIBROADRUNNER_ARCHIVE_URL + https://sourceforge.net/projects/libroadrunner/files/libroadrunner-1.4.18/roadrunner-osx-10.9-cp36m.tar.gz/download + ) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL + "arm64") + set(LIBROADRUNNER_ARCHIVE_URL + https://github.com/PhysiCell-Tools/intracellular_libs/raw/main/ode/roadrunner_macos_arm64.tar.gz + ) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(LIBROADRUNNER_ARCHIVE_URL + https://sourceforge.net/projects/libroadrunner/files/libroadrunner-1.4.18/roadrunner-win64-vs14-cp35m.zip/download + ) +else() + message(FATAL_ERROR "Unsupported OS: ${CMAKE_SYSTEM_NAME}") +endif() + +include(FetchContent) +FetchContent_Declare( + roadrunner + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + URL ${LIBROADRUNNER_ARCHIVE_URL}) +FetchContent_MakeAvailable(roadrunner) + +target_include_directories(libRoadrunner + PUBLIC ${roadrunner_SOURCE_DIR}/include/rr) +target_link_directories(libRoadrunner PUBLIC ${roadrunner_SOURCE_DIR}/lib) +target_link_libraries(libRoadrunner PUBLIC roadrunner_c_api) +target_compile_definitions(libRoadrunner PUBLIC ADDON_ROADRUNNER) + +find_package(OpenMP REQUIRED) +target_link_libraries(libRoadrunner PUBLIC OpenMP::OpenMP_CXX) diff --git a/addons/libRoadrunner/src/librr_intracellular.cpp b/addons/libRoadrunner/src/librr_intracellular.cpp index 1765c9532..3422a4575 100644 --- a/addons/libRoadrunner/src/librr_intracellular.cpp +++ b/addons/libRoadrunner/src/librr_intracellular.cpp @@ -3,56 +3,528 @@ #include #include +void RoadRunnerMapping::initialize_mapping( void ) +{ + bool use_for_input = io_type == "input"; + + int behavior_ind = PhysiCell::find_behavior_index(physicell_name); + int signal_ind = PhysiCell::find_signal_index(physicell_name); + if (behavior_ind != -1) + { + physicell_dictionary_name = "behaviors"; + index = behavior_ind; + if (use_for_input) + { + value_map = [this](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(this->sbml_species, PhysiCell::get_single_behavior(pCell, this->index)); }; + } + else + { + value_map = [this](PhysiCell::Cell *pCell) + { PhysiCell::set_single_behavior(pCell, this->index, pCell->phenotype.intracellular->get_parameter_value(this->sbml_species)); }; + } + } + else if (signal_ind != -1) + { + physicell_dictionary_name = "signals"; + index = signal_ind; + if (use_for_input) + { + value_map = [this](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(this->sbml_species, PhysiCell::get_single_signal(pCell, this->index)); }; + } + else + { + value_map = select_signal_setter(physicell_name, sbml_species); + } + } + else if (is_physicell_phenotype_token(physicell_name)) + { + physicell_dictionary_name = "tokens"; + if (use_for_input) + { + value_map = select_phenotype_by_token_inputter(physicell_name, sbml_species); + } + else + { + value_map = select_phenotype_by_token_outputter(physicell_name, sbml_species); + } + } + else + { + std::cerr << "ERROR: " << physicell_name << " is not a valid entry for the libRoadRunner mapping." << std::endl + << " The available entries are: " << std::endl + << " - signals (see dictionaries.txt in output)" << std::endl + << " - behaviors (see dictionaries.txt in output)" << std::endl + << " - tokens: " << std::endl + << " - mms, mpt, mmb, da, dn, vtsc, vtsn, vff" << std::endl + << " - ctr__" << std::endl + << " - _ where prefix is one of: sur, ssr, ssd, ser" << std::endl << std::endl + << "You can also defined your own pre- and post-update functions in the custom.cpp file." << std::endl + << " - Set them using `pCD->functions.pre_update_intracellular = foo;` and `pCD->functions.post_update_intracellular = bar;`" << std::endl + << " - These functions should have the signature `void foo(Cell* pCell, Phenotype& phenotype, double dt)`" << std::endl; + exit(-1); + } + mapping_initialized = true; + return; +} + +// PhysiCell does not have an API for setting signals, but libRoadRunner does in limited cases +MappingFunction select_signal_setter(const std::string& name, const std::string& sbml_species) +{ + // if "intracellular " or "internalized ", set the internalized substrate amount + if (name.find("intracellular") == 0 || name.find("internalized") == 0) + { + size_t space_ind = name.find(" "); + if (space_ind != std::string::npos) + { + std::string substrate_name = name.substr(space_ind + 1, std::string::npos); + int density_index = microenvironment.find_density_index(substrate_name); + if (density_index != -1) + { + return [density_index, sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.molecular.internalized_total_substrates[density_index] = pCell->phenotype.intracellular->get_parameter_value(sbml_species) * pCell->phenotype.volume.total; }; + } + } + std::cerr << "ERROR: \"" << name << "\" is not a valid signal that can be set using libRoadRunner." << std::endl + << " Somehow, this signal was recognized by the signals dictionary but now it seems malformed." << std::endl + << " I honestly don't know what to say...or how to help you :/" << std::endl; + exit(-1); + } + + // if "volume", set the cell volume + else if (name == "volume") + { + std::cout << "WARNING: setting the volume using libRoadRunner will do so by rescaling ALL cell volumes, not just setting the total volume." + << " To only set the total volume (or to set other components of the volume), use the `pCell->functions.post_update_intracellular`." << std::endl; + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.volume.multiply_by_ratio(pCell->phenotype.intracellular->get_parameter_value(sbml_species) / pCell->phenotype.volume.total); }; + } + + // if "damage", set the cell damage + else if (name == "damage") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.cell_integrity.damage = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + + // if begins with "custom", check that it is custom data and set that + else if (name.find("custom") == 0) + { + std::cerr << "ERROR: custom data should be handled using the behavior dictionary, not the signal dictionary." << std::endl + << " It is simpler to use the behavior setters." << std::endl + << " How did you even end up here?" << std::endl; + exit(-1); + } + else + { + std::cerr << "ERROR: \"" << name << "\" is not a signal that can be set using libRoadRunner." << std::endl + << " The available signals are: " << std::endl + << " - intracellular " << std::endl + << " - internalized " << std::endl + << " - volume" << std::endl + << " - damage" << std::endl + << " - custom:" << std::endl; + exit(-1); + } +} + +bool is_physicell_phenotype_token(const std::string& name) +{ + if (name[0] == 'm') + { + std::cout << "WARNING: The token \"" << name << "\" can be replaced with the relevant behavior name. The mapping is as follows:" << std::endl + << " - mms -> migration speed" << std::endl + << " - mpt -> persistence time" << std::endl + << " - mmb -> migration bias" << std::endl << std::endl + << "Doing so will allow the libRoadRunner addon to validate that there are not competing mappings." << std::endl; + return name == "mms" || name == "mpt" || name == "mmb"; + } + else if (name[0] == 'd') + { + std::cout << "WARNING: The token \"" << name << "\" can be replaced with the relevant death model name. The mapping is as follows:" << std::endl + << " - da -> apoptosis" << std::endl + << " - dn -> necrosis" << std::endl << std::endl + << "Doing so will allow the libRoadRunner addon to validate that there are not competing mappings." << std::endl; + return name == "da" || name == "dn"; + } + else if (name[0] == 's') + { + std::cout << "WARNING: The token \"" << name << "\" can be replaced with the relevant substrate name. The mapping is as follows:" << std::endl + << " - sur -> uptake" << std::endl + << " - ssr -> secretion" << std::endl + << " - ssd -> secretion target" << std::endl + << " - ser -> export" << std::endl + << "Doing so will allow the libRoadRunner addon to validate that there are not competing mappings." << std::endl; + if (name.substr(0, 3) != "sur" && name.substr(0, 3) != "ssr" && name.substr(0, 3) != "ssd" && name.substr(0, 3) != "ser") + { + return false; + } + if (name[3] != '_') + { + return false; + } + return microenvironment.find_density_index(name.substr(4, std::string::npos)) != -1; + } + else if (name[0] == 'c') + { + std::cout << "WARNING: The token \"" << name << "\" can be replaced with the relevant cycle transition rate name. The mapping is as follows:" << std::endl + << " - ctr__ -> exit from cycle phase " << std::endl + << "Doing so will allow the libRoadRunner addon to validate that there are not competing mappings." << std::endl; + std::vector indices = parse_ctr_token(name); + return indices[0] >= 0 && indices[1] >= 0; + } + else if (name[0] == 'v') + { + return name == "vtsc" || name == "vtsn" || name == "vff"; + } + return false; +} + +std::vector parse_ctr_token(const std::string &name) +{ + if (name.substr(0, 3) != "ctr" || name[3] != '_') + { + throw_invalid_ctr_token(name); + } + size_t pos = name.find("_", 4); + if (pos == std::string::npos) + { + throw_invalid_ctr_token(name); + } + int start_index; + int end_index; + try + { + start_index = atoi(name.substr(4, pos - 1).c_str()); + end_index = atoi(name.substr(pos + 1, std::string::npos).c_str()); + } + catch(const std::exception& e) + { + throw_invalid_ctr_token(name); + } + return {start_index, end_index}; +} + +void throw_invalid_ctr_token(const std::string& name) +{ + std::cerr << "ERROR: \"" << name << "\" is not a valid token format. The available cycle tranisition rate (ctr) tokens are \"ctr__\"." + << " For example: \"ctr_0_1\" or \"ctr_2_3\"." << std::endl; + exit(-1); +} + +MappingFunction select_phenotype_by_token_inputter(const std::string& name, const std::string& sbml_species) +{ + if (name[0] == 'm') + { + if (name == "mms") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.motility.migration_speed); }; + } + else if (name == "mpt") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.motility.persistence_time); }; + } + else if (name == "mmb") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.motility.migration_bias); }; + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized motility token. The available motility tokens are \"mms\", \"mpt\", and \"mmb\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + } + else if (name[0] == 'd') + { + if (name == "da") + { + int death_model_index = PhysiCell::cell_defaults.phenotype.death.find_death_model_index(PhysiCell::PhysiCell_constants::apoptosis_death_model); + return [sbml_species, death_model_index](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.death.rates[death_model_index]); }; + } + else if (name == "dn") + { + int death_model_index = PhysiCell::cell_defaults.phenotype.death.find_death_model_index(PhysiCell::PhysiCell_constants::necrosis_death_model); + return [sbml_species, death_model_index](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.death.rates[death_model_index]); }; + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized death token. The available death tokens are \"da\" and \"dn\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + } + else if (name[0] == 's') + { + size_t pos = name.find("_"); + std::string substrate_name = name.substr(pos + 1, std::string::npos); + int substrate_index = microenvironment.find_density_index(substrate_name); + + if (substrate_index == -1) + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << substrate_name << "\" is not a recognized substrate in this model." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + + std::string token_prefix = name.substr(0, 3); + + if (token_prefix == "sur") + { + return [sbml_species, substrate_index](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.secretion.uptake_rates[substrate_index]); }; + } + else if (token_prefix == "ssr") + { + return [sbml_species, substrate_index](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.secretion.secretion_rates[substrate_index]); }; + } + else if (token_prefix == "ssd") + { + return [sbml_species, substrate_index](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.secretion.saturation_densities[substrate_index]); }; + } + else if (token_prefix == "ser") + { + return [sbml_species, substrate_index](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.secretion.net_export_rates[substrate_index]); }; + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized secretion token. The available secretion tokens are \"sur\", \"ssr\", \"ssd\", and \"ser\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + } + else if (name[0] == 'c') + { + std::vector indices = parse_ctr_token(name); + int start_index = indices[0]; + int end_index = indices[1]; + + return [sbml_species, start_index, end_index](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.cycle.data.transition_rate(start_index, end_index)); }; + } + else if (name[0] == 'v') + { + if (name == "vtsc") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.volume.target_solid_cytoplasmic); }; + } + else if (name == "vtsn") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.volume.target_solid_nuclear); }; + } + else if (name == "vff") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.intracellular->set_parameter_value(sbml_species, pCell->phenotype.volume.target_fluid_fraction); }; + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized volume token. The available volume tokens are \"vtsc\", \"vtsn\", and \"vff\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized token. It must start with \"m\", \"d\", \"s\", \"c\", or \"v\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } +} + +MappingFunction select_phenotype_by_token_outputter(const std::string& name, const std::string& sbml_species) +{ + if (name[0] == 'm') + { + if (name == "mms") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.motility.migration_speed = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else if (name == "mpt") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.motility.persistence_time = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else if (name == "mmb") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.motility.migration_bias = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized motility token. The available motility tokens are \"mms\", \"mpt\", and \"mmb\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + } + else if (name[0] == 'd') + { + if (name == "da") + { + int death_model_index = PhysiCell::cell_defaults.phenotype.death.find_death_model_index(PhysiCell::PhysiCell_constants::apoptosis_death_model); + return [sbml_species, death_model_index](PhysiCell::Cell *pCell) + { pCell->phenotype.death.rates[death_model_index] = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else if (name == "dn") + { + int death_model_index = PhysiCell::cell_defaults.phenotype.death.find_death_model_index(PhysiCell::PhysiCell_constants::necrosis_death_model); + return [sbml_species, death_model_index](PhysiCell::Cell *pCell) + { pCell->phenotype.death.rates[death_model_index] = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized death token. The available death tokens are \"da\" and \"dn\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + } + else if (name[0] == 's') + { + size_t pos = name.find("_"); + std::string substrate_name = name.substr(pos + 1, std::string::npos); + int substrate_index = microenvironment.find_density_index(substrate_name); + + if (substrate_index == -1) + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << substrate_name << "\" is not a substrate name in this model." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + + std::string token_prefix = name.substr(0, 3); + + if (token_prefix == "sur") + { + return [sbml_species, substrate_index](PhysiCell::Cell *pCell) + { pCell->phenotype.secretion.uptake_rates[substrate_index] = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else if (token_prefix == "ssr") + { + return [sbml_species, substrate_index](PhysiCell::Cell *pCell) + { pCell->phenotype.secretion.secretion_rates[substrate_index] = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else if (token_prefix == "ssd") + { + return [sbml_species, substrate_index](PhysiCell::Cell *pCell) + { pCell->phenotype.secretion.saturation_densities[substrate_index] = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else if (token_prefix == "ser") + { + return [sbml_species, substrate_index](PhysiCell::Cell *pCell) + { pCell->phenotype.secretion.net_export_rates[substrate_index] = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized secretion token. The available secretion tokens are \"sur\", \"ssr\", \"ssd\", and \"ser\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + } + else if (name[0] == 'c') + { + std::vector indices = parse_ctr_token(name); + int start_index = indices[0]; + int end_index = indices[1]; + + return [sbml_species, start_index, end_index](PhysiCell::Cell *pCell) + { pCell->phenotype.cycle.data.transition_rate(start_index, end_index) = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else if (name[0] == 'v') + { + if (name == "vtsc") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.volume.target_solid_cytoplasmic = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else if (name == "vtsn") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.volume.target_solid_nuclear = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else if (name == "vff") + { + return [sbml_species](PhysiCell::Cell *pCell) + { pCell->phenotype.volume.target_fluid_fraction = pCell->phenotype.intracellular->get_parameter_value(sbml_species); }; + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized volume token. The available volume tokens are \"vtsc\", \"vtsn\", and \"vff\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } + } + else + { + std::cerr<< std::endl; + std::cerr << "ERROR: \"" << name << "\" is not a recognized token. It must start with \"m\", \"d\", \"s\", \"c\", or \"v\"." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); + } +} + RoadRunnerIntracellular::RoadRunnerIntracellular() : Intracellular() { intracellular_type = "sbml"; std::cout << "====== " << __FUNCTION__ << "() intracellular_type=" << intracellular_type << std::endl; std::cout << "====== " << __FUNCTION__ << "() sbml_filename = " << sbml_filename << std::endl; - // initial_values.clear(); - // mutations.clear(); parameters.clear(); } // constructor using XML node RoadRunnerIntracellular::RoadRunnerIntracellular(pugi::xml_node& node) { - // std::cout << "======rwh " << __FUNCTION__ << ": node.name() =" << node.name() << std::endl; intracellular_type = "roadrunner"; initialize_intracellular_from_pugixml(node); - // std::cout << "======rwh " << __FUNCTION__ << "(node) intracellular_type=" << intracellular_type << std::endl; - // std::cout << "======rwh " << __FUNCTION__ << "(node) sbml_filename = " << sbml_filename << std::endl; - // std::cout << "======rwh " << __FUNCTION__ << "(node) this=" << this << std::endl; - // std::cout << "======rwh " << __FUNCTION__ << "(node) this->sbml_filename=" << this->sbml_filename << std::endl; } -// Intracellular* RoadRunnerIntracellular::clone() // --> 'Intracellular' does not name a type -// { -// return static_cast(new RoadRunnerIntracellular(this)); -// } - -// rwh: review this RoadRunnerIntracellular::RoadRunnerIntracellular(RoadRunnerIntracellular* copy) { + update_time_step = copy->update_time_step; + previous_update_time = PhysiCell::PhysiCell_globals.current_time; + next_librr_run = PhysiCell::PhysiCell_globals.current_time + update_time_step; intracellular_type = copy->intracellular_type; sbml_filename = copy->sbml_filename; - // cfg_filename = copy->cfg_filename; - // time_step = copy->time_step; - // discrete_time = copy->discrete_time; - // time_tick = copy->time_tick; - // scaling = copy->scaling; - // initial_values = copy->initial_values; - // mutations = copy->mutations; parameters = copy->parameters; - + input_mappings = copy->input_mappings; + output_mappings = copy->output_mappings; + mappings_initialized = copy->mappings_initialized; } -// Parse the info in the .xml for (possibly) each , e.g. -// -// ./config/Toy_SBML_Model_2.xml -// 1 -// Oxy -// Glc -// Energy void RoadRunnerIntracellular::initialize_intracellular_from_pugixml(pugi::xml_node& node) { pugi::xml_node node_sbml = node.child( "sbml_filename" ); @@ -61,234 +533,189 @@ void RoadRunnerIntracellular::initialize_intracellular_from_pugixml(pugi::xml_no sbml_filename = PhysiCell::xml_get_my_string_value (node_sbml); std::cout << "\n------------- " << __FUNCTION__ << ": sbml_filename = " << sbml_filename << std::endl; } - - pugi::xml_node node_species = node.child( "map" ); - while( node_species ) - { - // --------- substrate - - std::string substrate_name = node_species.attribute( "PC_substrate" ).value(); - if( substrate_name != "" ) - { - //std::cout << "-----------" << node_species.attribute( "sbml_species" ).value() << std::endl; - std::string species_name = node_species.attribute( "sbml_species" ).value(); - substrate_species[substrate_name] = species_name; - std::cout << "\n------------- " << __FUNCTION__ << ": species_name= " << species_name << std::endl; - } - // --------- custom_data - std::string custom_data_name = node_species.attribute( "PC_custom_data" ).value(); - if( custom_data_name != "" ) - { - std::string species_name = node_species.attribute( "sbml_species" ).value(); - custom_data_species[custom_data_name] = species_name; - // std::cout << "\n------------- " << __FUNCTION__ << ": species_name= " << species_name << std::endl; - } - - - // --------- phenotype_data - std::string phenotype_name = node_species.attribute( "PC_phenotype" ).value(); - - if( phenotype_name != "" ) - { - std::string species_name = node_species.attribute( "sbml_species" ).value(); - phenotype_species[phenotype_name] = species_name; - // std::cout << "\n------------- " << __FUNCTION__ << ": species_name= " << species_name << std::endl; - } - - node_species = node_species.next_sibling( "map" ); - } - - std::cout << " ------- substrate_species map:" << std::endl; - for(auto elm : substrate_species) - { - std::cout << " " << elm.first << " -> " << elm.second << std::endl; - } - std::cout << " ------- custom_data_species map:" << std::endl; - for(auto elm : custom_data_species) - { - std::cout << " " << elm.first << " -> " << elm.second << std::endl; + + update_time_step = PhysiCell::intracellular_dt; // default to this, but overwrite below if defined in XML + pugi::xml_node node_update_time_step = node.child( "intracellular_dt" ); + if ( node_update_time_step ) + { + update_time_step = PhysiCell::xml_get_my_double_value (node_update_time_step); + std::cout << "\n------------- " << __FUNCTION__ << ": intracellular_dt = " << update_time_step << std::endl; } - std::cout << std::endl; - std::cout << " ------- phenotype_species map:" << std::endl; - for(auto elm : phenotype_species) + std::vector new_input_mappings; + std::vector new_output_mappings; + + pugi::xml_node node_map = node.child( "map" ); + while ( node_map ) { - std::cout << " " << elm.first << " -> " << elm.second << std::endl; + std::string io_type = node_map.attribute( "type" ).value(); + if (io_type != "io" && io_type != "input" && io_type != "output") + { + std::cout << "\n------------- " << __FUNCTION__ << ": ERROR: type must be io, input, or output" << std::endl; + exit(-1); + } + + std::string physicell_name = node_map.attribute( "physicell_name" ).value(); + std::string sbml_species = node_map.attribute( "sbml_species" ).value(); + + if (io_type == "input" || io_type == "io") + { + new_input_mappings.push_back(new RoadRunnerMapping(physicell_name, sbml_species, "input")); + } + if (io_type == "output" || io_type == "io") + { + new_output_mappings.push_back(new RoadRunnerMapping(physicell_name, sbml_species, "output")); + } + + node_map = node_map.next_sibling("map"); } - std::cout << std::endl; + validate_mappings(new_input_mappings, true); + validate_mappings(new_output_mappings, false); + input_mappings = std::move(new_input_mappings); + output_mappings = std::move(new_output_mappings); } +void validate_mappings(std::vector mappings, bool is_inputs) +{ + if (mappings.empty()) + { return; } + std::vector values_already_set; + std::string name_to_add; + for (auto mapping : mappings) + { + name_to_add = is_inputs ? mapping->sbml_species : mapping->physicell_name; + if (std::find(values_already_set.begin(), values_already_set.end(), name_to_add) != values_already_set.end()) + { + std::cout << "ERROR: the " << (is_inputs ? "SBML species " : "PhysiCell name ") << name_to_add << " is set by multiple " << (is_inputs ? "inputs" : "outputs") << std::endl; + exit(-1); + } + values_already_set.push_back(name_to_add); + } +} void RoadRunnerIntracellular::start() { // called when a new cell is created; creates the unique 'rrHandle' rrc::RRVectorPtr vptr; - //std::cout << "\n------------ " << __FUNCTION__ << ": librr_intracellular.cpp: start() called\n"; - // this->enabled = true; - - //std::cout << "\n------------ " << __FUNCTION__ << ": doing: rrHandle = createRRInstance()\n"; - rrHandle = createRRInstance(); - //std::cout << "\n------------ " << __FUNCTION__ << ": rrHandle = " << rrHandle << std::endl; - - // if (!rrc::loadSBML (rrHandle, get_cell_definition("lung epithelium").sbml_filename.c_str())) - //std::cout << " sbml_filename = " << sbml_filename << std::endl; - - // TODO: don't hard-code name if ( !rrc::loadSBML(rrHandle, (sbml_filename).c_str() ) ) - // std::cout << " for now, hard-coding sbml_file = ./config/Toy_SBML_Model_1.xml" << std::endl; - // if (!rrc::loadSBML(rrHandle, "./config/Toy_SBML_Model_1.xml") ) { std::cerr << "------------->>>>> Error while loading SBML file <-------------\n\n"; - // return -1; - // printf ("Error message: %s\n", getLastError()); - exit (0); + exit(-1); } - // std::cout << " rrHandle=" << rrHandle << std::endl; - int r = rrc::getNumberOfReactions(rrHandle); int m = rrc::getNumberOfFloatingSpecies(rrHandle); int b = rrc::getNumberOfBoundarySpecies(rrHandle); int p = rrc::getNumberOfGlobalParameters(rrHandle); int c = rrc::getNumberOfCompartments(rrHandle); - - //std::cerr << "Number of reactions = " << r << std::endl; - //std::cerr << "Number of floating species = " << m << std::endl; // 4 - //std::cerr << "Number of boundary species = " << b << std::endl; // 0 - //std::cerr << "Number of compartments = " << c << std::endl; // 1 - - //std::cerr << "Floating species names:\n"; - //std::cerr << "-----------------------\n"; std::string species_names_str = stringArrayToString(rrc::getFloatingSpeciesIds(rrHandle)); - //std::cerr << species_names_str <<"\n"<< std::endl; std::stringstream iss(species_names_str); std::string species_name; int idx = 0; while (iss >> species_name) { species_result_column_index[species_name] = idx; - //std::cout << species_name << " -> " << idx << std::endl; idx++; } vptr = rrc::getFloatingSpeciesConcentrations(rrHandle); - //std::cerr << vptr->Count << std::endl; -/* for (int kdx=0; kdxCount; kdx++) - { - std::cerr << kdx << ") " << vptr->Data[kdx] << std::endl; - } */ - //std::cerr << "---------- end start() -------------\n"; rrc::freeVector(vptr); - // return 0; + + initialize_mappings(); } bool RoadRunnerIntracellular::need_update() { - return PhysiCell::PhysiCell_globals.current_time >= this->next_librr_run; + return PhysiCell::PhysiCell_globals.current_time >= this->next_librr_run - 0.5 * PhysiCell::diffusion_dt; } -// solve the intracellular model -void RoadRunnerIntracellular::update() +void RoadRunnerIntracellular::update(PhysiCell::Cell* pCell, PhysiCell::Phenotype& phenotype, double dt) { + pre_update(pCell); + // update the intracellular model static double start_time = 0.0; - static double end_time = 0.01; - // static double end_time = 10.0; - // static int num_vals = 1; - // static int num_vals = 10; - static int num_vals = 2; + static int num_vals = 2; // start time and end time - // result = rrc::simulateEx (pCell->phenotype.molecular.model_rr, 0, 10, 10); // start time, end time, and number of points - //std::cout << __FUNCTION__ << " ----- update(): this=" << this << std::endl; - //std::cout << __FUNCTION__ << " ----- update(): rrHandle=" << this->rrHandle << std::endl; - - // if (this->result != 0) // apparently not necessary (done in freeRRCData hopefully) rrc::freeRRCData (this->result); - this->result = rrc::simulateEx (this->rrHandle, start_time, end_time, num_vals); // start time, end time, and number of points + this->result = rrc::simulateEx (this->rrHandle, start_time, PhysiCell::PhysiCell_globals.current_time - previous_update_time, num_vals); // start time, end time, and number of points + this->previous_update_time = PhysiCell::PhysiCell_globals.current_time; + this->next_librr_run = PhysiCell::PhysiCell_globals.current_time + update_time_step; + post_update(pCell); +} - // this->next_librr_run += this->rrHandle.get_time_to_update(); - // std::cout << "----- update(): result=" << result << std::endl; - //std::cout << "----- update(): result->CSize=" << this->result->CSize << std::endl; - //std::cout << "----- update(): result->RSize=" << this->result->RSize << std::endl; // should be = num_vals - // std::cout << "----- update(): result->ColumnHeaders[0]=" << result->ColumnHeaders[0] << std::endl; // = "time" +void RoadRunnerIntracellular::initialize_mappings() +{ + for (auto mapping : input_mappings) + { + if (mapping->mapping_initialized) + { continue; } + mapping->initialize_mapping(); + } + for (auto mapping : output_mappings) + { + if (mapping->mapping_initialized) + { continue; } + mapping->initialize_mapping(); + } + mappings_initialized = true; +} - // debug - does it generate expected data? - int index = 0; - // Print out column headers... typically time and species. - for (int col = 0; col < this->result->CSize; col++) +void RoadRunnerIntracellular::pre_update(PhysiCell::Cell* pCell) +{ + for (auto mapping : input_mappings) { - // std::cout << result->ColumnHeaders[index++]; - // std::cout << std::left << std::setw(15) << result->ColumnHeaders[index++]; - //std::cout << std::left << this->result->ColumnHeaders[index++]; - // if (col < result->CSize - 1) - // { - // // std::cout << "\t"; - // std::cout << " "; - // } + mapping->value_map(pCell); } - //std::cout << "\n"; +} - index = 0; - // Print out the data - for (int row = 0; row < this->result->RSize; row++) +void RoadRunnerIntracellular::post_update(PhysiCell::Cell* pCell) +{ + for (auto mapping : output_mappings) { - for (int col = 0; col < this->result->CSize; col++) + mapping->value_map(pCell); + } +} + +RoadRunnerMapping* RoadRunnerIntracellular::find_input_mapping(std::string sbml_species) +{ + for (auto mapping : input_mappings) + { + if (mapping->sbml_species == sbml_species) { - // std::cout << result->Data[index++]; - //std::cout << std::left << std::setw(15) << this->result->Data[index++]; - // if (col < result->CSize -1) - // { - // // std::cout << "\t"; - // std::cout << " "; - // } + return mapping; } - // std::cout << "\n"; } - // int idx = (result->RSize - 1) * result->CSize + 1; - // std::cout << "Saving last energy value (cell custom var) = " << result->Data[idx] << std::endl; - // pCell->custom_data[energy_cell_idx] = result->Data[idx]; + return nullptr; +} - // return 0; +RoadRunnerMapping* RoadRunnerIntracellular::find_output_mapping(std::string physicell_name) +{ + for (auto mapping : output_mappings) + { + if (mapping->physicell_name == physicell_name) + { + return mapping; + } + } + return nullptr; } double RoadRunnerIntracellular::get_parameter_value(std::string param_name) { rrc::RRVectorPtr vptr; - //std::cout << "-----------" << __FUNCTION__ << "-----------" << std::endl; - // std::cout << " substrate_name = " << substrate_name << std::endl; - //std::cout << " param_name = " << param_name << std::endl; - - // TODO: optimize this eventually - // std::map species_result_column_index; - // int num_columns = result->CSize; - // int offset = (num_rows_result_table-1) * result->CSize - 1; - // int offset = (num_rows_result_table-1) * result->CSize; - // offset += (num_rows_result_table-1) * result->CSize - 1; - - // int offset = species_result_column_index[name]; - // std::string species_name = this->substrate_species[substrate_name]; - // std::cout << " species_name = " << species_name << std::endl; - vptr = rrc::getFloatingSpeciesConcentrations(this->rrHandle); - //std::cerr << vptr->Count << std::endl; - for (int kdx=0; kdxCount; kdx++) - { - //std::cerr << kdx << ") " << vptr->Data[kdx] << std::endl; - } int offset = species_result_column_index[param_name]; - //std::cout << " result offset = "<< offset << std::endl; - // double res = this->result->Data[offset]; double res = vptr->Data[offset]; - //std::cout << " res = " << res << std::endl; rrc::freeVector(vptr); return res; } @@ -301,20 +728,21 @@ void RoadRunnerIntracellular::set_parameter_value(std::string species_name, doub vptr = rrc::getFloatingSpeciesConcentrations(this->rrHandle); int idx = species_result_column_index[species_name]; vptr->Data[idx] = value; - // rrc::setFloatingSpeciesConcentrations(pCell->phenotype.molecular.model_rr, vptr); rrc::setFloatingSpeciesConcentrations(this->rrHandle, vptr); rrc::freeVector(vptr); - // return 0; } RoadRunnerIntracellular* getRoadRunnerModel(PhysiCell::Phenotype& phenotype) { return static_cast(phenotype.intracellular); } +RoadRunnerIntracellular* getRoadRunnerModel(PhysiCell::Cell* pCell) { + return getRoadRunnerModel(pCell->phenotype); +} + void RoadRunnerIntracellular::save_libRR(std::string path, std::string index) { std::string state_file_name = path + "/states_" + index + ".dat"; -// std::string state_file_name = path + "/states_" + index + ".csv"; std::ofstream state_file( state_file_name ); state_file << "--------- dummy output from save_libRR ---------" << std::endl; state_file << "ID,state" << std::endl; @@ -328,349 +756,41 @@ std::string RoadRunnerIntracellular::get_state() return sbml_filename; } - -int RoadRunnerIntracellular::update_phenotype_parameters(PhysiCell::Phenotype& phenotype) +int RoadRunnerIntracellular::validate_PhysiCell_tokens(PhysiCell::Phenotype& phenotype) { - for(auto elm : phenotype_species) - { - // motility params - if (elm.first[0] == 'm') - { - if (elm.first == "mms") - { - phenotype.motility.migration_speed = phenotype.intracellular->get_parameter_value(elm.second); - } - else if (elm.first == "mpt") - { - phenotype.motility.persistence_time = phenotype.intracellular->get_parameter_value(elm.second); - } - else if (elm.first == "mmb") - { - phenotype.motility.migration_bias = phenotype.intracellular->get_parameter_value(elm.second); - } - else - { - } - } - // death params - else if (elm.first[0] == 'd') - { - if (elm.first == "da") - { - phenotype.death.rates[0] = phenotype.intracellular->get_parameter_value(elm.second); - } - else if (elm.first == "dn") - { - phenotype.death.rates[1] = phenotype.intracellular->get_parameter_value(elm.second); - } - else - { - } - } - // secretion params - else if (elm.first[0] == 's') - { - // parsing attribute and getting substrate name - std::string s = elm.first; - std::string delimiter = "_"; - - size_t pos = 0; - std::string token; - while ((pos = s.find(delimiter)) != std::string::npos) { - token = s.substr(0, pos); - s.erase(0, pos + delimiter.length()); - } - int sub_index = microenvironment.find_density_index(s); - - //transport types - //uptake rate - if (elm.first.substr(0,3) == "sur") - { - //std::cout << sub_index << std::endl; - //std::cout << "Before sur1 : " << phenotype.secretion.uptake_rates[sub_index] << std::endl; - phenotype.secretion.uptake_rates[1] = phenotype.intracellular->get_parameter_value(elm.second); - //std::cout << "After sur1 : " << phenotype.secretion.uptake_rates[sub_index] << std::endl; - } - //secretion rate - else if (elm.first.substr(0,3) == "ssr") - { - phenotype.secretion.secretion_rates[sub_index] = phenotype.intracellular->get_parameter_value(elm.second); - } - //secretion density - else if (elm.first.substr(0,3) == "ssd") - { - phenotype.secretion.saturation_densities[sub_index] = phenotype.intracellular->get_parameter_value(elm.second); - } - //net export rate - else if (elm.first.substr(0,3) == "ser") - { - phenotype.secretion.net_export_rates[sub_index] = phenotype.intracellular->get_parameter_value(elm.second); - } - else - { - } - } - - // cycle params - else if (elm.first[0] == 'c') - { - if (elm.first.substr(0,3) == "ctr") - { - // parsing attribute and getting substrate name - std::string s = elm.first; - std::string delimiter = "_"; - - size_t pos = 0; - std::string token; - int counter = 0; - int start_index; - while ((pos = s.find(delimiter)) != std::string::npos) { - token = s.substr(0, pos); - //std::cout << counter << " : "<< token << std::endl; - if (counter == 1) - { - start_index = atoi( token.c_str() ); - } - s.erase(0, pos + delimiter.length()); - counter += 1; - } - int end_index = atoi( s.c_str() ); - //std::cout << "START INDEX : " << start_index << std::endl; - //std::cout << "END INDEX : " << end_index << std::endl; - phenotype.cycle.data.transition_rate(start_index,end_index) = phenotype.intracellular->get_parameter_value(elm.second); - } - else - { - } - } - - // volume params - else if (elm.first[0] == 'v') - { - if (elm.first == "vtsc") - { - phenotype.volume.target_solid_cytoplasmic = phenotype.intracellular->get_parameter_value(elm.second); - } - else if (elm.first == "vtsn") - { - phenotype.volume.target_solid_nuclear = phenotype.intracellular->get_parameter_value(elm.second); - } - else if (elm.first == "vff") - { - phenotype.volume.target_fluid_fraction = phenotype.intracellular->get_parameter_value(elm.second); - } - else - { - } - } - else - { - } - - } - //std::cout << std::endl; + // the mappings are not yet intialized by this time because the signals/behaviors dictionaries are not yet initialized + int num_of_phases = (&(phenotype.cycle.model()))->phases.size(); + validate_cycle_mappings(input_mappings, num_of_phases); + validate_cycle_mappings(output_mappings, num_of_phases); return 0; } - -int RoadRunnerIntracellular::validate_PhysiCell_tokens(PhysiCell::Phenotype& phenotype) +void validate_cycle_mappings(std::vector mappings, int num_of_phases) { - for(auto elm : phenotype_species) + for (auto mapping : mappings) { - //std::cout << "PhysiCell_token_validation" << std::endl; - //std::cout << elm.first << " : " << elm.second << std::endl; - - // motility params - if (elm.first[0] == 'm') - { - if (elm.first == "mms") - { - } - else if (elm.first == "mpt") - { - } - else if (elm.first == "mmb") - { - } - else - { - std::cout<< std::endl; - std::cout << "ERROR: There is no specified token parameters in the name of \"" << elm.first << "\" at motility parameters. Please take a look token specifications." << std::endl; - std::cout<< std::endl; - std::cout<< std::endl; - exit (-1); - return -1; - } - } - // death params - else if (elm.first[0] == 'd') - { - if (elm.first == "da") - { - } - else if (elm.first == "dn") - { - } - else - { - std::cout<< std::endl; - std::cout << "ERROR: There is no specified token parameters in the name of \"" << elm.first << "\" at death parameters. Please take a look token specifications." << std::endl; - std::cout<< std::endl; - std::cout<< std::endl; - exit (-1); - return -1; - } - } - // secretion params - else if (elm.first[0] == 's') - { - // parsing attribute and getting substrate name - std::string s = elm.first; - std::string delimiter = "_"; - size_t pos = 0; - std::string token; - while ((pos = s.find(delimiter)) != std::string::npos) { - token = s.substr(0, pos); - s.erase(0, pos + delimiter.length()); - } - int sub_index = microenvironment.find_density_index(s); - //std::cout << "SUBSTRATE_INDEX = : " << sub_index << std::endl; - if ( sub_index < 0 ) - { - std::cout<< std::endl; - std::cout << "ERROR: There is no substrate named in the name of \"" << s << "\" at microenvironment. Please take a look token specifications." << std::endl; - std::cout<< std::endl; - std::cout<< std::endl; - exit (-1); - return -1; - } - - if (elm.first.substr(0,3) == "sur") - { - } - else if (elm.first.substr(0,3) == "ssr") - { - } - else if (elm.first.substr(0,3) == "ssd") - { - } - else if (elm.first.substr(0,3) == "ser") - { - } - else - { - std::cout<< std::endl; - std::cout << "ERROR: There is no specified token parameters in the name of \"" << elm.first << "\" at secretion parameters. Please take a look token specifications." << std::endl; - std::cout<< std::endl; - std::cout<< std::endl; - exit (-1); - return -1; - } - } - else if (elm.first[0] == 'c') - { - if (elm.first.substr(0,3) == "ctr") - { - // getting num of phases - int num_of_phases = (&(phenotype.cycle.model()))->phases.size(); - //std::cout << num_of_phases << std::endl; - - // getting start and end indices - std::string s = elm.first; - std::string delimiter = "_"; - size_t pos = 0; - std::string token; - int counter = 0; - int start_index; - while ((pos = s.find(delimiter)) != std::string::npos) { - token = s.substr(0, pos); - if (counter == 1) - { - start_index = atoi( token.c_str() ); - } - s.erase(0, pos + delimiter.length()); - counter += 1; - } - int end_index = atoi( s.c_str() ); - - // validating the indices - if ( start_index > num_of_phases - 1) - { - std::cout<< std::endl; - std::cout << "ERROR: Given transition start index is beyond cycle indices. Please double check it." << std::endl; - std::cout<< std::endl; - std::cout<< std::endl; - exit (-1); - return -1; - } - if ( end_index > num_of_phases - 1) - { - std::cout<< std::endl; - std::cout << "ERROR: Given transition end index is beyond cycle indices. Please double check it." << std::endl; - std::cout<< std::endl; - std::cout<< std::endl; - exit (-1); - return -1; - } - } - else - { - std::cout<< std::endl; - std::cout << "ERROR: There is no specified token parameters in the name of \"" << elm.first << "\" at cycle parameters. Please take a look token specifications." << std::endl; - std::cout<< std::endl; - std::cout<< std::endl; - exit (-1); - return -1; - } - } - - else if (elm.first[0] == 'v') - { - if (elm.first == "vtsc") - { - } - else if (elm.first == "vtsn") - { - } - else if (elm.first == "vff") - { - } - else - { - std::cout<< std::endl; - std::cout << "ERROR: There is no specified token parameters in the name of \"" << elm.first << "\" at volume parameters. Please take a look token specifications." << std::endl; - std::cout<< std::endl; - std::cout<< std::endl; - exit (-1); - return -1; - } - } - else + // check that the mapping uses a cycle transition rate, i.e. starts with "ctr_" + if (mapping->physicell_name.find("ctr_") != 0) + { continue; } + + std::vector indices = parse_ctr_token(mapping->physicell_name); + if (indices[0] > num_of_phases - 1 || indices[1] > num_of_phases - 1) { - std::cout<< std::endl; - std::cout << "ERROR: There is no specified token parameters in the name of \"" << elm.first << "\" at phenotypic parameters. Please take a look token specifications." << std::endl; - std::cout<< std::endl; - std::cout<< std::endl; - exit (-1); - return -1; + std::cerr << "ERROR: The token \"" << mapping->physicell_name << "\" is invalid for this cell type. The indices are out of range." << std::endl + << " The phase indices must be between 0 and " << num_of_phases - 1 << "." << std::endl; + exit(-1); } - } - std::cout << "---- Specified PhysiCell tokens at config file are validated. ----- " << std::endl; - - return 0; } int RoadRunnerIntracellular::validate_SBML_species() { - //std::cout << "---------VALIDATING_SBML_SPECIES START-------" << std::endl; - // reading SBML rrHandle = createRRInstance(); if ( !rrc::loadSBML(rrHandle, (sbml_filename).c_str() ) ) { std::cerr << "------------->>>>> Error while loading SBML file <-------------\n\n"; - return -1; + exit(-1); } // getting Species Names std::string species_names_str = stringArrayToString(rrc::getFloatingSpeciesIds(rrHandle)); @@ -684,105 +804,32 @@ int RoadRunnerIntracellular::validate_SBML_species() { species_result_column_index[species_name] = idx; all_species.push_back(species_name); - //std::cout << species_name << " -> " << idx << std::endl; idx++; } - // Phenotype Species - for (auto elm : phenotype_species) - { - bool exist = 0; - // std::cout << species_name.size() << std::endl; - for (int i=0; i < all_species.size(); i++) - { - //std::cout << all_species[i] << std::endl;; - //std::cout << "Comparing " << all_species[i] << " with " << elm.second << std::endl; - if ( all_species[i] == elm.second ) - { - //std::cout << "And they are the same..... " < all_species, std::vector mappings) +{ + for (auto mapping : mappings) { - bool exist = 0; - // std::cout << species_name.size() << std::endl; - for (int i=0; i < all_species.size(); i++) + if (std::find(all_species.begin(), all_species.end(), mapping->sbml_species) == all_species.end()) { - //std::cout << all_species[i] << std::endl;; - //std::cout << "Comparing " << all_species[i] << " with " << elm.second << std::endl; - if ( all_species[i] == elm.second ) - { - //std::cout << "And they are the same..... " <sbml_species << "\" at " << mapping->io_type << " mapping. Please take a look SBML species specifications." << std::endl; + std::cerr<< std::endl; + std::cerr<< std::endl; + exit(-1); } - //std::cout << "existence check : " << elm.second <<": " << exist << std::endl; - } - - - //std::cout << "---------VALIDATING_SBML_SPECIES END-------" << std::endl; - - - std::cout << "---- Specified SBML species at config file are validated. ----- " << std::endl; - return 0; + } } int RoadRunnerIntracellular::create_custom_data_for_SBML(PhysiCell::Phenotype& phenotype) { - //std::cout << "Test" << std::endl; - return 0; } diff --git a/addons/libRoadrunner/src/librr_intracellular.h b/addons/libRoadrunner/src/librr_intracellular.h index 4ca8b041a..fb5e32864 100644 --- a/addons/libRoadrunner/src/librr_intracellular.h +++ b/addons/libRoadrunner/src/librr_intracellular.h @@ -16,44 +16,68 @@ // #define STATIC_RRC // #include "rrc_api.h" // #include "rrc_types.h" -#include "../roadrunner/include/rr/C/rrc_api.h" -#include "../roadrunner/include/rr/C/rrc_types.h" +#include "rrc_api.h" +#include "rrc_types.h" +#include +#include + // #include "rrc_utilities.h" extern "C" rrc::RRHandle createRRInstance(); // #endif +typedef std::function MappingFunction; + +class RoadRunnerMapping +{ +public: + std::string physicell_name; + std::string sbml_species; + std::string io_type; + std::string physicell_dictionary_name; + int index; + MappingFunction value_map = [] (PhysiCell::Cell *pCell) {}; // default to a function that does nothing + bool mapping_initialized = false; + + RoadRunnerMapping() {}; + RoadRunnerMapping(std::string physicell_name, std::string sbml_species, std::string io_type) + : physicell_name(physicell_name), sbml_species(sbml_species), io_type(io_type) {}; + + void initialize_mapping(void); +}; + +MappingFunction select_signal_setter(const std::string& name, const std::string& sbml_species); + +bool is_physicell_phenotype_token(const std::string& name); +MappingFunction select_phenotype_by_token_inputter(const std::string& name, const std::string& sbml_species); +MappingFunction select_phenotype_by_token_outputter(const std::string& name, const std::string& sbml_species); + +void validate_mappings(std::vector mappings, bool is_inputs); + +std::vector parse_ctr_token(const std::string &name); +void throw_invalid_ctr_token(const std::string& name); +void validate_cycle_mappings(std::vector mappings, int num_of_phases); + class RoadRunnerIntracellular : public PhysiCell::Intracellular { private: public: - - // static long counter; - std::string sbml_filename; - // bool enabled = false; - int num_rows_result_table = 1; - - // double time_step = 12; - // bool discrete_time = false; - // double time_tick = 0.5; - // double scaling = 1.0; - - // std::map initial_values; + std::map parameters; - std::map substrate_species; - std::map custom_data_species; - std::map phenotype_species; + bool mappings_initialized = false; + void initialize_mappings(); + std::vector input_mappings; + std::vector output_mappings; std::map species_result_column_index; - // rrc::RRHandle rrHandle = createRRInstance(); rrc::RRHandle rrHandle; - // rrc::RRHandle rrHandle; - // rrc::RRVectorPtr vptr; rrc::RRCDataPtr result = 0; // start time, end time, and number of points - double next_librr_run = 0; + double update_time_step = 0.01; + double previous_update_time = 0.0; + double next_librr_run = 0.0; RoadRunnerIntracellular(); @@ -61,15 +85,9 @@ class RoadRunnerIntracellular : public PhysiCell::Intracellular RoadRunnerIntracellular(RoadRunnerIntracellular* copy); - // rwh: review this Intracellular* clone() { - // return static_cast(new RoadRunnerIntracellular(this)); RoadRunnerIntracellular* clone = new RoadRunnerIntracellular(this); - clone->sbml_filename = this->sbml_filename; - clone->substrate_species = this->substrate_species; - clone->phenotype_species = this->phenotype_species; - clone->custom_data_species = this->custom_data_species; return static_cast(clone); } @@ -78,26 +96,35 @@ class RoadRunnerIntracellular : public PhysiCell::Intracellular std::cout << "------ librr_intracellular: getIntracellularModel called\n"; return static_cast(this); } - - void initialize_intracellular_from_pugixml(pugi::xml_node& node); - - // Need 'int' return type to avoid bizarre compile errors? But 'void' to match MaBoSS. + + void initialize_intracellular_from_pugixml(pugi::xml_node &node); + + // Need 'int' return type to avoid bizarre compile errors? But 'void' to match MaBoSS. void start(); bool need_update(); - // Need 'int' return type to avoid bizarre compile errors. - void update(); - void update(PhysiCell::Cell* cell, PhysiCell::Phenotype& phenotype, double dt) { - update(); - update_phenotype_parameters(phenotype); - } + void update() {}; // needed because the base class has this function + void update(PhysiCell::Cell* cell, PhysiCell::Phenotype& phenotype, double dt); + + void pre_update(PhysiCell::Cell* cell); + void post_update(PhysiCell::Cell* cell); void inherit(PhysiCell::Cell * cell) {} - - int update_phenotype_parameters(PhysiCell::Phenotype& phenotype); + + // These find__mapping functions are not currently used, but since I made them, we'll keep them around. + RoadRunnerMapping *find_input_mapping(std::string sbml_species); // sbml_species is unique for inputs (below is for convenience) + RoadRunnerMapping *find_input_mapping(std::string physicell_name, std::string sbml_species) + { return find_input_mapping(sbml_species); } // sbml_species is unique for inputs + + RoadRunnerMapping *find_output_mapping(std::string physicell_name); // physicell_name is unique for outputs (below is for convenience) + RoadRunnerMapping *find_output_mapping(std::string physicell_name, std::string sbml_species) + { return find_output_mapping(physicell_name); } // physicell_name is unique for outputs + + int update_phenotype_parameters(PhysiCell::Phenotype& phenotype) {return 0;}; // all handled within update int validate_PhysiCell_tokens(PhysiCell::Phenotype& phenotype); int validate_SBML_species(); + void validate_SBML_species(std::vector all_species, std::vector mappings); int create_custom_data_for_SBML(PhysiCell::Phenotype& phenotype); double get_parameter_value(std::string name); @@ -111,7 +138,12 @@ class RoadRunnerIntracellular : public PhysiCell::Intracellular void set_boolean_variable_value(std::string name, bool value) {} void print_current_nodes() {} + double get_flux_value(std::string name); + double get_growth_rate(); + static void save_libRR(std::string path, std::string index); }; +RoadRunnerIntracellular* getRoadRunnerModel(PhysiCell::Phenotype& phenotype); +RoadRunnerIntracellular* getRoadRunnerModel(PhysiCell::Cell* pCell); #endif \ No newline at end of file diff --git a/beta/dfba_configurator.py b/beta/dfba_configurator.py new file mode 100755 index 000000000..bae9d8fc3 --- /dev/null +++ b/beta/dfba_configurator.py @@ -0,0 +1,413 @@ +#!/usr/bin/env python3 +""" +dfba_configurator.py + +Usage example: + python dfba_configurator.py \ + --output config_updated.xml \ + --config dfba_config.yaml \ + --sbml-folder ./models \ + --cell-prefix dfba_ + +Requirements: + pip install lxml pandas pyyaml physicell-settings +""" +import os +import argparse +import yaml + +from pathlib import Path +from lxml import etree + +from physicell_config import PhysiCellConfig + +# ---------------------- +# Utilities +# ---------------------- +def indent(elem, level=0): + """ + In-place indentation of XML element tree. + """ + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + +def xml_pretty_write(tree, path): + # Ensure indentation is applied + indent(tree.getroot()) + tree.write(path, pretty_print=True, xml_declaration=True, encoding="UTF-8") + +# ---------------------- +# Secretion block utilities (per cell_definition) +# ---------------------- +# (Removed ensure_secretion_has_substrate and _ensure_secretion_fields as physicell-settings handles this) + +# ---------------------- +# Chemotactic sensitivities utilities (per cell_definition) +# ---------------------- +# (Removed ensure_chemotactic_sensitivities as physicell-settings handles this) + +# ---------------------- +# Volume update utility +# ---------------------- +def set_cell_volume(cell_def, total_volume=None, nuclear_volume=None): + """ + Ensure phenotype->volume exists and update total and nuclear volumes if provided. + """ + phenotype = cell_def.find("./phenotype") + if phenotype is None: + phenotype = etree.SubElement(cell_def, "phenotype") + + volume = phenotype.find("volume") + if volume is None: + volume = etree.SubElement(phenotype, "volume") + + if total_volume is not None: + total_node = volume.find("total") + if total_node is None: + total_node = etree.SubElement(volume, "total", units="micron^3") + total_node.text = str(total_volume) + + if nuclear_volume is not None: + nuclear_node = volume.find("nuclear") + if nuclear_node is None: + nuclear_node = etree.SubElement(volume, "nuclear", units="micron^3") + nuclear_node.text = str(nuclear_volume) + + +# ---------------------- +# Intracellular dfba insertion +# ---------------------- +def make_exchange_block_xml(exchange_config): + """Create an exchange XML block from a YAML exchange configuration.""" + ex = etree.Element("exchange", substrate=str(exchange_config["substrate"])) + etree.SubElement(ex, "fba_flux").text = str(exchange_config["fba_flux"]) + etree.SubElement(ex, "Km", units="mM").text = str(exchange_config["Km"]) + etree.SubElement(ex, "Vmax", units="fmol/pg DW cell/min").text = str(exchange_config["Vmax"]) + return ex + + +def add_intracellular_dfba(cell_def, sbml_path, model_config): + """ + Adds (or replaces) phenotype->intracellular type="dfba" block in `cell_def`. + + Args: + cell_def: XML element for cell_definition + sbml_path: Path to SBML file + model_config: Dictionary with model configuration from YAML: + - exchanges: list of exchange dictionaries + - growth_model: dict with cell_density, reference_volume, max_growth_rate, objective_reaction + - settings: dict with intracellular_dt + - secondary_objective: dict with enabled, type, fraction_of_optimum + (optional; omitted from the XML entirely when absent, so an + existing YAML produces exactly the XML it produced before) + - death_model: dict with enabled flag + """ + phenotype = cell_def.find("./phenotype") + if phenotype is None: + phenotype = etree.SubElement(cell_def, "phenotype") + + # Remove existing intracellular dfba node if any + for old in phenotype.findall("intracellular"): + if old.get("type") == "dfba": + phenotype.remove(old) + + intracellular = etree.SubElement(phenotype, "intracellular", type="dfba") + + settings_dict = model_config.get("settings", {}) + + # Settings + settings = etree.SubElement(intracellular, "settings") + + sbml_node = etree.SubElement(settings, "sbml_filename") + sbml_node.text = sbml_path.as_posix() + + + dt = settings_dict.get("intracellular_dt") + if dt is None: + dt = "0.01" + etree.SubElement(settings, "intracellular_dt", units="min").text = str(dt) + + # Transport model (exchanges) + transport = etree.SubElement(intracellular, "transport_model") + exchanges = model_config.get("exchanges", []) + for exchange in exchanges: + transport.append(make_exchange_block_xml(exchange)) + + # Growth model + growth = etree.SubElement(intracellular, "growth_model") + growth_config = model_config.get("growth_model", {}) + + cell_density = growth_config.get("cell_density", "1.04") + reference_volume = growth_config.get("reference_volume", "2494") + nuclear_volume = growth_config.get("nuclear_volume", "540") + max_growth_rate = growth_config.get("max_growth_rate", "0.01") + objective_reaction = growth_config.get("objective_reaction", "R_biomass_reactions") + + etree.SubElement(growth, "cell_density", units="g/ml").text = str(cell_density) + etree.SubElement(growth, "reference_volume", units="pg").text = str(reference_volume) + etree.SubElement(growth, "max_growth_rate", units="1/min").text = str(max_growth_rate) + etree.SubElement(growth, "objective_reaction").text = str(objective_reaction) + + # Secondary objective (optional second optimisation stage, e.g. pFBA). + # Written only when the YAML asks for it: standard FBA stays the default. + secondary_config = model_config.get("secondary_objective") + if secondary_config: + etree.SubElement( + intracellular, + "secondary_objective", + type=str(secondary_config.get("type", "parsimonious")), + enabled=str(secondary_config.get("enabled", True)).lower(), + fraction_of_optimum=str(secondary_config.get("fraction_of_optimum", 1.0)), + ) + + # Death model + death_config = model_config.get("death_model", {}) + enabled = death_config.get("enabled", False) if death_config else False + death_model = etree.SubElement(intracellular, "death_model", enabled=str(enabled).lower()) + death_type = death_config.get("death_type", "necrosis") + death_trigger_flux = death_config.get("death_trigger_flux", "0.0") + death_flux_threshold = death_config.get("death_flux_threshold", "0.0") + death_rate_increase = death_config.get("death_rate_increase", "1.67e-5") + + etree.SubElement(death_model, "death_type", units="g/ml").text = str(death_type) + etree.SubElement(death_model, "death_trigger_flux", units="1/min").text = str(death_trigger_flux) + etree.SubElement(death_model, "death_flux_threshold").text = str(death_flux_threshold) + etree.SubElement(death_model, "death_rate_increase").text = str(death_rate_increase) + + +# ---------------------- +# Cell interactions matrix update +# ---------------------- +# (Removed ensure_cell_interactions_for_all as physicell-settings handles this) + +# ---------------------- +# Validation +# ---------------------- +def validate_yaml_config(config, config_yaml_path, sbml_folder): + """ + Lighter validation of YAML configuration. + Checks for existence of critical files and sections. + """ + if "models" not in config or not config["models"]: + raise ValueError("YAML config must contain a non-empty 'models' section") + + # Validate models and files + for model_name, model_config in config["models"].items(): + # Check SBML file existence + settings_dict = model_config.get("settings", {}) + settings_dict = model_config.get("settings", {}) + sbml_path_rel = settings_dict.get("sbml_path") + + if not sbml_path_rel: + raise ValueError(f"Model '{model_name}': missing 'sbml_path'") + + sbml_path = Path(sbml_path_rel) + sbml_folder = Path(sbml_folder) + + # Only prepend sbml_folder if needed + if not sbml_path.is_absolute() and sbml_path.parts[0] != sbml_folder.name: + sbml_path = sbml_folder / sbml_path + + if not sbml_path.exists(): + raise ValueError( + f"Model '{model_name}': SBML file not found: {sbml_path.as_posix()}" + ) + + # Check exchanges existence + if "exchanges" not in model_config: + raise ValueError(f"Model '{model_name}': missing 'exchanges' section") + + print(f"[OK] YAML configuration structure validated") + + +# ---------------------- +# Main orchestration +# ---------------------- +def update_config_with_dfba(output_xml_path, + config_yaml_path, + sbml_folder, + cell_prefix="", + verbose=False): + """ + Create PhysiCell config from scratch with dFBA models from YAML configuration. + + Args: + output_xml_path: Path to write updated XML + config_yaml_path: Path to YAML configuration file + sbml_folder: Folder containing SBML files (for resolving relative paths) + cell_prefix: Optional prefix for new cell definition names + """ + # Load YAML configuration + with open(config_yaml_path, 'r') as f: + yaml_config = yaml.safe_load(f) + + # Validate configuration before processing + validate_yaml_config(yaml_config, config_yaml_path, sbml_folder) + + # Initialize PhysiCellConfig from scratch + config = PhysiCellConfig() + print(f"[INFO] Initialized new PhysiCell configuration") + + # Handle Substrates + global_substrates = set() + if "substrates" in yaml_config: + # Sort substrates by name + sorted_substrates = sorted(yaml_config["substrates"], key=lambda x: x.get("name", "")) + + for sub_config in sorted_substrates: + name = sub_config.get("name") + global_substrates.add(name) + + diff = sub_config.get("diffusion_coefficient", 50000.0) + decay = sub_config.get("decay_rate", 0.0) + init = sub_config.get("initial_condition", 0.0) + + print(f"[INFO] Adding microenvironment variable for substrate '{name}' " + f"(diffusion={diff}, decay={decay}, init={init})") + + config.add_simple_substrate( + name=name, + diffusion_coeff=diff, + decay_rate=decay, + initial_value=init + ) + + global_substrates_sorted = sorted(global_substrates) + + # Create new cells + models = yaml_config["models"] + created_cell_names = [] + next_id = 0 + + for model_name, model_config in models.items(): + new_name = f"{cell_prefix}{model_name}" + + # Create default cell type + config.cell_types.add_cell_type(new_name) + + # Set ID + config.cell_types.cell_types[new_name]['ID'] = str(next_id) + + # Set cell cycle to "live" + config.cell_types.set_cycle_model(new_name, "live") + + # Update volume + growth_cfg = model_config.get("growth_model", {}) + total_volume = growth_cfg.get("reference_volume") + nuclear_volume = growth_cfg.get("nuclear_volume", 0.0) + + if total_volume is not None: + # Use helper if available or direct access + if hasattr(config.cell_types, 'set_volume_parameters'): + config.cell_types.set_volume_parameters(new_name, total=float(total_volume), nuclear=float(nuclear_volume)) + else: + # Fallback to direct access + if 'phenotype' not in config.cell_types.cell_types[new_name]: + config.cell_types.cell_types[new_name]['phenotype'] = {} + if 'volume' not in config.cell_types.cell_types[new_name]['phenotype']: + config.cell_types.cell_types[new_name]['phenotype']['volume'] = {} + + config.cell_types.cell_types[new_name]['phenotype']['volume']['total'] = str(total_volume) + config.cell_types.cell_types[new_name]['phenotype']['volume']['nuclear'] = str(nuclear_volume) + + print(f"[INFO] Created cell_definition '{new_name}' with ID={next_id}") + + created_cell_names.append(new_name) + next_id += 1 + + # Add random_seed to user params + config.add_user_parameter("random_seed", parameter_type="int", description="Random seed for simulation", value="0") + + # Generate XML string to perform low-level dFBA injections + print("[INFO] Generating intermediate XML for dFBA injection...") + xml_str = config.generate_xml() + + # Parse with lxml + root = etree.fromstring(xml_str.encode('utf-8')) + + # Get all cell names for interaction matrix + all_cell_defs = root.findall(".//cell_definition") + all_cell_names = [cd.get("name") for cd in all_cell_defs] + + # Inject dFBA blocks and ensure interactions + for cell_def in all_cell_defs: + name = cell_def.get("name") + + # Identify if this cell corresponds to a model + model_key = None + if name in created_cell_names: + if name.startswith(cell_prefix): + potential_model = name[len(cell_prefix):] + if potential_model in models: + model_key = potential_model + + if model_key: + model_config = models[model_key] + + settings_dict = model_config.get("settings", None) + if not settings_dict: + print(f"[WARNING] Model '{model_key}' missing 'settings' section; using defaults.") + continue + + sbml_path = Path(settings_dict.get("sbml_path")) + sbml_folder = Path(sbml_folder) + + if not sbml_path.is_absolute() and sbml_path.parts[0] != sbml_folder.name: + sbml_path = sbml_folder / sbml_path + + # Resolve SBML path + print((f"DEBUGG: {sbml_path}")) + add_intracellular_dfba(cell_def, sbml_path, model_config) + + # Ensure volume (redundant but safe) + growth_cfg = model_config.get("growth_model", {}) + total_volume = growth_cfg.get("reference_volume") + nuclear_volume = growth_cfg.get("nuclear_volume", 0.0) + if total_volume is not None: + set_cell_volume(cell_def, total_volume=total_volume, nuclear_volume=nuclear_volume) + else: + print(f"[WARNING] Skipping dFBA injection for cell_definition '{name}' (no matching model)") + # Write final output + tree = etree.ElementTree(root) + xml_pretty_write(tree, output_xml_path) + print(f"[OK] Wrote updated config to: {output_xml_path}") + + +# ---------------------- +# CLI +# ---------------------- +def main(): + print("[INFO] Running dfba_configurator.py (refactored with physicell-settings)") + parser = argparse.ArgumentParser(description="Generate PhysiCell config with dFBA blocks from scratch.") + parser.add_argument("--output", "-o", required=True, help="Path to write updated PhysiCell XML.") + parser.add_argument("--config", "-c", required=True, + help="YAML configuration file with models, SBML paths, exchanges, and parameters.") + parser.add_argument("--sbml-folder", "-s", default="", help="Folder containing SBML files (for resolving relative paths).") + parser.add_argument("--cell-prefix", default="", help="Optional prefix to prepend to new cell_definition names.") + parser.add_argument("--verbose", action="store_true", + help="Enable verbose debug output.") + args = parser.parse_args() + + update_config_with_dfba( + args.output, + args.config, + args.sbml_folder, + cell_prefix=args.cell_prefix, + verbose=args.verbose + ) + + +if __name__ == "__main__": + main() + diff --git a/beta/fba_packages.json b/beta/fba_packages.json index ede2550d3..3b2b0c8e0 100644 --- a/beta/fba_packages.json +++ b/beta/fba_packages.json @@ -1,40 +1,50 @@ { "coin-or": { - "_refurl": "https://www.coin-or.org/download/binary/Clp/", + "_refurl": "https://github.com/PhysiCell-Tools/intracellular_libs", "linux-x64": { - "version": "Clp-1.17.6-linux64.zip", - "url": "https://maboss.curie.fr/pub/Clp-1.17.6-linux64.zip" + "version": "Clp-1.17.10-linux64.tar.gz", + "url": "https://github.com/PhysiCell-Tools/intracellular_libs/releases/download/0.1.0/Clp-1.17.10-linux64.tar.gz" }, "win64": { - "version": "Clp-1.17.6-win64.zip", - "url": "https://maboss.curie.fr/pub/Clp-1.17.6-win64.zip" + "version": "Clp-1.17.10-win64.tar.gz", + "url": "https://github.com/PhysiCell-Tools/intracellular_libs/releases/download/0.1.0/Clp-1.17.10-win64.tar.gz" }, - "osx": + "osx-arm64": { - "version": "Clp-1.17.6-osx64.zip", - "url": "https://maboss.curie.fr/pub/Clp-1.17.6-osx64.zip" + "version": "Clp-1.17.10-osx-arm64.tar.gz", + "url": "https://github.com/PhysiCell-Tools/intracellular_libs/releases/download/0.1.0/Clp-1.17.10-osx-arm64.tar.gz" + }, + "osx-x86_64": + { + "version": "Clp-1.17.10-osx-x86_64.tar.gz", + "url": "https://github.com/PhysiCell-Tools/intracellular_libs/releases/download/0.1.0/Clp-1.17.10-osx-x86_64.tar.gz" } }, "libsbml": { - "_refurl": "https://sourceforge.net/projects/sbml/files/libsbml/5.14.0-experimental/binaries/", + "_refurl": "https://github.com/PhysiCell-Tools/intracellular_libs/releases/download/0.1.0/", "linux-x64": { - "version": "libSBML-5.19.0-linux64.zip", - "url": "https://maboss.curie.fr/pub/libSBML-5.19.0-linux64.zip" + "version": "libSBML-5.20.4-linux64.tar.gz", + "url": "https://github.com/PhysiCell-Tools/intracellular_libs/releases/download/0.1.0/libSBML-5.20.4-linux64.tar.gz" }, "win64": { - "version": "libSBML-5.19.0-win64.zip", - "url": "https://maboss.curie.fr/pub/libSBML-5.19.0-win64.zip" + "version": "libSBML-5.20.4-win64.tar.gz", + "url": "https://github.com/PhysiCell-Tools/intracellular_libs/releases/download/0.1.0/libSBML-5.20.4-win64.tar.gz" + }, + "osx-x86_64": + { + "version": "libSBML-5.20.4-osx-x86_64.tar.gz", + "url": "https://github.com/PhysiCell-Tools/intracellular_libs/releases/download/0.1.0/libSBML-5.20.4-osx-x86_64.tar.gz" }, - "osx": + "osx-arm64": { - "version": "libSBML-5.19.0-osx64.zip", - "url": "https://maboss.curie.fr/pub/libSBML-5.19.0-osx64.zip" + "version": "libSBML-5.20.4-osx-arm64.tar.gz", + "url": "https://github.com/PhysiCell-Tools/intracellular_libs/releases/download/0.1.0/libSBML-5.20.4-osx-arm64.tar.gz" } } } diff --git a/beta/generate_dfba_yaml.py b/beta/generate_dfba_yaml.py new file mode 100644 index 000000000..093dd6d4a --- /dev/null +++ b/beta/generate_dfba_yaml.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Script to generate a dfba YAML model from an SBML input file. +Based on the preprocess_dfba_model.ipynb notebook. +""" + +import argparse +import yaml +import pandas as pd + +from cobra.io import read_sbml_model +from cobra.flux_analysis import flux_variability_analysis + +def main(sbml_file, output_yaml, cell_type="MCF7_core", cell_volume=2494.0, ex_prefix="R_EX"): + # Default values + init_concentration_default = 5.0 + diffusion_coefficient_default = 50000.0 + decay_rate_default = 0.0 + Km_default = 0.001 # mM + + # Load SBML model + model = read_sbml_model(sbml_file, f_replace={'F_REACTION': lambda x: x}) + + # Find objective reaction + objective_reaction = None + for r in model.reactions: + if r.objective_coefficient != 0: + objective_reaction = r.id + break + if objective_reaction is None: + raise ValueError("No objective reaction found in the model.") + print(f"Objective reaction: {objective_reaction}") + + # Optimize model + solution = model.optimize() + max_growth_rate = solution.objective_value + print(f"Max growth rate: {max_growth_rate} 1/hr") + + # Run FVA + print("Running FVA...") + fva_df = flux_variability_analysis(model, processes=10, fraction_of_optimum=0.0) + + # Process exchange reactions + fva_ex_df = fva_df.filter(regex=f'^{ex_prefix}', axis=0) + + eps = 1e-7 + mask = fva_ex_df.abs().max(axis=1) < eps + to_remove = fva_ex_df.index[mask].tolist() + + # Find exchange reactions for protons and waters + additional_to_remove = [] + for r in model.exchanges: + if r.id.startswith(ex_prefix) and len(r.metabolites) == 1: + met = list(r.metabolites.keys())[0] + met_name_lower = met.name.lower() + if ('h+' in met_name_lower or 'proton' in met_name_lower or + 'h2o' in met_name_lower or 'water' in met_name_lower or + met.id.lower() in ['h_e', 'h2o_e', 'oh_e', 'oh1_e']): + additional_to_remove.append(r.id) + + to_remove.extend(additional_to_remove) + to_remove = list(set(to_remove)) # remove duplicates + + fva_ex_df = fva_ex_df.drop(to_remove, axis=0, errors='ignore') + + substrates_list = [] + exchanges_list = [] + + for i in fva_ex_df.index: + r = model.reactions.get_by_id(i) + substrate = list(r.metabolites)[0].name + + Vmax = 0.0 + init_concentration = 0.0 + lb = fva_ex_df.loc[i, 'minimum'].round(5) + if lb < 0.0: + Vmax = -1.0 * float(lb) + init_concentration = init_concentration_default + + substrates_dict = { + "name": substrate, + "diffusion_coefficient": diffusion_coefficient_default, + "decay_rate": decay_rate_default, + "initial_condition": init_concentration + } + + substrates_list.append(substrates_dict) + + exchange_dict = { + "substrate": substrate, + "fba_flux": r.id, + "Km": Km_default, + "Vmax": float(Vmax) + } + + exchanges_list.append(exchange_dict) + + # Create model dictionary + model_dict = { + "settings": { + "sbml_path": sbml_file, # Use the input path + "intracellular_dt": 0.01 + }, + "growth_model": { + "cell_density": 1.04, # Cell density (g/ml) + "reference_volume": cell_volume, # Reference volume for cell division (pg) + "nuclear_volume": 540, # Nuclear volume (pg) + "max_growth_rate": max_growth_rate, # Maximum growth rate (1/min) + "objective_reaction": objective_reaction # Objective reaction ID from SBML model + }, + "death_model": { + "enabled": False, + "death_type": "necrosis", # death type string + "death_trigger_flux": 0.0, # 1/min + "death_flux_threshold": 0.0, # dimensionless + "death_rate_increase": 1.67e-5 # rate increase + }, + "exchanges": exchanges_list + } + + dfba_config_dict = { + "substrates": substrates_list, + "models": { + cell_type: model_dict + } + } + + # Write to YAML + with open(output_yaml, "w") as f: + yaml.dump(dfba_config_dict, f, sort_keys=False, default_flow_style=False) + + print(f"YAML model written to {output_yaml}") + print("Substrates:") + print("\n".join([i['name'] for i in substrates_list])) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate dfba YAML model from SBML file.") + parser.add_argument("sbml_file", help="Path to the SBML file") + parser.add_argument("output_yaml", help="Path to the output YAML file") + parser.add_argument("--cell_type", default="MCF7_core", help="Cell type name (default: MCF7_core)") + parser.add_argument("--cell_volume", type=float, default=2494.0, help="Cell volume in um^3 (default: 2494.0)") + parser.add_argument("--ex_prefix", default="R_EX", help="Prefix for exchange reactions (default: R_EX)") + + args = parser.parse_args() + main(args.sbml_file, args.output_yaml, args.cell_type, args.cell_volume, args.ex_prefix) \ No newline at end of file diff --git a/beta/setup_coin_clp.py b/beta/setup_coin_clp.py deleted file mode 100755 index dde30d017..000000000 --- a/beta/setup_coin_clp.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 -# coding: utf-8 - -import sys, os -import json -import argparse - -import requests -import hashlib -import tarfile -import zipfile - -def param_parser(): - parser = argparse.ArgumentParser(description="") - parser.add_argument('--pkg', dest="pkg", required=True, help='Available packages', choices=PACKAGES) - parser.add_argument('--arch', dest="arch", required=True, choices=ARCHS, help='Current arch') - parser.add_argument('--checksum', dest="checksum", default=False, help='Check file integrity after downloading') - parser.add_argument('--path', dest='path', default='ext', help='Default folder destination to install third-party libs if changed, Makefiles you be updated according') - return parser - - -PACKAGES = ("coin-or", "libsbml") -ARCHS = ("linux-x64", "linux-x86", "win64", "win32", "osx") - -SBML_DIR = "libsbml" - - -def main(): - parser = param_parser() - args = parser.parse_args() - - # json_packages = os.path.join(os.curdir, 'config') - # json_packages = os.path.join(json_packages, 'packages.json') - json_packages = os.path.join('beta', 'fba_packages.json') - - packages_dict = {} - with open(json_packages) as fh: - packages_dict = json.load(fh) - - if not os.path.exists(args.path): - print("Creating %s folder" % args.path, end=" ") - os.makedirs(args.path) - print("Ok!") - - print("Moving to %s folder" % args.path) - os.chdir(args.path) - - pkg_dict = packages_dict[args.pkg][args.arch] - - print("Fetching package:", end="") - print("- %s (%s)" % (args.pkg, args.arch)) - print("Downaling from: %s" % pkg_dict['url']) - r = requests.get(pkg_dict['url'], allow_redirects=True) - - # if args.checksum: - # print("Package cheksum(sha256)", end=" ") - # hash_strn = hashlib.sha256(r.content).hexdigest() - # assert pkg_dict['sha256'] == hash_strn - # print("Ok!") - - - fname = pkg_dict["version"] - with open(fname, 'wb') as fh: - fh.write(r.content) - - if args.pkg == 'libsbml': - print("Extracting package in %s... " % args.pkg, end=" ") - if fname.endswith("zip"): - archiver = zipfile.ZipFile(fname, 'r') - archiver.extractall() - elif fname.endswith("tar.gz") or fname.endswith("gz"): - archiver = tarfile.open(fname, "r:gz") - archiver.extractall() - old_lib_dir = os.path.commonprefix(archiver.getnames()) - archiver.close() - os.rename(old_lib_dir, SBML_DIR) - if os.path.exists(old_lib_dir): - os.rmdir(old_lib_dir) - os.remove(fname) - - elif args.pkg == "coin-or": - print("Extracting package in %s... " % args.pkg, end=" ") - if fname.endswith("zip"): - archiver = zipfile.ZipFile(fname, 'r') - archiver.extractall() - elif fname.endswith("tar.gz") or fname.endswith("gz"): - archiver = tarfile.open(fname, "r:gz") - archiver.extractall(path=args.pkg) - archiver.close() - os.remove(fname) - - print("Ok!") - print("Dependency retrived correctly :-)\n") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/beta/setup_fba.py b/beta/setup_fba.py index 11d4433c8..d81ab1a06 100755 --- a/beta/setup_fba.py +++ b/beta/setup_fba.py @@ -1,86 +1,98 @@ #!/usr/bin/env python3 # coding: utf-8 -import sys, os +import os import json -import argparse import platform import urllib.request +import tarfile import zipfile +import sys -def param_parser(): - parser = argparse.ArgumentParser(description="") - parser.add_argument('--pkg', dest="pkg", required=True, help='Available packages', choices=PACKAGES) - parser.add_argument('--path', dest='path', default='addons/dFBA/ext', help='Default folder destination to install third-party libs if changed, Makefiles you be updated according') - return parser +def get_os_arch(): + """Determine the operating system and architecture.""" + os_type = platform.system().lower() + arch = platform.machine().lower() + if os_type == 'linux': + return 'linux-x64' if arch in ('x86_64', 'amd64') else 'linux-x86' + elif os_type == 'darwin': + if arch == 'arm64': + return 'osx-arm64' + elif arch == 'x86_64': + return 'osx-x86_64' + print(f"Unsupported architecture: {os_type} : {arch}") + sys.exit(1) + elif os_type.startswith('win'): + return 'win64' if arch == 'amd64' else 'win32' + else: + print(f"Unsupported OS: {os_type}") + sys.exit(1) -PACKAGES = ("coin-or", "libsbml") - +def ensure_directory_exists(path): + """Create directory if it does not exist.""" + if not os.path.exists(path): + os.makedirs(path) -def main(): - parser = param_parser() - args = parser.parse_args() - os_type = platform.system() +def download_and_extract(url, dest_path, is_zip=False): + """Download and extract a package from a URL.""" + # Ensure the destination directory exists + ensure_directory_exists(dest_path) - if os.path.exists(os.path.join(os.path.dirname(os.path.dirname(__file__)), "addons", "dFBA", "ext", args.pkg)): - print('\n%s already installed.\n' % args.pkg) - return + filename = url.split('/')[-1] + file_path = os.path.join(dest_path, filename) - json_packages = os.path.join('beta', 'fba_packages.json') + # Download the file + print(f"Downloading {filename} from {url}...") + urllib.request.urlretrieve(url, file_path) + + # Extract the file + print(f"Extracting {filename}...") + if is_zip: + with zipfile.ZipFile(file_path, 'r') as zip_ref: + zip_ref.extractall(dest_path) + else: + with tarfile.open(file_path, 'r:gz') as tar_ref: + tar_ref.extractall(dest_path) + + # Clean up + os.remove(file_path) + print(f"{filename} installed successfully.\n") + +def main(): + # Define paths + current_folder = os.path.abspath(os.path.dirname(__file__)) + base_folder = os.path.abspath(os.path.join(current_folder, os.pardir)) + default_libs_path = os.path.join(base_folder, "addons", "dFBA", "ext") - packages_dict = {} + # Load package information + json_packages = os.path.join(base_folder, 'beta', 'fba_packages.json') with open(json_packages) as fh: packages_dict = json.load(fh) - if not os.path.exists(args.path): - print("Creating %s folder" % args.path, end=" ") - os.makedirs(args.path) - print("Ok!") - - print("Moving to %s folder" % args.path) - os.chdir(args.path) - - arch = None - if os_type.lower() == 'darwin': - arch = "osx" - elif os_type.lower().startswith("win"): - arch = "win64" - elif os_type.lower().startswith("linux"): - arch = "linux-x64" - else: - print("OS not supported !") - return - - pkg_dict = packages_dict[args.pkg][arch] - - print("Fetching package:", end="") - print("- %s (%s)" % (args.pkg, arch)) - print("Downaling from: %s" % pkg_dict['url']) - - def download_cb(blocknum, blocksize, totalsize): - readsofar = blocknum * blocksize - if totalsize > 0: - percent = readsofar * 1e2 / totalsize - s = "\r%5.1f%% %*d / %d" % ( - percent, len(str(totalsize)), readsofar, totalsize) - sys.stderr.write(s) - if readsofar >= totalsize: # near the end - sys.stderr.write("\n") - else: # total size is unknown - sys.stderr.write("read %d\n" % (readsofar,)) - - urllib.request.urlretrieve(pkg_dict['url'], pkg_dict['version'], download_cb) - - fname = pkg_dict["version"] - - print("Extracting package in %s... " % args.pkg, end=" ") - if fname.endswith("zip"): - archiver = zipfile.ZipFile(fname, 'r') - archiver.extractall(args.pkg) - - print("Ok!") - print("Dependency retrived correctly :-)\n") + # Determine OS and architecture + arch = get_os_arch() + + # Install packages + for pkg in ("coin-or", "libsbml"): + pkg_path = os.path.join(default_libs_path, pkg) + + # Skip download if package folder already exists and is populated + if os.path.exists(pkg_path) and os.listdir(pkg_path): + print(f"Package {pkg} is already installed. Skipping...") + continue + + # Ensure the package directory exists + ensure_directory_exists(pkg_path) + + pkg_dict = packages_dict.get(pkg, {}).get(arch) + if not pkg_dict: + print(f"No package information found for {pkg} on {arch}. Skipping...") + continue + + url = pkg_dict['url'] + is_zip = url.endswith("zip") + download_and_extract(url, pkg_path, is_zip) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/beta/setup_libroadrunner.py b/beta/setup_libroadrunner.py index bf6148f06..e0f6b882d 100644 --- a/beta/setup_libroadrunner.py +++ b/beta/setup_libroadrunner.py @@ -8,7 +8,6 @@ import urllib.request import os import sys -import tarfile import zipfile def reminder_dynamic_link_path_macos(): @@ -27,15 +26,12 @@ def reminder_dynamic_link_path_linux(): print("\n* To make this permanent, add this line to the bottom of the respective shell startup file, e.g., .bashrc, .bash_profile, or .zshenv in your home directory.") print("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n") -os_type = platform.system() - -# Old: -# if os.path.exists(os.path.join(os.path.dirname(os.path.dirname(__file__)), "addons", "libRoadrunner", "roadrunner")): +os_type = platform.system() # can test that it downloads correct OS .zip by overriding this value # New: July 2023 - trying to be smarter about deciding whether to (re)download libRR # NOTE: needs to be tested cross-platform! -if os.path.exists(os.path.join(os.path.dirname(os.path.dirname(__file__)), "addons", "libRoadrunner", "roadrunner","include","rr","C","rrc_api.h")): - print('\nlibroadrunner already installed.\n') +if os.path.exists(os.path.join(os.path.dirname(os.path.dirname(__file__)), "addons", "libRoadrunner", "roadrunner","include","rr","rrc_api.h")): + print('\nNote: libroadrunner is already installed so we will not re-install it. \nIf you want to force a re-install, delete the "roadrunner" directory in /addons/libRoadrunner then re-compile.\n') # regardless, let's remind the user about the env var requirement! if os_type.lower() == 'darwin': @@ -55,38 +51,31 @@ def reminder_dynamic_link_path_linux(): if os_type.lower() == 'darwin': reminder_dynamic_link_path_macos() if "ARM64" in platform.uname().version: - # pass - # print('... for the arm64 processor.') - # url = "https://github.com/PhysiCell-Tools/intracellular_libs/raw/main/ode/libs/macos12_arm64/libroadrunner_c_api.dylib" - rr_file = "roadrunner_macos_arm64.tar.gz" - url = "https://github.com/PhysiCell-Tools/intracellular_libs/raw/main/ode/roadrunner_macos_arm64.tar.gz" + rr_file = "roadrunner_macos_arm64.zip" + url = "https://github.com/PhysiCell-Tools/intracellular_libs/raw/main/ode/roadrunner_macos_arm64.zip" mac_silicon = True else: - rr_file = "roadrunner-osx-10.9-cp36m.tar.gz" - url = "https://sourceforge.net/projects/libroadrunner/files/libroadrunner-1.4.18/" + rr_file + "/download" + rr_file = "roadrunner_macos_x86_64.zip" + url = "https://github.com/PhysiCell-Tools/intracellular_libs/raw/main/ode/roadrunner_macos_x86_64.zip" elif os_type.lower().startswith("win"): - rr_file = "roadrunner-win64-vs14-cp35m.zip" - url = "https://sourceforge.net/projects/libroadrunner/files/libroadrunner-1.4.18/" + rr_file + "/download" + rr_file = "roadrunner_win_x86_64.zip" + url = "https://github.com/PhysiCell-Tools/intracellular_libs/raw/main/ode/roadrunner_win_x86_64.zip" elif os_type.lower().startswith("linux"): reminder_dynamic_link_path_linux() - rr_file = "cpplibroadrunner-1.3.0-linux_x86_64.tar.gz" - url = "https://sourceforge.net/projects/libroadrunner/files/libroadrunner-1.3/" + rr_file + "/download" + # the following "manylinux" seems to work on more flavors/releases of Linux than the roadrunner_ubuntu_24.zip which still exists on github.com/PhysiCell-Tools/intracellular_libs + rr_file = "roadrunner_manylinux.zip" + url = "https://github.com/PhysiCell-Tools/intracellular_libs/raw/main/ode/roadrunner_manylinux.zip" else: - print("Your operating system seems to be unsupported. Please submit a ticket at https://sourceforge.net/p/physicell/tickets/ ") + print("Your operating system seems to be unsupported. Please create an issue on the PhysiCell GitHub repo or reach out on our Slack channel.") sys.exit(1) print("url=",url) - if mac_silicon: - fname = url.split('/')[-1] - else: - fname = url.split('/')[-2] + fname = url.split('/')[-1] print("fname=",fname) - # home = os.path.expanduser("~") print('libRoadRunner will now be installed into this location:') - # dir_name = os.path.join(home, 'libroadrunner') dir_name = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons', 'libRoadrunner') - print(dir_name + '\n') + print(f'Will create {dir_name} if it does not exist.') # print(' - Press ENTER to confirm the location') # print(' - Press CTL-C to abort the installation') # print(' - Or specify a different location below\n') @@ -121,17 +110,8 @@ def reminder_dynamic_link_path_linux(): my_file = os.path.join(dir_name, fname) print('my_file = ',my_file) - if os_type.lower().startswith("win"): - rrlib_dir = my_file[:-4] - else: # darwin or linux - if mac_silicon: - # idx_end = my_file.rindex('/') - # rrlib_dir = my_file[:idx_end] - rrlib_dir = my_file[:-7] - # rrlib_dir = my_file - else: - rrlib_dir = my_file[:-7] - print('rrlib_dir = ',rrlib_dir) + # rrlib_dir = my_file[:-4] # assume all are .zip + # print('rrlib_dir = ',rrlib_dir) def download_cb(blocknum, blocksize, totalsize): readsofar = blocknum * blocksize @@ -148,44 +128,31 @@ def download_cb(blocknum, blocksize, totalsize): urllib.request.urlretrieve(url, my_file, download_cb) new_dir_name = "roadrunner" + print(f"chdir to {dir_name}") os.chdir(dir_name) print('installing (uncompressing) the file...') - if os_type.lower().startswith("win"): - try: - with zipfile.ZipFile(rr_file) as zf: - zf.extractall('.') - os.rename("roadrunner-win64-vs14-cp35m", new_dir_name) - except: - print('error unzipping the file') - exit(1) - else: # Darwin or Linux - try: - print("untarring ",rr_file) - tar = tarfile.open(rr_file) - tar.extractall() - tar.close() - if 'darwin' in os_type.lower(): - if mac_silicon: - os.rename("roadrunner_macos_arm64", new_dir_name) - else: - os.rename("roadrunner-osx-10.9-cp36m", new_dir_name) - else: - os.rename("libroadrunner", new_dir_name) - except: - if mac_silicon: - print() - # pass - else: - print('error untarring the file') - exit(1) - - print('Done.\n') - - # # LIBRR_DIR := /Users/heiland/libroadrunner/roadrunner-osx-10.9-cp36m - # print("Replace the following variables in your PhysiCell Makefile with these:\n") - # #print("LIBRR_DIR := /Users/heiland/libroadrunner/roadrunner-osx-10.9-cp36m") - # print("LIBRR_DIR := " + rrlib_dir) - # if os_type == 'Windows': - # print("LIBRR_LIBS := " + rrlib_dir + "/bin\n") - # else: - # print("LIBRR_LIBS := " + rrlib_dir + "/lib\n") + try: + with zipfile.ZipFile(rr_file) as zf: + zf.extractall('.') # should create "roadrunner" directory + except: + print('error unzipping the file') + exit(1) + + + # Hack to maintain backwards compatibility. Originally our header path was .../include/rr/C, + # but newer releases of the libroadrunner C API seems to have dropped the "/C" directory and put everything in "/rr" + dst = "roadrunner/include/rr/C" + try: + os.remove(dst) + except FileNotFoundError: + print(f"File '{dst}' does not exist.") + + try: + src = "." + os.symlink(src, dst) + print(f"Symbolic link '{dst}' created, pointing to '{src}'") + except FileExistsError: + print(f"Symlink '{dst}' already exists") + except OSError as e: + print(f"Error creating symlink: {e}") + print('Done.\n') diff --git a/beta/setup_windows_dep.py b/beta/setup_windows_dep.py index d7967e659..fe82bc7b7 100644 --- a/beta/setup_windows_dep.py +++ b/beta/setup_windows_dep.py @@ -16,10 +16,10 @@ raise ImportError("pip install zstandard") libs = { - "https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-gcc-libs-13.2.0-3-any.pkg.tar.zst": [ + "https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-gcc-libs-15.2.0-8-any.pkg.tar.zst": [ "mingw64/bin/libgcc_s_seh-1.dll", "mingw64/bin/libgomp-1.dll" ], - "https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-libwinpthread-git-11.0.0.r551.g86a5e0f41-1-any.pkg.tar.zst": [ + "https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-libwinpthread-git-12.0.0.r747.g1a99f8514-1-any.pkg.tar.zst": [ "mingw64/bin/libwinpthread-1.dll" ] } diff --git a/config/PhysiCell_settings.xml b/config/PhysiCell_settings.xml index e3cd03b4a..eac0f092e 100644 --- a/config/PhysiCell_settings.xml +++ b/config/PhysiCell_settings.xml @@ -1,20 +1,29 @@ + + + + config + rules.csv + + + + - -500 - 500 - -500 - 500 - -10 - 10 - 20 - 20 - 20 + -350.0 + 350.0 + -350.0 + 350.0 + -1.5 + 1.5 + 1.0 + 1.0 + 1.0 true - 14400 + 600 min micron 0.01 @@ -23,18 +32,24 @@ - 6 + 8 output - 60 + 10 true - 60 - true + 1 + false + + oxygen + 0.0 + 0.1 + YlOrRd + false @@ -42,34 +57,35 @@ - false true + false + 50 100000.0 - 0.1 + 0.0 - 38 - 0 + 5 + 100 - 38 - 10 - 10 - 38 - 0 - 0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 - + - 10 - .1 + 46800.0 + 0.0 - 0 - 0 + 8.0 + 0 0 0 @@ -79,13 +95,13 @@ 0 - + - 10 - 0.1 + 72000.00 + 0.0 - 0 - 0 + 0.0 + 0.0 0 0 @@ -95,9 +111,25 @@ 0 + + + 115000 + 0.0 + + 0.0 + 0.0 + + + + + + + + + - true - true + false + false ./config/initial.mat @@ -108,16 +140,19 @@ - + - 0.000 + 0.0 + + 1.0 + - 5.31667e-05 + 0 516 @@ -131,11 +166,11 @@ - 2.80E-03 - - 0 - 86400 - + 0.0 + + 9000000000.0 + 1.15741e-05 + 1.11667e-2 8.33333e-4 @@ -147,12 +182,12 @@ - 2494 + 1.3 0.75 - 540 + 0 0.05 0.0045 - 0.0055 + 0 0 0 2.0 @@ -162,17 +197,16 @@ 10.0 1.25 - 1 + 1.0 1.8 15.12 - 4.0 - 10.0 0.01 0.0 0.0 + 12 1 @@ -184,15 +218,16 @@ false oxygen - 1 + -1 false false 0.0 - 0.0 - 0.0 + 0.0 + 0.0 + 0.0 @@ -200,72 +235,125 @@ 0 - 1 - 10 + 0 + 0 0 - - 0.0 - 1 - 0.0 - 0.0 + + 0 + 0 + 0 + 0 + + + 0 + 0 + 0 + 0 - + 0.0 - 1 + 0.0 0.0 0.0 - 0 + 0.0 + 0.0 + 0.0 - 0 - 0 + 0.0 - 0 - 0 + 0.0 - 1 + 1.0 + 0.1 - 0 - 0 + 0.0 - 0 - 0 + 0.0 + + 0.0 + 0.0 + + + + config/Ecoli_core.xml + 0.01 + + + + R_EX_glc__D_e + 0.02 + 8.0 + + + R_EX_ac_e + 1 + 5.0 + + + R_EX_o2_e + 0.002 + 18 + + + R_EX_co2_e + 1 + 0.0 + + + + 1.04 + 1.3 + 0.86 + R_Biomass_Ecoli_core + + + necrosis + + + 1.67e-5 + + - 1.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + + - ./config - cells.csv + ./config/ + cells_reduced.csv - - - - ./config - cell_rules_v2.csv - - - - - - - 0 - 0 + 60 + 30.0 + 0 + True + oxygen + 650 + 10 + 60 + 210 + 5 \ No newline at end of file diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 000000000..6c672022d --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,36 @@ +# Here we are creating two libraries: +# 1) PhysiCell-core: includes all core PhysiCell functionality +# 2) PhysiCell-core-no-addon: includes all core PhysiCell functionality, excluding the cell class +# The latter is necessary for projects that define ADDON_* macro for the specific intracellular support + +add_library(PhysiCell-core OBJECT) +add_library(PhysiCell-core-no-addon OBJECT) + +target_sources( + PhysiCell-core + PRIVATE PhysiCell_basic_signaling.cpp + PhysiCell_cell_container.cpp + PhysiCell_cell.cpp + PhysiCell_constants.cpp + PhysiCell_custom.cpp + PhysiCell_phenotype.cpp + PhysiCell_rules.cpp + PhysiCell_signal_behavior.cpp + PhysiCell_standard_models.cpp + PhysiCell_utilities.cpp) + +target_sources( + PhysiCell-core-no-addon + PRIVATE PhysiCell_basic_signaling.cpp + PhysiCell_cell_container.cpp + PhysiCell_constants.cpp + PhysiCell_custom.cpp + PhysiCell_phenotype.cpp + PhysiCell_rules.cpp + PhysiCell_signal_behavior.cpp + PhysiCell_standard_models.cpp + PhysiCell_utilities.cpp) + +find_package(OpenMP REQUIRED) +target_link_libraries(PhysiCell-core PUBLIC OpenMP::OpenMP_CXX) +target_link_libraries(PhysiCell-core-no-addon PUBLIC OpenMP::OpenMP_CXX) diff --git a/core/PhysiCell_cell.cpp b/core/PhysiCell_cell.cpp index a14c70b99..ed5dd976c 100644 --- a/core/PhysiCell_cell.cpp +++ b/core/PhysiCell_cell.cpp @@ -238,12 +238,15 @@ Cell_State::Cell_State() neighbors.resize(0); spring_attachments.resize(0); + attacked_by.resize(0); + orientation.resize( 3 , 0.0 ); simple_pressure = 0.0; attached_cells.clear(); spring_attachments.clear(); + attacked_by.clear(); number_of_nuclei = 1; @@ -457,6 +460,8 @@ Cell::~Cell() { // release any attached cells (as of 1.7.2 release) this->remove_all_attached_cells(); + this->remove_all_attackers(); + this->remove_self_from_attacked(); // 1.11.0 this->remove_all_spring_attachments(); @@ -549,8 +554,12 @@ Cell* Cell::divide( ) // phenotype.flagged_for_division = false; // phenotype.flagged_for_removal = false; - // make sure ot remove adhesions + // make sure to remove adhesions remove_all_attached_cells(); + // the springs below include the one holding an attack together, so end the + // attack rather than leave it spring-less + remove_all_attackers(); + remove_self_from_attacked(); remove_all_spring_attachments(); // version 1.10.3: @@ -608,11 +617,26 @@ Cell* Cell::divide( ) normalize( &rand_vec ); rand_vec *= radius; // multiply direction times the displacement */ - - std::vector rand_vec = cell_division_orientation(); - rand_vec = rand_vec- phenotype.geometry.polarity*(rand_vec[0]*state.orientation[0]+ - rand_vec[1]*state.orientation[1]+rand_vec[2]*state.orientation[2])*state.orientation; - rand_vec *= phenotype.geometry.radius; + + // direction to displace daughter cell + std::vector rand_vec; + if( this->functions.cell_division_direction_function ) + { + rand_vec = this->functions.cell_division_direction_function( this ); + if( default_microenvironment_options.simulate_2D == true ) // ensure vec in XY plane + { rand_vec[2] = 0.0; } + } + else + { + rand_vec = cell_division_orientation(); + // make it orthogonal to the cell's orientation + rand_vec = rand_vec- phenotype.geometry.polarity*(rand_vec[0]*state.orientation[0]+ + rand_vec[1]*state.orientation[1]+rand_vec[2]*state.orientation[2])*state.orientation; + } + // make sure it is a unit vector + normalize( &rand_vec ); + // push the cells far enough apart to avoid strong overlap, but still maintain some overlap + rand_vec *= 0.5 * phenotype.geometry.radius; child->assign_position(position[0] + rand_vec[0], position[1] + rand_vec[1], @@ -620,8 +644,8 @@ Cell* Cell::divide( ) //change my position to keep the center of mass intact // and then see if I need to update my voxel index - static double negative_one_half = -0.5; - axpy( &position, negative_one_half , rand_vec ); // position = position - 0.5*rand_vec; + static double negative_one = -1.0; + axpy( &position, negative_one , rand_vec ); // position = position - rand_vec; //If this cell has been moved outside of the boundaries, mark it as such. //(If the child cell is outside of the boundaries, that has been taken care of in the assign_position function.) @@ -639,6 +663,7 @@ Cell* Cell::divide( ) set_total_volume(phenotype.volume.total); // child->set_phenotype( phenotype ); + // pAttackTarget was cleared above, so the daughter inherits a NULL one child->phenotype = phenotype; if (child->phenotype.intracellular){ @@ -867,7 +892,7 @@ void Cell::update_position( double dt ) if( default_microenvironment_options.simulate_2D == true ) { velocity[2] = 0.0; } - std::vector old_position(position); + // std::vector old_position(position); axpy( &position , d1 , velocity ); axpy( &position , d2 , previous_velocity ); // overwrite previous_velocity for future use @@ -931,7 +956,17 @@ void Cell::update_voxel_in_container() container->add_agent_to_voxel(this, updated_current_mechanics_voxel_index); } current_mechanics_voxel_index=updated_current_mechanics_voxel_index; + + // Update max_cell_interactive_distance_in_voxel if needed + if( get_container()->max_cell_interactive_distance_in_voxel[current_mechanics_voxel_index] < + phenotype.geometry.radius * phenotype.mechanics.relative_maximum_adhesion_distance ) + { + get_container()->max_cell_interactive_distance_in_voxel[current_mechanics_voxel_index] = phenotype.geometry.radius + * phenotype.mechanics.relative_maximum_adhesion_distance; + } } + + return; } @@ -1054,7 +1089,7 @@ void Cell::add_potentials(Cell* other_agent) state.neighbors.push_back(other_agent); // move here in 1.10.2 so non-adhesive cells also added. } ///////////////////////////////////////////////////////////////// - if( fabs(temp_r) < 1e-16 ) + if( fabs(temp_r) < 1e-16 || is_movable == false ) { return; } temp_r /= distance; // for( int i = 0 ; i < 3 ; i++ ) @@ -1132,6 +1167,14 @@ void Cell::convert_to_cell_definition( Cell_Definition& cd ) Geometry cell_geometry = phenotype.geometry; Molecular cell_molecular = phenotype.molecular; Custom_Cell_Data cell_custom_data = custom_data; + + // pAttackTarget is phenotype state, and the assignment below replaces it, so + // end this cell's own attack. attacked_by and the spring are in state, which + // transformation preserves, so attacks against this cell continue. + remove_self_from_attacked(); + // should we also remove all attackers? That would be a change in behavior, so for now we don't. + // remove_all_attackers(); + // use the cell defaults; type = cd.type; type_name = cd.name; @@ -1198,6 +1241,8 @@ void delete_cell( int index ) // release any attached cells (as of 1.7.2 release) pDeleteMe->remove_all_attached_cells(); + pDeleteMe->remove_all_attackers(); + pDeleteMe->remove_self_from_attacked(); // 1.11.0 pDeleteMe->remove_all_spring_attachments(); @@ -1232,6 +1277,8 @@ void delete_cell_original( int index ) // before June 11, 2020 // release any attached cells (as of 1.7.2 release) (*all_cells)[index]->remove_all_attached_cells(); + (*all_cells)[index]->remove_all_attackers(); + (*all_cells)[index]->remove_self_from_attacked(); // 1.11.0 (*all_cells)[index]->remove_all_spring_attachments(); @@ -1335,132 +1382,136 @@ void Cell::ingest_cell( Cell* pCell_to_eat ) if( pCell_to_eat == this ) { return; } - // don't ingest a cell that's already ingested - if( pCell_to_eat->phenotype.volume.total < 1e-15 ) - { return; } - // make this thread safe #pragma omp critical { - /* - if( pCell_to_eat->phenotype.death.dead == true ) - { std::cout << this->type_name << " (" << this << ")" << " eats dead " << pCell_to_eat->type_name << " (" << pCell_to_eat - << ") of size " << pCell_to_eat->phenotype.volume.total << std::endl; } - else - { std::cout << this->type_name << " (" << this << ")" << " eats live " << pCell_to_eat->type_name << " (" << pCell_to_eat - << ") of size " << pCell_to_eat->phenotype.volume.total << std::endl; } - */ - - // mark it as dead - pCell_to_eat->phenotype.death.dead = true; - // set secretion and uptake to zero - pCell_to_eat->phenotype.secretion.set_all_secretion_to_zero( ); - pCell_to_eat->phenotype.secretion.set_all_uptake_to_zero( ); - - // deactivate all custom function - pCell_to_eat->functions.custom_cell_rule = NULL; - pCell_to_eat->functions.update_phenotype = NULL; - pCell_to_eat->functions.contact_function = NULL; - pCell_to_eat->functions.cell_division_function = NULL; - - // should set volume fuction to NULL too! - pCell_to_eat->functions.volume_update_function = NULL; + // don't ingest a cell that's already ingested + if( pCell_to_eat->phenotype.volume.total >= 1e-15 ) + { + /* + if( pCell_to_eat->phenotype.death.dead == true ) + { std::cout << this->type_name << " (" << this << ")" << " eats dead " << pCell_to_eat->type_name << " (" << pCell_to_eat + << ") of size " << pCell_to_eat->phenotype.volume.total << std::endl; } + else + { std::cout << this->type_name << " (" << this << ")" << " eats live " << pCell_to_eat->type_name << " (" << pCell_to_eat + << ") of size " << pCell_to_eat->phenotype.volume.total << std::endl; } + */ + + // mark it as dead + pCell_to_eat->phenotype.death.dead = true; + // set secretion and uptake to zero + pCell_to_eat->phenotype.secretion.set_all_secretion_to_zero( ); + pCell_to_eat->phenotype.secretion.set_all_uptake_to_zero( ); + + // deactivate all custom function + pCell_to_eat->functions.custom_cell_rule = NULL; + pCell_to_eat->functions.update_phenotype = NULL; + pCell_to_eat->functions.contact_function = NULL; + pCell_to_eat->functions.cell_division_function = NULL; + pCell_to_eat->functions.cell_division_direction_function = NULL; + + // should set volume fuction to NULL too! + pCell_to_eat->functions.volume_update_function = NULL; - // set cell as unmovable and non-secreting - pCell_to_eat->is_movable = false; - pCell_to_eat->is_active = false; + // set cell as unmovable and non-secreting + pCell_to_eat->is_movable = false; + pCell_to_eat->is_active = false; - // absorb all the volume(s) + // absorb all the volume(s) - // absorb fluid volume (all into the cytoplasm) - phenotype.volume.cytoplasmic_fluid += pCell_to_eat->phenotype.volume.fluid; - pCell_to_eat->phenotype.volume.cytoplasmic_fluid = 0.0; - - // absorb nuclear and cyto solid volume (into the cytoplasm) - phenotype.volume.cytoplasmic_solid += pCell_to_eat->phenotype.volume.cytoplasmic_solid; - pCell_to_eat->phenotype.volume.cytoplasmic_solid = 0.0; - - phenotype.volume.cytoplasmic_solid += pCell_to_eat->phenotype.volume.nuclear_solid; - pCell_to_eat->phenotype.volume.nuclear_solid = 0.0; - - // consistency calculations - - phenotype.volume.fluid = phenotype.volume.nuclear_fluid + - phenotype.volume.cytoplasmic_fluid; - pCell_to_eat->phenotype.volume.fluid = 0.0; - - phenotype.volume.solid = phenotype.volume.cytoplasmic_solid + - phenotype.volume.nuclear_solid; - pCell_to_eat->phenotype.volume.solid = 0.0; - - // no change to nuclear volume (initially) - pCell_to_eat->phenotype.volume.nuclear = 0.0; - pCell_to_eat->phenotype.volume.nuclear_fluid = 0.0; - - phenotype.volume.cytoplasmic = phenotype.volume.cytoplasmic_solid + - phenotype.volume.cytoplasmic_fluid; - pCell_to_eat->phenotype.volume.cytoplasmic = 0.0; - - phenotype.volume.total = phenotype.volume.nuclear + - phenotype.volume.cytoplasmic; - pCell_to_eat->phenotype.volume.total = 0.0; + // absorb fluid volume (all into the cytoplasm) + phenotype.volume.cytoplasmic_fluid += pCell_to_eat->phenotype.volume.fluid; + pCell_to_eat->phenotype.volume.cytoplasmic_fluid = 0.0; + + // absorb nuclear and cyto solid volume (into the cytoplasm) + phenotype.volume.cytoplasmic_solid += pCell_to_eat->phenotype.volume.cytoplasmic_solid; + pCell_to_eat->phenotype.volume.cytoplasmic_solid = 0.0; + + phenotype.volume.cytoplasmic_solid += pCell_to_eat->phenotype.volume.nuclear_solid; + pCell_to_eat->phenotype.volume.nuclear_solid = 0.0; + + // consistency calculations + + phenotype.volume.fluid = phenotype.volume.nuclear_fluid + + phenotype.volume.cytoplasmic_fluid; + pCell_to_eat->phenotype.volume.fluid = 0.0; + + phenotype.volume.solid = phenotype.volume.cytoplasmic_solid + + phenotype.volume.nuclear_solid; + pCell_to_eat->phenotype.volume.solid = 0.0; + + // no change to nuclear volume (initially) + pCell_to_eat->phenotype.volume.nuclear = 0.0; + pCell_to_eat->phenotype.volume.nuclear_fluid = 0.0; + + phenotype.volume.cytoplasmic = phenotype.volume.cytoplasmic_solid + + phenotype.volume.cytoplasmic_fluid; + pCell_to_eat->phenotype.volume.cytoplasmic = 0.0; + + phenotype.volume.total = phenotype.volume.nuclear + + phenotype.volume.cytoplasmic; + pCell_to_eat->phenotype.volume.total = 0.0; - phenotype.volume.fluid_fraction = phenotype.volume.fluid / - ( phenotype.volume.total + 1e-16 ); - pCell_to_eat->phenotype.volume.fluid_fraction = 0.0; + phenotype.volume.fluid_fraction = phenotype.volume.fluid / + ( phenotype.volume.total + 1e-16 ); + pCell_to_eat->phenotype.volume.fluid_fraction = 0.0; - phenotype.volume.cytoplasmic_to_nuclear_ratio = phenotype.volume.cytoplasmic_solid / - ( phenotype.volume.nuclear_solid + 1e-16 ); + phenotype.volume.cytoplasmic_to_nuclear_ratio = phenotype.volume.cytoplasmic_solid / + ( phenotype.volume.nuclear_solid + 1e-16 ); + + // update corresponding BioFVM parameters (self-consistency) + set_total_volume( phenotype.volume.total ); + pCell_to_eat->set_total_volume( 0.0 ); - // update corresponding BioFVM parameters (self-consistency) - set_total_volume( phenotype.volume.total ); - pCell_to_eat->set_total_volume( 0.0 ); - - // absorb the internalized substrates - - // multiply by the fraction that is supposed to be ingested (for each substrate) + // absorb the internalized substrates + + // multiply by the fraction that is supposed to be ingested (for each substrate) - *(pCell_to_eat->internalized_substrates) *= - *(pCell_to_eat->fraction_transferred_when_ingested); // + *(pCell_to_eat->internalized_substrates) *= + *(pCell_to_eat->fraction_transferred_when_ingested); // - *internalized_substrates += *(pCell_to_eat->internalized_substrates); - static int n_substrates = internalized_substrates->size(); - pCell_to_eat->internalized_substrates->assign( n_substrates , 0.0 ); + *internalized_substrates += *(pCell_to_eat->internalized_substrates); + static int n_substrates = internalized_substrates->size(); + pCell_to_eat->internalized_substrates->assign( n_substrates , 0.0 ); - // conserved quantitites in custom data during phagocytosis - // so that phagocyte cell absorbs the full amount from the engulfed cell; - for( int nn = 0 ; nn < custom_data.variables.size() ; nn++ ) - { - if( custom_data.variables[nn].conserved_quantity == true ) + // conserved quantitites in custom data during phagocytosis + // so that phagocyte cell absorbs the full amount from the engulfed cell; + for( int nn = 0 ; nn < custom_data.variables.size() ; nn++ ) { - custom_data.variables[nn].value += - pCell_to_eat->custom_data.variables[nn].value; + if( custom_data.variables[nn].conserved_quantity == true ) + { + custom_data.variables[nn].value += + pCell_to_eat->custom_data.variables[nn].value; + } } - } - for( int nn = 0 ; nn < custom_data.vector_variables.size() ; nn++ ) - { - if( custom_data.vector_variables[nn].conserved_quantity == true ) + for( int nn = 0 ; nn < custom_data.vector_variables.size() ; nn++ ) { - custom_data.vector_variables[nn].value += - pCell_to_eat->custom_data.vector_variables[nn].value; + if( custom_data.vector_variables[nn].conserved_quantity == true ) + { + custom_data.vector_variables[nn].value += + pCell_to_eat->custom_data.vector_variables[nn].value; + } } - } - - // trigger removal from the simulation - // pCell_to_eat->die(); // I don't think this is safe if it's in an OpenMP loop - - // flag it for removal - // pCell_to_eat->flag_for_removal(); + + // trigger removal from the simulation + // pCell_to_eat->die(); // I don't think this is safe if it's in an OpenMP loop + + // flag it for removal + // pCell_to_eat->flag_for_removal(); - // remove all adhesions - // pCell_to_eat->remove_all_attached_cells(); + // remove all adhesions + // pCell_to_eat->remove_all_attached_cells(); + } } - // things that have their own thread safety + // Teardown is deferred rather than done here. flag_for_removal() queues this + // cell, and the serial cells_ready_to_die -> die() -> delete_cell() pass does + // the same four steps off-thread. Doing them here means mutating other cells' + // lists from inside the mechanics parallel-for, where remove_all_* walks a + // vector unlocked while attach/detach_cell_as_spring edits it under the + // critical -- a real race, not a theoretical one. pCell_to_eat->flag_for_removal(); - pCell_to_eat->remove_all_attached_cells(); - pCell_to_eat->remove_all_spring_attachments(); return; } @@ -1495,139 +1546,148 @@ void Cell::attack_cell( Cell* pCell_to_attack , double dt ) void Cell::fuse_cell( Cell* pCell_to_fuse ) { - // don't ingest a cell that's already fused or fuse self - if( pCell_to_fuse->phenotype.volume.total < 1e-15 || this == pCell_to_fuse ) + // don't fuse self + if( this == pCell_to_fuse ) { return; } - + // make this thread safe #pragma omp critical { + // don't fuse a cell that's already fused or ingested + if( pCell_to_fuse->phenotype.volume.total >= 1e-15) + { - // set new position at center of volume - // x_new = (vol_B * x_B + vol_S * x_S ) / (vol_B + vol_S ) - - std::vector new_position = position; // x_B - new_position *= phenotype.volume.total; // vol_B * x_B - double total_volume = phenotype.volume.total; - total_volume += pCell_to_fuse->phenotype.volume.total ; - - axpy( &new_position , pCell_to_fuse->phenotype.volume.total , pCell_to_fuse->position ); // vol_B*x_B + vol_S*x_S - new_position /= total_volume; // (vol_B*x_B+vol_S*x_S)/(vol_B+vol_S); - - static double xL = get_default_microenvironment()->mesh.bounding_box[0]; - static double xU = get_default_microenvironment()->mesh.bounding_box[3]; + // set new position at center of volume + // x_new = (vol_B * x_B + vol_S * x_S ) / (vol_B + vol_S ) + + std::vector new_position = position; // x_B + new_position *= phenotype.volume.total; // vol_B * x_B + double total_volume = phenotype.volume.total; + total_volume += pCell_to_fuse->phenotype.volume.total ; - static double yL = get_default_microenvironment()->mesh.bounding_box[1]; - static double yU = get_default_microenvironment()->mesh.bounding_box[4]; + axpy( &new_position , pCell_to_fuse->phenotype.volume.total , pCell_to_fuse->position ); // vol_B*x_B + vol_S*x_S + new_position /= total_volume; // (vol_B*x_B+vol_S*x_S)/(vol_B+vol_S); - static double zL = get_default_microenvironment()->mesh.bounding_box[2]; - static double zU = get_default_microenvironment()->mesh.bounding_box[5]; + static double xL = get_default_microenvironment()->mesh.bounding_box[0]; + static double xU = get_default_microenvironment()->mesh.bounding_box[3]; - if( new_position[0] < xL || new_position[0] > xU || - new_position[1] < yL || new_position[1] > yU || - new_position[2] < zL || new_position[2] > zU ) - { - std::cout << "cell fusion at " << new_position << " violates domain bounds" << std::endl; - std::cout << get_default_microenvironment()->mesh.bounding_box << std::endl << std::endl; - } - position = new_position; - update_voxel_in_container(); + static double yL = get_default_microenvironment()->mesh.bounding_box[1]; + static double yU = get_default_microenvironment()->mesh.bounding_box[4]; - // set number of nuclei + static double zL = get_default_microenvironment()->mesh.bounding_box[2]; + static double zU = get_default_microenvironment()->mesh.bounding_box[5]; - state.number_of_nuclei += pCell_to_fuse->state.number_of_nuclei; + if( new_position[0] < xL || new_position[0] > xU || + new_position[1] < yL || new_position[1] > yU || + new_position[2] < zL || new_position[2] > zU ) + { + std::cout << "cell fusion at " << new_position << " violates domain bounds" << std::endl; + std::cout << get_default_microenvironment()->mesh.bounding_box << std::endl << std::endl; + } + position = new_position; + update_voxel_in_container(); - // absorb all the volume(s) + // set number of nuclei - // absorb fluid volume (all into the cytoplasm) - phenotype.volume.cytoplasmic_fluid += pCell_to_fuse->phenotype.volume.cytoplasmic_fluid; - pCell_to_fuse->phenotype.volume.cytoplasmic_fluid = 0.0; + state.number_of_nuclei += pCell_to_fuse->state.number_of_nuclei; - phenotype.volume.nuclear_fluid += pCell_to_fuse->phenotype.volume.nuclear_fluid; - pCell_to_fuse->phenotype.volume.nuclear_fluid = 0.0; + // absorb all the volume(s) - // absorb nuclear and cyto solid volume (into the cytoplasm) - phenotype.volume.cytoplasmic_solid += pCell_to_fuse->phenotype.volume.cytoplasmic_solid; - pCell_to_fuse->phenotype.volume.cytoplasmic_solid = 0.0; - - phenotype.volume.nuclear_solid += pCell_to_fuse->phenotype.volume.nuclear_solid; - pCell_to_fuse->phenotype.volume.nuclear_solid = 0.0; + // absorb fluid volume (all into the cytoplasm) + phenotype.volume.cytoplasmic_fluid += pCell_to_fuse->phenotype.volume.cytoplasmic_fluid; + pCell_to_fuse->phenotype.volume.cytoplasmic_fluid = 0.0; - // consistency calculations - - phenotype.volume.fluid = phenotype.volume.nuclear_fluid + - phenotype.volume.cytoplasmic_fluid; - pCell_to_fuse->phenotype.volume.fluid = 0.0; - - phenotype.volume.solid = phenotype.volume.cytoplasmic_solid + - phenotype.volume.nuclear_solid; - pCell_to_fuse->phenotype.volume.solid = 0.0; - - phenotype.volume.nuclear = phenotype.volume.nuclear_fluid + - phenotype.volume.nuclear_solid; - pCell_to_fuse->phenotype.volume.nuclear = 0.0; + phenotype.volume.nuclear_fluid += pCell_to_fuse->phenotype.volume.nuclear_fluid; + pCell_to_fuse->phenotype.volume.nuclear_fluid = 0.0; - phenotype.volume.cytoplasmic = phenotype.volume.cytoplasmic_fluid + - phenotype.volume.cytoplasmic_solid; - pCell_to_fuse->phenotype.volume.cytoplasmic = 0.0; - - phenotype.volume.total = phenotype.volume.nuclear + - phenotype.volume.cytoplasmic; - pCell_to_fuse->phenotype.volume.total = 0.0; + // absorb nuclear and cyto solid volume (into the cytoplasm) + phenotype.volume.cytoplasmic_solid += pCell_to_fuse->phenotype.volume.cytoplasmic_solid; + pCell_to_fuse->phenotype.volume.cytoplasmic_solid = 0.0; + + phenotype.volume.nuclear_solid += pCell_to_fuse->phenotype.volume.nuclear_solid; + pCell_to_fuse->phenotype.volume.nuclear_solid = 0.0; - phenotype.volume.fluid_fraction = phenotype.volume.fluid / - ( phenotype.volume.total + 1e-16 ); - pCell_to_fuse->phenotype.volume.fluid_fraction = 0.0; + // consistency calculations + + phenotype.volume.fluid = phenotype.volume.nuclear_fluid + + phenotype.volume.cytoplasmic_fluid; + pCell_to_fuse->phenotype.volume.fluid = 0.0; + + phenotype.volume.solid = phenotype.volume.cytoplasmic_solid + + phenotype.volume.nuclear_solid; + pCell_to_fuse->phenotype.volume.solid = 0.0; + + phenotype.volume.nuclear = phenotype.volume.nuclear_fluid + + phenotype.volume.nuclear_solid; + pCell_to_fuse->phenotype.volume.nuclear = 0.0; - phenotype.volume.cytoplasmic_to_nuclear_ratio = phenotype.volume.cytoplasmic_solid / - ( phenotype.volume.nuclear_solid + 1e-16 ); + phenotype.volume.cytoplasmic = phenotype.volume.cytoplasmic_fluid + + phenotype.volume.cytoplasmic_solid; + pCell_to_fuse->phenotype.volume.cytoplasmic = 0.0; - // update corresponding BioFVM parameters (self-consistency) - set_total_volume( phenotype.volume.total ); - pCell_to_fuse->set_total_volume( 0.0 ); + phenotype.volume.total = phenotype.volume.nuclear + + phenotype.volume.cytoplasmic; + pCell_to_fuse->phenotype.volume.total = 0.0; - // absorb the internalized substrates - - *internalized_substrates += *(pCell_to_fuse->internalized_substrates); - static int n_substrates = internalized_substrates->size(); - pCell_to_fuse->internalized_substrates->assign( n_substrates , 0.0 ); + phenotype.volume.fluid_fraction = phenotype.volume.fluid / + ( phenotype.volume.total + 1e-16 ); + pCell_to_fuse->phenotype.volume.fluid_fraction = 0.0; - // set target volume(s) + phenotype.volume.cytoplasmic_to_nuclear_ratio = phenotype.volume.cytoplasmic_solid / + ( phenotype.volume.nuclear_solid + 1e-16 ); + + // update corresponding BioFVM parameters (self-consistency) + set_total_volume( phenotype.volume.total ); + pCell_to_fuse->set_total_volume( 0.0 ); - phenotype.volume.target_solid_cytoplasmic += pCell_to_fuse->phenotype.volume.target_solid_cytoplasmic; - phenotype.volume.target_solid_nuclear += pCell_to_fuse->phenotype.volume.target_solid_nuclear; - - // trigger removal from the simulation - // pCell_to_eat->die(); // I don't think this is safe if it's in an OpenMP loop - - // flag it for removal - // pCell_to_eat->flag_for_removal(); - // mark it as dead - pCell_to_fuse->phenotype.death.dead = true; - // set secretion and uptake to zero - pCell_to_fuse->phenotype.secretion.set_all_secretion_to_zero( ); - pCell_to_fuse->phenotype.secretion.set_all_uptake_to_zero( ); - - // deactivate all custom function - pCell_to_fuse->functions.custom_cell_rule = NULL; - pCell_to_fuse->functions.update_phenotype = NULL; - pCell_to_fuse->functions.contact_function = NULL; - pCell_to_fuse->functions.cell_division_function = NULL; - pCell_to_fuse->functions.volume_update_function = NULL; + // absorb the internalized substrates + + *internalized_substrates += *(pCell_to_fuse->internalized_substrates); + static int n_substrates = internalized_substrates->size(); + pCell_to_fuse->internalized_substrates->assign( n_substrates , 0.0 ); - // remove all adhesions - // pCell_to_eat->remove_all_attached_cells(); - - // set cell as unmovable and non-secreting - pCell_to_fuse->is_movable = false; - pCell_to_fuse->is_active = false; + // set target volume(s) + + phenotype.volume.target_solid_cytoplasmic += pCell_to_fuse->phenotype.volume.target_solid_cytoplasmic; + phenotype.volume.target_solid_nuclear += pCell_to_fuse->phenotype.volume.target_solid_nuclear; + + // trigger removal from the simulation + // pCell_to_eat->die(); // I don't think this is safe if it's in an OpenMP loop + + // flag it for removal + // pCell_to_eat->flag_for_removal(); + // mark it as dead + pCell_to_fuse->phenotype.death.dead = true; + // set secretion and uptake to zero + pCell_to_fuse->phenotype.secretion.set_all_secretion_to_zero( ); + pCell_to_fuse->phenotype.secretion.set_all_uptake_to_zero( ); + + // deactivate all custom function + pCell_to_fuse->functions.custom_cell_rule = NULL; + pCell_to_fuse->functions.update_phenotype = NULL; + pCell_to_fuse->functions.contact_function = NULL; + pCell_to_fuse->functions.cell_division_function = NULL; + pCell_to_fuse->functions.cell_division_direction_function = NULL; + pCell_to_fuse->functions.volume_update_function = NULL; + + // remove all adhesions + // pCell_to_eat->remove_all_attached_cells(); + + // set cell as unmovable and non-secreting + pCell_to_fuse->is_movable = false; + pCell_to_fuse->is_active = false; + } } // things that have their own thread safety + // Teardown is deferred rather than done here. flag_for_removal() queues this + // cell, and the serial cells_ready_to_die -> die() -> delete_cell() pass does + // the same four steps off-thread. Doing them here means mutating other cells' + // lists from inside the mechanics parallel-for, where remove_all_* walks a + // vector unlocked while attach/detach_cell_as_spring edits it under the + // critical -- a real race, not a theoretical one. pCell_to_fuse->flag_for_removal(); - pCell_to_fuse->remove_all_attached_cells(); - pCell_to_fuse->remove_all_spring_attachments(); return; } @@ -1638,12 +1698,12 @@ void Cell::lyse_cell( void ) if( phenotype.volume.total < 1e-15 ) { return; } + // mark it as dead (before flagging, to match ingest_cell and fuse_cell) + phenotype.death.dead = true; + // flag for removal flag_for_removal(); // should be safe now - // mark it as dead - phenotype.death.dead = true; - // set secretion and uptake to zero phenotype.secretion.set_all_secretion_to_zero( ); phenotype.secretion.set_all_uptake_to_zero( ); @@ -1653,10 +1713,16 @@ void Cell::lyse_cell( void ) functions.update_phenotype = NULL; functions.contact_function = NULL; functions.cell_division_function = NULL; + functions.cell_division_direction_function = NULL; // remove all adhesions - remove_all_attached_cells(); + // Teardown is deferred rather than done here. flag_for_removal() queues this + // cell, and the serial cells_ready_to_die -> die() -> delete_cell() pass does + // the same four steps off-thread. Doing them here means mutating other cells' + // lists from inside the mechanics parallel-for, where remove_all_* walks a + // vector unlocked while attach/detach_cell_as_spring edits it under the + // critical -- a real race, not a theoretical one. // set volume to zero set_total_volume( 0.0 ); @@ -1750,6 +1816,13 @@ void display_ptr_as_bool( void (*ptr)(Cell*,Cell*), std::ostream& os ) os << "false"; return; } +void display_ptr_as_bool( std::vector (*ptr)(Cell*), std::ostream& os ) +{ + if( ptr ) + { os << "true"; return; } + os << "false"; + return; +} void display_cell_definitions( std::ostream& os ) { @@ -1836,6 +1909,8 @@ void display_cell_definitions( std::ostream& os ) os << std::endl; os << "\t\t cell division function: "; display_ptr_as_bool( pCF->cell_division_function , std::cout ); os << std::endl; + os << "\t\t cell division direction function: "; display_ptr_as_bool( pCF->cell_division_direction_function , std::cout ); + os << std::endl; // summarize motility @@ -3144,31 +3219,24 @@ Cell_Definition* initialize_cell_definition_from_pugixml( pugi::xml_node cd_node // If it has already be copied if (pParent != NULL && pParent->phenotype.intracellular != NULL) { - // std::cout << "------ " << __FUNCTION__ << ": copying another\n"; pCD->phenotype.intracellular->initialize_intracellular_from_pugixml(node); } // Otherwise we need to create a new one else { - std::cout << "\n------ " << __FUNCTION__ << ": creating new RoadRunnerIntracellular\n"; RoadRunnerIntracellular* pIntra = new RoadRunnerIntracellular(node); pCD->phenotype.intracellular = pIntra->getIntracellularModel(); - pCD->phenotype.intracellular->validate_PhysiCell_tokens(pCD->phenotype); - pCD->phenotype.intracellular->validate_SBML_species(); } + pCD->phenotype.intracellular->validate_PhysiCell_tokens(pCD->phenotype); + pCD->phenotype.intracellular->validate_SBML_species(); } #endif #ifdef ADDON_PHYSIDFBA if (model_type == "dfba") { - // If it has already be copied - if (pParent != NULL && pParent->phenotype.intracellular != NULL) { - pCD->phenotype.intracellular->initialize_intracellular_from_pugixml(node); - // Otherwise we need to create a new one - } else { - dFBAIntracellular* pIntra = new dFBAIntracellular(node); - pCD->phenotype.intracellular = pIntra->getIntracellularModel(); - } + // Always create a new instance per cell definition + PhysiCelldFBA::dFBAIntracellular* pIntra = new PhysiCelldFBA::dFBAIntracellular(node); + pCD->phenotype.intracellular = pIntra->getIntracellularModel(); } #endif @@ -3433,6 +3501,78 @@ void Cell::remove_all_spring_attachments( void ) return; } +void Cell::add_attacker( Cell* pAddMe ) +{ + #pragma omp critical + { + if( std::find( state.attacked_by.begin() , state.attacked_by.end() , pAddMe ) + == state.attacked_by.end() ) + { state.attacked_by.push_back( pAddMe ); } + } + return; +} + +void Cell::remove_attacker( Cell* pRemoveMe ) +{ + #pragma omp critical + { + auto search = std::find( state.attacked_by.begin() , state.attacked_by.end() , pRemoveMe ); + if( search != state.attacked_by.end() ) + { + // copy last entry over it, then shrink by one + *search = state.attacked_by.back(); + state.attacked_by.pop_back(); + } + } + return; +} + +void Cell::remove_all_attackers( void ) +{ + // Swap the list out under the same lock add_attacker / remove_attacker use, then + // walk the local copy. Holding the lock across the loop is not an option: + // detach_cells_as_spring() takes the same unnamed critical and OpenMP criticals + // are not reentrant, so that would deadlock. + std::vector attackers; + #pragma omp critical + { attackers.swap( state.attacked_by ); } + + for( int i = 0; i < attackers.size() ; i++ ) + { + Cell* pAttacker = attackers[i]; + if( pAttacker->phenotype.cell_interactions.pAttackTarget == this ) + { pAttacker->phenotype.cell_interactions.pAttackTarget = NULL; } + detach_cells_as_spring( pAttacker , this ); + } + return; +} + +bool Cell::is_attacked_by( Cell* pCell ) +{ + bool found = false; + #pragma omp critical + { + found = std::find( state.attacked_by.begin() , state.attacked_by.end() , pCell ) + != state.attacked_by.end(); + } + return found; +} + +void Cell::remove_self_from_attacked( void ) +{ + Cell* pTarget = phenotype.cell_interactions.pAttackTarget; + if( pTarget == NULL ) + { return; } + phenotype.cell_interactions.pAttackTarget = NULL; + pTarget->remove_attacker( this ); + // mutual attackers share one spring, so only drop it once both are done. + // "does pTarget attack me?" is asked of our own attacked_by, which we can lock, + // rather than by reading pTarget's pAttackTarget unsynchronised. + if( !is_attacked_by( pTarget ) ) + { detach_cells_as_spring( this , pTarget ); } + return; +} + void attach_cells( Cell* pCell_1, Cell* pCell_2 ) { @@ -3459,6 +3599,39 @@ void detach_cells_as_spring( Cell* pCell_1 , Cell* pCell_2 ) { pCell_1->detach_cell_as_spring( pCell_2 ); pCell_2->detach_cell_as_spring( pCell_1 ); + return; +} + +void begin_attack( Cell* pAttacker , Cell* pTarget ) +{ + // no self-attack, and nothing to do without both ends + if( pAttacker == NULL || pTarget == NULL || pAttacker == pTarget ) + { return; } + + // a cell attacks at most one target at a time. if it is already attacking + // something, close that attack out cleanly rather than orphaning the link. + if( pAttacker->phenotype.cell_interactions.pAttackTarget != NULL ) + { pAttacker->remove_self_from_attacked(); } + + pAttacker->phenotype.cell_interactions.pAttackTarget = pTarget; + pTarget->add_attacker( pAttacker ); + // spring-link these cells + attach_cells_as_spring( pAttacker , pTarget ); + return; +} + +void end_attack( Cell* pAttacker , Cell* pTarget ) +{ + if( pAttacker == NULL || pTarget == NULL ) + { return; } + + if( pAttacker->phenotype.cell_interactions.pAttackTarget == pTarget ) + { pAttacker->phenotype.cell_interactions.pAttackTarget = NULL; } + pTarget->remove_attacker( pAttacker ); + // two cells can attack each other, and attach_cell_as_spring dedupes, so the + // two attacks share ONE spring. Only drop it once nobody still needs it. + if( !pAttacker->is_attacked_by( pTarget ) ) + { detach_cells_as_spring( pAttacker , pTarget ); } return; } diff --git a/core/PhysiCell_cell.h b/core/PhysiCell_cell.h index 5b7c52d96..4c5e17f6e 100644 --- a/core/PhysiCell_cell.h +++ b/core/PhysiCell_cell.h @@ -145,6 +145,10 @@ class Cell_State std::vector attached_cells; std::vector spring_attachments; + // reverse of phenotype.cell_interactions.pAttackTarget. in state, not + // phenotype, so it survives convert_to_cell_definition() + std::vector attacked_by; + std::vector neighbors; std::vector orientation; @@ -239,6 +243,13 @@ class Cell : public Basic_Agent void detach_cell_as_spring( Cell* pRemoveMe ); // done void remove_all_spring_attachments( void ); // done + void add_attacker( Cell* pAddMe ); // pAddMe has started attacking me + void remove_attacker( Cell* pRemoveMe ); // pRemoveMe has stopped attacking me + // call these wherever remove_all_spring_attachments() is called + void remove_all_attackers( void ); // everyone attacking me stops + void remove_self_from_attacked( void ); // I stop attacking whomever I attack + bool is_attacked_by( Cell* pCell ); // is pCell currently attacking me? + // I want to eventually deprecate this, by ensuring that // critical BioFVM and PhysiCell data elements are synced when they are needed @@ -296,6 +307,11 @@ void detach_cells( Cell* pCell_1 , Cell* pCell_2 ); void attach_cells_as_spring( Cell* pCell_1, Cell* pCell_2 ); void detach_cells_as_spring( Cell* pCell_1 , Cell* pCell_2 ); +// maintain pAttackTarget, attacked_by, and the spring together. use these +// rather than writing pAttackTarget directly. +void begin_attack( Cell* pAttacker , Cell* pTarget ); +void end_attack( Cell* pAttacker , Cell* pTarget ); + std::vector find_nearby_cells( Cell* pCell ); // new in 1.8.0 std::vector find_nearby_interacting_cells( Cell* pCell ); // new in 1.8.0 diff --git a/core/PhysiCell_cell_container.cpp b/core/PhysiCell_cell_container.cpp index 0c5850b1d..ab6a206ec 100644 --- a/core/PhysiCell_cell_container.cpp +++ b/core/PhysiCell_cell_container.cpp @@ -124,6 +124,14 @@ void Cell_Container::update_all_cells(double t, double phenotype_dt_ , double me { // secretions and uptakes. Syncing with BioFVM is automated. + static double time_since_last_phenotype = phenotype_dt_; + static double time_since_last_mechanics = mechanics_dt_; + static double phenotype_threshold = phenotype_dt_ - 0.5 * diffusion_dt_; + static double mechanics_threshold = mechanics_dt_ - 0.5 * diffusion_dt_; + + bool time_for_phenotype = time_since_last_phenotype > phenotype_threshold; + bool time_for_mechanics = time_since_last_mechanics > mechanics_threshold; + #pragma omp parallel for for( int i=0; i < (*all_cells).size(); i++ ) { @@ -132,44 +140,12 @@ void Cell_Container::update_all_cells(double t, double phenotype_dt_ , double me (*all_cells)[i]->phenotype.secretion.advance( (*all_cells)[i], (*all_cells)[i]->phenotype , diffusion_dt_ ); } } - - //if it is the time for running cell cycle, do it! - double time_since_last_cycle= t- last_cell_cycle_time; - - static double phenotype_dt_tolerance = 0.001 * phenotype_dt_; - static double mechanics_dt_tolerance = 0.001 * mechanics_dt_; // intracellular update. called for every diffusion_dt, but actually depends on the intracellular_dt of each cell (as it can be noisy) - - #pragma omp parallel for - for( int i=0; i < (*all_cells).size(); i++ ) - { - if( (*all_cells)[i]->is_out_of_domain == false && initialzed ) { - - if( (*all_cells)[i]->phenotype.intracellular != NULL && (*all_cells)[i]->phenotype.intracellular->need_update()) - { - if ((*all_cells)[i]->functions.pre_update_intracellular != NULL) - (*all_cells)[i]->functions.pre_update_intracellular( (*all_cells)[i], (*all_cells)[i]->phenotype , diffusion_dt_ ); - - (*all_cells)[i]->phenotype.intracellular->update( (*all_cells)[i], (*all_cells)[i]->phenotype , diffusion_dt_ ); - - if ((*all_cells)[i]->functions.post_update_intracellular != NULL) - (*all_cells)[i]->functions.post_update_intracellular( (*all_cells)[i], (*all_cells)[i]->phenotype , diffusion_dt_ ); - } - } - } + update_all_cells_intracellular(); - if( time_since_last_cycle > phenotype_dt_ - 0.5 * diffusion_dt_ || !initialzed ) + if( time_for_phenotype ) { - // Reset the max_radius in each voxel. It will be filled in set_total_volume - // It might be better if we calculate it before mechanics each time - // std::fill(max_cell_interactive_distance_in_voxel.begin(), max_cell_interactive_distance_in_voxel.end(), 0.0); - - if(!initialzed) - { - time_since_last_cycle = phenotype_dt_; - } - // new as of 1.2.1 -- bundles cell phenotype parameter update, volume update, geometry update, // checking for death, and advancing the cell cycle. Not motility, though. (that's in mechanics) #pragma omp parallel for @@ -177,7 +153,7 @@ void Cell_Container::update_all_cells(double t, double phenotype_dt_ , double me { if( (*all_cells)[i]->is_out_of_domain == false ) { - (*all_cells)[i]->advance_bundled_phenotype_functions( time_since_last_cycle ); + (*all_cells)[i]->advance_bundled_phenotype_functions( time_since_last_phenotype ); } } @@ -195,19 +171,13 @@ void Cell_Container::update_all_cells(double t, double phenotype_dt_ , double me cells_ready_to_die.clear(); cells_ready_to_divide.clear(); - last_cell_cycle_time= t; + time_since_last_phenotype = 0.0; // reset and then increment below for next cycle } + + time_since_last_phenotype += diffusion_dt_; - double time_since_last_mechanics= t- last_mechanics_time; - - // if( time_since_last_mechanics>= mechanics_dt || !initialzed) - if( time_since_last_mechanics > mechanics_dt_ - 0.5 * diffusion_dt_ || !initialzed ) + if( time_for_mechanics ) { - if(!initialzed) - { - time_since_last_mechanics = mechanics_dt_; - } - // new February 2018 // if we need gradients, compute them if( default_microenvironment_options.calculate_gradients ) @@ -240,7 +210,7 @@ void Cell_Container::update_all_cells(double t, double phenotype_dt_ , double me for( int i=0; i < (*all_cells).size(); i++ ) { Cell* pC = (*all_cells)[i]; - if( pC->functions.update_velocity && pC->is_out_of_domain == false && pC->is_movable ) + if( pC->functions.update_velocity && pC->is_out_of_domain == false ) { pC->functions.update_velocity( pC,pC->phenotype,time_since_last_mechanics ); } } @@ -264,7 +234,6 @@ void Cell_Container::update_all_cells(double t, double phenotype_dt_ , double me for( int j=0; j < pC->state.spring_attachments.size(); j++ ) { Cell* pC1 = pC->state.spring_attachments[j]; - // standard_elastic_contact_function_confluent_rest_length(pC,pC->phenotype,pC1,pC1->phenotype,time_since_last_mechanics); standard_elastic_contact_function(pC,pC->phenotype,pC1,pC1->phenotype,time_since_last_mechanics); } } @@ -280,15 +249,9 @@ void Cell_Container::update_all_cells(double t, double phenotype_dt_ , double me standard_cell_cell_interactions(pC,pC->phenotype,time_since_last_mechanics); } // super-critical to performance! clear the "dummy" cells from phagocytosis / fusion - // otherwise, comptuational cost increases at polynomial rate VERY fast, as O(10,000) // dummy cells of size zero are left ot interact mechanically, etc. if( cells_ready_to_die.size() > 0 ) { - /* - std::cout << "\tClearing dummy cells from phagocytosis and fusion events ... " << std::endl; - std::cout << "\t\tClearing " << cells_ready_to_die.size() << " cells ... " << std::endl; - // there might be a lot of "dummy" cells ready for removal. Let's do it. - */ for( int i=0; i < cells_ready_to_die.size(); i++ ) { cells_ready_to_die[i]->die(); } cells_ready_to_die.clear(); @@ -305,19 +268,64 @@ void Cell_Container::update_all_cells(double t, double phenotype_dt_ , double me { pC->update_position(time_since_last_mechanics); } } - // When somebody reviews this code, let's add proper braces for clarity!!! - // Update cell indices in the container for( int i=0; i < (*all_cells).size(); i++ ) - if(!(*all_cells)[i]->is_out_of_domain && (*all_cells)[i]->is_movable) - (*all_cells)[i]->update_voxel_in_container(); - last_mechanics_time=t; + { + if (!(*all_cells)[i]->is_out_of_domain && (*all_cells)[i]->is_movable) + { (*all_cells)[i]->update_voxel_in_container(); } + } + time_since_last_mechanics = 0.0; // reset and then increment below for next cycle } - - initialzed=true; + + time_since_last_mechanics += diffusion_dt_; + + return; } +void Cell_Container::update_all_cells_intracellular( void ) +{ + bool anything_to_update = false; // Use a simple boolean for reduction + std::vector ready_to_update_intracellular = std::vector( (*all_cells).size(), false ); + + #pragma omp parallel for reduction(||:anything_to_update) + for( int i=0; i < (*all_cells).size(); i++ ) + { + if( (*all_cells)[i]->is_out_of_domain == false ) { + if( (*all_cells)[i]->phenotype.intracellular != NULL && (*all_cells)[i]->phenotype.intracellular->need_update()) + { + ready_to_update_intracellular[i] = true; + anything_to_update = true; // Set to true if any cell needs an update + if ((*all_cells)[i]->functions.pre_update_intracellular != NULL) + { + (*all_cells)[i]->functions.pre_update_intracellular((*all_cells)[i], (*all_cells)[i]->phenotype, diffusion_dt); + } + } + } + } + + if (!anything_to_update) + { return; } + + #pragma omp parallel for + for ( int i=0; i < (*all_cells).size(); i++ ) + { + if (ready_to_update_intracellular[i]) + { + (*all_cells)[i]->phenotype.intracellular->update((*all_cells)[i], (*all_cells)[i]->phenotype, diffusion_dt); + } + } + + #pragma omp parallel for + for ( int i=0; i < (*all_cells).size(); i++ ) + { + if (ready_to_update_intracellular[i] && (*all_cells)[i]->functions.post_update_intracellular != NULL) + { + (*all_cells)[i]->functions.post_update_intracellular((*all_cells)[i], (*all_cells)[i]->phenotype, diffusion_dt); + } + } +} + void Cell_Container::register_agent( Cell* agent ) { agent_grid[agent->get_current_mechanics_voxel_index()].push_back(agent); diff --git a/core/PhysiCell_cell_container.h b/core/PhysiCell_cell_container.h index f0b2cb9ee..b2c4b7f8d 100644 --- a/core/PhysiCell_cell_container.h +++ b/core/PhysiCell_cell_container.h @@ -84,7 +84,6 @@ class Cell_Container : public BioFVM::Agent_Container std::vector cells_ready_to_divide; // the index of agents ready to divide std::vector cells_ready_to_die; int boundary_condition_for_pushed_out_agents; // what to do with pushed out cells - bool initialzed = false; public: BioFVM::Cartesian_Mesh underlying_mesh; @@ -104,7 +103,9 @@ class Cell_Container : public BioFVM::Agent_Container void update_all_cells(double t); void update_all_cells(double t, double dt); void update_all_cells(double t, double phenotype_dt, double mechanics_dt); - void update_all_cells(double t, double phenotype_dt, double mechanics_dt, double diffusion_dt ); + void update_all_cells(double t, double phenotype_dt, double mechanics_dt, double diffusion_dt ); + + void update_all_cells_intracellular( void ); void register_agent( Cell* agent ); void add_agent_to_outer_voxel(Cell* agent); diff --git a/core/PhysiCell_phenotype.cpp b/core/PhysiCell_phenotype.cpp index 4d03249be..1f1b9d5f5 100644 --- a/core/PhysiCell_phenotype.cpp +++ b/core/PhysiCell_phenotype.cpp @@ -70,6 +70,7 @@ #include "../BioFVM/BioFVM.h" #include "./PhysiCell_constants.h" #include "./PhysiCell_utilities.h" +#include "./PhysiCell_cell.h" using namespace BioFVM; @@ -1151,8 +1152,9 @@ Cell_Functions::Cell_Functions() pre_update_intracellular = NULL; post_update_intracellular = NULL; + custom_optimization = NULL; - update_velocity = NULL; + update_velocity = NULL; add_cell_basement_membrane_interactions = NULL; calculate_distance_to_membrane = NULL; diff --git a/core/PhysiCell_phenotype.h b/core/PhysiCell_phenotype.h index 701d816b5..507d9458f 100644 --- a/core/PhysiCell_phenotype.h +++ b/core/PhysiCell_phenotype.h @@ -505,8 +505,9 @@ class Cell_Functions void (*pre_update_intracellular) ( Cell* pCell, Phenotype& phenotype, double dt ); void (*post_update_intracellular) ( Cell* pCell, Phenotype& phenotype, double dt ); + void (*custom_optimization) ( Cell* pCell, Phenotype& phenotype, double dt ); - void (*update_velocity)( Cell* pCell, Phenotype& phenotype, double dt ); + void (*update_velocity)( Cell* pCell, Phenotype& phenotype, double dt ); void (*add_cell_basement_membrane_interactions)(Cell* pCell, Phenotype& phenotype, double dt ); double (*calculate_distance_to_membrane)( Cell* pCell, Phenotype& phenotype, double dt ); @@ -517,6 +518,7 @@ class Cell_Functions Cell* pOther, Phenotype& other_phenotype, double dt ); void (*cell_division_function)(Cell* pCell1, Cell* pCell2 ); + std::vector (*cell_division_direction_function)(Cell* pCell1); /* prototyping / beta in 1.5.0 */ /* @@ -657,6 +659,10 @@ class Intracellular virtual ~Intracellular(){}; + // ================ specific to "dFBA" ================ + virtual double get_flux_value(std::string name) = 0; + virtual double get_growth_rate() = 0; + // ================ specific to "maboss" ================ virtual bool has_variable(std::string name) = 0; virtual bool get_boolean_variable_value(std::string name) = 0; diff --git a/core/PhysiCell_rules.cpp b/core/PhysiCell_rules.cpp index 3b73a38a3..bed76bc9c 100644 --- a/core/PhysiCell_rules.cpp +++ b/core/PhysiCell_rules.cpp @@ -250,8 +250,9 @@ void Hypothesis_Rule::English_display_HTML( std::ostream& os ) void Hypothesis_Rule::add_signal( std::string signal , double half_max , double hill_power , std::string response ) { + int signal_index = find_signal_index(signal); // check: is this a valid signal? (is it in the dictionary?) - if( find_signal_index(signal) < 0 ) + if( signal_index < 0 ) { std::cout << "Error! Attempted to add signal " << signal << " which is not in the dictionary." << std::endl; std::cout << "Either fix your model or add the missing signal to the simulation." << std::endl; @@ -261,6 +262,9 @@ void Hypothesis_Rule::add_signal( std::string signal , double half_max , double exit(-1); } + // canonicalize the name so that synonyms of the same signal are treated as one signal + signal = signal_name( signal_index ); + // check to see if the signal and response already there int n = find_signal(signal); bool bResponse = false; // true if up-regulate, false if down @@ -278,8 +282,8 @@ void Hypothesis_Rule::add_signal( std::string signal , double half_max , double exit(-1); } - // add the signal; - signals_map[signal] = signals_map.size(); + // add the signal; + signals_map[signal] = signals.size(); signals.push_back( signal ); half_maxes.push_back( half_max ); @@ -411,6 +415,11 @@ void Hypothesis_Rule::sync_to_cell_definition( std::string cell_name ) int Hypothesis_Rule::find_signal( std::string name ) { + int index = find_signal_index( name ); + if( index < 0 ) + { return -1; } + name = signal_name( index ); + auto search = signals_map.find(name); if( search == signals_map.end() ) @@ -420,7 +429,11 @@ int Hypothesis_Rule::find_signal( std::string name ) } void Hypothesis_Rule::set_half_max( std::string name , double hm ) -{ +{ + int index = find_signal_index( name ); + if( index >= 0 ) + { name = signal_name( index ); } + int n = find_signal( name ); if( n < 0 ) { return; } @@ -448,6 +461,10 @@ void Hypothesis_Rule::set_half_max( std::string name , double hm ) void Hypothesis_Rule::set_hill_power( std::string name , double hp ) { + int index = find_signal_index( name ); + if( index >= 0 ) + { name = signal_name( index ); } + int n = find_signal( name ); if( n < 0 ) { return; } @@ -474,6 +491,10 @@ void Hypothesis_Rule::set_hill_power( std::string name , double hp ) void Hypothesis_Rule::set_response( std::string name , std::string response ) { + int index = find_signal_index( name ); + if( index >= 0 ) + { name = signal_name( index ); } + int n = find_signal( name ); if( n < 0 ) { return; } @@ -757,7 +778,8 @@ void Hypothesis_Ruleset::sync_to_cell_definition( Cell_Definition* pCD ) Hypothesis_Rule* Hypothesis_Ruleset::add_behavior( std::string behavior , double min_behavior, double max_behavior ) { // check: is this a valid signal? (is it in the dictionary?) - if( find_behavior_index(behavior) < 0 ) + int behavior_index = find_behavior_index(behavior); + if( behavior_index < 0 ) { std::cout << "Warning! Attempted to add behavior " << behavior << " which is not in the dictionary." << std::endl; std::cout << "Either fix your model or add the missing behavior to the simulation." << std::endl; @@ -767,6 +789,10 @@ Hypothesis_Rule* Hypothesis_Ruleset::add_behavior( std::string behavior , double exit(-1); } + // canonicalize the name so that synonyms of the same behavior share a single rule + // (and hence a single multivariate Hill response) instead of one overwriting another + behavior = behavior_name( behavior_index ); + // first, check. Is there already a ruleset? auto search = rules_map.find( behavior ); @@ -812,6 +838,11 @@ void Hypothesis_Ruleset::sync_to_cell_definition( std::string cell_name ) Hypothesis_Rule* Hypothesis_Ruleset::find_behavior( std::string name ) { + int index = find_behavior_index( name ); + if( index < 0 ) + { return NULL; } + name = behavior_name( index ); + auto search = rules_map.find( name); if( search == rules_map.end() ) { @@ -854,7 +885,7 @@ void intialize_hypothesis_rulesets( void ) { hypothesis_rulesets.clear(); // empty(); - for( int n; n < cell_definitions_by_index.size() ; n++ ) + for( int n=0; n < cell_definitions_by_index.size() ; n++ ) { Cell_Definition* pCD = cell_definitions_by_index[n]; add_hypothesis_ruleset(pCD); @@ -903,12 +934,6 @@ void add_rule( std::string cell_type, std::string signal, std::string behavior , exit(-1); } - if( pHRS->find_behavior(behavior) ) - { - if( (*pHRS)[behavior].behavior != behavior ) - { (*pHRS)[behavior].behavior = behavior; std::cout << "wha?" << std::endl; } - } - pHRS->add_behavior(behavior); (*pHRS)[behavior].add_signal(signal,response); @@ -934,12 +959,6 @@ void add_rule( std::string cell_type, std::string signal, std::string behavior , exit(-1); } - if( pHRS->find_behavior(behavior) ) - { - if( (*pHRS)[behavior].behavior != behavior ) - { (*pHRS)[behavior].behavior = behavior; std::cout << "wha?" << std::endl; } - } - pHRS->add_behavior(behavior); (*pHRS)[behavior].add_signal(signal,response); @@ -1535,8 +1554,9 @@ void parse_csv_rules_v0( std::string filename ) while( fs.eof() == false ) { - std::string line; - std::getline( fs , line, '\n'); + std::string line; + std::getline( fs , line, '\n'); + trim_cr(line); if( line.size() > 0 ) { parse_csv_rule_v0(line); } } @@ -1671,8 +1691,9 @@ void parse_csv_rules_v1( std::string filename ) while( fs.eof() == false ) { - std::string line; - std::getline( fs , line, '\n'); + std::string line; + std::getline( fs , line, '\n'); + trim_cr(line); if( line.size() > 0 ) { parse_csv_rule_v1(line); } } @@ -1804,8 +1825,9 @@ void parse_csv_rules_v3( std::string filename ) while( fs.eof() == false ) { - std::string line; - std::getline( fs , line, '\n'); + std::string line; + std::getline( fs , line, '\n'); + trim_cr(line); if( line.size() > 0 ) { parse_csv_rule_v3(line); } } @@ -2321,28 +2343,19 @@ std::vector UniformInAnnulus( double r1, double r2 ) return {x,y,0.0}; } -std::vector UniformInShell( double r1, double r2 ) +std::vector UniformInShell(double r1, double r2) { - static double two_pi = 6.283185307179586; + double r1_3 = r1 * r1 * r1; + double r2_3 = r2 * r2 * r2; + + double xi = UniformRandom(); + double r = pow(r1_3 + xi * (r2_3 - r1_3), 1.0 / 3.0); - double T = UniformRandom(); - double sqrt_T = sqrt(T); - double sqrt_one_minus_T = 1.0; - sqrt_one_minus_T -= T; - sqrt_one_minus_T = sqrt( sqrt_one_minus_T ); + std::vector dir = UniformOnUnitSphere(); + for (int i = 0; i < 3; ++i) + dir[i] *= r; - double param1 = pow( UniformRandom() , 0.33333333333333333333333333333333333333 ); // xi^(1/3), - // param1 *= (r2-r1); - // param1 += r1; - double param2 = param1; // xi^(1/3) - param2 *= 2.0; // 2 * xi^(1/3) - param2 *= sqrt_T; // 2 * xi(1) * T^(1/2) - param2 *= sqrt_one_minus_T; // 2 * xi(1) * T^(1/2) * (1-T)^(1/2) - - double theta = UniformRandom(); // U(0,1) - theta *= two_pi; // U(0,2*pi) - - return { param2*sin(theta) , param2*cos(theta), param1*(1-2*T) }; + return dir; } void setup_cell_rules( void ) diff --git a/core/PhysiCell_signal_behavior.cpp b/core/PhysiCell_signal_behavior.cpp index 8780e8d7c..258151280 100644 --- a/core/PhysiCell_signal_behavior.cpp +++ b/core/PhysiCell_signal_behavior.cpp @@ -79,6 +79,8 @@ std::map behavior_to_int; std::map int_to_signal; std::map int_to_behavior; +int contact_with_built_in = 5; // live, dead, apoptotic, necrotic, other + void setup_signal_behavior_dictionaries( void ) { extern std::unordered_map cell_definition_indices_by_name; @@ -484,7 +486,7 @@ void setup_signal_behavior_dictionaries( void ) behavior_to_int[temp] = map_index; // synonym - temp = "phagocytosis of " + std::to_string(pCD->type); + temp = "phagocytosis of " + pCD->name; behavior_to_int[temp] = map_index; } @@ -725,10 +727,8 @@ int find_signal_index( std::string signal_name ) auto search = signal_to_int.find( signal_name ); // safety first! if( search != signal_to_int.end() ) - { return search->second; } - - std::cout << "having trouble finding " << signal_name << std::endl; - + { return search->second; } + return -1; } @@ -804,7 +804,7 @@ std::vector get_signals( Cell* pCell ) signals[volume_ind] = pCell->phenotype.volume.total; // physical contact with cells (of each type) - // increment signals + // increment signals int dead_cells = 0; int apop_cells = 0; int necro_cells = 0; @@ -964,7 +964,7 @@ std::vector get_cell_contact_signals( Cell* pCell ) // rescale std::string search_for = "contact with " + cell_definitions_by_type[0]->name; static int scaling_start_index = find_signal_index( search_for ); - for( int i=0; i < n+2 ; i++ ) + for( int i=0; i < n+contact_with_built_in; i++ ) { output[i] /= signal_scales[scaling_start_index+i]; } return output; @@ -986,7 +986,7 @@ std::vector get_selected_signals( Cell* pCell , std::vector indices for( int i=0; i < indices.size() ; i++ ) { int ind = indices[i]; - if( ind >= contact_start_index && ind < contact_start_index+n+2) + if( ind >= contact_start_index && ind < contact_start_index+n+contact_with_built_in ) { signals[i] = contact_signals[ind-contact_start_index]; } else { signals[i] = get_single_signal( pCell , ind ); } @@ -1065,7 +1065,7 @@ double get_single_signal( Cell* pCell, int index ) // physical contact with cells (of each type) // individual contact signals are a bit costly static int contact_ind = find_signal_index( "contact with " + cell_definitions_by_type[0]->name ); - if( contact_ind <= index && index < contact_ind + n+2 ) + if( contact_ind <= index && index < contact_ind+n+contact_with_built_in ) { std::vector counts( n , 0 ); // process all neighbors diff --git a/core/PhysiCell_standard_models.cpp b/core/PhysiCell_standard_models.cpp index 48a70ccb8..166e229d5 100644 --- a/core/PhysiCell_standard_models.cpp +++ b/core/PhysiCell_standard_models.cpp @@ -691,7 +691,6 @@ void standard_domain_edge_avoidance_interactions( Cell* pCell, Phenotype& phenot { pCell->functions.calculate_distance_to_membrane = distance_to_domain_edge; } phenotype.mechanics.cell_BM_repulsion_strength = 100; - double max_interactive_distance = phenotype.mechanics.relative_maximum_adhesion_distance * phenotype.geometry.radius; double distance = pCell->functions.calculate_distance_to_membrane(pCell,phenotype,dt); //Note that the distance_to_membrane function must set displacement values (as a normal vector) @@ -1097,64 +1096,8 @@ double distance_to_domain_edge(Cell* pCell, Phenotype& phenotype, double dummy) min_distance = temp_distance; nearest_boundary = 5; } - - // check for 3D exceptions - - // lines - if( fabs( (pCell->position[0]) - (pCell->position[1]) ) < tolerance && - fabs( (pCell->position[1]) - (pCell->position[2]) ) < tolerance && - fabs( (pCell->position[0]) - (pCell->position[2]) ) < tolerance ) - { - if( pCell->position[0] > 0 ) - { - if( pCell->position[0] > 0 && pCell->position[1] > 0 ) - { pCell->displacement = { -one_over_sqrt_3 , -one_over_sqrt_3 , -one_over_sqrt_3 }; } - if( pCell->position[0] < 0 && pCell->position[1] > 0 ) - { pCell->displacement = { one_over_sqrt_3 , -one_over_sqrt_3 , -one_over_sqrt_3 }; } - - if( pCell->position[0] > 0 && pCell->position[1] < 0 ) - { pCell->displacement = { -one_over_sqrt_3 , one_over_sqrt_3 , -one_over_sqrt_3 }; } - if( pCell->position[0] < 0 && pCell->position[1] < 0 ) - { pCell->displacement = { one_over_sqrt_3 , one_over_sqrt_3 , -one_over_sqrt_3 }; } - } - else - { - if( pCell->position[0] > 0 && pCell->position[1] > 0 ) - { pCell->displacement = { -one_over_sqrt_3 , -one_over_sqrt_3 , one_over_sqrt_3 }; } - if( pCell->position[0] < 0 && pCell->position[1] > 0 ) - { pCell->displacement = { one_over_sqrt_3 , -one_over_sqrt_3 , one_over_sqrt_3 }; } - - if( pCell->position[0] > 0 && pCell->position[1] < 0 ) - { pCell->displacement = { -one_over_sqrt_3 , one_over_sqrt_3 , one_over_sqrt_3 }; } - if( pCell->position[0] < 0 && pCell->position[1] < 0 ) - { pCell->displacement = { one_over_sqrt_3 , one_over_sqrt_3 , one_over_sqrt_3 }; } - } - return min_distance; - } - - // planes - let's not worry for today - - } - else - { - // check for 2D exceptions - - if( fabs( (pCell->position[0]) - (pCell->position[1]) ) < tolerance ) - { - if( pCell->position[0] > 0 && pCell->position[1] > 0 ) - { pCell->displacement = { -one_over_sqrt_2 , -one_over_sqrt_2 , 0 }; } - if( pCell->position[0] < 0 && pCell->position[1] > 0 ) - { pCell->displacement = { one_over_sqrt_2 , -one_over_sqrt_2 , 0 }; } - - if( pCell->position[0] > 0 && pCell->position[1] < 0 ) - { pCell->displacement = { -one_over_sqrt_2 , one_over_sqrt_2 , 0 }; } - if( pCell->position[0] < 0 && pCell->position[1] < 0 ) - { pCell->displacement = { one_over_sqrt_2 , one_over_sqrt_2 , 0 }; } - return min_distance; - } } - // no exceptions switch(nearest_boundary) { case 0: @@ -1266,7 +1209,9 @@ void standard_cell_cell_interactions( Cell* pCell, Phenotype& phenotype, double { if( UniformRandom() < probability ) { - pCell->phenotype.cell_interactions.pAttackTarget = pTarget; + // sets pAttackTarget, registers pCell in pTarget's + // attacked_by, and spring-links the two + begin_attack( pCell , pTarget ); attacked = true; /* std::cout << "********* ********* ******** start atack **** " << PhysiCell_globals.current_time << std::endl; @@ -1274,8 +1219,6 @@ void standard_cell_cell_interactions( Cell* pCell, Phenotype& phenotype, double << "attack duration: " << pCell->phenotype.cell_interactions.attack_duration << " " << "attack damage rate: " << pCell->phenotype.cell_interactions.attack_damage_rate << std::endl; */ - // spring-link these cells - attach_cells_as_spring(pCell,pTarget); } } @@ -1317,11 +1260,14 @@ void standard_cell_cell_interactions( Cell* pCell, Phenotype& phenotype, double // move effector attack here. - if( pCell->phenotype.cell_interactions.pAttackTarget != NULL ) + // read the target once. attack_cell() dereferences without a null check, so + // testing this field and then reloading it would let any concurrent clear + // hand it a NULL. One load is free and does not depend on which teardown + // paths currently run inside this loop. + Cell* pAttackTarget = pCell->phenotype.cell_interactions.pAttackTarget; + if( pAttackTarget != NULL ) { - Cell* pTarget = pCell->phenotype.cell_interactions.pAttackTarget; - - pCell->attack_cell(pTarget,dt); + pCell->attack_cell(pAttackTarget,dt); attacked = true; // attacked at least one cell in this time step // attack_cell @@ -1331,20 +1277,18 @@ void standard_cell_cell_interactions( Cell* pCell, Phenotype& phenotype, double probability = dt / (1e-15 + pCell->phenotype.cell_interactions.attack_duration); - if( UniformRandom() < probability || pTarget->phenotype.death.dead ) + if( UniformRandom() < probability || pAttackTarget->phenotype.death.dead ) { /* std::cout << "********* ********* ******** attack done **** " << PhysiCell_globals.current_time << " " << probability << " " - << "attack time: " << pTarget->state.total_attack_time << " " - << "damage: " << pTarget->phenotype.cell_integrity.damage << " " - << "dead? " << (int) pTarget->phenotype.death.dead << " " + << "attack time: " << pAttackTarget->state.total_attack_time << " " + << "damage: " << pAttackTarget->phenotype.cell_integrity.damage << " " + << "dead? " << (int) pAttackTarget->phenotype.death.dead << " " << "damage delivered: " << pCell->phenotype.cell_interactions.total_damage_delivered << std::endl; */ - detach_cells_as_spring(pCell,pTarget); - - pCell->phenotype.cell_interactions.pAttackTarget = NULL; + end_attack( pCell , pAttackTarget ); } } @@ -1382,7 +1326,8 @@ void standard_asymmetric_division_function( Cell* pCell_parent, Cell* pCell_daug double sym_div_prob = pCell_parent->phenotype.cycle.asymmetric_division.asymmetric_division_probabilities[pCell_parent->type] + 1.0 - total; if (sym_div_prob < 0.0) { - throw std::runtime_error("Error: Asymmetric division probabilities for " + pCD_parent->name + " sum to greater than 1.0 and cannot be normalized."); + std::cerr << "Error: Asymmetric division probabilities for " + pCD_parent->name + " sum to greater than 1.0 and cannot be normalized." << std::endl; + exit(-1); } pCell_parent->phenotype.cycle.asymmetric_division.asymmetric_division_probabilities[pCell_parent->type] = sym_div_prob; pCell_daughter->phenotype.cycle.asymmetric_division.asymmetric_division_probabilities[pCell_daughter->type] = sym_div_prob; @@ -1423,7 +1368,8 @@ void dynamic_attachments( Cell* pCell , Phenotype& phenotype, double dt ) { // check for detachments double detachment_probability = phenotype.mechanics.detachment_rate * dt; - for( int j=0; j < pCell->state.attached_cells.size(); j++ ) + // detach_cells swaps the detached cell with the last cell in the vector, so we need to iterate backwards + for( int j=pCell->state.attached_cells.size()-1; j >= 0; j-- ) { Cell* pTest = pCell->state.attached_cells[j]; if( UniformRandom() <= detachment_probability ) @@ -1441,8 +1387,6 @@ void dynamic_attachments( Cell* pCell , Phenotype& phenotype, double dt ) while( done == false && j < pCell->state.neighbors.size() ) { Cell* pTest = pCell->state.neighbors[j]; - if (phenotype.cell_interactions.pAttackTarget==pTest || pTest->phenotype.cell_interactions.pAttackTarget==pCell) // do not let attackers detach randomly - { continue; } if( pTest->state.number_of_attached_cells() < pTest->phenotype.mechanics.maximum_number_of_attachments ) { // std::string search_string = "adhesive affinity to " + pTest->type_name; @@ -1467,9 +1411,27 @@ void dynamic_spring_attachments( Cell* pCell , Phenotype& phenotype, double dt ) { // check for detachments double detachment_probability = phenotype.mechanics.detachment_rate * dt; - for( int j=0; j < pCell->state.spring_attachments.size(); j++ ) + + // This runs inside the mechanics parallel-for, and this loop is not the only + // thing touching pCell's spring list: another thread running this same function + // for a neighbor calls attach_cells_as_spring(), which reaches into THIS cell's + // spring_attachments and push_back()s. Those writers serialize on the unnamed + // critical in Cell::attach_cell_as_spring() / detach_cell_as_spring(); reading + // here without it means a concurrent push_back that reallocates leaves the loop + // indexing a freed buffer and dereferencing garbage. Copy under the same lock + // and walk the copy. + // The lock is not held across the loop body: detach_cells_as_spring() takes the + // same unnamed critical, and OpenMP criticals are not reentrant. + std::vector spring_attachments_snapshot; + #pragma omp critical + { spring_attachments_snapshot = pCell->state.spring_attachments; } + + // detach_cells_as_spring swaps the detached cell with the last cell in the vector, so we need to iterate backwards + for( int j=spring_attachments_snapshot.size()-1; j >= 0; j-- ) { - Cell* pTest = pCell->state.spring_attachments[j]; + Cell* pTest = spring_attachments_snapshot[j]; + if (phenotype.cell_interactions.pAttackTarget==pTest || pTest->phenotype.cell_interactions.pAttackTarget==pCell) // do not let attackers detach randomly + { continue; } if( UniformRandom() <= detachment_probability ) { detach_cells_as_spring( pCell , pTest ); } } diff --git a/core/PhysiCell_utilities.cpp b/core/PhysiCell_utilities.cpp index 91f0cd582..457eb36c7 100644 --- a/core/PhysiCell_utilities.cpp +++ b/core/PhysiCell_utilities.cpp @@ -370,7 +370,7 @@ void copy_file_to_output(std::string filename) std::cout << "Copying " << filename << " to output folder." << std::endl; // copy the file to the output folder std::string basename = filename; - size_t found = basename.find_last_of("/"); // find the end of the path + size_t found = basename.find_last_of("/\\"); if (found != std::string::npos) { basename = basename.substr(found + 1); diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 000000000..45510581f --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,9 @@ +set(ALL_EXAMPLES cell_cycle DCIS HDS mechanics_1 mechanics_2 volume) + +foreach(example ${ALL_EXAMPLES}) + add_executable(${example}) + target_sources(${example} PRIVATE PhysiCell_test_${example}.cpp) + + target_link_libraries(${example} PRIVATE BioFVM PhysiCell-core + PhysiCell-modules) +endforeach() diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt new file mode 100644 index 000000000..337b8975e --- /dev/null +++ b/modules/CMakeLists.txt @@ -0,0 +1,32 @@ +# Here we are creating two libraries: +# 1) PhysiCell-modules: includes all modules functionality +# 2) PhysiCell-modules-no-addon: includes all modules functionality, excluding the multicellds class +# The latter is necessary for projects that define ADDON_* macro for the specific intracellular support + +add_library(PhysiCell-modules OBJECT) +add_library(PhysiCell-modules-no-addon OBJECT) + +target_sources( + PhysiCell-modules + PRIVATE PhysiCell_geometry.cpp + PhysiCell_MultiCellDS.cpp + PhysiCell_pathology.cpp + PhysiCell_POV.cpp + PhysiCell_pugixml.cpp + PhysiCell_settings.cpp + PhysiCell_SVG.cpp + PhysiCell_various_outputs.cpp) + +target_sources( + PhysiCell-modules-no-addon + PRIVATE PhysiCell_geometry.cpp + PhysiCell_pathology.cpp + PhysiCell_POV.cpp + PhysiCell_pugixml.cpp + PhysiCell_settings.cpp + PhysiCell_SVG.cpp + PhysiCell_various_outputs.cpp) + +find_package(OpenMP REQUIRED) +target_link_libraries(PhysiCell-modules PUBLIC OpenMP::OpenMP_CXX) +target_link_libraries(PhysiCell-modules-no-addon PUBLIC OpenMP::OpenMP_CXX) diff --git a/modules/PhysiCell_MultiCellDS.cpp b/modules/PhysiCell_MultiCellDS.cpp index 92b06743e..6ac94df5f 100644 --- a/modules/PhysiCell_MultiCellDS.cpp +++ b/modules/PhysiCell_MultiCellDS.cpp @@ -1294,4 +1294,926 @@ void write_spring_attached_cells_graph( std::string filename ) return; } + +int resume_from_MultiCellDS(std::string folder_path, std::string xml_filename, bool create_cells, bool debug_print) +{ + pugi::xpath_node xpath_node; + pugi::xml_node node; + + std::string xml_filepath = folder_path + "/" + xml_filename; + std::cout << xml_filepath << std::endl; + std::cout << __FUNCTION__ << " xml_filepath= " << xml_filepath << std::endl; + + pugi::xml_document doc; + if (!doc.load_file( xml_filepath.c_str() ) ) + { + std::cout << "Invalid file: " << xml_filepath << std::endl; + return -1; + } + // std::cout << "-- successfully read" << std::endl; + + xpath_node = doc.select_node("//metadata//current_time"); + node = xpath_node.node(); + if (node) + { + std::cout << "\n Success reading current_time\n" << std::endl; + PhysiCell_globals.current_time = xml_get_my_double_value(node); + std::cout << "--- resetting PhysiCell_globals.current_time " << PhysiCell_globals.current_time << std::endl; + } + else + { + std::cout << " --- Error reading metadata//current_time\n" << std::endl; + return -1; + } + + pugi::xml_node microenv_data = doc.child("MultiCellDS").child("microenvironment").child("domain").child("data"); + if (!microenv_data) + { + std::cout << "Error parsing microenv data: " << std::endl; + return -1; + } + + xpath_node = doc.select_node("//microenvironment//domain//data[@type='matlab']"); + node = xpath_node.node(); + if (!node) + { + std::cout << "\n --- Error parsing microenv matlab data\n" << std::endl; + return -1; + } + std::string matlab_name = node.child_value("filename"); + std::string microenv_mat_filename = folder_path + "/" + matlab_name; + // std::cout << "\n--- calling read_microenvironment_from_matlab" << std::endl; + bool read_microenv_flag = read_microenvironment_from_matlab( microenv_mat_filename ); + if (read_microenv_flag ) + { + std::cout << "\n Success!\n" << std::endl; + } + else + { + std::cout << " Error reading microenv matlab data " << std::endl; + return -1; + } + + // read/skip over all labels until the last one, damage_repair_rate, so we can parse custom data vars + xpath_node = doc.select_node("//cellular_information//cell_populations//custom//simplified_data//labels//label[@index=0]"); + node = xpath_node.node(); + if (!node) + { + std::cout << " --- Error: could not find