diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 000000000..fd43e5e9c --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1 @@ +indent_submodule = true diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 2afd0558c..8765dd6e8 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -37,8 +37,7 @@ jobs: - name: "Run CompatHelper" run: | import CompatHelper - CompatHelper.main(master_branch="develop") + CompatHelper.main() shell: julia --color=yes {0} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 703836343..96c189425 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,10 @@ name: CI + on: push: branches: - master - - develop + pull_request: jobs: test: @@ -13,8 +14,8 @@ jobs: fail-fast: false matrix: version: - - '1.6' # Oldest supported version for COBREXA.jl - '1' # This is always the latest stable release in the 1.X series + - '1.6' # LTS #- 'nightly' os: - ubuntu-latest @@ -23,21 +24,12 @@ jobs: arch: - x64 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- + - uses: julia-actions/cache@v1 - uses: julia-actions/julia-buildpkg@latest - run: | git config --global user.name Tester @@ -45,6 +37,6 @@ jobs: - uses: julia-actions/julia-runtest@latest continue-on-error: ${{ matrix.version == 'nightly' }} - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: file: lcov.info diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..4b533eb24 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,26 @@ + +# ref: https://juliadocs.github.io/Documenter.jl/stable/man/hosting/#GitHub-Actions-1 +name: Documentation + +on: + pull_request: + push: + branches: + - master + tags: '*' + release: + types: [published, created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@latest + - uses: julia-actions/cache@v1 + - name: Install dependencies + run: julia --color=yes --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key + run: julia --color=yes --project=docs/ docs/make.jl diff --git a/.github/workflows/pr-format.yml b/.github/workflows/pr-format.yml index 98b3bbbeb..3ff70d143 100644 --- a/.github/workflows/pr-format.yml +++ b/.github/workflows/pr-format.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone the repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout the pull request code # this checks out the actual branch so that one can commit into it if: github.event_name == 'issue_comment' env: diff --git a/.gitignore b/.gitignore index 076160902..e9648181c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,46 +1,27 @@ -#ignore data files -test/downloaded/ -test/tmpfiles/ -test/data -.DS_Store -# ignore VScode clutter +# ignore various editor clutter +.DS_Store /.vscode /vscode *.code-workspace +.*.swp -# Build artifacts for creating documentation generated by the Documenter package +# ignore autogenerated docs docs/build/ -docs/site/ # ignore file types -*.mat -*.xml -*.json -*.h5 *.jl.*.cov -.*.swp -# File generated by Pkg, the package manager, based on a corresponding Project.toml -# It records a fixed state of all packages used by the project. As such, it should not be -# committed for packages, but should be committed for applications that require a static -# environment. +# Pkg.jl stuff Manifest.toml -# Ignore temporary files for testing functions -temp.* - -# Ignore jupyter notebook stuff +# Ignore any jupyter notebook stuff .ipynb_checkpoints -# add generated tutorial specifics -docs/src/examples/* -!docs/src/examples/*.jl - -# add generated docs and tutorial specifics -docs/src/index.md -docs/src/howToContribute.md -docs/src/assets/output.gif - # ignore container files *.sif + +# ignore models +*.xml +*.json +*.mat diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 2199e9255..000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,324 +0,0 @@ - -stages: - - test # this checks the viability of the code - - assets # this builds assets to be included in documentation and distribution binaries - - documentation # this processes the documentation - - test-compat # this runs many additional compatibility tests - -variables: - GIT_STRATEGY: clone - DOCKER_DRIVER: overlay2 - DOCKER_TLS_CERTDIR: "" - APPTAINER_DOCKER_TAG: "v3.9.4" - DOCKER_HUB_TAG: "lcsbbiocore/cobrexa.jl" - DOCKER_GHCR_TAG: "ghcr.io/lcsb-biocore/docker/cobrexa.jl" - APPTAINER_GHCR_TAG: "lcsb-biocore/apptainer/cobrexa.jl" - -# -# Predefined conditions for triggering jobs -# - -.global_trigger_pull_request: &global_trigger_pull_request - rules: - - if: $CI_COMMIT_BRANCH == "develop" - when: never - - if: $CI_COMMIT_BRANCH == "master" - when: never - - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" - -.global_trigger_build_doc: &global_trigger_build_doc - rules: - - if: $CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME == "master" && $CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME == "develop" - - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" - when: never - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "master" - - if: $CI_COMMIT_TAG =~ /^v/ - -.global_trigger_full_tests: &global_trigger_full_tests - rules: - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "master" - - if: $CI_COMMIT_TAG =~ /^v/ - -.global_trigger_compat_tests: &global_trigger_compat_tests - rules: - - if: $CI_COMMIT_BRANCH == "master" - - if: $CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME == "master" - -.global_trigger_test_containers: &global_trigger_test_containers - rules: - - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" - when: never - - if: $CI_COMMIT_BRANCH == "develop" - -.global_trigger_release_containers: &global_trigger_release_containers - rules: - - if: $CI_COMMIT_TAG =~ /^v/ - -# -# Test environment & platform settings -# - -.global_dind: &global_dind - image: docker:20.10.12 - tags: - - privileged - services: - - name: docker:20.10.12-dind - command: ["--tls=false", "--mtu=1458", "--registry-mirror", "https://docker-registry.lcsb.uni.lu"] - before_script: - - docker login -u $CI_USER_NAME -p $GITLAB_ACCESS_TOKEN $CI_REGISTRY - -.global_julia18: &global_julia18 - variables: - JULIA_VER: "v1.8.3" - -.global_julia16: &global_julia16 - variables: - JULIA_VER: "v1.6.0" - -.global_env_linux: &global_env_linux - script: - - $ARTENOLIS_SOFT_PATH/julia/$JULIA_VER/bin/julia --inline=yes --check-bounds=yes --color=yes --project=@. -e 'import Pkg; Pkg.test(; coverage = true)' - -.global_env_win: &global_env_win - script: - - $global:LASTEXITCODE = 0 # Note the global prefix. - - Invoke-Expression $Env:ARTENOLIS_SOFT_PATH"\julia\"$Env:JULIA_VER"\bin\julia --inline=yes --check-bounds=yes --color=yes --project=@. -e 'import Pkg; Pkg.test(; coverage = true)'" - - exit $LASTEXITCODE - -.global_env_win8: &global_env_win8 - tags: - - windows8 - <<: *global_env_win - -.global_env_win10: &global_env_win10 - tags: - - windows10 - <<: *global_env_win - -.global_env_mac: &global_env_mac - tags: - - mac - script: - - $ARTENOLIS_SOFT_PATH_MAC/julia/$JULIA_VER/Contents/Resources/julia/bin/julia --inline=yes --check-bounds=yes --color=yes --project=@. -e 'import Pkg; Pkg.test(; coverage = true)' - -.global_build_apptainer: &global_build_apptainer - image: - name: "quay.io/singularity/singularity:$APPTAINER_DOCKER_TAG" - # the image entrypoint is the singularity binary by default - entrypoint: ["/bin/sh", "-c"] - tags: - - privileged - -# -# TESTS -# -# The "basic" required test that gets triggered for the basic testing, runs in -# any available docker and current julia -# - -docker:julia1.8: - stage: test - image: $CI_REGISTRY/r3/docker/julia-custom - script: - - julia --check-bounds=yes --inline=yes --project=@. -e "import Pkg; Pkg.test(; coverage = true)" - after_script: - - julia --project=test/coverage test/coverage/coverage-summary.jl - <<: *global_trigger_pull_request - -# -# The required compatibility test to pass on branches&tags before the docs get -# built & deployed -# - -linux:julia1.8: - stage: test - tags: - - slave01 - <<: *global_trigger_full_tests - <<: *global_julia18 - <<: *global_env_linux - -linux:julia1.6: - stage: test - tags: - - slave02 - <<: *global_trigger_full_tests - <<: *global_julia16 - <<: *global_env_linux - -# -# Additional platform&environment compatibility tests -# - -windows8:julia1.8: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia18 - <<: *global_env_win8 - -windows10:julia1.8: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia18 - <<: *global_env_win10 - -mac:julia1.8: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia18 - <<: *global_env_mac - -windows8:julia1.6: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia16 - <<: *global_env_win8 - -windows10:julia1.6: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia16 - <<: *global_env_win10 - -mac:julia1.6: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia16 - <<: *global_env_mac - -# -# ASSETS -# -# This builds the development history gif using gource, and some containers. -# - -gource: - stage: assets - needs: [] # allow faster start - script: - - docker run -v "$PWD":/visualization $CI_REGISTRY/r3/docker/gource - artifacts: - paths: ['output.gif'] - expire_in: 1 year - <<: *global_trigger_build_doc - <<: *global_dind - -apptainer-test: - stage: assets - script: | - alias apptainer=singularity - apptainer build cobrexa-test.sif cobrexa.def - <<: *global_build_apptainer - <<: *global_trigger_test_containers - -apptainer-release: - stage: assets - script: - - | - # build the container - alias apptainer=singularity - apptainer build cobrexa.sif cobrexa.def - - | - # push to GHCR - alias apptainer=singularity - export SINGULARITY_DOCKER_USERNAME="$GITHUB_ACCESS_USERNAME" - export SINGULARITY_DOCKER_PASSWORD="$GITHUB_ACCESS_TOKEN" - apptainer push cobrexa.sif "oras://ghcr.io/$APPTAINER_GHCR_TAG:latest" - apptainer push cobrexa.sif "oras://ghcr.io/$APPTAINER_GHCR_TAG:$CI_COMMIT_TAG" - <<: *global_build_apptainer - <<: *global_trigger_release_containers - -docker-test: - stage: assets - script: - - docker build -t "$DOCKER_HUB_TAG:testing" . - <<: *global_dind - <<: *global_trigger_test_containers - -docker-release: - stage: assets - script: - - docker build -t "$DOCKER_HUB_TAG:latest" . - # alias and push to docker hub - - docker tag "$DOCKER_HUB_TAG:latest" "$DOCKER_HUB_TAG:$CI_COMMIT_TAG" - - echo "$DOCKER_IO_ACCESS_TOKEN" | docker login --username "$DOCKER_IO_USER" --password-stdin - - docker push "$DOCKER_HUB_TAG:latest" - - docker push "$DOCKER_HUB_TAG:$CI_COMMIT_TAG" - # make 2 extra aliases and push to GHCR - - docker tag "$DOCKER_HUB_TAG:latest" "$DOCKER_GHCR_TAG:latest" - - docker tag "$DOCKER_HUB_TAG:latest" "$DOCKER_GHCR_TAG:$CI_COMMIT_TAG" - - echo "$GITHUB_ACCESS_TOKEN" | docker login ghcr.io --username "$GITHUB_ACCESS_USERNAME" --password-stdin - - docker push "$DOCKER_GHCR_TAG:latest" - - docker push "$DOCKER_GHCR_TAG:$CI_COMMIT_TAG" - <<: *global_dind - <<: *global_trigger_release_containers - -# -# DOCUMENTATION TESTS -# -# In pull requests, triggered after the tests succeed to avoid unnecessary -# double failure. In normal branch testing, these get triggered with normal -# tests (the error should be visible ASAP). We avoid a separate stage to keep -# the pipeline parallelizable. -# - -.global_doctests: &global_doctests - image: $CI_REGISTRY/r3/docker/julia-custom - script: - - julia --project=@. -e 'import Pkg; Pkg.instantiate();' - - julia --project=@. --color=yes test/doctests.jl - -doc-tests-pr:julia1.8: - stage: documentation - <<: *global_doctests - <<: *global_trigger_pull_request - -doc-tests:julia1.8: - stage: test - <<: *global_doctests - <<: *global_trigger_full_tests - -# -# DOCUMENTATION -# - -pages: - stage: documentation - dependencies: - - gource - # Note: This dependency is also implied by the stage ordering, but let's - # be sure. As of Nov 2021, the assets are not used directly, but referred - # to externally from the docs. - image: $CI_REGISTRY/r3/docker/julia-custom - script: - # resolve and build packages from the docs/Project.toml file - - julia --project=docs -e 'using Pkg; Pkg.resolve(); Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate();' - - # build and deploy docs (this doesn't upload the gource animation asset) - - julia --project=docs --color=yes docs/make.jl - - # move to the directory to be picked up by Gitlab pages (with assets) - - mv docs/build public - artifacts: - paths: - - public - <<: *global_trigger_build_doc - -# -# EXTERNAL REPOSITORIES -# -# This trigger the test pipeline in external repo as defined by gitlab -# variables. -# - -trigger: - stage: test-compat - image: curlimages/curl - tags: - - privileged - script: - - curl --silent --output /dev/null -X POST -F token=$EXTERNAL_REPO_TOKEN -F ref=$EXTERNAL_REPO_BRANCH $EXTERNAL_REPO - <<: *global_trigger_full_tests diff --git a/Project.toml b/Project.toml index 3d2d1f1d5..c9997d9ba 100644 --- a/Project.toml +++ b/Project.toml @@ -1,40 +1,37 @@ name = "COBREXA" uuid = "babc4406-5200-4a30-9033-bf5ae714c842" authors = ["The developers of COBREXA.jl"] -version = "1.4.3" +version = "2.0.0" [deps] +AbstractFBCModels = "5a4f3dfa-1789-40f8-8221-69268c29937c" +ConstraintTrees = "5515826b-29c3-47a5-8849-8513ac836620" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" DistributedData = "f6a0035f-c5ac-4ad0-b410-ad102ced35df" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -MAT = "23992714-dd62-5051-b70f-ba57cb901cac" -MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -SBML = "e5567a89-2604-4b09-9718-f5f78e97c3bb" -Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] -Clarabel = "0.3" -DistributedData = "0.1.4, 0.2" +AbstractFBCModels = "0.2.2" +Aqua = "0.7" +Clarabel = "0.6" +ConstraintTrees = "0.9.4" +DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" -HDF5 = "0.16" -JSON = "0.21" +Downloads = "1" +GLPK = "1" +JSONFBCModels = "0.1" JuMP = "1" -MAT = "0.10" -MacroTools = "0.5.6" -OrderedCollections = "1.4" -SBML = "~1.3" +SBMLFBCModels = "0.1" +SHA = "0.7, 1" StableRNGs = "1.0" -Tulip = "0.7.0, 0.8.0, 0.9.2" +Test = "1" +Tulip = "0.9" julia = "1.5" [extras] @@ -42,9 +39,11 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" +JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" +SBMLFBCModels = "3e8f9d1a-ffc1-486d-82d6-6c7276635980" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" [targets] -test = ["Aqua", "Clarabel", "Downloads", "GLPK", "SHA", "Test", "Tulip"] +test = ["Aqua", "Clarabel", "Downloads", "GLPK", "JSONFBCModels", "SBMLFBCModels", "SHA", "Test", "Tulip"] diff --git a/README.md b/README.md index 47237b005..b9fd793f1 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml" model = load_model("e_coli_core.xml") # run a FBA -fluxes = flux_balance_analysis_dict(model, Tulip.Optimizer) +fluxes = flux_balance_dict(model, Tulip.Optimizer) ``` The variable `fluxes` will now contain a dictionary of the computed optimal diff --git a/docs/Project.toml b/docs/Project.toml index 1f523ccd8..c8ea85a2a 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,18 +1,13 @@ [deps] +AbstractFBCModels = "5a4f3dfa-1789-40f8-8221-69268c29937c" COBREXA = "babc4406-5200-4a30-9033-bf5ae714c842" -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" -Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" -ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Escher = "8cc96de1-1b23-48cb-9272-618d67962629" -GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" [compat] -Documenter = "0.26" -Literate = "2.8" +Documenter = "1" +Literate = "2" diff --git a/docs/make.jl b/docs/make.jl index cec126f49..bff410b4e 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,15 +1,28 @@ + +# Copyright (c) 2023, University of Luxembourg +# Copyright (c) 2023, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + using Documenter using Literate, JSON using COBREXA -# some settings -dev_docs_folder = "dev" -pages_branch = "gh-pages" - -# This must match the repo slug on github! -github_repo_slug = ENV["CI_PROJECT_NAMESPACE"] * "/" * ENV["CI_PROJECT_NAME"] +# testing constants +const TEST_TOLERANCE = 1e-3 +const QP_TEST_TOLERANCE = 1e-2 # for Clarabel -# generate examples +# build the examples examples_path = joinpath(@__DIR__, "src", "examples") examples_basenames = sort(filter(x -> endswith(x, ".jl"), readdir(examples_path))) @info "base names:" examples_basenames @@ -17,47 +30,14 @@ examples = joinpath.(examples_path, examples_basenames) examples_outdir = joinpath(@__DIR__, "src", "examples") for example in examples - #TODO improve how the nbviewer and binder links are inserted. Direct link to ipynb would be cool Literate.markdown( example, examples_outdir; - repo_root_url = "https://github.com/$github_repo_slug/blob/master", - nbviewer_root_url = "https://nbviewer.jupyter.org/github/$github_repo_slug/blob/gh-pages/$dev_docs_folder", - binder_root_url = "https://mybinder.org/v2/gh/$github_repo_slug/$pages_branch?filepath=$dev_docs_folder", + repo_root_url = "https://github.com/LCSB-BioCore/COBREXA.jl/blob/master", ) Literate.notebook(example, examples_outdir) end -# extract shared documentation parts from README.md -readme_md = open(f -> read(f, String), joinpath(@__DIR__, "..", "README.md")) -quickstart = - match(r"\n([^\0]*)", readme_md).captures[1] -acks = match( - r"\n([^\0]*)", - readme_md, -).captures[1] -ack_logos = - match(r"\n([^\0]*)", readme_md).captures[1] - -# insert the shared documentation parts into index and quickstart templates -#TODO use direct filename read/write -index_md = open(f -> read(f, String), joinpath(@__DIR__, "src", "index.md.template")) -index_md = replace(index_md, "\n" => acks) -index_md = replace(index_md, "\n" => ack_logos) -open(f -> write(f, index_md), joinpath(@__DIR__, "src", "index.md"), "w") - -quickstart_md = - open(f -> read(f, String), joinpath(@__DIR__, "src", "quickstart.md.template")) -quickstart_md = replace(quickstart_md, "\n" => quickstart) -open(f -> write(f, quickstart_md), joinpath(@__DIR__, "src", "quickstart.md"), "w") - -# copy the contribution guide -cp( - joinpath(@__DIR__, "..", ".github", "CONTRIBUTING.md"), - joinpath(@__DIR__, "src", "howToContribute.md"), - force = true, -) - # a helper for sourcing the documentation files from directories find_mds(path) = joinpath.( @@ -65,12 +45,6 @@ find_mds(path) = filter(x -> endswith(x, ".md"), readdir(joinpath(@__DIR__, "src", path))), ) -# Documenter tries to guess the repo slug from git remote URL but that doesn't -# work really well here, this is the only fallback. If this breaks, "Edit on -# GitHub" links will stop working. (See Documenter.jl source in -# src/Utilities/Utilities.jl, in November 2021 it was around line 500) -mk -ENV["TRAVIS_REPO_SLUG"] = github_repo_slug - # build the docs makedocs( modules = [COBREXA], @@ -83,9 +57,9 @@ makedocs( ), authors = "The developers of COBREXA.jl", linkcheck = !("skiplinks" in ARGS), + warnonly = true, # TODO: remove later pages = [ "Home" => "index.md", - "COBREXA.jl in 10 minutes" => "quickstart.md", "Examples" => [ "Contents" => "examples.md" find_mds("examples") @@ -98,31 +72,14 @@ makedocs( "Contents" => "concepts.md" find_mds("concepts") ], - "Reference (Functions and types)" => [ - "Contents" => "functions.md" - find_mds("functions") - ], - "How to contribute" => "howToContribute.md", + "Reference" => "reference.md", + #[ # TODO re-add this when the reference gets bigger + #"Contents" => "reference.md" + #find_mds("reference") + #], ], ) -# remove the workaround (this would cause deploydocs() to get confused and try -# to deploy the travis way) -delete!(ENV, "TRAVIS_REPO_SLUG") - -# replace the "edit this" links for the generated documentation -function replace_in_doc(filename, replacement) - contents = open(f -> read(f, String), joinpath(@__DIR__, "build", filename)) - contents = replace(contents, replacement) - open(f -> write(f, contents), joinpath(@__DIR__, "build", filename), "w") -end - -replace_in_doc("index.html", "blob/master/docs/src/index.md" => "") -replace_in_doc( - joinpath("howToContribute", "index.html"), - "blob/master/docs/src/howToContribute.md" => "blob/master/.github/CONTRIBUTING.md", -) - # clean up examples -- we do not need to deploy all the stuff that was # generated in the process # @@ -142,25 +99,10 @@ for (root, dirs, files) in walkdir(joinpath(@__DIR__, "build", "examples")) end end -# remove the template files -rm(joinpath(@__DIR__, "build", "index.md.template")) -rm(joinpath(@__DIR__, "build", "quickstart.md.template")) - -# Binder actually has 1.6.2 kernel (seen in October 2021), but for whatever -# reason it's called julia-1.1. Don't ask me. -for ipynb in joinpath.(@__DIR__, "build", "examples", ipynb_names) - @info "changing julia version to 1.1 in `$ipynb'" - js = JSON.parsefile(ipynb) - js["metadata"]["kernelspec"]["name"] = "julia-1.1" - js["metadata"]["kernelspec"]["display_name"] = "Julia 1.1.0" - js["metadata"]["language_info"]["version"] = "1.1.0" - open(f -> JSON.print(f, js), ipynb, "w") -end - # deploy the result deploydocs( - repo = "github.com/$github_repo_slug.git", + repo = "github.com/LCSB-BioCore/COBREXA.jl.git", target = "build", - branch = pages_branch, - devbranch = "develop", + branch = "gh-pages", + devbranch = "master", ) diff --git a/docs/src/concepts/1_screen.md b/docs/src/concepts/1_screen.md index e3023b582..cb482a4b6 100644 --- a/docs/src/concepts/1_screen.md +++ b/docs/src/concepts/1_screen.md @@ -14,7 +14,7 @@ of [`screen`](@ref) that is called [`screen_variants`](@ref), which works as follows: ```julia -m = load_model(StandardModel, "e_coli_core.json") +m = load_model(ObjectModel, "e_coli_core.json") screen_variants( m, # the model for screening @@ -24,7 +24,7 @@ screen_variants( [with_changed_bound("O2t", lb = 0, ub = 0)], # disable O2 transport [with_changed_bound("CO2t", lb = 0, ub = 0), with_changed_bound("O2t", lb = 0, ub = 0)], # disable both transports ], - m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], + m -> flux_balance_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], ) ``` The call specifies a model (the `m` that we have loaded) that is being tested, @@ -89,7 +89,7 @@ res = screen_variants(m, ["EX_h2o_e", "EX_co2_e", "EX_o2_e", "EX_nh4_e"], # and this set of exchanges ) ], - m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], + m -> flux_balance_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], ) ``` @@ -162,7 +162,7 @@ model where this change is easy to perform (generally, not all variants may be feasible on all model types). ```julia -with_disabled_oxygen_transport = (model::StandardModel) -> begin +with_disabled_oxygen_transport = (model::ObjectModel) -> begin # make "as shallow as possible" copy of the `model`. # Utilizing `deepcopy` is also possible, but inefficient. @@ -180,7 +180,7 @@ Finally, the whole definition may be parameterized as a normal function. The following variant removes any user-selected reaction: ```julia -with_disabled_reaction(reaction_id) = (model::StandardModel) -> begin +with_disabled_reaction(reaction_id) = (model::ObjectModel) -> begin new_model = copy(model) new_model.reactions = copy(model.reactions) delete!(new_model.reactions, reaction_id) # use the parameter from the specification @@ -199,7 +199,7 @@ screen_variants( [with_disabled_oxygen_transport], [with_disabled_reaction("NH4t")], ], - m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], + m -> flux_balance_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], ) ``` @@ -222,7 +222,7 @@ That should get you the results for all new variants of the model: Some analysis functions may take additional arguments, which you might want to vary for the analysis. `modifications` argument of -[`flux_balance_analysis_dict`](@ref) is one example of such argument, allowing +[`flux_balance_dict`](@ref) is one example of such argument, allowing you to specify details of the optimization procedure. [`screen`](@ref) function allows you to do precisely that -- apart from @@ -242,9 +242,9 @@ iterations needed for Tulip solver to find a feasible solution: screen(m, args = [(i,) for i in 5:15], # the iteration counts, packed in 1-tuples analysis = (m,a) -> # `args` elements get passed as the extra parameter here - flux_balance_analysis_vec(m, + flux_balance_vec(m, Tulip.Optimizer; - modifications=[change_optimizer_attribute("IPM_IterationsLimit", a)], + modifications=[modify_optimizer_attribute("IPM_IterationsLimit", a)], ), ) ``` diff --git a/docs/src/concepts/2_modifications.md b/docs/src/concepts/2_modifications.md deleted file mode 100644 index 5517ca975..000000000 --- a/docs/src/concepts/2_modifications.md +++ /dev/null @@ -1,70 +0,0 @@ - -# Writing custom optimizer modifications - -Functions such as [`flux_balance_analysis`](@ref) internally create a JuMP -model out of the [`MetabolicModel`](@ref), and run the optimizer on that. To be -able to make some modifications on the JuMP model before the optimizer is -started, most of the functions accept a `modifications` argument, where one can -list callbacks that do the changes to the prepared optimization model. - -The callbacks available in COBREXA.jl include functions that may help with -tuning the optimizer, or change the raw values in the linear model, such as: - -- [`change_constraint`](@ref) and [`change_objective`](@ref) -- [`change_sense`](@ref), [`change_optimizer`](@ref), [`change_optimizer_attribute`](@ref) -- [`silence`](@ref) -- [`knockout`](@ref), [`add_crowding_constraints`](@ref) -- [`add_loopless_constraints`](@ref) - -Compared to the [variant system](1_screen.md) and the [model -wrappers](4_wrappers.md), optimizer modifications are slightly more powerful -(they can do anything they want with the optimizer!), but do not compose well --- it is very easy to break the semantics of the model or erase the previous -changes by carelessly adding the modifications. - -Here, we show how to construct the modifications. Their semantics is similar to -the [variant-generating functions](1_screen.md), which receive a model (of type -[`MetabolicModel`](@ref)), and are expected to create another (modified) model. -Contrary to that, modifications receive both the [`MetabolicModel`](@ref) and a -JuMP model structure, and are expected to cause a side effect on the latter. - -A trivial modification that does not do anything can thus be written as: - -```julia -change_nothing() = (model, opt_model) -> println("Not touching anything.") -``` - -and applied as: -```julia -flux_balance_analysis(model, GLPK.Optimizer, modifications=[change_nothing()]) -flux_variability_analysis(model, GLPK.Optimizer, modifications=[change_nothing()]) -``` - -At the call time of the modifier function, `opt_model` is usually the model -that was returned from [`make_optimization_model`](@ref) -- refer to the -function for actual model layout. The function can freely change anything in -that model. - -For demonstration, we show how to implement an impractical but illustrative -modification that adds an extra constraint that makes sure that all fluxes sum -to a certain value: - -```julia -using JuMP - -add_sum_constraint(total::Float64) = - (model, opt_model) -> begin - v = opt_model[:x] # retrieve the variable vector - @constraint(opt_model, total, sum(v) == total) # create the constraint using JuMP macro - end -``` - -The modification can be used at the expectable position: -```julia -v = flux_balance_analysis_vec( - load_model("e_coli_core.xml"), - GLPK.Optimizer, - modifications = [add_sum_constraint(100.0)]) - -sum(v) # should print ~100.0 -``` diff --git a/docs/src/concepts/3_custom_models.md b/docs/src/concepts/3_custom_models.md deleted file mode 100644 index 957b35552..000000000 --- a/docs/src/concepts/3_custom_models.md +++ /dev/null @@ -1,150 +0,0 @@ - -# Working with custom models - -It may happen that the intuitive representation of your data does not really -match what is supported by a given COBRA package. COBREXA.jl attempts to avoid -this problem by providing a flexible framework for containing any data -structure that can, somehow, represent the constraint-based model. - -The task of having such a polymorphic model definition can be split into 2 -separate concerns: - -- How to allow the analysis functions to gather the required information from - any user-specified model data structure? -- How to make the reconstruction functions (i.e., reaction or gene deletions) - work properly on any data structure? - -To solve the first concern, COBREXA.jl specifies a set of generic accessors -that work over the abstract type [`MetabolicModel`](@ref). To use your data -structure in a model, you just make it a subtype of [`MetabolicModel`](@ref) -and overload the required accessors. The accessors are functions that extract -some relevant information, such as [`stoichiometry`](@ref) and -[`bounds`](@ref), returning a fixed simple data type that can be further used -by COBREXA. You may see a complete list of accessors -[here](../functions.md#Base-Types). - -A good solution to the second concern is a slightly more involved, as writing -generic data modifiers is notoriously hard. Still, there is support for easily -making small changes to the model using the modifications system, with -functions such as [`with_added_reactions`](@ref) and -[`with_changed_bound`](@ref). - -## Writing the generic accessors - -Let's write a data structure that represents a very small model that contains N -metabolites that are converted in a circle through N linear, coupled reactions. -(E.g., for N=3, we would have a conversion of metabolites A, B and C ordered as -A → B → C → A.) This may be useful for testing purposes; we will use it for a -simple demonstration. - -The whole model can thus be specified with a single integer N that represents -the length of the reaction cycle: - -```julia -struct CircularModel <: MetabolicModel - size::Int -end -``` - -First, define the reactions and metabolites: - -```julia -COBREXA.n_reactions(m::CircularModel) = m.size -COBREXA.n_metabolites(m::CircularModel) = m.size - -COBREXA.reactions(m::CircularModel) = ["rxn$i" for i in 1:n_reactions(m)] -COBREXA.metabolites(m::CircularModel) = ["met$i" for i in 1:n_metabolites(m)] -``` - -It is useful to re-use the already defined functions, as that improves the code -maintainability. - -We can continue with the actual linear model properties: - -```julia -function COBREXA.objective(m::CircularModel) - c = spzeros(n_reactions(m)) - c[1] = 1 #optimize the first reaction - return c -end - -COBREXA.bounds(m::CircularModel) = ( - zeros(n_reactions(m)), # lower bounds - ones(n_reactions(m)), # upper bounds -) - -function COBREXA.stoichiometry(m::CircularModel) - nr = n_reactions(m) - stoi(i,j) = - i == j ? 1.0 : - (i % nr + 1) == j ? -1.0 : - 0.0 - - sparse([stoi(i,j) for i in 1:nr, j in 1:nr]) -end -``` - -You may check that the result now works just as with [`CoreModel`](@ref) and -[`StandardModel`](@ref): - -```julia -julia> m = CircularModel(5) -Metabolic model of type CircularModel - - 1.0 -1.0 ⋅ ⋅ ⋅ - ⋅ 1.0 -1.0 ⋅ ⋅ - ⋅ ⋅ 1.0 -1.0 ⋅ - ⋅ ⋅ ⋅ 1.0 -1.0 - -1.0 ⋅ ⋅ ⋅ 1.0 -Number of reactions: 5 -Number of metabolites: 5 - -``` - -This interface is sufficient to run most of the basic analyses, especially the flux balance finding ones: - -```julia -julia> flux_balance_analysis_dict(m, Tulip.Optimizer) -Dict{String, Float64} with 5 entries: - "rxn5" => 1.0 - "rxn2" => 1.0 - "rxn1" => 1.0 - "rxn3" => 1.0 - "rxn4" => 1.0 - -``` - -## Writing generic model modifications - -The custom model structure can also be made compatible with many of the -existing variant-generating functions and analysis modifiers. - -The functions prepared for use as "variants" in [`screen`](@ref), usually -prefixed by `with_`, have their generic variants that only call simpler, -overloadable functions for each specific model. This choice is based on the -overloading dispatch system of Julia. For -example,[`with_removed_metabolites`](@ref) is implemented very generically by -reducing the problem to some specific [`remove_metabolites`](@ref) functions -selected by the dispatch, as follows: - -```julia -with_removed_metabolites(args...; kwargs...) = - m -> remove_metabolites(m, args...; kwargs...) -``` - -To be able to use [`with_removed_metabolites`](@ref) in your model, we can just -overload the actual inner function. For the simple circular model, the -modification might as well look like this: - -```julia -COBREXA.remove_metabolites(m::CircularModel, n::Int) = - return CircularModel(m.size - n) -``` - -!!! danger "Functions that generate model variants must be pure" - Notice that the function is "pure", i.e., does not make any in-place - modifications to the original model structure. That property is required - for [`screen`](@ref) and other functions to properly and predictably apply - the modifications to the model. To expose potential in-place modifications - to your codebase, you should instead overload the "bang" counterpart of - remove metabolites, called [`remove_metabolites!`](@ref). diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md deleted file mode 100644 index fcbd3f8b5..000000000 --- a/docs/src/concepts/4_wrappers.md +++ /dev/null @@ -1,154 +0,0 @@ - -# Extending the models - -To simplify doing (and undoing) simple modifications to the existing model -structure, COBREXA.jl supports a class of model _wrappers_, which are basically -small layers that add or change the functionality of a given base models. - -Types [`Serialized`](@ref), [`CoreCoupling`](@ref), [`SMomentModel`](@ref), and -[`GeckoModel`](@ref) all work in this manner -- add some extra functionality to -the "base". Technically, they are all subtypes of the abstract type -[`ModelWrapper`](@ref), which itself is a subtype of [`MetabolicModel`](@ref) -and can thus be used in all standard analysis functions. Similarly, the model -wraps can be stacked -- it is easy to e.g. serialize a [`GeckoModel`](@ref), or -to add coupling to an existing [`SMomentModel`](@ref). - -As the main benefit of the approach, creating model variants using the wrapper -approach is usually more efficient than recomputing the models in place. The -wrappers are thin, and if all values can get computed and materialized only once -the model data is actually needed, we may save a great amount of computing -power. - -At the same time, since the original model stays unchanged (and may even be -immutable), undoing the modifications caused by the wrapper is extremely easy -and fast -- we just discard the wrapper. - -## Writing a model wrapper - -Creating a model wrapper structure is simple -- by declaring it a subtype of -[`ModelWrapper`](@ref) and implementing a single function -[`unwrap_model`](@ref), we get default implementations of all accessors that -should work for any [`MetabolicModel`](@ref). - -As a technical example, we may make a minimal model wrapper that does not do -anything: - -```julia -struct IdentityWrap <: ModelWrapper - mdl::MetabolicModel -end - -COBREXA.unwrap_model(x::IdentityWrap) = x.mdl -``` - -This is instantly usable in all analysis functions, although there is no -actual "new" functionality: - -```julia -m = IdentityWrap(load_model("e_coli_core.xml")) -flux_balance_analysis_vec(m, GLPK.Optimizer) -``` - -To modify the functionality, we simply add specific methods for accessors that -we want modified, such as [`bounds`](@ref), [`stoichiometry`](@ref) and -[`objective`](@ref). We demonstrate that on several examples below. - -## Example 1: Slower model - -Here, we construct a type `RateChangedModel` that has all bounds multiplied by -a constant factor. This can be used to e.g. simulate higher or lower abundance -of certain organism in a model. - -```julia -struct RateChangedModel <: ModelWrapper - factor::Float64 - mdl::MetabolicModel -end -``` - -The overloaded accessors typically reach for basic information into the "inner" -wrapped model, and modify them in a certain way. - -```julia -COBREXA.unwrap_model(x::RateChangedModel) = x.mdl -function COBREXA.bounds(x::RateChangedModel) - (l, u) = bounds(x.mdl) # extract the original bounds - return (l .* x.factor, u .* x.factor) # return customized bounds -end -``` - -To make a 2 times faster or slower model from a base model, we can run: -```julia -faster_e_coli = RateChangedModel(2.0, load_model("e_coli_core.xml")) -slower_e_coli = RateChangedModel(1/2, load_model("e_coli_core.xml")) -``` - -## Example 2: Leaky model - -As the second example, we construct a hypothetical model that is "leaking" all -metabolites at once at a constant fixed small rate. Again, the modification is -not quite realistic, but may be useful to validate the mathematical robustness -of the models. - -```julia -struct LeakyModel <: ModelWrapper - leaking_metabolites::Vector{String} - leak_rate::Float64 - mdl::MetabolicModel -end -``` - -Technically, we implement the leaks by adding an extra reaction bounded to the -precise `leak_rate`, which permanently removes all metabolites. That is done by -modifying the reaction list, stoichiometry, and bounds: - -```julia -COBREXA.unwrap_model(x::LeakyModel) = x.mdl -COBREXA.n_reactions(x::LeakyModel) = n_reactions(x.mdl) + 1 -COBREXA.reactions(x::LeakyModel) = [reactions(x.mdl); "The Leak"] -COBREXA.stoichiometry(x::LeakyModel) = [stoichiometry(x.mdl) [m in x.leaking_metabolites ? -1.0 : 0.0 for m = metabolites(x.mdl)]] -function COBREXA.bounds(x::LeakyModel) - (l, u) = bounds(x.mdl) - return ([l; x.leak_rate], [u; x.leak_rate]) -end -``` - -To make the wrapper complete and consistent, we also have to modify the -accessors that depend on correct sizes of the model items. - -```julia -COBREXA.objective(x::LeakyModel) = [objective(x.mdl); 0] -COBREXA.reaction_flux(x::LeakyModel) = [reaction_flux(x.mdl); zeros(1, n_reactions(x.mdl))] -COBREXA.coupling(x::LeakyModel) = [coupling(x.mdl) zeros(n_coupling_constraints(x.mdl))] -``` -(Among other, we modified the [`reaction_flux`](@ref) so that all analysis -methods ignore the leak reaction.) - -Now, any model can be made to lose some chosen metabolites as follows: -```julia -leaks = ["M_o2_c", "M_pi_c", "M_glx_c"] -leaky_e_coli = LeakyModel(leaks, 5, load_model("e_coli_core.xml")) -``` - -## Example 3: Combining the wrappers - -With both wrappers implemented individually, it is easy to combine them by -re-wrapping. We can easily create a model that is slowed down and moreover -leaks the metabolites as follows: -```julia -leaky_slow_e_coli = LeakyModel(leaks, 5, RateChangedModel(1/2, load_model("e_coli_core.xml"))) -``` - -As with all wrapping operations, take care about the exact order of applying -the wraps. The other combination of the model wraps differs by also changing -the rate of the metabolite leaks, which did not happen with the -`leaky_slow_e_coli` above: -```julia -slowly_leaking_slow_e_coli = RateChangedModel(1/2, LeakyModel(leaks, 5, load_model("e_coli_core.xml"))) -``` - -Expectably, the model can be solved with standard functions: -```julia -v = flux_balance_analysis_dict(slowly_leaking_slow_e_coli, GLPK.Optimizer) -v["R_BIOMASS_Ecoli_core_w_GAM"] # prints out ~0.38 -``` diff --git a/docs/src/distributed/4_serialized.md b/docs/src/distributed/4_serialized.md deleted file mode 100644 index ea31ba902..000000000 --- a/docs/src/distributed/4_serialized.md +++ /dev/null @@ -1,50 +0,0 @@ - -# Faster model distribution - -When working with a large model, it may happen that the time required to send -the model to the worker nodes takes a significant portion of the total -computation time. - -You can use Julia serialization to prevent this. Because the shared filesystem -is a common feature of most HPC installations around, you can very easily -utilize it to broadcast a serialized version of the model to all worker nodes. -In COBREXA, that functionality is wrapped in a [`Serialized`](@ref) model type, which provides a tiny abstraction around this functionality: - -- you call [`serialize_model`](@ref) before you start your analysis to place - the model to the shared storage (and, possibly, free the RAM required to hold - the model) -- the freshly created [`Serialized`](@ref) model type is tiny -- it only stores - the file name where the model data can be found -- all analysis functions automatically call [`precache!`](@ref) on the model to - get the actual model data loaded before the model is used, which - transparently causes the loading from the shared media, and thus the fast - distribution - -The use is pretty straightforward. After you have your model loaded, you simply -convert it to the small serialized form: - -```julia -model = load_model(...) -# ... prepare the model ... - -cachefile = tempname(".") -sm = serialize_model(model, cachefile) -``` - -Then, the analysis functions is run as it would be with a "normal" model: -```julia -screen(sm, ...) -``` - -After the analysis is done, it is useful to remove the temporary file: -```julia -rm(cachefile) -``` - -!!! warn "Caveats of working with temporary files" - Always ensure that the temporary filenames are unique -- if there are two - jobs running in parallel and both use the same filename to cache and - distribute a model, both are likely going to crash, and almost surely - produce wrong results. At the same time, avoid creating the temporary - files in the default location of `/tmp/*`, as the temporary folder is - local, thus seldom shared between HPC nodes. diff --git a/docs/src/examples/01-loading-and-saving.jl b/docs/src/examples/01-loading-and-saving.jl new file mode 100644 index 000000000..7db77f046 --- /dev/null +++ b/docs/src/examples/01-loading-and-saving.jl @@ -0,0 +1,23 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Loading and saving models! + +using COBREXA + +# TODO: download the models into a single directory that can get cached. Probably best have a fake mktempdir(). +# +# TODO: demonstrate download_model here and explain how to get hashes (simply not fill them in for the first time) diff --git a/docs/src/examples/01_loading.jl b/docs/src/examples/01_loading.jl deleted file mode 100644 index 211393f4a..000000000 --- a/docs/src/examples/01_loading.jl +++ /dev/null @@ -1,73 +0,0 @@ - -# # Loading models - -# `COBREXA` can load models stored in `.mat`, `.json`, and `.xml` formats (with -# the latter denoting SBML formatted models). -# -# We will primarily use the *E. coli* "core" model to demonstrate the utilities -# found in `COBREXA`. First, let's download the model in several formats. - -## Downloads the model files if they don't already exist -!isfile("e_coli_core.mat") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.mat", "e_coli_core.mat"); -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json"); -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml"); - -#md # !!! tip "Save bandwidth!" -#md # The published models usually do not change very often. It is -#md # therefore pretty useful to save them to a central location and load -#md # them from there. That saves your time, and does not unnecessarily -#md # consume the connectivity resources of the model repository. - -# Load the models using the [`load_model`](@ref) function. Models are able to -# "pretty-print" themselves, hiding the inner complexity: - -using COBREXA - -mat_model = load_model("e_coli_core.mat") -# - -json_model = load_model("e_coli_core.json") -# - -sbml_model = load_model("e_coli_core.xml") -# - -#md # !!! note "Note: `load_model` infers the input type from the file extension" -#md # Notice how each model was read into memory as a model type corresponding -#md # to its file type, i.e. the file ending with `.json` loaded as a -#md # [`JSONModel`](@ref), the file ending with `.mat` loaded as [`MATModel`](@ref), and the -#md # file ending with `.xml` loaded as an [`SBMLModel`](@ref). - -# The loaded models contain the data in a format that is preferably as -# compatible as possible with the original representation. In particular, the -# JSON model contains the representation of the JSON tree: - -json_model.json - -# SBML models contain a complicated structure from [`SBML.jl` -# package](https://github.com/LCSB-BioCore/SBML.jl): - -typeof(sbml_model.sbml) - -# MAT models contain MATLAB data: - -mat_model.mat - -# In all cases, you can access the data in the model in the same way, e.g., -# using [`reactions`](@ref) to get a list of the reactions in the models: - -reactions(mat_model)[1:5] -# - -reactions(json_model)[1:5] - -# You can use the [generic accessors](03_exploring.md) to gather more information about -# the model contents, [convert the models](02_convert_save.md) into formats more suitable for -# hands-on processing, and export them back to disk after the -# modification. -# -# All model types can be directly [used in analysis functions](05a_fba.md), such as -# [`flux_balance_analysis`](@ref). diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl new file mode 100644 index 000000000..7527c8698 --- /dev/null +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -0,0 +1,70 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Flux balance analysis (FBA) + +# Here we use [`flux_balance_analysis`](@ref) and several related functions to +# find an optimal flux in the *E. coli* "core" model. We will need the model, +# which we can download using [`download_model`](@ref): + +using COBREXA + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +# Additionally to COBREXA and the model format package, we will need a solver +# -- let's use GLPK here: + +import JSONFBCModels +import GLPK + +model = load_model("e_coli_core.json") + +# ## Running a FBA +# +# There are many possibilities on how to arrange the metabolic model into the +# optimization framework and how to actually solve it. The "usual" assumed one +# is captured in the default behavior of function +# [`flux_balance_analysis`](@ref): + +solution = flux_balance_analysis(model, GLPK.Optimizer) + +@test isapprox(solution.objective, 0.8739, atol = TEST_TOLERANCE) #src + +# The result contains a tree of all optimized values in the model, including +# fluxes, the objective value, and possibly others (given by what the model +# contains). +# +# You can explore the dot notation to explore the solution, extracting e.g. the +# value of the objective: + +solution.objective + +# ...or the value of the flux through the given reaction (note the solution is +# not unique in FBA): + +solution.fluxes.PFK + +# ...or make a "table" of all fluxes through all reactions: + +collect(solution.fluxes) + +# ## Advanced: Finding flux balance via the low-level interface + +# TODO ConstraintTrees (maybe put this into a separate example?) diff --git a/docs/src/examples/02_convert_save.jl b/docs/src/examples/02_convert_save.jl deleted file mode 100644 index 85fced321..000000000 --- a/docs/src/examples/02_convert_save.jl +++ /dev/null @@ -1,91 +0,0 @@ - -# # Converting, modifying and saving models - -# COBREXA.jl can export JSON and MATLAB-style model formats, which can be -# useful when exchanging the model data with other software. -# -# For a test, let's download and open a SBML model: - -using COBREXA - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml"); - -sbml_model = load_model("e_coli_core.xml") - - -# You can save the model as `.json` or `.mat` file using the -# [`save_model`](@ref) function: - -save_model(sbml_model, "converted_e_coli.json") -save_model(sbml_model, "converted_e_coli.mat") - - -# ## Using serialization for quick loading and saving - -# If you are saving the models only for future processing in Julia environment, -# it is often wasteful to encode the models to external formats and decode them -# back. Instead, you can use the "native" Julia data format, accessible with -# package `Serialization`. -# -# This way, you can use `serialize` to save any model format (even the -# complicated [`StandardModel`](@ref), which does not have a "native" file format -# representation): - -using Serialization - -sm = convert(StandardModel, sbml_model) - -open(f -> serialize(f, sm), "myModel.stdmodel", "w") - -# The models can then be loaded back using `deserialize`: - -sm2 = deserialize("myModel.stdmodel") -issetequal(metabolites(sm), metabolites(sm2)) - -# This form of loading operation is usually pretty quick: -t = @elapsed deserialize("myModel.stdmodel") -@info "Deserialization took $t seconds" -# Notably, large and complicated models with thousands of reactions and -# annotations can take tens of seconds to decode properly. Serialization allows -# you to minimize this overhead, and scales well to tens of millions of -# reactions. - -#md # !!! warning "Compatibility" -#md # The format of serialized models may change between Julia versions. -#md # In particular, never use the the serialized format for publishing models -- others will have hard time finding the correct Julia version to open them. -#md # Similarly, never use serialized models for long-term storage -- your future self will have hard time finding the historic Julia version that was used to write the data. - -# ## Converting and saving a modified model - -# To modify the models easily, it is useful to convert them to a format that -# simplifies this modification. You may use e.g. [`CoreModel`](@ref) that -# exposes the usual matrix-and-vectors structure of models as used in MATLAB -# COBRA implementations, and [`StandardModel`](@ref) that contains structures, -# lists and dictionaries of model contents, as typical in Python COBRA -# implementations. The object-oriented nature of [`StandardModel`](@ref) is -# better for making small modifications that utilize known identifiers of model -# contents. -# -# Conversion of any model to [`StandardModel`](@ref) can be performed using the -# standard Julia `convert`: - -sm = convert(StandardModel, sbml_model) - -# The conversion can be also achieved right away when loading the model, using -# an extra parameter of [`load_model`](@ref): - -sm = load_model(StandardModel, "e_coli_core.json") - -# As an example, we change an upper bound on one of the reactions: - -sm.reactions["PFK"].ub = 10.0 - -# After [possibly applying more modifications](04_standardmodel.md), you can again save the -# modified model in a desirable exchange format: - -save_model(sm, "modified_e_coli.json") -save_model(sm, "modified_e_coli.mat") - -# More information about [`StandardModel`](@ref) internals is available [in a -# separate example](04_standardmodel.md). diff --git a/docs/src/examples/02a-optimizer-parameters.jl b/docs/src/examples/02a-optimizer-parameters.jl new file mode 100644 index 000000000..0bc355c99 --- /dev/null +++ b/docs/src/examples/02a-optimizer-parameters.jl @@ -0,0 +1,73 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Changing optimizer parameters +# +# Many optimizers require fine-tuning to produce best results. You can pass in +# additional optimizer settings via the `settings` parameter of +# [`flux_balance_analysis`](@ref). These include e.g. +# +# - [`set_optimizer_attribute`](@ref) (typically allowing you to tune e.g. +# iteration limits, tolerances, or floating-point precision) +# - [`set_objective_sense`](@ref) (allowing you to change and reverse the +# optimization direction, if required) +# - [`silence`](@ref) to disable the debug output of the optimizer +# - and even [`set_optimizer`](@ref), which changes the optimizer +# implementation used (this is not quite useful in this case, but becomes +# beneficial with more complex, multi-stage optimization problems) +# +# To demonstrate this, we'll use the usual toy model: + +using COBREXA +import JSONFBCModels, Tulip + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +model = load_model("e_coli_core.json") + +# Running a FBA with a silent optimizer that has slightly increased iteration +# limit for IPM algorithm may now look as follows: +solution = flux_balance_analysis( + model, + Tulip.Optimizer; + settings = [silence, set_optimizer_attribute("IPM_IterationsLimit", 1000)], +) + +@test !isnothing(solution) #src + +# To see some of the effects of the configuration changes, you may e.g. +# deliberately cripple the optimizer's possibilities to a few iterations, which +# will cause it to fail, return no solution, and verbosely describe what +# happened: + +solution = flux_balance_analysis( + model, + Tulip.Optimizer; + settings = [set_optimizer_attribute("IPM_IterationsLimit", 2)], +) + +println(solution) + +@test isnothing(solution) #src + +# Applicable optimizer attributes are documented in the documentations of the +# respective optimizers. To browse the possibilities, you may want to see the +# [JuMP documentation page that summarizes the references to the available +# optimizers](https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers). diff --git a/docs/src/examples/02b-model-modifications.jl b/docs/src/examples/02b-model-modifications.jl new file mode 100644 index 000000000..cb281a9b1 --- /dev/null +++ b/docs/src/examples/02b-model-modifications.jl @@ -0,0 +1,164 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Making adjustments to the model +# +# Typically, we do not need to solve the models as they come from the authors +# (someone else already did that!), but we want to perform various +# perturbations in the model structure and conditions, and explore how the +# model behaves in the changed conditions. +# +# With COBREXA, there are 2 different approaches that one can take: +# 1. We can change the model structure and use the changed metabolic model. +# This is better for doing simple and small but systematic modifications, +# such as removing metabolites, adding reactions, etc. +# 2. We can intercept the pipeline that converts the metabolic model to +# constraints and then to the optimizer representation, and make small +# modifications along that way. This is better for various technical model +# adjustments, such as using combined objectives or adding reaction-coupling +# constraints. +# +# Here we demonstrate the first, "modelling" approach. The main advantage of +# that approach is that the modified model is still a FBC model, and you can +# export, save and share it via the AbstractFBCModels interace. The main +# disadvantage is that the "common" FBC model interface does not easily express +# various complicated constructions (communities, reaction coupling, enzyme +# constraints, etc.) -- see the [example about modifying the +# constraints](02c-constraint-modifications.md) for a closer look on how to +# modify even such complex constructions. +# +# ## Getting the base model + +using COBREXA + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +import JSONFBCModels + +# For applying the modifications, we will use the canonical model as exported +# from package `AbstractFBCModels`. There are other possibilities, but the +# canonical one is easiest to use for common purposes. + +import AbstractFBCModels.CanonicalModel as CM + +# We can now load the model: + +model = convert(CM.Model, load_model("e_coli_core.json")) + +# The canonical model is quite easy to work with, made basically of the most +# accessible Julia structures possible. For example, you can observe a reaction +# as such: + +model.reactions["PFK"] + +# + +model.reactions["CS"].stoichiometry + +# ## Running FBA on modified models +# +# Since the canonical model is completely mutable, you can change it in any way you like and feed it directly into [`flux_balance_analysis`](@ref). Let's first find a "original" solution, so that we have a base solution for comparing: + +import GLPK + +base_solution = flux_balance_analysis(model, GLPK.Optimizer) +base_solution.objective + +# Now, for example, we can limit the intake of glucose by the model: + +model.reactions["EX_glc__D_e"] + +# Since the original intake limit is 10 units, let's try limiting that to 5: + +model.reactions["EX_glc__D_e"].lower_bound = -5.0 + +# ...and solve the modified model: +# +low_glucose_solution = flux_balance_analysis(model, GLPK.Optimizer) +low_glucose_solution.objective + +@test isapprox(low_glucose_solution.objective, 0.41559777, atol = TEST_TOLERANCE) #src + +# ## Preventing reference-based sharing problems with `deepcopy` +# +# People often want to try different perturbations with a single base model. It +# would therefore look feasible to save retain the "unmodified" model in a +# single variable, and make copies of that with the modifications applied. +# Let's observe what happens: + +base_model = convert(CM.Model, load_model("e_coli_core.json")) # load the base + +modified_model = base_model # copy for modification + +modified_model.reactions["EX_glc__D_e"].lower_bound = -123.0 # modify the glucose intake limit + +# Surprisingly, the base model got modified too! + +base_model.reactions["EX_glc__D_e"] + +# This is because Julia uses reference-based sharing whenever anything mutable +# is copied using the `=` operator. While this is extremely useful in many +# scenarios for data processing efficiency and computational speed, it +# unfortunately breaks this simple use-case. +# +# To fix the situation, you should always ensure to make an actual copy of the +# model data by either carefully copying the changed parts with `copy()`, or +# simply by copying the whole model structure as is with `deepcopy()`. Let's +# try again: + +base_model = convert(CM.Model, load_model("e_coli_core.json")) +modified_model = deepcopy(base_model) # this forces an actual copy of the data +modified_model.reactions["EX_glc__D_e"].lower_bound = -123.0 + +# With `deepcopy`, the result works as intended: + +( + modified_model.reactions["EX_glc__D_e"].lower_bound, + base_model.reactions["EX_glc__D_e"].lower_bound, +) + +@test modified_model.reactions["EX_glc__D_e"].lower_bound != #src + base_model.reactions["EX_glc__D_e"].lower_bound #src + +#md # !!! danger "Avoid overwriting base models when using in-place modifications" +#md # Whenever you are changing a copy of the model, make sure that you are not changing it by a reference. Always use some copy mechanism such as `copy` or `deepcopy` to prevent the default reference-based sharing. + +# ## Observing the differences +# +# We already have a `base_solution` and `low_glucose_solution` from above. What +# is the easiest way to see what has changed? We can quite easily compute +# squared distance between all dictionary entries using Julia function for +# merging dictionaries (called `mergewith`). + +# With that, we can extract the plain difference in fluxes: +flux_differences = mergewith(-, base_solution.fluxes, low_glucose_solution.fluxes) + +# ...and see what were the biggest directional differences: +sort(collect(flux_differences), by = last) + +# ...or compute the squared distance, to see the "absolute" changes: +flux_changes = + mergewith((x, y) -> (x - y)^2, base_solution.fluxes, low_glucose_solution.fluxes) + +# ...and again see what changed most: +sort(collect(flux_changes), by = last) + +#md # !!! tip "For realistic comparisons always use a uniquely defined flux solution" +#md # Since the usual flux balance allows a lot of freedom in the "solved" flux and the only value that is "reproducible" by the analysis is the objective, one should never compare the flux distributions directly. Typically, that may result in false-positive (and sometimes false-negative) differences. Use e.g. [parsimonious FBA](03-parsimonious-flux-balance.md) to obtain uniquely determined and safely comparable flux solutions. diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl new file mode 100644 index 000000000..4495771e0 --- /dev/null +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -0,0 +1,93 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Making adjustments to the constraint system +# +# In the [previous example about model +# adjustments](02b-model-modifications.md), we noted that some constraint +# systems may be to complex to be changed within the limits of the usual FBC +# model view, and we may require a sharper tool to do the changes we need. This +# example shows how to do that by modifying the constraint systems that are +# generated within COBREXA to represent the metabolic model contents. +# +# ## Background: Model-to-optimizer pipeline +# +# ## Background: Constraint trees +# +# ## Changing the model-to-optimizer pipeline +# +# TODO clean up the stuff below: + +using COBREXA + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +import JSONFBCModels +import GLPK + +model = load_model("e_coli_core.json") + +# ## Customizing the model + +# We can also modify the model. The most explicit way to do this is +# to make a new constraint tree representation of the model. + +import ConstraintTrees as C + +ctmodel = flux_balance_constraints(model) + +fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value + +forced_mixed_fermentation = + ctmodel * :fermentation^C.Constraint(fermentation, (10.0, 1000.0)) # new modified model is created + +vt = optimized_constraints( + forced_mixed_fermentation, + objective = forced_mixed_fermentation.objective.value, + optimizer = GLPK.Optimizer, +) + +@test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src + +# Models that cannot be solved return `nothing`. In the example below, the +# underlying model is modified. + +ctmodel.fluxes.ATPM.bound = C.Between(1000.0, 10000.0) + +#TODO explicitly show here how false sharing looks like + +vt = optimized_constraints( + ctmodel, + objective = ctmodel.objective.value, + optimizer = GLPK.Optimizer, +) + +@test isnothing(vt) #src + +# Models can also be piped into the analysis functions + +ctmodel.fluxes.ATPM.bound = C.Between(8.39, 10000.0) # revert +vt = optimized_constraints( + ctmodel, + objective = ctmodel.objective.value, + optimizer = GLPK.Optimizer, +) + +@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl new file mode 100644 index 000000000..0a7fbb0c9 --- /dev/null +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -0,0 +1,110 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Parsimonious flux balance analysis + +# We will use [`parsimonious_flux_balance_analysis`](@ref) and +# [`minimize_metabolic_adjustment`](@ref) to find the optimal flux +# distribution in the *E. coli* "core" model. +# +# TODO pFBA citation + +# If it is not already present, download the model and load the package: +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# next, load the necessary packages + +using COBREXA + +import JSONFBCModels +import Clarabel # can solve QPs + +model = load_model("e_coli_core.json") # load the model + +# Use the convenience function to run standard pFBA on + +vt = parsimonious_flux_balance_analysis(model, Clarabel.Optimizer; settings = [silence]) + +@test isapprox(vt.objective, 0.87392; atol = TEST_TOLERANCE) #src +@test sum(x^2 for x in values(vt.fluxes)) < 15000 #src + +#= + +# Alternatively, you can construct your own constraint tree model with +# the quadratic objective (this approach is much more flexible). + +ctmodel = flux_balance_constraints(model) +ctmodel *= :l2objective^squared_sum_value(ctmodel.fluxes) +ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks + +opt_model = optimization_model( + ctmodel; + objective = ctmodel.:l2objective.value, + optimizer = Clarabel.Optimizer, + sense = Minimal, +) + +J.optimize!(opt_model) # JuMP is called J in COBREXA + +is_solved(opt_model) # check if solved + +vt = C.substitute_values(ctmodel, J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA + +@test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds + +# It is likewise as simple to run MOMA using the convenience functions. + +ref_sol = Dict("ATPS4r" => 33.0, "CYTBD" => 22.0) + +vt = minimize_metabolic_adjustment(model, ref_sol, Gurobi.Optimizer) + +# Or use the piping functionality + +model |> +minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; settings = [silence]) + +@test isapprox(vt.:momaobjective, 0.81580806; atol = TEST_TOLERANCE) #src + +# Alternatively, you can construct your own constraint tree model with +# the quadratic objective (this approach is much more flexible). + +ctmodel = flux_balance_constraints(model) +ctmodel *= + :minoxphospho^squared_sum_error_value( + ctmodel.fluxes, + Dict(:ATPS4r => 33.0, :CYTBD => 22.0), + ) +ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks + +opt_model = optimization_model( + ctmodel; + objective = ctmodel.minoxphospho.value, + optimizer = Clarabel.Optimizer, + sense = Minimal, +) + +J.optimize!(opt_model) # JuMP is called J in COBREXA + +is_solved(opt_model) # check if solved + +vt = C.substitute_values(ctmodel, J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA + +@test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds + +=# diff --git a/docs/src/examples/03_exploring.jl b/docs/src/examples/03_exploring.jl deleted file mode 100644 index d8659084a..000000000 --- a/docs/src/examples/03_exploring.jl +++ /dev/null @@ -1,51 +0,0 @@ -# # Exploring model contents - -# For practical reasons, COBREXA.jl supports many different model types. These -# comprise ones that reflect the storage formats (such as [`JSONModel`](@ref) -# and [`SBMLModel`](@ref)), and ones that are more easily accessible for users -# and mimic the usual workflows in COBRA methodology: -# -# - [`StandardModel`](@ref), which contains and object-oriented representation -# of model internals, built out of [`Reaction`](@ref), [`Metabolite`](@ref) -# and [`Gene`](@ref) structures, in a way similar to e.g. -# [COBRApy](https://github.com/opencobra/cobrapy/) -# - [`CoreModel`](@ref), which contains array-oriented representation of the -# model structures, such as stoichiometry matrix and the bounds vector, in a -# way similar to e.g. [COBRA -# toolbox](https://github.com/opencobra/cobratoolbox) - -# The fields in [`StandardModel`](@ref) structure can be discovered using `fieldnames` as follows: - -using COBREXA - -fieldnames(StandardModel) - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json"); - -sm = load_model(StandardModel, "e_coli_core.json") -typeof(sm.reactions) - -fieldnames(Reaction) - -# This process (along with e.g. Tab completion in REPL) allows you to pick -# various information about many objects, for example about a specific -# reaction: - -sm.reactions["TALA"].name -# -sm.reactions["TALA"].grr #gene-reaction relationship -# -sm.reactions["TALA"].subsystem -# -sm.reactions["TALA"].ub #upper rate bound - -# The same applies to [`CoreModel`](@ref): - -fieldnames(CoreModel) -# -cm = load_model(CoreModel, "e_coli_core.json") -# -cm.S -# -cm.rxns[1:10] diff --git a/docs/src/examples/03b_accessors.jl b/docs/src/examples/03b_accessors.jl deleted file mode 100644 index 125afc9ee..000000000 --- a/docs/src/examples/03b_accessors.jl +++ /dev/null @@ -1,54 +0,0 @@ - -# # Generic accessors - -# To prevent the complexities of object representation, `COBREXA.jl` uses a set -# of generic interface functions that can extract various important information -# from any supported model type. This approach ensures that the analysis -# functions can work on any data. - -# For example, you can check the reactions and metabolites contained in any -# model type ([`SBMLModel`](@ref), [`JSONModel`](@ref), [`CoreModel`](@ref), -# [`StandardModel`](@ref), and any other) using the same accessor: - -using COBREXA - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json"); - -js = load_model("e_coli_core.json") -reactions(js) -# -std = convert(CoreModel, js) -reactions(std) - -# All accessors allow systematic access to information about reactions, -# stoichiometry, metabolite properties and chemistry, genes, and various model -# annotations. -# -# The most notable ones include: -# -# - [`reactions`](@ref), [`metabolites`](@ref) and [`genes`](@ref) return -# respective vectors of identifiers of reactions, metabolites and genes present -# in the model, -# - [`stoichiometry`](@ref) returns the S matrix -# - [`balance`](@ref) returns the right-hand vector of the linear model in form `Ax=b` -# - [`bounds`](@ref) return lower and upper bounds of reaction rates -# - [`metabolite_charge`](@ref) and [`metabolite_formula`](@ref) return details about metabolites -# - [`objective`](@ref) returns the objective of the model (usually labeled as `c`) -# - [`reaction_gene_association`](@ref) describes the dependency of a reaction on gene products -# -# A complete, up-to-date list of accessors can be always generated using `methodswith`: - -using InteractiveUtils - -accessors = [ - x.name for x in methodswith(MetabolicModel, COBREXA) if - endswith(String(x.file), "MetabolicModel.jl") -] - -println.(accessors); - -#md # !!! note "Note: Not all accessors may be implemented for all the models" -#md # It is possible that not all the accessors are implemented for all the model -#md # types. If this is the case, usually `nothing` or an empty data structure is -#md # returned. If you need a specific accessor, just overload the function you require! diff --git a/docs/src/examples/04-minimization-of-metabolic-adjustment.jl b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl new file mode 100644 index 000000000..48165fdb1 --- /dev/null +++ b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl @@ -0,0 +1,43 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Minimization of metabolic adjustment analysis + +# TODO MOMA citation + +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +using COBREXA +import AbstractFBCModels.CanonicalModel as CM +import JSONFBCModels +import Clarabel + +# TODO this might do the convert immediately as with the old cobrexa... +# probably better have an actual output-type argument tho rather than force the +# guessing. +model = convert(CM.Model, load_model("e_coli_core.json")) + +reference_fluxes = + parsimonious_flux_balance_analysis( + model, + Clarabel.Optimizer, + settings = [silence], + ).fluxes + +# TODO MOMA from here diff --git a/docs/src/examples/04_core_model.jl b/docs/src/examples/04_core_model.jl deleted file mode 100644 index 84ec6f4a7..000000000 --- a/docs/src/examples/04_core_model.jl +++ /dev/null @@ -1,53 +0,0 @@ -# # `CoreModel` usage - -#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/notebooks/@__NAME__.ipynb) -#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/@__NAME__.ipynb) - -# In this tutorial we will introduce `COBREXA`'s `CoreModel` and -# `CoreModelCoupled`. We will use *E. coli*'s toy model to start with. - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA - -# ## Loading a `CoreModel` - -model = load_model(CoreModel, "e_coli_core.xml") # we specifically want to load a CoreModel from the model file - -# ## Basic analysis on `CoreModel` - -# As before, for optimization based analysis we need to load an optimizer. Here we -# will use [`Tulip.jl`](https://github.com/ds4dm/Tulip.jl) to optimize the linear -# programs of this tutorial. Refer to the examples of [analysis](05a_fba.md) -# and [analysis modifications](05b_fba_mods.md) for details and explanations. - -using Tulip - -dict_sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_objective("R_BIOMASS_Ecoli_core_w_GAM"), - change_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), - change_constraint("R_EX_o2_e"; lb = 0, ub = 0), - ], -) - -# ## Structure of `CoreModel` - -# `CoreModel` is optimized for analysis of models that utilizes the matrix, -# linearly-algebraic "view" of the models. It stores data in a sparse format -# wherever possible. -# -# The structure contains fields that contain the expectable model elements: - -fieldnames(CoreModel) -# -model.S - -# Contrary to the usual implementations, the model representation does not -# contain reaction coupling boudns; these can be added to any model by wrapping -# it with [`CoreCoupling`](@ref). You may also use the prepared -# [`CoreModelCoupled`](@ref) to get a version of [`CoreModel`](@ref) with this -# coupling. diff --git a/docs/src/examples/04_standardmodel.jl b/docs/src/examples/04_standardmodel.jl deleted file mode 100644 index 13fda3b2a..000000000 --- a/docs/src/examples/04_standardmodel.jl +++ /dev/null @@ -1,140 +0,0 @@ -# # Basic usage of `StandardModel` - -#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/notebooks/@__NAME__.ipynb) -#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/@__NAME__.ipynb) - -# In this tutorial we will use `COBREXA`'s `StandardModel` and functions that -# specifically operate on it. As usual we will use the toy model of *E. coli* -# for demonstration. - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -using COBREXA - -# ## Loading a model in the StandardModel format - -model = load_model(StandardModel, "e_coli_core.json") # we specifically want to load a StandardModel from the model file - -#md # !!! note "Note: Loading `StandardModel`s implicitly uses `convert`" -#md # When using `load_model(StandardModel, file_location)` the model at -#md # `file_location` is first loaded into its inferred format and is then -#md # converted to a `StandardModel` using the generic accessor interface. -#md # Thus, data loss may occur. Always check your model to ensure that -#md # nothing important has been lost. - -#nb # When using `load_model(StandardModel, file_location)` the model at -#nb # `file_location` is first loaded into its inferred format and is then -#nb # converted to a `StandardModel` using the generic accessor interface. -#nb # Thus, data loss may occur. Always check your model to ensure that -#nb # nothing important has been lost. - -# ## Internals of `StandardModel` - -# A benefit of `StandardModel` is that it supports a richer internal -# infrastructure that can be used to manipulate internal model attributes in a -# systematic way. Specifically, the genes, reactions, and metabolites with of a -# model each have a type. This is particularly useful when modifying or even -# constructing a model from scratch. - -# ## `Gene`s, `Reaction`s, and `Metabolite`s - -# `StandardModel` is composed of ordered dictionaries of `Gene`s, `Metabolite`s -# and `Reaction`s. Ordered dictionaries are used because the order of the -# reactions and metabolites are important for constructing a stoichiometric -# matrix since the rows and columns should correspond to the order of the metabolites -# and reactions returned by calling the accessors `metabolites` and `reactions`. - -# Each `StandardModel` is composed of the following fields: - -fieldnames(StandardModel) # fields of a StandardModel - -# The `:genes` field of a `StandardModel` contains an ordered dictionary of gene ids mapped to `Gene`s. - -model.genes # the keys of this dictionary are the same as genes(model) - -# The `Gene` type is a struct that can be used to store information about genes -# in a `StandardModel`. Each `Gene` is composed of the following fields: - -fieldnames(Gene) - -#md # !!! tip "Tip: Use complete to explore the structure of types" -#md # Use to quickly explore the fields of a struct. For example, -#md # Gene. will list all the fields shown above. - -#nb # Use to quickly explore the fields of a struct. For example, -#nb # Gene. will list all the fields shown above. - -# The keys used in the ordered dictionaries in -# `model.genes` are the ids returned using the generic accessor `genes`. `Gene`s -# have pretty printing, as demonstrated below for a random gene drawn from the -# model: - -random_gene_id = genes(model)[rand(1:n_genes(model))] -model.genes[random_gene_id] - -# The same idea holds for both metabolites (stored as `Metabolite`s) and -# reactions (stored as `Reaction`s). This is demonstrated below. - -random_metabolite_id = metabolites(model)[rand(1:n_metabolites(model))] -model.metabolites[random_metabolite_id] -# -random_reaction_id = reactions(model)[rand(1:n_reactions(model))] -model.reactions[random_reaction_id] - -# `StandardModel` can be used to build your own metabolic model or modify an -# existing one. One of the main use cases for `StandardModel` is that it can be -# used to merge multiple models or parts of multiple models together. Since the -# internals are uniform inside each `StandardModel`, attributes of other model -# types are squashed into the required format (using the generic accessors). -# This ensures that the internals of all `StandardModel`s are the same - -# allowing easy systematic evaluation. - -#md # !!! warning "Warning: Combining models with different namespaces is tricky" -#md # Combining models that use different namespaces requires care. -#md # For example, in some models the water exchange reaction is called -#md # `EX_h2o_e`, while in others it is called `R_EX_h2o_s`. This needs to -#md # manually addressed to prevent duplicates, e.g. reactions, -#md # from being added. - -# ## Checking the internals of `StandardModel`s: `annotation_index` - -# Often when models are automatically reconstructed duplicate genes, reactions -# or metabolites end up in a model. `COBREXA` exports `annotation_index` to -# check for cases where the id of a struct may be different, but the annotations -# the same (possibly suggesting a duplication). `annotation_index` builds a -# dictionary mapping annotation features to the ids of whatever struct you are -# inspecting. This makes it easy to find structs that share certain annotation features. - -rxn_annotations = annotation_index(model.reactions) -# -rxn_annotations["ec-code"] - -# The `annotation_index` function can also be used on `Reaction`s and -# `Gene`s in the same way. - -# ## Checking the internals of `StandardModel`s: `check_duplicate_reaction` - -# Another useful function is `check_duplicate_reaction`, which checks for -# reactions that have duplicate (or similar) reaction equations. - -pgm_duplicate = Reaction() -pgm_duplicate.id = "pgm2" # Phosphoglycerate mutase -pgm_duplicate.metabolites = Dict{String,Float64}("3pg_c" => 1, "2pg_c" => -1) -pgm_duplicate -# -check_duplicate_reaction(pgm_duplicate, model.reactions; only_metabolites = false) # can also just check if only the metabolites are the same but different stoichiometry is used - -# ## Checking the internals of `StandardModel`s: `reaction_mass_balanced` - -# Finally, [`reaction_mass_balanced`](@ref) can be used to check if a reaction is mass -# balanced based on the formulas of the reaction equation. - -rxn_dict = Dict{String,Float64}("3pg_c" => 1, "2pg_c" => -1, "h2o_c" => 1) -reaction_mass_balanced(model, rxn_dict) - -# Now to determine which atoms are unbalanced, you can use `reaction_atom_balance` -reaction_atom_balance(model, rxn_dict) - -# Note, since `pgm_duplicate` is not in the model, we cannot use the other variants of this -# function because they find the reaction equation stored inside the `model`. diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl deleted file mode 100644 index 37d60b900..000000000 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ /dev/null @@ -1,84 +0,0 @@ -# # Model construction and modification - -# `COBREXA` can load models stored in `.mat`, `.json`, and `.xml` formats; and convert -# these into `StandardModel`s. However, it is also possible to construct models -# from scratch, and modify existing models. This will be demonstrated -# here. - -using COBREXA - -# In `COBREXA`, model construction is primarily supported through `StandardModel`s. -# To begin, create an empty `StandardModel`. - -model = StandardModel("FirstModel") # assign model id = "FirstModel" - -# Next, genes, metabolites and reactions need to be added to the model. - -# ### Add genes to the model -gene_list = [Gene(string("g", num)) for num = 1:8] - -#md # !!! warning "Warning: Don't accidentally overwrite the generic accessors" -#md # It may be tempting to call a variable `genes`, `metabolites`, or -#md # `reactions`. However, these names conflict with generic accessors -#md # functions and will create problems downstream. - -add_genes!(model, gene_list) - -# ### Add metabolites to the model -metabolite_list = [Metabolite(string("m", num)) for num = 1:4] - -metabolite_list[1].formula = "C6H12O6" # can edit metabolites, etc. directly - -add_metabolites!(model, metabolite_list) - -# ### Add reactions to the model - -# There are two ways to create and add reactions to a model. -# These are using functions, or macros. - -r_m1 = Reaction("EX_m1", Dict("m1" => -1.0), :bidirectional) # exchange reaction: m1 <-> (is the same as m1 ↔ nothing) -r1 = Reaction("r1", Dict("m1" => -1.0, "m2" => 1.0), :forward) -r1.grr = [["g1", "g2"], ["g3"]] # add some gene reaction rules -r2 = Reaction("r2", Dict("m2" => -1.0, "m1" => 1.0), :reverse) -r3 = Reaction("r3", Dict("m2" => -1.0, "m3" => 1.0), :bidirectional) - -add_reactions!(model, [r1, r2, r3, r_m1]) # function approach - -m1 = metabolite_list[1] -m2 = metabolite_list[2] -m3 = metabolite_list[3] -m4 = metabolite_list[4] - -@add_reactions! model begin # macro approach - "r4", m2 → m4, 0, 1000 - "r_m3", m3 ↔ nothing, -1000, 1000 - "r_m4", m4 → nothing - "r5", m4 → m2 -end - -model.reactions["r4"].grr = [["g5"], ["g6", "g7"], ["g8"]] - -#md # !!! note "Note: Writing unicode arrows" -#md # The reaction arrows can be easily written by using the `LaTeX` -#md # completions built into Julia shell (and many Julia-compatible -#md # editors). You can type: -#md # -#md # - `→` as `\rightarrow` (press `Tab` to complete) -#md # - `←` as `\leftarrow` -#md # - `↔` as `\leftrightarrow` - -# The constructed model can now be inspected. -model - -# ## Modifying existing models - -# It is also possible to modify a model by deleting certain genes. -# This is simply achieved by calling `remove_genes!`. - -remove_genes!(model, ["g1", "g2"]; knockout_reactions = false) -model - -# Likewise, reactions and metabolites can also be deleted. - -remove_metabolite!(model, "m1") -model diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl new file mode 100644 index 000000000..358ef9d03 --- /dev/null +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -0,0 +1,340 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Enzyme constrained models + +using COBREXA + +# Here we will construct an enzyme constrained variant of the *E. coli* "core" +# model. We will need the model, which we can download if it is not already present. + +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# Additionally to COBREXA and the model format package, we will need a solver +# -- let's use GLPK here: + +import AbstractFBCModels as A +import JSONFBCModels +import GLPK + +model = load_model("e_coli_core.json") + +# Enzyme constrained models require parameters that are usually not used by +# conventional constraint based models. These include reaction specific turnover +# numbers, molar masses of enzymes, and capacity bounds. + +# ## Reaction turnover numbers + +# Enzyme constrained models require reaction turnover numbers, which are often +# isozyme specfic. Many machine learning tools, or experimental data sets, can +# be used to estimate these parameters. + +#md # ```@raw html +#md #
Data for reaction turnover numbers +#md # ``` +# This data is taken from: *Heckmann, David, et al. "Machine learning applied +# to enzyme turnover numbers reveals protein structural correlates and improves +# metabolic models." Nature communications 9.1 (2018): 1-10.* +const ecoli_core_reaction_kcats = Dict( + "ACALD" => 568.11, + "PTAr" => 1171.97, + "ALCD2x" => 75.95, + "PDH" => 529.76, + "MALt2_2" => 234.03, + "CS" => 113.29, + "PGM" => 681.4, + "TKT1" => 311.16, + "ACONTa" => 191.02, + "GLNS" => 89.83, + "ICL" => 17.45, + "FBA" => 373.42, + "FORt2" => 233.93, + "G6PDH2r" => 589.37, + "AKGDH" => 264.48, + "TKT2" => 467.42, + "FRD7" => 90.20, + "SUCOAS" => 18.49, + "ICDHyr" => 39.62, + "AKGt2r" => 234.99, + "GLUSy" => 33.26, + "TPI" => 698.30, + "FORt" => 234.38, + "ACONTb" => 159.74, + "GLNabc" => 233.80, + "RPE" => 1772.485, + "ACKr" => 554.61, + "THD2" => 24.73, + "PFL" => 96.56, + "RPI" => 51.77, + "D_LACt2" => 233.51, + "TALA" => 109.05, + "PPCK" => 218.42, + "PGL" => 2120.42, + "NADTRHD" => 186.99, + "PGK" => 57.64, + "LDH_D" => 31.11, + "ME1" => 487.01, + "PIt2r" => 233.86, + "ATPS4r" => 71.42, + "GLCpts" => 233.90, + "GLUDy" => 105.32, + "CYTBD" => 153.18, + "FUMt2_2" => 234.37, + "FRUpts2" => 234.19, + "GAPD" => 128.76, + "PPC" => 165.52, + "NADH16" => 971.74, + "PFK" => 1000.46, + "MDH" => 25.93, + "PGI" => 468.11, + "ME2" => 443.09, + "GND" => 240.12, + "SUCCt2_2" => 234.18, + "GLUN" => 44.76, + "ADK1" => 111.64, + "SUCDi" => 680.31, + "ENO" => 209.35, + "MALS" => 252.75, + "GLUt2r" => 234.22, + "PPS" => 706.14, + "FUM" => 1576.83, +) +#md # ```@raw html +#md #
+#md # ``` + +# We have these here: + +ecoli_core_reaction_kcats + +# Each reaction in a constraint-based model usually has gene reaction rules +# associated with it. These typically take the form of, possibly multiple, +# isozymes that can catalyze a reaction. A turnover number needs to be assigned +# to each isozyme, as shown below. + +reaction_isozymes = Dict{String,Dict{String,Isozyme}}() # a mapping from reaction IDs to isozyme IDs to isozyme structs. +for rid in A.reactions(model) + grrs = A.reaction_gene_association_dnf(model, rid) + isnothing(grrs) && continue # skip if no grr available + haskey(ecoli_core_reaction_kcats, rid) || continue # skip if no kcat data available + for (i, grr) in enumerate(grrs) + d = get!(reaction_isozymes, rid, Dict{String,Isozyme}()) + d["isozyme_"*string(i)] = Isozyme( # each isozyme gets a unique name + gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes + kcat_forward = ecoli_core_reaction_kcats[rid] * 3.6, # forward reaction turnover number units = 1/h + kcat_reverse = ecoli_core_reaction_kcats[rid] * 3.6, # reverse reaction turnover number units = 1/h + ) + end +end + +#md #!!! warning "Turnover number units" +#md # Take care with the units of the turnover numbers. In literature they are usually reported in 1/s. However, flux units are typically mmol/gDW/h, suggesting that you should rescale the turnover numbers to 1/h if you want to use the conventional flux units. + +### Enzyme molar masses + +# We also require the mass of each enzyme, to properly weight the contribution +# of each flux/isozyme in the capacity bound(s). These data can typically be +# found in uniprot. + +#md # ```@raw html +#md #
Gene product masses +#md # ``` +# This data is downloaded from Uniprot for E. coli K12, gene mass in kDa. To +# obtain these data yourself, go to [Uniprot](https://www.uniprot.org/) and +# search using these terms: `reviewed:yes AND organism:"Escherichia coli +# (strain K12) [83333]"`. +const ecoli_core_gene_product_masses = Dict( + "b4301" => 23.214, + "b1602" => 48.723, + "b4154" => 65.972, + "b3236" => 32.337, + "b1621" => 56.627, + "b1779" => 35.532, + "b3951" => 85.96, + "b1676" => 50.729, + "b3114" => 85.936, + "b1241" => 96.127, + "b2276" => 52.044, + "b1761" => 48.581, + "b3925" => 35.852, + "b3493" => 53.389, + "b3733" => 31.577, + "b2926" => 41.118, + "b0979" => 42.424, + "b4015" => 47.522, + "b2296" => 43.29, + "b4232" => 36.834, + "b3732" => 50.325, + "b2282" => 36.219, + "b2283" => 100.299, + "b0451" => 44.515, + "b2463" => 82.417, + "b0734" => 42.453, + "b3738" => 30.303, + "b3386" => 24.554, + "b3603" => 59.168, + "b2416" => 63.562, + "b0729" => 29.777, + "b0767" => 36.308, + "b3734" => 55.222, + "b4122" => 60.105, + "b2987" => 53.809, + "b2579" => 14.284, + "b0809" => 26.731, + "b1524" => 33.516, + "b3612" => 56.194, + "b3735" => 19.332, + "b3731" => 15.068, + "b1817" => 35.048, + "b1603" => 54.623, + "b1773" => 30.81, + "b4090" => 16.073, + "b0114" => 99.668, + "b3962" => 51.56, + "b2464" => 35.659, + "b2976" => 80.489, + "b1818" => 27.636, + "b2285" => 18.59, + "b1702" => 87.435, + "b1849" => 42.434, + "b1812" => 50.97, + "b0902" => 28.204, + "b3403" => 59.643, + "b1612" => 60.299, + "b1854" => 51.357, + "b0811" => 27.19, + "b0721" => 14.299, + "b2914" => 22.86, + "b1297" => 53.177, + "b0723" => 64.422, + "b3919" => 26.972, + "b3115" => 43.384, + "b4077" => 47.159, + "b3528" => 45.436, + "b0351" => 33.442, + "b2029" => 51.481, + "b1819" => 30.955, + "b0728" => 41.393, + "b2935" => 72.212, + "b2415" => 9.119, + "b0727" => 44.011, + "b0116" => 50.688, + "b0485" => 32.903, + "b3736" => 17.264, + "b0008" => 35.219, + "b3212" => 163.297, + "b3870" => 51.904, + "b4014" => 60.274, + "b2280" => 19.875, + "b2133" => 64.612, + "b2278" => 66.438, + "b0118" => 93.498, + "b2288" => 16.457, + "b3739" => 13.632, + "b3916" => 34.842, + "b3952" => 32.43, + "b2925" => 39.147, + "b2465" => 73.043, + "b2297" => 77.172, + "b2417" => 18.251, + "b4395" => 24.065, + "b3956" => 99.063, + "b0722" => 12.868, + "b2779" => 45.655, + "b0115" => 66.096, + "b0733" => 58.205, + "b1478" => 35.38, + "b2492" => 30.565, + "b0724" => 26.77, + "b0755" => 28.556, + "b1136" => 45.757, + "b2286" => 68.236, + "b0978" => 57.92, + "b1852" => 55.704, + "b2281" => 20.538, + "b2587" => 47.052, + "b2458" => 36.067, + "b0904" => 30.991, + "b1101" => 50.677, + "b0875" => 23.703, + "b3213" => 52.015, + "b2975" => 58.92, + "b0720" => 48.015, + "b0903" => 85.357, + "b1723" => 32.456, + "b2097" => 38.109, + "b3737" => 8.256, + "b0810" => 24.364, + "b4025" => 61.53, + "b1380" => 36.535, + "b0356" => 39.359, + "b2277" => 56.525, + "b1276" => 97.677, + "b4152" => 15.015, + "b1479" => 63.197, + "b4153" => 27.123, + "b4151" => 13.107, + "b2287" => 25.056, + "b0474" => 23.586, + "b2284" => 49.292, + "b1611" => 50.489, + "b0726" => 105.062, + "b2279" => 10.845, + "s0001" => 0.0, +) +#md # ```@raw html +#md #
+#md # ``` + +# We have the molar masses here: + +ecoli_core_gene_product_masses + +#md # !!! warning "Molar mass units" +#md # Take care with the units of the molar masses. In literature they are usually reported in Da or kDa (g/mol). However, as noted above, flux units are typically mmol/gDW/h. Since the enzyme kinetic equation is `v = k * e`, where `k` is the turnover number, it suggests that the enzyme variable will have units of mmol/gDW. The molar masses come into play when setting the capacity limitations, e.g. usually a sum over all enzymes weighted by their molar masses: `e * mm`. Thus, if your capacity limitation has units of g/gDW, then the molar masses must have units of g/mmol (= kDa). + +### Capacity limitation + +# The capacity limitation usually denotes an upper bound of protein available to +# the cell. + +total_enzyme_capacity = 50.0 # mg of enzyme/gDW + +### Running a basic enzyme constrained model + +# With all the parameters specified, we can directly use the enzyme constrained +# convenience function to run enzyme constrained FBA in one shot: + +ec_solution = enzyme_constrained_flux_balance_analysis( + model; + reaction_isozymes, + gene_product_molar_masses = ecoli_core_gene_product_masses, + capacity = total_enzyme_capacity, + optimizer = GLPK.Optimizer, +) + +#src these values should be unique (glucose transporter is the only way to get carbon into the system) +@test isapprox(ec_solution.objective, 0.706993382849705, atol = TEST_TOLERANCE) #src +@test isapprox(ec_solution.gene_product_capacity, 50.0, atol = TEST_TOLERANCE) #src +@test isapprox(ec_solution.fluxes.EX_glc__D_e, -10, atol = TEST_TOLERANCE) #src +@test isapprox( #src + ec_solution.gene_product_amounts.b2417, #src + 0.011875920383431717, #src + atol = TEST_TOLERANCE, #src +) #src diff --git a/docs/src/examples/05a_fba.jl b/docs/src/examples/05a_fba.jl deleted file mode 100644 index ed9e01c06..000000000 --- a/docs/src/examples/05a_fba.jl +++ /dev/null @@ -1,53 +0,0 @@ -# # Flux balance analysis (FBA) - -# We will use [`flux_balance_analysis`](@ref) and several related functions to find the optimal flux in the *E. coli* "core" model. - -# If it is not already present, download the model and load the package: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA - -model = load_model("e_coli_core.xml") - -# To perform any optimization-based analysis, we need to use a linear programming -# solver (also called an optimizer). Any of the [`JuMP.jl`-supported -# optimizers](https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers) -# will work. Here, we will demonstrate -# [`Tulip.jl`](https://github.com/ds4dm/Tulip.jl) and -# [GLPK](https://www.gnu.org/software/glpk/); other solvers will likely work just as -# well. - -using Tulip - -solved_model = flux_balance_analysis(model, Tulip.Optimizer) - -# `solved_model` is now an instance of optimized JuMP model. To get the -# variable values out manually, we can use `JuMP.value` function. Flux variables -# are stored as vector `x`: - -using JuMP -value.(solved_model[:x]) - -# To simplify things, there is a variant of the FBA function that does this for -# us automatically: - -flux_balance_analysis_vec(model, Tulip.Optimizer) - -# Likewise, there is another variant that returns the fluxes annotated by -# reaction names, in a dictionary: - -flux_balance_analysis_dict(model, Tulip.Optimizer) - -# Switching solvers is easy, and may be useful in case we need advanced -# functionality or performance present only in certain solvers. To switch to -# GLPK, we simply load the package and use a different optimizer to run the -# analysis: - -using GLPK -flux_balance_analysis_dict(model, GLPK.Optimizer) - -# To get a shortened but useful overview of what was found in the analysis, you -# can use [`flux_summary`](@ref) function: -flux_summary(flux_balance_analysis_dict(model, GLPK.Optimizer)) diff --git a/docs/src/examples/05b_fba_mods.jl b/docs/src/examples/05b_fba_mods.jl deleted file mode 100644 index 8beff7a04..000000000 --- a/docs/src/examples/05b_fba_mods.jl +++ /dev/null @@ -1,38 +0,0 @@ - -# # Extending FBA with modifications - -# It is often desirable to add a slight modification to the problem before -# performing the analysis, to see e.g. differences of the model behavior caused -# by the change introduced. -# -# First, let us load everything that will be required: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK, Tulip, JuMP - -model = load_model("e_coli_core.xml") - -# `COBREXA.jl` supports [many modifications](../concepts/2_modifications.md), -# which include changing objective sense, optimizer attributes, flux -# constraints, optimization objective, reaction and gene knockouts, and others. -# These modifications are applied to the optimization built within the supplied -# optimizer (in this case GLPK) in order as they are specified. User needs to -# manually ensure that the modification ordering is sensible. - -# The following example applies multiple different modifications to the *E. -# coli* core model: - -fluxes = flux_balance_analysis_dict( - model, - GLPK.Optimizer; - modifications = [ # modifications are applied in order - change_objective("R_BIOMASS_Ecoli_core_w_GAM"), # maximize production - change_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), # fix an exchange rate - knockout(["b0978", "b0734"]), # knock out two genes - change_optimizer(Tulip.Optimizer), # ignore the above optimizer and switch to Tulip - change_optimizer_attribute("IPM_IterationsLimit", 1000), # customize Tulip - change_sense(JuMP.MAX_SENSE), # explicitly tell Tulip to maximize the objective - ], -) diff --git a/docs/src/examples/06-mmdf.jl b/docs/src/examples/06-mmdf.jl new file mode 100644 index 000000000..df3e672c9 --- /dev/null +++ b/docs/src/examples/06-mmdf.jl @@ -0,0 +1,127 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Thermodynamic models + +using COBREXA + +# Here we will solve the max min driving force analysis problem using the +# glycolysis pathway of *E. coli*. In essence, the method attempts to find +# metabolite concentrations (NB: not fluxes) that maximize the smallest +# thermodynamic driving force through each reaction. See Noor, et al., "Pathway +#thermodynamics highlights kinetic obstacles in central metabolism.", PLoS +#computational biology, 2014, for more details. + +# To do this, we will first need a model that includes glycolysis, which we can +# download if it is not already present. + +import Downloads: download + +#TODO use AFBCMs functionality +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# Additionally to COBREXA, and the model format package, we will need a solver +# -- let's use GLPK here: + +using COBREXA +import JSONFBCModels +import GLPK + +model = load_model("e_coli_core.json") + +# ## Thermodynamic data + +# We will need ΔᵣG⁰ data for each reaction we want to include in the +# thermodynamic model. To generate this data manually, go to +# https://equilibrator.weizmann.ac.il/. To generate automatically, you may use +# the eQuilibrator.jl package. + +reaction_standard_gibbs_free_energies = Dict{String,Float64}( + "ENO" => -3.8108376097261782, + "FBA" => 23.376920310319235, + "GAPD" => 0.5307809794271634, + "GLCpts" => -45.42430981510088, + "LDH_D" => 20.04059765689044, + "PFK" => -18.546314942995934, + "PGI" => 2.6307087407442395, + "PGK" => 19.57192102020454, + "PGM" => -4.470553692565886, + "PYK" => -24.48733600711958, + "TPI" => 5.621932460512994, +) + + +# (The units of the energies are kJ/mol.) + +# ## Running basic max min driving force analysis + +# If a reference flux is not specified, it is assumed that every reaction in the +# model should be included in the thermodynamic model, and that each reaction +# proceeds in the forward direction. This is usually not intended, and can be +# prevented by inputting a reference flux dictionary as shown below. This +# dictionary can be a flux solution, the sign of each flux is used to determine +# if the reaction runs forward or backward. + +# ## Using a reference solution + +# Frequently it is useful to check the max-min driving force of a specific FBA +# solution. In this case, one is usually only interested in a subset of all the +# reactions in a model. These reactions can be specified as a the +# `reference_flux`, to only compute the MMDF of these reactions, and ignore all +# other reactions. + +reference_flux = Dict( + "ENO" => 1.0, + "FBA" => 1.0, + "GAPD" => 1.0, + "GLCpts" => 1.0, + "LDH_D" => -1.0, + "PFK" => 1.0, + "PGI" => 1.0, + "PGK" => -1.0, + "PGM" => -1.0, + "PYK" => 1.0, + "TPI" => 1.0, +) + +#!!! warning "Only the signs are extracted from the reference solution" +# It is most convenient to pass a flux solution into `reference_flux`, but +# take care to round fluxes near 0 to their correct sign if they should be +# included in the resultant thermodynamic model. Otherwise, remove them from +# reference flux input. + +# ## Solving the MMDF problem + +mmdf_solution = max_min_driving_force_analysis( + model, + reaction_standard_gibbs_free_energies; + reference_flux, + concentration_ratios = Dict( + "atp" => ("atp_c", "adp_c", 10.0), + "nadh" => ("nadh_c", "nad_c", 0.13), + ), + proton_metabolites = ["h_c", "h_e"], + water_metabolites = ["h2o_c", "h2o_e"], + concentration_lower_bound = 1e-6, # M + concentration_upper_bound = 1e-1, # M + T = 298.15, # Kelvin + R = 8.31446261815324e-3, # kJ/K/mol + optimizer = GLPK.Optimizer, +) + +# TODO verify correctness +@test isapprox(mmdf_solution.min_driving_force, 2.79911, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/06_fva.jl b/docs/src/examples/06_fva.jl deleted file mode 100644 index 6e548af8f..000000000 --- a/docs/src/examples/06_fva.jl +++ /dev/null @@ -1,89 +0,0 @@ -# # Flux variability analysis (FVA) - -# Here we will use [`flux_variability_analysis`](@ref) to analyze the *E. coli* -# core model. - -# As usual, if not already present, download the model and load the required -# packages. We picked the GLPK solver, but others may work as well: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# The FVA implementation in [`flux_variability_analysis`](@ref) returns -# maximized and minimized reaction fluxes in a 2-column matrix. -# The bounds parameter function here (constructed with -# [`objective_bounds`](@ref)) sets that the objective value is allowed to vary -# by 1% from the optimum found by FBA on the same model: - -flux_variability_analysis(model, GLPK.Optimizer; bounds = objective_bounds(0.99)) - -# (You may also use [`gamma_bounds`](@ref).) - -# ## Detailed variability analysis with modifications -# -# A dictionary-returning variant in [`flux_variability_analysis_dict`](@ref), -# returns the result in a slightly more structured way. At the same time, we -# can specify additional [modifications](../concepts/2_modifications.md) to be -# applied to the model: - -min_fluxes, max_fluxes = flux_variability_analysis_dict( - model, - GLPK.Optimizer; - bounds = objective_bounds(0.99), - modifications = [ - change_constraint("R_EX_glc__D_e"; lb = -10, ub = -10), - change_constraint("R_EX_o2_e"; lb = 0.0, ub = 0.0), - ], -) - -# The dictionaries can be easily used to explore the whole state of the model -# when certain reactions are maximized or minimized. For example, we can take -# the maximal acetate exchange flux when the acetate exchange is maximized: - -max_fluxes["R_EX_ac_e"]["R_EX_ac_e"] - -# We can also check that the modifications really had the desired effect on -# oxygen consumption: - -max_fluxes["R_EX_ac_e"]["R_O2t"] - -# ...and see how much carbon dioxide would produced under at the given -# metabolic extreme: - -max_fluxes["R_EX_ac_e"]["R_EX_co2_e"] - - -# ## Summarizing the flux variability -# -# A convenience function [`flux_variability_summary`](@ref) is able to display -# this information in a nice overview: -flux_variability_summary((min_fluxes, max_fluxes)) - -# ## Retrieving details about FVA output -# -# Parameter `ret` of [`flux_variability_analysis`](@ref) can be used to extract -# specific pieces of information from the individual solved (minimized and -# maximized) optimization problems. Here we show how to extract the value of -# biomass "growth" along with the minimized/maximized reaction flux. - -# First, find the index of biomass reaction in all reactions -biomass_idx = first(indexin(["R_BIOMASS_Ecoli_core_w_GAM"], reactions(model))) - -# Now run the FVA: -vs = flux_variability_analysis( - model, - GLPK.Optimizer; - bounds = objective_bounds(0.50), # objective can vary by up to 50% of the optimum - modifications = [ - change_constraint("R_EX_glc__D_e"; lb = -10, ub = -10), - change_constraint("R_EX_o2_e"; lb = 0.0, ub = 0.0), - ], - ret = optimized_model -> ( - COBREXA.JuMP.objective_value(optimized_model), - COBREXA.JuMP.value(optimized_model[:x][biomass_idx]), - ), -) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl new file mode 100644 index 000000000..b29b09516 --- /dev/null +++ b/docs/src/examples/07-loopless-models.jl @@ -0,0 +1,48 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Loopless flux balance analysis (ll-FBA) + +# Here we wil add loopless constraints to a flux balance model to ensure that +# the resultant solution is thermodynamically consistent. As before, we will use +# the core *E. coli* model, which we can download using +# [`download_model`](@ref): + +using COBREXA + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +# Additionally to COBREXA and the JSON model format package. We will also need a +# solver that can solve mixed interger linear programs like GLPK. + +import JSONFBCModels +import GLPK + +model = load_model("e_coli_core.json") + +# ## Running a loopless FBA (ll-FBA) + +# One can directly use `loopless_flux_balance_analysis` to solve an FBA problem +# based on `model` where loopless constraints are added to all fluxes. This is +# the direct approach. + +solution = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) + +@test isapprox(solution.objective, 0.8739215069684303, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/07_gene_deletion.jl b/docs/src/examples/07_gene_deletion.jl deleted file mode 100644 index 6362ea7f9..000000000 --- a/docs/src/examples/07_gene_deletion.jl +++ /dev/null @@ -1,113 +0,0 @@ -# # Gene knockouts - -# Here we will use the [`knockout`](@ref) function to modify the optimization -# model before solving, in order to simulate genes knocked out. We can pass -# [`knockout`](@ref) to many analysis functions that support parameter -# `modifications`, including [`flux_balance_analysis`](@ref), -# [`flux_variability_analysis`](@ref), and others. - -# ## Deleting a single gene - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# First, let's compute the "original" flux, with no knockouts. -original_flux = flux_balance_analysis_dict(model, GLPK.Optimizer); - -# One can find gene IDs that we can knock out using [`genes`](@ref) and -# [`gene_name`](@ref) functions: -genes(model) -# It is possible to sort the genes by gene name to allow easier lookups: -sort(gene_name.(Ref(model), genes(model)) .=> genes(model)) - -# Compute the flux with a genes knocked out: -flux_with_knockout = - flux_balance_analysis_dict(model, GLPK.Optimizer, modifications = [knockout("G_b3236")]) - -# We can see there is a small decrease in production upon knocking out the gene: -biomass_id = "R_BIOMASS_Ecoli_core_w_GAM" -flux_with_knockout[biomass_id] / original_flux[biomass_id] - -# Similarly, we can explore how the flux variability has changed once the gene -# is knocked out: -variability_with_knockout = - flux_variability_analysis(model, GLPK.Optimizer, modifications = [knockout("G_b3236")]) - -# ## Knocking out multiple genes - -# Multiple genes can be knocked out by simply passing a vector of genes to the -# knockout modification. This knocks out all genes that can run the FBA -# reaction: - -reaction_gene_association(model, "R_FBA") -# -flux_with_double_knockout = flux_balance_analysis_dict( - model, - GLPK.Optimizer, - modifications = [knockout(["G_b2097", "G_b1773", "G_b2925"])], -) -# -flux_with_double_knockout[biomass_id] / original_flux[biomass_id] - -# ## Processing all single gene knockouts -# -# Function [`screen`](@ref) provides a parallelizable and extensible way to run -# the flux balance analysis with the knockout over all genes: - -knockout_fluxes = screen( - model, - args = tuple.(genes(model)), - analysis = (m, gene) -> begin - res = flux_balance_analysis_dict(m, GLPK.Optimizer, modifications = [knockout(gene)]) - if !isnothing(res) - res[biomass_id] - end - end, -) - -# It is useful to display the biomass growth rates of the knockout models -# together with the gene name: -sort(gene_name.(Ref(model), genes(model)) .=> knockout_fluxes, by = first) - -# ## Processing all multiple-gene deletions -# -# ### Double gene knockouts -# -# Since we can generate any kind of argument matrix for [`screen`](@ref) to -# process, it is straightforward to generate the matrix of all double gene -# knockouts and let the function process it. This computes the biomass -# production of all double-gene knockouts: - -gene_groups = [[g1, g2] for g1 in genes(model), g2 in genes(model)]; -double_knockout_fluxes = screen( - model, - args = tuple.(gene_groups), - analysis = (m, gene_groups) -> begin - res = flux_balance_analysis_dict( - m, - GLPK.Optimizer, - modifications = [knockout(gene_groups)], - ) - if !isnothing(res) - res[biomass_id] - end - end, -) - -# The results can be converted to an easily scrutinizable form as follows: -reshape([gene_name.(Ref(model), p) for p in gene_groups] .=> double_knockout_fluxes, :) - -# ### Triple gene knockouts (and others) -# -# We can extend the same analysis to triple or other gene knockouts by -# generating a different array of gene pairs. For example, one can generate -# gene_groups for triple gene deletion screening: -gene_groups = [[g1, g2, g3] for g1 in genes(model), g2 in genes(model), g3 in genes(model)]; - -#md # !!! warning Full triple gene deletion analysis may take a long time to compute. -#md # We may use parallel processing with [`screen`](@ref) to speed up the -#md # analysis. Alternatively, process only a subset of the genes triples. diff --git a/docs/src/examples/07_restricting_reactions.jl b/docs/src/examples/07_restricting_reactions.jl deleted file mode 100644 index 5a9261491..000000000 --- a/docs/src/examples/07_restricting_reactions.jl +++ /dev/null @@ -1,141 +0,0 @@ - -# # Restricting and disabling individual reactions - -# Here, we show several methods how to explore the effect of disabling or choking the reactions in the models. - -# First, download the demonstration data and load the packages as usual: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model(StandardModel, "e_coli_core.json") - -# ## Disabling a reaction - -# There are several possible ways to disable a certain reaction in the model. -# The easiest way is to use [`change_bound`](@ref) or [`change_bounds`](@ref) -# to create a variant of the model that has the corresponding bounds modified -# (or, alternatively, a pipeable "variant" version -# [`with_changed_bound`](@ref)). -# -# Alternatively, you could utilize [`change_constraint`](@ref) as a -# modification that acts directly on the JuMP optimization model. That may be -# useful if you first apply some kind of complicated constraint scheme -# modification, such as [`add_loopless_constraints`](@ref). -# -# In turn, in the simple case, the following 2 ways of disabling the FBA -# reaction are equivalent. The first, making a variant of the model structure, -# might be slightly preferred because it better composes with other changes; -# the second does not compose as well but may be more efficient (and thus -# faster) in certain situations: - -flux1 = flux_balance_analysis_vec( - model |> with_changed_bound("FBA", lower = 0.0, upper = 0.0), - GLPK.Optimizer, -); - -flux2 = flux_balance_analysis_vec( - model, - GLPK.Optimizer, - modifications = [change_constraint("FBA", lb = 0.0, ub = 0.0)], -); - -# The solutions should not differ a lot: -sum((flux1 .- flux2) .^ 2) - -# ## Restricting a reaction -# -# Quite naturally, you can restruct the reaction to a limited flow, simulating -# e.g. nutrient deficiency: - -original_flux = flux_balance_analysis_dict(model, GLPK.Optimizer); - -restricted_flux = flux_balance_analysis_dict( - model, - GLPK.Optimizer, - modifications = [change_constraint("EX_o2_e", lb = -0.1, ub = 0.0)], -); - -# The growth in the restricted case is, expectably, lower than the original one: -original_flux["BIOMASS_Ecoli_core_w_GAM"], restricted_flux["BIOMASS_Ecoli_core_w_GAM"] - -# ## Screening for sensitive and critical reactions -# -# Using higher-order analysis scheduling functions (in particular -# [`screen`](@ref)), you can easily determine which reactions play a crucial -# role for the model viability and which are not very important. - -# We can take all reactions where the flux is not zero: - -running_reactions = [(rid, x) for (rid, x) in original_flux if abs(x) > 1e-3] - -# ...and choke these reactions to half that flux, computing the relative loss -# of the biomass production:: - -screen( - model, - variants = [ - [with_changed_bound(rid, lower = -0.5 * abs(x), upper = 0.5 * abs(x))] for - (rid, x) in running_reactions - ], - args = running_reactions, - analysis = (m, rid, _) -> - rid => - flux_balance_analysis_dict(m, GLPK.Optimizer)["BIOMASS_Ecoli_core_w_GAM"] / - original_flux["BIOMASS_Ecoli_core_w_GAM"], -) - -# (You may notice that restricting the ATP maintenance pseudo-reaction (`ATPM`) -# had a mildly surprising effect of actually increasing the biomass production -# by a few percent. That is because the cells are not required to produce ATP -# to survive and may invest the nutrients and energy elsewhere.) - -# ## Screening with reaction combinations - -# The same analysis can be scaled up to screen for combinations of critical -# reactions, giving possibly more insight into the redundancies in the model: - -running_reaction_combinations = [ - (rid1, rid2, x1, x2) for (rid1, x1) in running_reactions, - (rid2, x2) in running_reactions -] - -biomass_mtx = screen( - model, - variants = [ - [ - with_changed_bound(rid1, lower = -0.5 * abs(x1), upper = 0.5 * abs(x1)), - with_changed_bound(rid2, lower = -0.5 * abs(x2), upper = 0.5 * abs(x2)), - ] for (rid1, rid2, x1, x2) in running_reaction_combinations - ], - analysis = m -> - flux_balance_analysis_dict(m, GLPK.Optimizer)["BIOMASS_Ecoli_core_w_GAM"] / - original_flux["BIOMASS_Ecoli_core_w_GAM"], -) - -# Finally, let's plot the result: - -using CairoMakie, Clustering - -order = - hclust([ - sum((i .- j) .^ 2) for i in eachcol(biomass_mtx), j in eachcol(biomass_mtx) - ]).order - -labels = first.(running_reactions)[order]; -positions = collect(eachindex(labels)) - -f = Figure(fontsize = 8) -ax = Axis(f[1, 1], xticks = (positions, labels), yticks = (positions, labels)) -heatmap!(ax, positions, positions, biomass_mtx[order, order]) -ax.xticklabelrotation = π / 3 -ax.xticklabelalign = (:right, :center) -ax.yticklabelrotation = π / 6 -ax.yticklabelalign = (:right, :center) -f - -# Remember that [`screen`](@ref) can be parallelized just [by supplying worker -# IDs](../distributed/1_functions.md). Use that to gain significant speedup -# with analyses of larger models. diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl new file mode 100644 index 000000000..daa293827 --- /dev/null +++ b/docs/src/examples/08-community-models.jl @@ -0,0 +1,115 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + +# # Community FBA models + +using COBREXA + +# Here we will construct a community FBA model of two *E. coli* "core" models +# that can interact by exchanging selected metabolites. To do this, we will need +# the model, which we can download if it is not already present. + +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# Additionally to COBREXA and the model format package, we will need a solver +# -- let's use GLPK here: + +import JSONFBCModels +import GLPK +import ConstraintTrees as C + +model = load_model("e_coli_core.json") + +# Community models work by joining its members together through their exchange +# reactions, weighted by the abundance of each microbe. These exchange reactions +# are then linked to an environmental exchange. For more theoretical details, +# see "Gottstein, et al, 2016, Constraint-based stoichiometric modelling from +# single organisms to microbial communities, Journal of the Royal Society +# Interface". + +# ## Building a community of two *E. coli*s + +# Here we will construct a simple community of two interacting microbes. To do +# this, we need to import the models. We will represent the models only as +# constraint trees, because it is easier to build the model explicitly than +# rely on an opaque one-shot function. + +ecoli1 = flux_balance_constraints(model, interface = :sbo) +ecoli2 = flux_balance_constraints(model, interface = :sbo) + +# Since the models are usually used in a mono-culture context, the glucose input +# for each individual member is limited. We need to undo this limitation, and +# rather rely on the constrained environmental exchange reaction (and the bounds +# we set for it earlier). +ecoli1.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0) +ecoli2.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0) + +# To make the community interesting, we can limit different reactions in both +# members to see how the models cope together: +ecoli1.fluxes.CYTBD.bound = C.Between(-10.0, 10.0) +ecoli2.fluxes.ACALD.bound = C.Between(-5.0, 5.0) + +# Because we created the trees with interfaces, we can connect them easily to +# form a new model with the interface. For simplicity, we use the +# interface-scaling functionality of [`interface_constraints`](@ref +# ConstraintTrees.interface_constraints) to bring in cFBA-like community member +# abundances: + +cc = interface_constraints( + :bug1 => (ecoli1, ecoli1.interface, 0.2), + :bug2 => (ecoli2, ecoli2.interface, 0.8), +) + +# To make the community behave as expected, we need to force equal (scaled) +# growth of all members: + +cc *= + :equal_growth^equal_value_constraint( + cc.bug1.fluxes.BIOMASS_Ecoli_core_w_GAM, + cc.bug2.fluxes.BIOMASS_Ecoli_core_w_GAM, + ) + +# Now we can simulate the community growth by optimizing the new "interfaced" +# biomass: + +optimized_cc = optimized_constraints( + cc, + objective = cc.interface.biomass.BIOMASS_Ecoli_core_w_GAM.value, + optimizer = GLPK.Optimizer, +) + +# We can now e.g. observe the differences in individual pairs of exchanges: + +C.zip( + tuple, + optimized_cc.bug1.interface.exchanges, + optimized_cc.bug2.interface.exchanges, + Tuple{Float64,Float64}, +) + +@test isapprox( #src + optimized_cc.interface.biomass.BIOMASS_Ecoli_core_w_GAM, #src + 15.9005, #src + atol = TEST_TOLERANCE, #src +) #src +@test isapprox( #src + optimized_cc.bug1.fluxes.BIOMASS_Ecoli_core_w_GAM, #src + optimized_cc.bug2.fluxes.BIOMASS_Ecoli_core_w_GAM, #src + atol = TEST_TOLERANCE, #src +) #src diff --git a/docs/src/examples/08_pfba.jl b/docs/src/examples/08_pfba.jl deleted file mode 100644 index c10fbfd02..000000000 --- a/docs/src/examples/08_pfba.jl +++ /dev/null @@ -1,74 +0,0 @@ -# # Parsimonious flux balance analysis (pFBA) - -# Parsimonious flux balance analysis attempts to find a realistic flux of a -# model, by trying to minimize squared sum of all fluxes while maintaining the -# reached optimum. COBREXA.jl implements it in function -# [`parsimonious_flux_balance_analysis`](@ref) (accompanied by vector- and -# dictionary-returning variants -# [`parsimonious_flux_balance_analysis_vec`](@ref) and -# [`parsimonious_flux_balance_analysis_dict`](@ref)). -# -# As usual, we demonstrate the functionality on the *E. coli* model: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, Tulip, Clarabel - -model = load_model("e_coli_core.xml") - -# Because the parsimonious objective is quadratic, we need a an optimizer -# capable of solving quadratic programs. -# -# As the simplest choice, we can use -# [`Clarabel.jl`](https://osqp.org/docs/get_started/julia.html), but any any -# [`JuMP.jl`-supported -# optimizer](https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers) -# that supports quadratic programming will work. - -#md # !!! note "Note: Clarabel can be sensitive" -#md # We recommend reading the documentation of `Clarabel` before using it, since -#md # it may give inconsistent results depending on what settings -#md # you use. Commercial solvers like `Gurobi`, `Mosek`, `CPLEX`, etc. -#md # require less user engagement. - -# Running of basic pFBA is perfectly analogous to running of [FBA](05a_fba.md) -# and other analyses. We add several modifications that improve the solution -# (using functions [`silence`](@ref), and -# [`change_optimizer_attribute`](@ref)), and fix the glucose exchange (using -# [`change_constraint`](@ref)) in order to get a more reasonable result: - -fluxes = parsimonious_flux_balance_analysis_dict( - model, - Clarabel.Optimizer; - modifications = [ - silence, # optionally silence the optimizer (Clarabel is very verbose by default) - change_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), # fix glucose consumption rate - ], -) - -# ## Using different optimizers for linear and quadratic problems -# -# It is quite useful to use specialized optimizers for specialized tasks in -# pFBA. In particular, one would usually require to get a precise solution from -# the linear programming part (where the precision is achievable), and trade -# off a little precision for vast improvements in computation time in the -# quadratic programming part. -# -# In pFBA, we can use the `modifications` and `qp_modifications` parameters to -# switch and parametrize the solvers in the middle of the process, which allows -# us to implement precisely that improvement. We demonstrate the switching on a -# vector-returning variant of pFBA: - -flux_vector = parsimonious_flux_balance_analysis_vec( - model, - Tulip.Optimizer; # start with Tulip - modifications = [ - change_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), - change_optimizer_attribute("IPM_IterationsLimit", 500), # we may change Tulip-specific attributes here - ], - qp_modifications = [ - change_optimizer(Clarabel.Optimizer), # now switch to Clarabel (Tulip wouldn't be able to finish the computation) - silence, # and make it quiet. - ], -) diff --git a/docs/src/examples/09_loopless.jl b/docs/src/examples/09_loopless.jl deleted file mode 100644 index 5bb6a2186..000000000 --- a/docs/src/examples/09_loopless.jl +++ /dev/null @@ -1,45 +0,0 @@ -# # Loopless FBA - -# Here we will use [`flux_balance_analysis`](@ref) and -# [`flux_variability_analysis`](@ref) to analyze a toy model of *E. coli* that -# is constrained in a way that removes all thermodynamically infeasible loops in -# the flux solution. For more details about the algorithm, see [Schellenberger, -# and, Palsson., "Elimination of thermodynamically infeasible loops in -# steady-state metabolic models.", Biophysical Journal, -# 2011](https://doi.org/10.1016/j.bpj.2010.12.3707). - -# If it is not already present, download the model: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# In COBREXA.jl, the Loopless FBA is implemented as a modification of the -# normal FBA, called [`add_loopless_constraints`](@ref). -# We use GLPK optimizer here, because the loopless constraints add integer -# programming into the problem. Simpler solvers (such as Tulip) may not be able -# to solve the mixed integer-linear (MILP) programs. - -loopless_flux = flux_balance_analysis_vec( - model, - GLPK.Optimizer, - modifications = [add_loopless_constraints()], -) - -# The representation is particularly convenient since it allows to also explore -# other properties of loopless models, such as variability and parsimonious -# balance, as well as other analyses that accept `modifications` parameter: - -loopless_variability = flux_variability_analysis( - model, - GLPK.Optimizer, - modifications = [add_loopless_constraints()], -) - -# For details about the loopless method, refer to Schellenberger, Jan, Nathan -# E. Lewis, and Bernhard Ø. Palsson: "Elimination of thermodynamically -# infeasible loops in steady-state metabolic models." *Biophysical journal* -# 100, no. 3 (2011), pp. 544-553. diff --git a/docs/src/examples/10_crowding.jl b/docs/src/examples/10_crowding.jl deleted file mode 100644 index bd4f40a29..000000000 --- a/docs/src/examples/10_crowding.jl +++ /dev/null @@ -1,51 +0,0 @@ -# # FBA with crowding - -# Here we will use [`flux_balance_analysis`](@ref) to explore the metabolism of -# the toy *E. coli* model that additionally respects common protein crowding -# constraints. In particular, the model is limited by the amount of protein -# required to run certain reactions. If that data is available, the predictions -# are accordingly more realistic. See [Beg, et al., "Intracellular crowding -# defines the mode and sequence of substrate uptake by Escherichia coli and -# constrains its metabolic activity.", Proceedings of the National Academy of -# Sciences,2007](https://doi.org/10.1073/pnas.0609845104) for more details. -# -# As usual, the same model modification can be transparently used with many -# other analysis functions, including [`flux_variability_analysis`](@ref) and -# [`parsimonious_flux_balance_analysis`](@ref). - -# Let's starting with loading the models and packages. - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, Tulip - -model = load_model("e_coli_core.xml") - -# To describe the protein crowding, each of the enzymes that catalyze the -# reactions gets an associated weight per unit of reaction conversion rate. The -# total sum of all weights multiplied by the flux in the model must be lower -# than 1. -# -# The weights are prepared in a dictionary; for simplicity we assume that the -# relative weight of all enzymes is random between 0.002 and 0.005. -# enzymes are of the same size. Reactions that are not present in the -# dictionary (typically exchanges) are ignored. - -import Random -Random.seed!(1) # for repeatability of random numbers below - -rid_crowding_weight = Dict( - rid => 0.002 + 0.003 * rand() for rid in reactions(model) if - !looks_like_biomass_reaction(rid) && !looks_like_exchange_reaction(rid) -) - -# With this, the crowding constraints are added with modification -# [`add_crowding_constraints`](@ref): -loopless_crowding_fluxes = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [add_crowding_constraints(rid_crowding_weight)], -) -# -flux_summary(loopless_crowding_fluxes) diff --git a/docs/src/examples/11_growth.jl b/docs/src/examples/11_growth.jl deleted file mode 100644 index 35be46d46..000000000 --- a/docs/src/examples/11_growth.jl +++ /dev/null @@ -1,132 +0,0 @@ -# # Growth media analysis - -# Nutrient availability is a major driving factor for growth of microorganisms -# and energy production in cells. Here, we demonstrate two main ways to examine -# the nutrient consumption with COBREXA.jl: Simulating deficiency of nutrients, -# and finding the minimal flux of nutrients required to support certain model -# output. - -# As always, we work on the toy model of *E. coli*: - -using COBREXA, GLPK - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -model = load_model(StandardModel, "e_coli_core.xml") - -# ## What nutrients does my model need to grow? - -# The models usually ingest nutrients through exchange reactions. By changing -# the bounds on the exchange reactions, you can limit the intake of the -# nutrients and thus simulate the nutrient deficiency. If applied -# programatically to multiple exchanges, this can give you a good overview of -# what nutrients impact the model most. -# -# To check the viability of a single nutrient, you can simply change a bound on -# a selected exchange reaction and simulate the model with a limited amount. - -biomass = "R_BIOMASS_Ecoli_core_w_GAM" - -model_limited = change_bound(model, "R_EX_glc__D_e", lower = -1.0) - -#md # !!! tip "Exchange directions" -#md # By a convention, the direction of exchange reaction usually goes from the -#md # model into the environment, representing the "production". Limiting the -#md # intake thus happens by disabling the "negative production", i.e., placing -#md # a lower bound. - -original_production = flux_balance_analysis_dict(model, GLPK.Optimizer)[biomass] -limited_production = flux_balance_analysis_dict(model_limited, GLPK.Optimizer)[biomass] - -original_production, limited_production - -# Function [`flux_summary`](@ref) can help with quickly spotting what has -# changed: - -flux_summary(flux_balance_analysis_dict(model_limited, GLPK.Optimizer)) - -# Similarly, you can check that the model can survive without oxygen, at the cost -# of switching the metabolism to ethanol production: - -flux_summary( - flux_balance_analysis_dict( - change_bound(model, "R_EX_o2_e", lower = 0.0), - GLPK.Optimizer, - ), -) - -# The effect of all nutrients on the metabolism can be scanned using [`screen`](@ref). The [`change_bound`](@ref) function is, for this purpose, packed in a variant specified [`with_changed_bound`](@ref): - -exchanges = filter(looks_like_exchange_reaction, reactions(model)) - -exchanges .=> screen( - model, - variants = [[with_changed_bound(exchange, lower = 0.0)] for exchange in exchanges], - analysis = m -> begin - res = flux_balance_analysis_dict(m, GLPK.Optimizer) - isnothing(res) ? nothing : res[biomass] - end, -) - - -# Similarly to gene knockouts, you can also examine the effect of combined -# nutrient deficiencies. To obtain a more interesting result, we may examine -# the effect of slight deficiencies of pairs of intake metabolites. For -# simplicity, we show the result only on a small subset of the exchanges: - -selected_exchanges = [ - "R_EX_pi_e", - "R_EX_gln__L_e", - "R_EX_nh4_e", - "R_EX_pyr_e", - "R_EX_fru_e", - "R_EX_glu__L_e", - "R_EX_glc__D_e", - "R_EX_o2_e", -] - -screen( - model, - variants = [ - [with_changed_bounds([e1, e2], lower = [-1.0, -0.1])] for e1 in selected_exchanges, - e2 in selected_exchanges - ], - analysis = m -> begin - res = flux_balance_analysis_dict(m, GLPK.Optimizer) - isnothing(res) ? nothing : res[biomass] - end, -) - -# The result describes combinations of nutrient deficiencies -- the nutrient -# that corresponds to the row is mildly deficient (limited to uptake 1.0), and -# the one that corresponds to the column is severely limited (to uptake 0.1). - -#md # !!! tip "Screening can be easily parallelized" -#md # To speed up larger analyses, remember that execution of [`screen`](@ref) -#md # can be [parallelized to gain speedup](../distributed/1_functions.md). Parallelization in `screen` is optimized to avoid -#md # unnecessary data transfers that may occur when using trivial `pmap`. - -# ## What is the minimal flux of nutrients for my model to grow? - -# You can compute the minimal flux (i.e., mass per time) of required nutrients -# by constraining the model growth to a desired lower bound, and then optimize -# the model with an objective that minimizes intake of all exchanges (i.e., -# given the directionality convention of the exchanges, actually maximizes the -# flux through all exchange reactions along their direction). - -model_with_bounded_production = change_bound(model, biomass, lower = 0.1) #minimum required growth - -minimal_intake_production = flux_balance_analysis_dict( - model_with_bounded_production, - GLPK.Optimizer, - modifications = [change_objective(exchanges)], -); - -# Metabolite "cost" data may be supplemented using the `weights` argument of -# [`change_objective`](@ref), to reflect e.g. the different molar masses or -# energetic values of different nutrients. -# -# In our simple case, we obtain the following minimal required intake: - -flux_summary(minimal_intake_production) diff --git a/docs/src/examples/12_mmdf.jl b/docs/src/examples/12_mmdf.jl deleted file mode 100644 index f16138922..000000000 --- a/docs/src/examples/12_mmdf.jl +++ /dev/null @@ -1,196 +0,0 @@ - -# # Maximum-minimum driving force analysis - -# Here, we use the max-min driving force analysis (MMDF) to find optimal -# concentrations for the metabolites in glycolysis to ensure that the smallest -# driving force across all the reactions in the model is as large as possible. -# The method is described in more detail by [Flamholz, et al., "Glycolytic -# strategy as a tradeoff between energy yield and protein cost.", Proceedings of -# the National Academy of Sciences, -# 2013](https://doi.org/10.1073/pnas.1215283110). - -# We start as usual, with loading models and packages: - -using COBREXA, GLPK - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -model = load_model("e_coli_core.json") - -# For MMDF to work, we need thermodynamic data about the involved reactions. -# -# In particular, we will use reaction Gibbs free energies (ΔG⁰) that can be -# obtained e.g. from [eQuilibrator](https://equilibrator.weizmann.ac.il/) -# (possibly using the existing [Julia -# wrapper](https://github.com/stelmo/eQuilibrator.jl) that allows you to -# automate this step in Julia). -# -# Here, we have gathered a dictionary that maps the reaction IDs to calculated -# Gibbs free energy of reaction for each metabolic reaction (including the -# transporters). The units of the measurements are not crucial for the -# computation, but we use the usual kJ/mol for consistency. - -reaction_standard_gibbs_free_energies = Dict( - "ACALD" => -21.26, - "PTAr" => 8.65, - "ALCD2x" => 17.47, - "PDH" => -34.24, - "PYK" => -24.48, - "CO2t" => 0.00, - "MALt2_2" => -6.83, - "CS" => -39.33, - "PGM" => -4.47, - "TKT1" => -1.49, - "ACONTa" => 8.46, - "GLNS" => -15.77, - "ICL" => 9.53, - "FBA" => 23.37, - "SUCCt3" => -43.97, - "FORt2" => -3.42, - "G6PDH2r" => -7.39, - "AKGDH" => -28.23, - "TKT2" => -10.31, - "FRD7" => 73.61, - "SUCOAS" => -1.15, - "FBP" => -11.60, - "ICDHyr" => 5.39, - "AKGt2r" => 10.08, - "GLUSy" => -47.21, - "TPI" => 5.62, - "FORt" => 13.50, - "ACONTb" => -1.62, - "GLNabc" => -30.19, - "RPE" => -3.38, - "ACKr" => 14.02, - "THD2" => -33.84, - "PFL" => -19.81, - "RPI" => 4.47, - "D_LACt2" => -3.42, - "TALA" => -0.94, - "PPCK" => 10.65, - "ACt2r" => -3.41, - "NH4t" => -13.60, - "PGL" => -25.94, - "NADTRHD" => -0.01, - "PGK" => 19.57, - "LDH_D" => 20.04, - "ME1" => 12.08, - "PIt2r" => 10.41, - "ATPS4r" => -37.57, - "PYRt2" => -3.42, - "GLCpts" => -45.42, - "GLUDy" => 32.83, - "CYTBD" => -59.70, - "FUMt2_2" => -6.84, - "FRUpts2" => -42.67, - "GAPD" => 0.53, - "H2Ot" => 0.00, - "PPC" => -40.81, - "NADH16" => -80.37, - "PFK" => -18.54, - "MDH" => 25.91, - "PGI" => 2.63, - "O2t" => 0.00, - "ME2" => 12.09, - "GND" => 10.31, - "SUCCt2_2" => -6.82, - "GLUN" => -14.38, - "ETOHt2r" => -16.93, - "ADK1" => 0.38, - "ACALDt" => 0.00, - "SUCDi" => -73.61, - "ENO" => -3.81, - "MALS" => -39.22, - "GLUt2r" => -3.49, - "PPS" => -6.05, - "FUM" => -3.42, -); - -# COBREXA implementation of MMDF enforces that `ΔᵣG .* v ≤ 0` (where `v` is the -# flux solution). This is slightly less restrictive than the original -# formulation of MMDF, where all fluxes are enforced to be positive; instead, -# the COBREXA solution needs a pre-existing thermodynamically consistent -# solution that is used as a reference. -# -# We can generate a well-suited reference solution using e.g. the [loopless -# FBA](09_loopless.md): - -flux_solution = flux_balance_analysis_dict( - model, - GLPK.Optimizer; - modifications = [add_loopless_constraints()], -) - -# We can now run the MMDF. -# -# In the call, we specify the metabolite IDs of protons and water so that they -# are omitted from concentration calculations. Also, the water transport -# reaction should typically also be ignored. Additionally, we can fix the -# concentration ratios of certain metabolites directly. -# -# The reason for removing the protons and water from the concentration -# calculations is because the Gibbs free energies of biochemical reactions are -# measured at constant pH in aqueous environments. Allowing the model to change -# the pH would break the assumptions about validity of the thermodynamic -# measurements. - -sol = max_min_driving_force( - model, - reaction_standard_gibbs_free_energies, - GLPK.Optimizer; - flux_solution = flux_solution, - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - concentration_ratios = Dict( - ("atp_c", "adp_c") => 10.0, - ("nadh_c", "nad_c") => 0.13, - ("nadph_c", "nadp_c") => 1.3, - ), - concentration_lb = 1e-6, # 1 uM - concentration_ub = 0.1, # 100 mM - ignore_reaction_ids = [ - "H2Ot", # ignore water transport - ], -) - -sol.mmdf - -#md # !!! note "Note: transporters" -#md # Transporters can be included in MMDF analysis, however water and proton -#md # transporters must be excluded explicitly in `ignore_reaction_ids`. -#md # In turn, the ΔᵣG for these transport reactions -#md # will always be 0. If you do not exclude the transport of the metabolites, -#md # the MMDF will likely only have a zero solution. - -# Finally, we show how the concentrations are optimized to ensure that each -# reaction proceeds "down the hill" (ΔᵣG < 0). We can explore the glycolysis -# pathway reactions: - -glycolysis_pathway = - ["GLCpts", "PGI", "PFK", "FBA", "TPI", "GAPD", "PGK", "PGM", "ENO", "PYK"] - -# We additionally scale the fluxes according to their stoichiometry in the -# pathway. From the output, we can clearly see that metabolite concentrations -# play a large role in ensuring the thermodynamic consistency of in vivo -# reactions. - -using CairoMakie - -standard_dg = cumsum([ - reaction_standard_gibbs_free_energies[rid] * flux_solution[rid] for - rid in glycolysis_pathway -]); -optimal_dg = - cumsum([sol.dg_reactions[rid] * flux_solution[rid] for rid in glycolysis_pathway]); - -f = Figure(); -ax = Axis(f[1, 1], ylabel = "Cumulative ΔG", xticks = (1:10, glycolysis_pathway)); -lines!(ax, 1:10, standard_dg .- first(standard_dg), color = :blue, label = "ΔG⁰"); -lines!(ax, 1:10, optimal_dg .- first(optimal_dg), color = :red, label = "MMDF solution"); -axislegend(ax) -f - -#md # !!! tip "Thermodynamic variability" -#md # As with normal flux variability, thermodynamic constraints in a model also allow a certain amount of parameter selection freedom. -#md # Specialized [`max_min_driving_force_variability`](@ref) can be used to explore the thermodynamic solution space more easily. diff --git a/docs/src/examples/13_moma.jl b/docs/src/examples/13_moma.jl deleted file mode 100644 index 200ccd07d..000000000 --- a/docs/src/examples/13_moma.jl +++ /dev/null @@ -1,54 +0,0 @@ -# # Minimization of metabolic adjustment (MOMA) - -# MOMA allows you to find a feasible solution of the model that is closest (in -# an Euclidean metric) to a reference solution. Often this gives a realistic -# estimate of the organism behavior that has undergone a radical change (such -# as a gene knockout) that prevents it from metabolizing optimally, but the -# rest of the metabolism has not yet adjusted to compensate for the change. - -# The original description of MOMA is by [Segre, Vitkup, and Church, "Analysis -# of optimality in natural and perturbed metabolic networks", Proceedings of the -# National Academy of Sciences, 2002](https://doi.org/10.1073/pnas.232349399). - -# As always, let's start with downloading a model. - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA - -model = load_model(StandardModel, "e_coli_core.xml") - -# MOMA analysis requires solution of a quadratic model, we will thus use Clarabel as the main optimizer. - -using Clarabel - -# We will need a reference solution, which represents the original state of the -# organism before the change. -reference_flux = - flux_balance_analysis_dict(model, Clarabel.Optimizer; modifications = [silence]) - -# As the change here, we manually knock out CYTBD reaction: -changed_model = change_bound(model, "R_CYTBD", lower = 0.0, upper = 0.0); - -# Now, let's find a flux that minimizes the organism's metabolic adjustment for -# this model: -flux_summary( - minimize_metabolic_adjustment_analysis_dict( - changed_model, - reference_flux, - Clarabel.Optimizer; - modifications = [silence], - ), -) - -# For illustration, you can compare the result to the flux that is found by -# simple optimization: - -flux_summary( - flux_balance_analysis_dict( - changed_model, - Clarabel.Optimizer; - modifications = [silence], - ), -) diff --git a/docs/src/examples/14_smoment.jl b/docs/src/examples/14_smoment.jl deleted file mode 100644 index 8f7b16625..000000000 --- a/docs/src/examples/14_smoment.jl +++ /dev/null @@ -1,105 +0,0 @@ -# # sMOMENT - -# sMOMENT algorithm can be used to easily adjust the metabolic activity within -# the cell to respect known enzymatic parameters and enzyme mass constraints -# measured by proteomics and other methods. -# -# The original description from sMOMENT is by [Bekiaris, and Klamt, "Automatic -# construction of metabolic models with enzyme constraints.", BMC -# bioinformatics, 2020](https://doi.org/10.1186/s12859-019-3329-9) -# -# Let's load some packages: - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -using COBREXA, GLPK - -model = load_model("e_coli_core.json") - -# We will necessarily need the enzyme turnover numbers (aka "kcats") and masses -# of the required gene products. You do not necessarily need to know all data -# for the given model, but the more you have, the better the approximation will -# be. -# -# For the demonstration purpose, we will generate the data randomly. In a -# realistic setting, you would input experimental or database-originating data -# here: - -import Random -Random.seed!(1) # repeatability - -gene_product_masses = Dict(genes(model) .=> randn(n_genes(model)) .* 10 .+ 60) - -# We only take the reactions that have gene products (i.e., enzymes) associated with them): -rxns = filter( - x -> - !looks_like_biomass_reaction(x) && - !looks_like_exchange_reaction(x) && - !isnothing(reaction_gene_association(model, x)), - reactions(model), -) - -# The information about each enzyme and its capabilities is stored in an -# [`Isozyme`](@ref) structure. For simplicity, sMOMENT ignores much of the -# information about the multiplicity of required gene products and -# other. - -rxn_isozymes = Dict( - rxn => Isozyme( - Dict(vcat(reaction_gene_association(model, rxn)...) .=> 1), - randn() * 100 + 600, #forward kcat - randn() * 100 + 500, #reverse kcat - ) for rxn in rxns -) - -# In case some of the reactions are missing in `rxn_isozymes`, sMOMENT simply -# ignores them. -# -# Once the data is gathered, we create a model that wraps the original model -# with additional sMOMENT structure: - -smoment_model = - model |> with_smoment( - reaction_isozyme = rxn_isozymes, - gene_product_molar_mass = gene_product_masses, - total_enzyme_capacity = 50.0, - ) - -# (You could alternatively use the [`make_smoment_model`](@ref) to create the -# structure more manually; but [`with_smoment`](@ref) is easier to use e.g. -# with [`screen`](@ref).) - -# In turn, you should have a complete model with unidirectional reactions and -# additional coupling, as specified by the sMOMENT method: - -[stoichiometry(smoment_model); coupling(smoment_model)] - -# the type (SMomentModel) is a model wrapper -- it is a thin additional layer -# that just adds the necessary sMOMENT-relevant information atop the original -# model, which is unmodified. That makes the process very efficient and -# suitable for large-scale data processing. You can still access the original -# "base" model hidden in the SMomentModel using [`unwrap_model`](@ref). - -# Other than that, the [`SMomentModel`](@ref) is a model type like any other, -# and you can run any analysis you want on it, such as FBA: - -flux_balance_analysis_dict(smoment_model, GLPK.Optimizer) - -# (Notice that the total reaction fluxes are reported despite the fact that -# reactions are indeed split in the model! The underlying mechanism is provided -# by [`reaction_flux`](@ref) accessor.) - -# [Variability](06_fva.md) of the sMOMENT model can be explored as such: - -flux_variability_analysis(smoment_model, GLPK.Optimizer, bounds = gamma_bounds(0.95)) - -# ...and a sMOMENT model sample can be obtained [as usual with -# sampling](16_hit_and_run.md): - -( - affine_hit_and_run( - smoment_model, - warmup_from_variability(smoment_model, GLPK.Optimizer), - )' * reaction_flux(smoment_model) -) diff --git a/docs/src/examples/15_gecko.jl b/docs/src/examples/15_gecko.jl deleted file mode 100644 index 030fda407..000000000 --- a/docs/src/examples/15_gecko.jl +++ /dev/null @@ -1,100 +0,0 @@ -# # GECKO - -# GECKO algorithm can be used to easily adjust the metabolic activity within the -# cell to respect many known parameters, measured by proteomics and other -# methods. -# -# The original description from GECKO is by: [Sánchez, et. al., "Improving the -# phenotype predictions of a yeast genome‐scale metabolic model by incorporating -# enzymatic constraints.", Molecular systems biology, -# 2017](https://doi.org/10.15252/msb.20167411). -# -# The analysis method and implementation in COBREXA is similar to -# [sMOMENT](14_smoment.md), but GECKO is able to process and represent much -# larger scale of the constraints -- mainly, it supports multiple isozymes for -# each reaction, and the isozymes can be grouped into "enzyme mass groups" to -# simplify interpretation of data from proteomics. - -# For demonstration, we will generate artificial random data in a way similar -# to the [sMOMENT example](14_smoment.md): - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -using COBREXA, GLPK - -model = load_model("e_coli_core.json") - -import Random -Random.seed!(1) # repeatability - -gene_product_masses = Dict(genes(model) .=> randn(n_genes(model)) .* 10 .+ 60) - -rxns = filter( - x -> - !looks_like_biomass_reaction(x) && - !looks_like_exchange_reaction(x) && - !isnothing(reaction_gene_association(model, x)), - reactions(model), -) - -# The main difference from sMOMENT comes from allowing multiple isozymes per -# reaction (reactions with missing isozyme informations will be ignored, -# leaving them as-is): -rxn_isozymes = Dict( - rxn => [ - Isozyme( - Dict(isozyme_genes .=> 1), - randn() * 100 + 600, #forward kcat - randn() * 100 + 500, #reverse kcat - ) for isozyme_genes in reaction_gene_association(model, rxn) - ] for rxn in rxns -) - -# We also construct similar bounds for total gene product amounts: -gene_product_bounds = Dict(genes(model) .=> Ref((0.0, 10.0))) - -# With this, the construction of the model constrained by all enzymatic -# information is straightforward: - -gecko_model = - model |> with_gecko(; - reaction_isozymes = rxn_isozymes, - gene_product_bounds, - gene_product_molar_mass = gene_product_masses, - gene_product_mass_group = _ -> "uncategorized", # all products belong to the same "uncategorized" category - gene_product_mass_group_bound = _ -> 100.0, # the total limit of mass in the single category - ) - -# (Alternatively, you may use [`make_gecko_model`](@ref), which does the same -# without piping by `|>`.) - -# The stoichiometry and coupling in the gecko model is noticeably more complex; -# you may notice new "reactions" added that simulate the gene product -# utilization: - -[stoichiometry(gecko_model); coupling(gecko_model)] - -# Again, the resulting model can be used in any type of analysis. For example, flux balance analysis: - -opt_model = flux_balance_analysis(gecko_model, GLPK.Optimizer) - -# Get the fluxes - -flux_sol = flux_dict(gecko_model, opt_model) - -# Get the gene product concentrations - -gp_concs = gene_product_dict(gecko_model, opt_model) - -# Get the total masses assigned to each mass group - -gene_product_mass_group_dict(gecko_model, opt_model) - -# Variability: - -flux_variability_analysis(gecko_model, GLPK.Optimizer, bounds = gamma_bounds(0.95)) - -# ...and sampling: -affine_hit_and_run(gecko_model, warmup_from_variability(gecko_model, GLPK.Optimizer))' * -reaction_flux(gecko_model) diff --git a/docs/src/examples/16_hit_and_run.jl b/docs/src/examples/16_hit_and_run.jl deleted file mode 100644 index 807238988..000000000 --- a/docs/src/examples/16_hit_and_run.jl +++ /dev/null @@ -1,73 +0,0 @@ -# # Hit and run sampling - -# Sampling the feasible space of the model allows you to gain a realistic -# insight into the distribution of the flow and its probabilistic nature, often -# better describing the variance and correlations of the possible fluxes better -# (but more approximately) than e.g. [flux variability analysis](06_fva.md). - -# COBREXA supports a variant of hit-and-run sampling adjusted to the -# complexities of metabolic models; in particular, it implements a version -# where the next sample (and indirectly, the next run direction) is generated -# as an affine combination of the samples in the current sample set. This gives -# a result similar to the artificially centered hit-and-run sampling, with a -# slightly (unsubstantially) different biases in the generation of the next -# hits. - -# As always, we start by loading everything that is needed: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# The sampling procedure requires a set of "seed" points that will form the -# basis for the first iteration of runs. This is commonly called a warm-up. You -# can generate these points manually with any method of choice, or you can use -# the COBREXA implementation of warmup that uses the extreme edges of the -# polytope, similarly to the way used by flux variability analysis: - -warmup_points = warmup_from_variability(model, GLPK.Optimizer) - -# This generates a matrix of fluxes, where each column is one sample point, and -# rows correspond to the reactions in the model. - -# With this, starting the sampling procedure is straightforward: - -samples = affine_hit_and_run(model, warmup_points) - -# As seen above, the result again contains 1 sample per column, with reactions -# in the same order with rows as before. To get a different sample, you can -# tune the parameters the function. `sample_iters` allows you to specify the -# iterations at which you want the current sample to be collected and reported -# -- by default, that is done 5 times on each 100th iteration. In the example -# below, we catch the samples in 10 iterations right after the 200th iteration -# passed. Similarly, to avoid possible degeneracy, you can choose to run more -# hit-and-run batch chains than 1, using the `chains` parameters. The total -# number of points collected is the number of points in warmup times the number -# of sample-collecting iterations times the number of chains. - -samples = affine_hit_and_run(model, warmup_points, sample_iters = 201:210, chains = 2) - -#md # !!! tip "Parallelization" -#md # Both procedures used for sampling in this example -#md # ([`warmup_from_variability`](@ref), [`affine_hit_and_run`](@ref)) can be -#md # effectively parallelized by adding `workers=` parameter, as summarized -#md # in [the documentation](../distributed/1_functions.md). Due to the nature of the algorithm, parallelization -#md # of the sampling requires at least 1 chain per worker. - -# ## Visualizing the samples - -# Samples can be displayed very efficiently in a scatterplot or a density plot, -# which naturally show correlations and distributions of the fluxes: - -using CairoMakie - -o2, co2 = indexin(["R_EX_o2_e", "R_EX_co2_e"], reactions(model)) - -scatter( - samples[o2, :], - samples[co2, :]; - axis = (; xlabel = "Oxygen exchange", ylabel = "Carbon dioxide exchange"), -) diff --git a/docs/src/examples/17_envelopes.jl b/docs/src/examples/17_envelopes.jl deleted file mode 100644 index ef763ae05..000000000 --- a/docs/src/examples/17_envelopes.jl +++ /dev/null @@ -1,83 +0,0 @@ - -# # Production envelopes - -# Production envelopes show what a model is capable of doing on a wide range of -# parameters. Usually, you choose a regular grid of a small dimension in the -# parameter space, and get an information about how well the model runs at each -# point. - -# As usual, we start by loading everything: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# The envelope analysis "structure" is similar to what you can obtain using -# [`screen`](@ref); but COBREXA provides a special functions that run the -# process in a very optimized manner. For envelopes, there is -# [`envelope_lattice`](@ref) that generates the rectangular lattice of points -# for a given model and reactions, and [`objective_envelope`](@ref) that -# computes the output (usually as the objective value) of the model at the -# lattice points. You do not need to call [`envelope_lattice`](@ref) directly -# because it is taken as a "default" way to create the lattice by -# [`objective_envelope`](@ref). - -# In short, we can compute the envelope of a single reaction in the *E. coli* -# model as follows: - -envelope = objective_envelope( - model, - ["R_EX_o2_e"], - GLPK.Optimizer, - lattice_args = (ranges = [(-50, 0)],), -) - -# (The named tuple given in `lattice_args` argument is passed to the internal -# call of [`envelope_lattice`](@ref), giving you an easy way to customize its -# behavior.) - -# The result has 2 fields which can be used to easily plot the envelope. We -# also need to "fix" the missing values (represented as `nothing`) where the -# model failed to solve -- we will simply omit them here). - -using CairoMakie - -valid = .!(isnothing.(envelope.values)) -lines(envelope.lattice[1][valid], float.(envelope.values[valid])) - -# Additional resolution can be obtained either by supplying your own larger -# lattice, or simply forwarding the `samples` argument to the internal call of -# [`envelope_lattice`](@ref): - -envelope = objective_envelope( - model, - ["R_EX_co2_e"], - GLPK.Optimizer, - lattice_args = (samples = 1000, ranges = [(-50, 100)]), -) - -valid = .!(isnothing.(envelope.values)) -lines(envelope.lattice[1][valid], float.(envelope.values[valid])) - -# ## Multi-dimensional envelopes - -# The lattice functions generalize well to more dimensions; you can easily -# explore the production of the model depending on the relative fluxes of 2 -# reactions: - -envelope = objective_envelope( - model, - ["R_EX_o2_e", "R_EX_co2_e"], - GLPK.Optimizer, - lattice_args = (samples = 100, ranges = [(-60, 0), (-15, 60)]), -) - -heatmap( - envelope.lattice[1], - envelope.lattice[2], - [isnothing(x) ? 0 : x for x in envelope.values], - axis = (; xlabel = "Oxygen exchange", ylabel = "Carbon dioxide exchange"), -) diff --git a/docs/src/functions.md b/docs/src/functions.md deleted file mode 100644 index 9d26940b0..000000000 --- a/docs/src/functions.md +++ /dev/null @@ -1,6 +0,0 @@ -# API reference - -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("functions", join=true)) -Depth = 2 -``` diff --git a/docs/src/functions/analysis.md b/docs/src/functions/analysis.md deleted file mode 100644 index f37bbaa36..000000000 --- a/docs/src/functions/analysis.md +++ /dev/null @@ -1,22 +0,0 @@ -# Analysis functions - -## Common analysis functions - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("analysis", file), readdir("../src/analysis")) -``` - -## Sampling - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("analysis", "sampling", file), readdir("../src/analysis/sampling")) -``` - -## Analysis modifiers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("analysis", "modifications", file), readdir("../src/analysis/modifications")) -``` diff --git a/docs/src/functions/base.md b/docs/src/functions/base.md deleted file mode 100644 index eff4308b7..000000000 --- a/docs/src/functions/base.md +++ /dev/null @@ -1,6 +0,0 @@ -# Base functions - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", file), readdir("../src/base")) -``` diff --git a/docs/src/functions/io.md b/docs/src/functions/io.md deleted file mode 100644 index c7d908914..000000000 --- a/docs/src/functions/io.md +++ /dev/null @@ -1,15 +0,0 @@ -# Input and output - -## File I/O and serialization - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("io", file), readdir("../src/io")) -``` - -## Pretty printing - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("io", "show", file), readdir("../src/io/show")) -``` diff --git a/docs/src/functions/reconstruction.md b/docs/src/functions/reconstruction.md deleted file mode 100644 index f85bddaf9..000000000 --- a/docs/src/functions/reconstruction.md +++ /dev/null @@ -1,15 +0,0 @@ -# Model construction functions - -## Functions for changing the models - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("reconstruction", file), readdir("../src/reconstruction")) -``` - -## Variant specifiers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("reconstruction", "modifications", file), readdir("../src/reconstruction/modifications")) -``` diff --git a/docs/src/functions/types.md b/docs/src/functions/types.md deleted file mode 100644 index c952444c3..000000000 --- a/docs/src/functions/types.md +++ /dev/null @@ -1,20 +0,0 @@ -# Types - -## Base types - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "types", "abstract", file), readdir("../src/base/types/abstract")) -``` - -## Model types and contents -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "types", file), readdir("../src/base/types")) -``` - -## Model type wrappers -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "types", "wrappers", file), readdir("../src/base/types/wrappers")) -``` diff --git a/docs/src/functions/utils.md b/docs/src/functions/utils.md deleted file mode 100644 index b38635b54..000000000 --- a/docs/src/functions/utils.md +++ /dev/null @@ -1,22 +0,0 @@ -# Utilities - -## Helper functions - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "utils", file), readdir("../src/base/utils")) -``` - -## Macro-generated functions and internal helpers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "macros", file), readdir("../src/base/macros")) -``` - -## Logging and debugging helpers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "logging", file), readdir("../src/base/logging")) -``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 000000000..74950b077 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,7 @@ + +# COBREXA.jl + +```@autodocs +Modules = [COBREXA] +Pages = ["src/COBREXA.jl"] +``` diff --git a/docs/src/index.md.template b/docs/src/index.md.template deleted file mode 100644 index d5d49ca5a..000000000 --- a/docs/src/index.md.template +++ /dev/null @@ -1,83 +0,0 @@ -```@raw html -
-
- - -
-
-``` - -# Constraint-Based Reconstruction and EXascale Analysis - -| **Repository** | **Tests** | **Coverage** | **How to contribute?** | -|:--------------:|:---------:|:------------:|:----------------------:| -| [![GitHub](https://img.shields.io/github/stars/LCSB-BioCore/COBREXA.jl?label=COBREXA.jl&style=social)](http://github.com/LCSB-BioCore/COBREXA.jl) | [![CI](https://github.com/LCSB-BioCore/COBREXA.jl/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/LCSB-BioCore/COBREXA.jl/actions/workflows/ci.yml) | [![codecov](https://codecov.io/gh/LCSB-BioCore/COBREXA.jl/branch/master/graph/badge.svg?token=H3WSWOBD7L)](https://codecov.io/gh/LCSB-BioCore/COBREXA.jl) | [![contrib](https://img.shields.io/badge/contributions-start%20here-green)](howToContribute.md) | - - -COBREXA is a toolkit for working with large constraint-based metabolic models, -and running very large numbers of analysis tasks on these models in parallel. -Its main purpose is to make the methods of Constraint-based Reconstruction and -Analysis (COBRA) scale to problem sizes that require the use of huge computer -clusters and HPC environments, which allows them to be realistically applied to -pre-exascale-sized models. - -In this package, you will find the usual COBRA-like functions that interface to -underlying linear programming solvers. We use -[`JuMP.jl`](https://github.com/jump-dev/JuMP.jl) as the unified interface for -many solvers; you can plug in whichever compatible solver you want, including -the popular [`Tulip.jl`](https://github.com/ds4dm/Tulip.jl), -[`GLPK.jl`](https://github.com/jump-dev/GLPK.jl), -[`OSQP.jl`](https://github.com/osqp/OSQP.jl), and -[`Gurobi.jl`](https://github.com/jump-dev/Gurobi.jl). - -```@raw html -
-history
-Development history of COBREXA.jl. -
-``` - -## Quick start guide - -A dedicated [quick start guide](quickstart.md) is available for quickly trying out the -analysis with COBREXA.jl. - -## Example notebooks and workflows - -Detailed example listing is [available here](examples.md). -All examples are also avaialble as JuPyteR notebooks. - -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("examples", join=true)) -Depth = 1 -``` - -## Core concept documentation - -Detailed concept guide listing is [available here](concepts.md). - -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("concepts", join=true)) -Depth = 1 -``` - -## Functions and types API reference - -Detailed table of contents of the API documentation is [available -here](functions.md). - -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("functions", join=true)) -Depth = 1 -``` - -## Contribution guide - -If you wish to contribute code, patches or improvements to `COBREXA.jl`, please -follow the [contribution guidelines and hints](howToContribute.md). - - - -```@raw html - -``` diff --git a/docs/src/quickstart.md b/docs/src/quickstart.md deleted file mode 100644 index 11c9709fa..000000000 --- a/docs/src/quickstart.md +++ /dev/null @@ -1,152 +0,0 @@ - -# Quick start guide - -You can install COBREXA from Julia repositories. Start `julia`, **press `]`** to -switch to the Packaging environment, and type: -``` -add COBREXA -``` - -You also need to install your favorite solver supported by `JuMP.jl` (such as -Gurobi, Mosek, CPLEX, GLPK, Clarabel, etc., see a [list -here](https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers)). For -example, you can install `Tulip.jl` solver by typing: -``` -add Tulip -``` - -Alternatively, you may use [prebuilt Docker and Apptainer images](#prebuilt-images). - -If you are running COBREXA.jl for the first time, it is very likely that upon -installing and importing the packages, your Julia installation will need to -precompile their source code from the scratch. In fresh installations, the -precompilation process should take less than 5 minutes. - -When the packages are installed, switch back to the "normal" julia shell by -pressing Backspace (the prompt should change color back to green). After that, -you can download [a SBML model from the -internet](http://bigg.ucsd.edu/models/e_coli_core) and perform a -flux balance analysis as follows: - -```julia -using COBREXA # loads the package -using Tulip # loads the optimization solver - -# download the model -download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -# open the SBML file and load the contents -model = load_model("e_coli_core.xml") - -# run a FBA -fluxes = flux_balance_analysis_dict(model, Tulip.Optimizer) -``` - -The variable `fluxes` will now contain a dictionary of the computed optimal -flux of each reaction in the model: -``` -Dict{String,Float64} with 95 entries: - "R_EX_fum_e" => 0.0 - "R_ACONTb" => 6.00725 - "R_TPI" => 7.47738 - "R_SUCOAS" => -5.06438 - "R_GLNS" => 0.223462 - "R_EX_pi_e" => -3.2149 - "R_PPC" => 2.50431 - "R_O2t" => 21.7995 - "R_G6PDH2r" => 4.95999 - "R_TALA" => 1.49698 - ⋮ => ⋮ -``` - -#### Model variant processing - -The main feature of COBREXA.jl is the ability to easily specify and process -many analyses in parallel. To demonstrate, let's see how the organism would -perform if some reactions were disabled independently: - -```julia -# convert to a model type that is efficient to modify -m = convert(StandardModel, model) - -# find the model objective value if oxygen or carbon dioxide transports are disabled -screen(m, # the base model - variants=[ # this specifies how to generate the desired model variants - [], # one with no modifications, i.e. the base case - [with_changed_bound("R_O2t", lower=0.0, upper=0.0)], # disable oxygen - [with_changed_bound("R_CO2t", lower=0.0, upper=0.0)], # disable CO2 - [with_changed_bound("R_O2t", lower=0.0, upper=0.0), - with_changed_bound("R_CO2t", lower=0.0, upper=0.0)], # disable both - ], - # this specifies what to do with the model variants (received as the argument `x`) - analysis = x -> - flux_balance_analysis_dict(x, Tulip.Optimizer)["R_BIOMASS_Ecoli_core_w_GAM"], -) -``` -You should receive a result showing that missing oxygen transport makes the -biomass production much harder: -```julia -4-element Vector{Float64}: - 0.8739215022674809 - 0.21166294973372796 - 0.46166961413944896 - 0.21114065173865457 -``` - -Most importantly, such analyses can be easily specified by automatically -generating long lists of modifications to be applied to the model, and -parallelized. - -Knocking out each reaction in the model is efficiently accomplished: - -```julia -# load the task distribution package, add several worker nodes, and load -# COBREXA and the solver on the nodes -using Distributed -addprocs(4) -@everywhere using COBREXA, Tulip - -# get a list of the workers -worker_list = workers() - -# run the processing in parallel for many model variants -res = screen(m, - variants=[ - # create one variant for each reaction in the model, with that reaction knocked out - [with_changed_bound(reaction_id, lower=0.0, upper=0.0)] - for reaction_id in reactions(m) - ], - analysis = model -> begin - # we need to check if the optimizer even found a feasible solution, - # which may not be the case if we knock out important reactions - sol = flux_balance_analysis_dict(model, Tulip.Optimizer) - isnothing(sol) ? nothing : sol["R_BIOMASS_Ecoli_core_w_GAM"] - end, - # run the screening in parallel on all workers in the list - workers = worker_list, -) -``` - -In result, you should get a long list of the biomass production for each -reaction knockout. Let's decorate it with reaction names: -```julia -Dict(reactions(m) .=> res) -``` -...which should output an easily accessible dictionary with all the objective -values named, giving a quick overview of which reactions are critical for the -model organism to create biomass: -```julia -Dict{String, Union{Nothing, Float64}} with 95 entries: - "R_ACALD" => 0.873922 - "R_PTAr" => 0.873922 - "R_ALCD2x" => 0.873922 - "R_PDH" => 0.796696 - "R_PYK" => 0.864926 - "R_CO2t" => 0.46167 - "R_EX_nh4_e" => 1.44677e-15 - "R_MALt2_2" => 0.873922 - "R_CS" => 2.44779e-14 - "R_PGM" => 1.04221e-15 - "R_TKT1" => 0.864759 - ⋮ => ⋮ -``` diff --git a/docs/src/quickstart.md.template b/docs/src/quickstart.md.template deleted file mode 100644 index 1d7cae079..000000000 --- a/docs/src/quickstart.md.template +++ /dev/null @@ -1,96 +0,0 @@ - -# Quick start guide - - - -#### Model variant processing - -The main feature of COBREXA.jl is the ability to easily specify and process -many analyses in parallel. To demonstrate, let's see how the organism would -perform if some reactions were disabled independently: - -```julia -# convert to a model type that is efficient to modify -m = convert(StandardModel, model) - -# find the model objective value if oxygen or carbon dioxide transports are disabled -screen(m, # the base model - variants=[ # this specifies how to generate the desired model variants - [], # one with no modifications, i.e. the base case - [with_changed_bound("R_O2t", lower=0.0, upper=0.0)], # disable oxygen - [with_changed_bound("R_CO2t", lower=0.0, upper=0.0)], # disable CO2 - [with_changed_bound("R_O2t", lower=0.0, upper=0.0), - with_changed_bound("R_CO2t", lower=0.0, upper=0.0)], # disable both - ], - # this specifies what to do with the model variants (received as the argument `x`) - analysis = x -> - flux_balance_analysis_dict(x, Tulip.Optimizer)["R_BIOMASS_Ecoli_core_w_GAM"], -) -``` -You should receive a result showing that missing oxygen transport makes the -biomass production much harder: -```julia -4-element Vector{Float64}: - 0.8739215022674809 - 0.21166294973372796 - 0.46166961413944896 - 0.21114065173865457 -``` - -Most importantly, such analyses can be easily specified by automatically -generating long lists of modifications to be applied to the model, and -parallelized. - -Knocking out each reaction in the model is efficiently accomplished: - -```julia -# load the task distribution package, add several worker nodes, and load -# COBREXA and the solver on the nodes -using Distributed -addprocs(4) -@everywhere using COBREXA, Tulip - -# get a list of the workers -worker_list = workers() - -# run the processing in parallel for many model variants -res = screen(m, - variants=[ - # create one variant for each reaction in the model, with that reaction knocked out - [with_changed_bound(reaction_id, lower=0.0, upper=0.0)] - for reaction_id in reactions(m) - ], - analysis = model -> begin - # we need to check if the optimizer even found a feasible solution, - # which may not be the case if we knock out important reactions - sol = flux_balance_analysis_dict(model, Tulip.Optimizer) - isnothing(sol) ? nothing : sol["R_BIOMASS_Ecoli_core_w_GAM"] - end, - # run the screening in parallel on all workers in the list - workers = worker_list, -) -``` - -In result, you should get a long list of the biomass production for each -reaction knockout. Let's decorate it with reaction names: -```julia -Dict(reactions(m) .=> res) -``` -...which should output an easily accessible dictionary with all the objective -values named, giving a quick overview of which reactions are critical for the -model organism to create biomass: -```julia -Dict{String, Union{Nothing, Float64}} with 95 entries: - "R_ACALD" => 0.873922 - "R_PTAr" => 0.873922 - "R_ALCD2x" => 0.873922 - "R_PDH" => 0.796696 - "R_PYK" => 0.864926 - "R_CO2t" => 0.46167 - "R_EX_nh4_e" => 1.44677e-15 - "R_MALt2_2" => 0.873922 - "R_CS" => 2.44779e-14 - "R_PGM" => 1.04221e-15 - "R_TKT1" => 0.864759 - ⋮ => ⋮ -``` diff --git a/docs/src/reference.md b/docs/src/reference.md new file mode 100644 index 000000000..d791bd478 --- /dev/null +++ b/docs/src/reference.md @@ -0,0 +1,66 @@ +# API reference + +## Helper types + +```@autodocs +Modules = [COBREXA] +Pages = ["src/types.jl"] +``` + +## Model loading and saving + +```@autodocs +Modules = [COBREXA] +Pages = ["src/io.jl"] +``` + +## Solver interface + +```@autodocs +Modules = [COBREXA] +Pages = ["src/solver.jl"] +``` + +## Constraint system building + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/core.jl"] +``` + +### Genetic constraints + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/genes.jl"] +``` + +### Objective function helpers + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/objectives.jl"] +``` + +### Bounds&tolerances helpers + +```@autodocs +Modules = [COBREXA] +Pages = ["src/misc/bounds.jl"] +``` + +## Analysis functions + +```@autodocs +Modules = [COBREXA] +Pages = ["src/analysis/flux_balance.jl", "src/analysis/parsimonious_flux_balance.jl"] +``` + +### Analysis modifications + +```@autodocs +Modules = [COBREXA] +Pages = ["src/analysis/modifications.jl"] +``` + +## Distributed analysis diff --git a/docs/src/reference/builders.md b/docs/src/reference/builders.md new file mode 100644 index 000000000..26e230364 --- /dev/null +++ b/docs/src/reference/builders.md @@ -0,0 +1,21 @@ + +## Constraint system building + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/core.jl"] +``` + +### Genetic constraints + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/genes.jl"] +``` + +### Objective function helpers + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/objectives.jl"] +``` diff --git a/docs/src/reference/solver.md b/docs/src/reference/solver.md new file mode 100644 index 000000000..c01aabf78 --- /dev/null +++ b/docs/src/reference/solver.md @@ -0,0 +1,7 @@ + +# Solver interface + +```@autodocs +Modules = [COBREXA] +Pages = ["src/solver.jl"] +``` diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 9e75888f9..9719c4b56 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -1,89 +1,93 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ -``` -\\\\\\\\\\ // // | COBREXA.jl v$(COBREXA.COBREXA_VERSION) - \\\\ \\\\// // | - \\\\ \\/ // | COnstraint-Based Reconstruction - \\\\ // | and EXascale Analysis in Julia - // \\\\ | - // /\\ \\\\ | See documentation and examples at: - // //\\\\ \\\\ | https://lcsb-biocore.github.io/COBREXA.jl -// // \\\\\\\\\\ | -``` + module COBREXA + +COnstraint Based Reconstruction and EXascale Analysis. COBREXA provides +functions for construction, modification, simulation and analysis of +constraint-based metabolic models that follows the COBRA methodology. -To start up quickly, install your favorite optimizer, load a metabolic model in -a format such as SBML or JSON, and run a metabolic analysis such as the flux -balance analysis: -``` -import Pkg; Pkg.add("GLPK") -using COBREXA, GLPK -model = load_model("e_coli_core.xml") -x = flux_balance_analysis_dict(model, GLPK.Optimizer) -flux_summary(x) -``` +COBREXA is built as a front-end for the combination of `AbstractFBCModels.jl` +(provides the model I/O), `ConstraintTrees.jl` (provides the constraint system +organization), `Distributed.jl` (provides HPC execution capability), and +`JuMP.jl` (provides the solvers). -A complete overview of the functionality can be found in the documentation. +See the online documentation for a complete description of functionality aided +by copy-pastable examples. + +To start quickly, load your favorite JuMP-compatible solver, use +[`load_model`](@ref) to read a metabolic model from the disk, and solve it with +[`flux_balance`](@ref). """ module COBREXA -using Distributed -using DistributedData -using HDF5 -using JSON -using JuMP -using LinearAlgebra -using MAT -using MacroTools -using OrderedCollections -using Random -using Serialization -using SparseArrays -using StableRNGs -using Statistics using DocStringExtensions -import Base: findfirst, getindex, show -import Pkg -import SBML # conflict with Reaction struct name +import AbstractFBCModels as A +import ConstraintTrees as C +import Distributed as D +import JuMP as J +import LinearAlgebra +import SparseArrays + +include("types.jl") +include("config.jl") -const _PKG_ROOT_DIR = normpath(joinpath(@__DIR__, "..")) -include_dependency(joinpath(_PKG_ROOT_DIR, "Project.toml")) +# core functionality +include("io.jl") +include("solver.jl") +include("worker_data.jl") -const COBREXA_VERSION = - VersionNumber(Pkg.TOML.parsefile(joinpath(_PKG_ROOT_DIR, "Project.toml"))["version"]) +# generic analysis functions +include("analysis/envelope.jl") +include("analysis/parsimonious.jl") +include("analysis/sample.jl") +include("analysis/screen.jl") +include("analysis/solver.jl") +include("analysis/variability.jl") -# autoloading -const _inc(path...) = include(joinpath(path...)) -const _inc_all(dir) = _inc.(joinpath.(dir, filter(fn -> endswith(fn, ".jl"), readdir(dir)))) +# conversion of various stuff to constraint trees +include("builders/compare.jl") +include("builders/enzymes.jl") +include("builders/fbc.jl") +include("builders/interface.jl") +include("builders/knockouts.jl") +include("builders/loopless.jl") +include("builders/objectives.jl") +include("builders/scale.jl") +include("builders/unsigned.jl") -_inc_all.( - joinpath.( - @__DIR__, - [ - joinpath("base", "types", "abstract"), - joinpath("base", "logging"), - joinpath("base", "macros"), - joinpath("base", "types"), - joinpath("base", "types", "wrappers"), - joinpath("base", "ontologies"), - "base", - "io", - joinpath("io", "show"), - "reconstruction", - joinpath("reconstruction", "modifications"), - "analysis", - joinpath("analysis", "modifications"), - joinpath("analysis", "sampling"), - joinpath("base", "utils"), - ], - ), -) +# simplified front-ends for the above +include("frontend/balance.jl") +include("frontend/envelope.jl") +include("frontend/enzymes.jl") +include("frontend/knockout.jl") +include("frontend/loopless.jl") +include("frontend/mmdf.jl") +include("frontend/moma.jl") +include("frontend/parsimonious.jl") +include("frontend/sample.jl") +include("frontend/variability.jl") -# export everything that isn't prefixed with _ (inspired by JuMP.jl, thanks!) -for sym in names(@__MODULE__, all = true) - if sym in [Symbol(@__MODULE__), :eval, :include] || startswith(string(sym), ['_', '#']) - continue - end - @eval export $sym -end +# utilities +include("misc/bounds.jl") +include("misc/breaks.jl") +include("misc/maybe.jl") +include("misc/settings.jl") +include("misc/trees.jl") -end # module +end # module COBREXA diff --git a/src/analysis/envelope.jl b/src/analysis/envelope.jl new file mode 100644 index 000000000..2f1619384 --- /dev/null +++ b/src/analysis/envelope.jl @@ -0,0 +1,67 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Optimize the system given by `constraints` and `objective` with `optimizer` +(with custom `settings`) for all combination of constriants given by `dims`. + +`dims` should be compatible with pairs that assign a sequence of breaks to a +`ConstraintTrees.Value`: For example, `organism.fluxes.PFK => 1:3` will compute +optima of the model with the flux through `PFK` constrained to be equal to 1, 2 +and 3. + +In turn, all `dims` are converted to groups of equality constraints, and the +model is solved for all combinations. Shape of the output matrix corresponds to +`Iterators.product(last.(dims)...)`. + +Operation is parallelized by distribution over `workers`; by default all +`Distributed` workers are used. +""" +function constraints_objective_envelope( + constraints::C.ConstraintTree, + dims...; + objective::C.Value, + sense = Maximal, + optimizer, + settings = [], + workers = D.workers(), +) + values = first.(dims) + ranges = last.(dims) + + screen_optimization_model( + constraints, + Iterators.product(ranges...); + objective, + sense, + optimizer, + settings, + workers, + ) do om, coords + con_refs = [ + begin + J.@constraint(om, con_ref, C.substitute(v, om[:x]) == x) + con_ref + end for (v, x) in zip(values, coords) + ] + J.optimize!(om) + res = is_solved(om) ? J.objective_value(om) : nothing + J.delete.(con_refs) + res + end +end diff --git a/src/analysis/envelopes.jl b/src/analysis/envelopes.jl deleted file mode 100644 index ddf22bc09..000000000 --- a/src/analysis/envelopes.jl +++ /dev/null @@ -1,125 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Version of [`envelope_lattice`](@ref) that works on string reaction IDs instead -of integer indexes. -""" -envelope_lattice(model::MetabolicModel, rids::Vector{String}; kwargs...) = - envelope_lattice(model, Vector{Int}(indexin(rids, reactions(model))); kwargs...) - -""" -$(TYPEDSIGNATURES) - -Create a lattice (list of "tick" vectors) for reactions at indexes `ridxs` in a -model. Arguments `samples`, `ranges`, and `reaction_samples` may be optionally -specified to customize the lattice creation process. -""" -envelope_lattice( - model::MetabolicModel, - ridxs::Vector{Int}; - samples = 10, - ranges = collect(zip(bounds(model)...))[ridxs], - reaction_samples = fill(samples, length(ridxs)), -) = ( - lb .+ (ub - lb) .* ((1:s) .- 1) ./ max(s - 1, 1) for - (s, (lb, ub)) in zip(reaction_samples, ranges) -) - -""" -$(TYPEDSIGNATURES) - -Version of [`objective_envelope`](@ref) that works on string reaction IDs -instead of integer indexes. -""" -objective_envelope(model::MetabolicModel, rids::Vector{String}, args...; kwargs...) = - objective_envelope( - model, - Vector{Int}(indexin(rids, reactions(model))), - args...; - kwargs..., - ) - -""" -$(TYPEDSIGNATURES) - -Compute an array of objective values for the `model` for rates of reactions -specified `ridxs` fixed to a regular range of values between their respective -lower and upper bounds. - -This can be used to compute a "production envelope" of a metabolite; but -generalizes to any specifiable objective and to multiple dimensions of the -examined space. For example, to retrieve a production envelope of any -metabolite, set the objective coefficient vector of the `model` to a vector -that contains a single `1` for the exchange reaction that "outputs" this -metabolite, and run [`objective_envelope`](@ref) with the exchange reaction of -the "parameter" metabolite specified in `ridxs`. - -Returns a named tuple that contains `lattice` with reference values of the -metabolites, and an N-dimensional array `values` with the computed objective -values, where N is the number of specified reactions. Because of the -increasing dimensionality, the computation gets very voluminous with increasing -length of `ridxs`. The `lattice` for computing the optima can be supplied in -the argument; by default it is created by [`envelope_lattice`](@ref) called on -the model and reaction indexes. Additional arguments for the call to -[`envelope_lattice`](@ref) can be optionally specified in `lattice_args`. - -`kwargs` are internally forwarded to [`screen_optmodel_modifications`](@ref). -`modifications` are appended to the list of modifications after the lattice -bounds are set. By default, this returns the objective values for all points in -the lattice; alternate outputs can be implemented via the `analysis` argument. - -# Example -``` -julia> m = load_model("e_coli_core.xml"); - -julia> envelope = objective_envelope(m, ["R_EX_gln__L_e", "R_EX_fum_e"], - Tulip.Optimizer; - lattice_args=(samples=6,)); - -julia> envelope.lattice # the reaction rates for which the optima were computed -2-element Vector{Vector{Float64}}: - [0.0, 200.0, 400.0, 600.0, 800.0, 1000.0] - [0.0, 200.0, 400.0, 600.0, 800.0, 1000.0] - -julia> envelope.values # the computed flux objective values for each reaction rate combination -6×6 Matrix{Float64}: - 0.873922 9.25815 17.4538 19.56 20.4121 20.4121 - 13.0354 17.508 19.9369 21.894 22.6825 22.6825 - 16.6666 18.6097 20.2847 21.894 22.6825 22.6825 - 16.6666 18.6097 20.2847 21.894 22.6825 22.6825 - 16.6666 18.6097 20.2847 21.894 22.6825 22.6825 - 16.6666 18.6097 20.2847 21.894 22.6825 22.6825 -``` -""" -objective_envelope( - model::MetabolicModel, - ridxs::Vector{Int}, - optimizer; - modifications = [], - lattice_args = (), - lattice = envelope_lattice(model, ridxs; lattice_args...), - analysis = screen_optimize_objective, - kwargs..., -) = ( - lattice = collect.(lattice), - values = screen_optmodel_modifications( - model, - optimizer; - modifications = collect( - vcat( - [ - (_, optmodel) -> begin - for (i, ridx) in enumerate(ridxs) - set_normalized_rhs(optmodel[:lbs][ridx], -fluxes[i]) - set_normalized_rhs(optmodel[:ubs][ridx], fluxes[i]) - end - end, - ], - modifications, - ) for fluxes in Iterators.product(lattice...) - ), - analysis = analysis, - kwargs..., - ), -) diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl deleted file mode 100644 index 495f4545f..000000000 --- a/src/analysis/flux_balance_analysis.jl +++ /dev/null @@ -1,84 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A variant of FBA that returns a vector of fluxes in the same order as reactions -of the model, if the solution is found. - -Arguments are passed to [`flux_balance_analysis`](@ref). - -This function is kept for backwards compatibility, use [`flux_vector`](@ref) -instead. -""" -flux_balance_analysis_vec( - model::MetabolicModel, - args...; - kwargs..., -)::Maybe{Vector{Float64}} = - flux_vector(model, flux_balance_analysis(model, args...; kwargs...)) - -""" -$(TYPEDSIGNATURES) - -A variant of FBA that returns a dictionary assigning fluxes to reactions, if -the solution is found. Arguments are passed to [`flux_balance_analysis`](@ref). - -This function is kept for backwards compatibility, use [`flux_dict`](@ref) -instead. -""" -flux_balance_analysis_dict( - model::MetabolicModel, - args...; - kwargs..., -)::Maybe{Dict{String,Float64}} = - flux_dict(model, flux_balance_analysis(model, args...; kwargs...)) - -""" -$(TYPEDSIGNATURES) - -Run flux balance analysis (FBA) on the `model` optionally specifying -`modifications` to the problem. Basically, FBA solves this optimization problem: -``` -max cᵀx -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -See "Orth, J., Thiele, I. & Palsson, B. What is flux balance analysis?. Nat -Biotechnol 28, 245-248 (2010). https://doi.org/10.1038/nbt.1614" for more -information. - -The `optimizer` must be set to a `JuMP`-compatible optimizer, such as -`GLPK.Optimizer` or `Tulip.Optimizer` - -Optionally, you may specify one or more modifications to be applied to the -model before the analysis, such as [`change_optimizer_attribute`](@ref), -[`change_objective`](@ref), and [`change_sense`](@ref). - -Returns an optimized `JuMP` model. - -# Example -``` -model = load_model("e_coli_core.json") -solution = flux_balance_analysis(model, GLPK.optimizer) -value.(solution[:x]) # extract flux steady state from the optimizer - -biomass_reaction_id = findfirst(model.reactions, "BIOMASS_Ecoli_core_w_GAM") - -modified_solution = flux_balance_analysis(model, GLPK.optimizer; - modifications=[change_objective(biomass_reaction_id)]) -``` -""" -function flux_balance_analysis( - model::M, - optimizer; - modifications = [], -) where {M<:MetabolicModel} - - opt_model = make_optimization_model(model, optimizer) - - for mod in modifications - mod(model, opt_model) - end - - optimize!(opt_model) - return opt_model -end diff --git a/src/analysis/flux_variability_analysis.jl b/src/analysis/flux_variability_analysis.jl deleted file mode 100644 index 9bfa841d0..000000000 --- a/src/analysis/flux_variability_analysis.jl +++ /dev/null @@ -1,221 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Flux variability analysis solves a pair of optimization problems in `model` for -each flux `f` described in `fluxes`: -``` -min,max fᵀxᵢ -s.t. S x = b - xₗ ≤ x ≤ xᵤ - cᵀx ≥ bounds(Z₀)[1] - cᵀx ≤ bounds(Z₀)[2] -``` -where Z₀:= cᵀx₀ is the objective value of an optimal solution of the associated -FBA problem (see [`flux_balance_analysis`](@ref) for a related analysis, also -for explanation of the `modifications` argument). - -The `bounds` is a user-supplied function that specifies the objective bounds -for the variability optimizations, by default it restricts the flux objective -value to the precise optimum reached in FBA. It can return `-Inf` and `Inf` in -first and second pair to remove the limit. Use [`gamma_bounds`](@ref) and -[`objective_bounds`](@ref) for simple bounds. - -`optimizer` must be set to a `JuMP`-compatible optimizer. The computation of -the individual optimization problems is transparently distributed to `workers` -(see `Distributed.workers()`). The value of Z₀ can be optionally supplied in -argument `optimal_objective_value`, which prevents this function from calling -the non-parallelizable FBA. Separating the single-threaded FBA and -multithreaded variability computation can be used to improve resource -allocation efficiency in many common use-cases. - -`ret` is a function used to extract results from optimized JuMP models of the -individual fluxes. By default, it calls and returns the value of -`JuMP.objective_value`. More information can be extracted e.g. by setting it to -a function that returns a more elaborate data structure; such as `m -> -(JuMP.objective_value(m), JuMP.value.(m[:x]))`. - -Returns a matrix of extracted `ret` values for minima and maxima, of total size -(`size(fluxes,2)`,2). The optimizer result status is checked with -[`is_solved`](@ref); `nothing` is returned if the optimization failed for any -reason. - -# Example -``` -model = load_model("e_coli_core.json") -flux_variability_analysis(model, GLPK.optimizer) -``` -""" -function flux_variability_analysis( - model::MetabolicModel, - fluxes::SparseMat, - optimizer; - modifications = [], - workers = [myid()], - optimal_objective_value = nothing, - bounds = z -> (z, Inf), - ret = objective_value, -) - if size(fluxes, 1) != n_reactions(model) - throw( - DomainError( - size(fluxes, 1), - "Flux matrix size is not compatible with model reaction count.", - ), - ) - end - - Z = bounds( - isnothing(optimal_objective_value) ? - objective_value( - flux_balance_analysis(model, optimizer; modifications = modifications), - ) : optimal_objective_value, - ) - - flux_vector = [fluxes[:, i] for i = 1:size(fluxes, 2)] - - return screen_optmodel_modifications( - model, - optimizer; - common_modifications = vcat( - modifications, - [ - (model, opt_model) -> begin - Z[1] > -Inf && @constraint( - opt_model, - objective(model)' * opt_model[:x] >= Z[1] - ) - Z[2] < Inf && @constraint( - opt_model, - objective(model)' * opt_model[:x] <= Z[2] - ) - end, - ], - ), - args = tuple.([flux_vector flux_vector], [MIN_SENSE MAX_SENSE]), - analysis = (_, opt_model, flux, sense) -> - _max_variability_flux(opt_model, flux, sense, ret), - workers = workers, - ) -end - -""" -$(TYPEDSIGNATURES) - -An overload of [`flux_variability_analysis`](@ref) that explores the fluxes specified by integer indexes -""" -function flux_variability_analysis( - model::MetabolicModel, - flux_indexes::Vector{Int}, - optimizer; - kwargs..., -) - if any((flux_indexes .< 1) .| (flux_indexes .> n_fluxes(model))) - throw(DomainError(flux_indexes, "Flux index out of range")) - end - - flux_variability_analysis( - model, - reaction_flux(model)[:, flux_indexes], - optimizer; - kwargs..., - ) -end - -""" -$(TYPEDSIGNATURES) - -A simpler version of [`flux_variability_analysis`](@ref) that maximizes and -minimizes all declared fluxes in the model. Arguments are forwarded. -""" -flux_variability_analysis(model::MetabolicModel, optimizer; kwargs...) = - flux_variability_analysis(model, reaction_flux(model), optimizer; kwargs...) - -""" -$(TYPEDSIGNATURES) -A variant of [`flux_variability_analysis`](@ref) that returns the individual -maximized and minimized fluxes as two dictionaries (of dictionaries). All -keyword arguments except `ret` are passed through. - -# Example -``` -mins, maxs = flux_variability_analysis_dict( - model, - Tulip.Optimizer; - bounds = objective_bounds(0.99), - modifications = [ - change_optimizer_attribute("IPM_IterationsLimit", 500), - change_constraint("EX_glc__D_e"; lb = -10, ub = -10), - change_constraint("EX_o2_e"; lb = 0, ub = 0), - ], -) -``` -""" -function flux_variability_analysis_dict(model::MetabolicModel, optimizer; kwargs...) - vs = flux_variability_analysis( - model, - optimizer; - kwargs..., - ret = sol -> flux_vector(model, sol), - ) - flxs = fluxes(model) - dicts = zip.(Ref(flxs), vs) - - return (Dict(flxs .=> Dict.(dicts[:, 1])), Dict(flxs .=> Dict.(dicts[:, 2]))) -end - -""" -$(TYPEDSIGNATURES) - -Internal helper for maximizing reactions in optimization model. -""" -function _max_variability_flux(opt_model, flux, sense, ret) - @objective(opt_model, sense, sum(flux .* opt_model[:x])) - optimize!(opt_model) - - is_solved(opt_model) ? ret(opt_model) : nothing -end - -""" -$(TYPEDSIGNATURES) - -A variant for [`flux_variability_analysis`](@ref) that examines actual -reactions (selected by their indexes in `reactions` argument) instead of whole -fluxes. This may be useful for models where the sets of reactions and fluxes -differ. -""" -function reaction_variability_analysis( - model::MetabolicModel, - reaction_indexes::Vector{Int}, - optimizer; - kwargs..., -) - if any((reaction_indexes .< 1) .| (reaction_indexes .> n_reactions(model))) - throw(DomainError(reaction_indexes, "Flux index out of range")) - end - - flux_variability_analysis( - model, - sparse( - reaction_indexes, - 1:length(reaction_indexes), - 1.0, - n_reactions(model), - length(reaction_indexes), - ), - optimizer; - kwargs..., - ) -end - -""" -$(TYPEDSIGNATURES) - -Shortcut for [`reaction_variability_analysis`](@ref) that examines all reactions. -""" -reaction_variability_analysis(model::MetabolicModel, optimizer; kwargs...) = - reaction_variability_analysis( - model, - collect(1:n_reactions(model)), - optimizer; - kwargs..., - ) diff --git a/src/analysis/gecko.jl b/src/analysis/gecko.jl deleted file mode 100644 index b4b5dcab7..000000000 --- a/src/analysis/gecko.jl +++ /dev/null @@ -1,154 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Wrap a model into a [`GeckoModel`](@ref), following the structure given by -GECKO algorithm (see [`GeckoModel`](@ref) documentation for details). - -# Arguments - -- `reaction_isozymes` is a function that returns a vector of [`Isozyme`](@ref)s - for each reaction, or empty vector if the reaction is not enzymatic. -- `gene_product_bounds` is a function that returns lower and upper bound for - concentration for a given gene product (specified by the same string gene ID as in - `reaction_isozymes`), as `Tuple{Float64,Float64}`. -- `gene_product_molar_mass` is a function that returns a numeric molar mass of - a given gene product specified by string gene ID. -- `gene_product_mass_group` is a function that returns a string group identifier for a - given gene product, again specified by string gene ID. By default, all gene - products belong to group `"uncategorized"` which is the behavior of original - GECKO. -- `gene_product_mass_group_bound` is a function that returns the maximum mass for a given - mass group. - -Alternatively, all function arguments may be also supplied as dictionaries that -provide the same data lookup. -""" -function make_gecko_model( - model::MetabolicModel; - reaction_isozymes::Union{Function,Dict{String,Vector{Isozyme}}}, - gene_product_bounds::Union{Function,Dict{String,Tuple{Float64,Float64}}}, - gene_product_molar_mass::Union{Function,Dict{String,Float64}}, - gene_product_mass_group::Union{Function,Dict{String,String}} = _ -> "uncategorized", - gene_product_mass_group_bound::Union{Function,Dict{String,Float64}}, -) - ris_ = - reaction_isozymes isa Function ? reaction_isozymes : - (rid -> get(reaction_isozymes, rid, [])) - gpb_ = - gene_product_bounds isa Function ? gene_product_bounds : - (gid -> gene_product_bounds[gid]) - gpmm_ = - gene_product_molar_mass isa Function ? gene_product_molar_mass : - (gid -> gene_product_molar_mass[gid]) - gmg_ = - gene_product_mass_group isa Function ? gene_product_mass_group : - (gid -> gene_product_mass_group[gid]) - gmgb_ = - gene_product_mass_group_bound isa Function ? gene_product_mass_group_bound : - (grp -> gene_product_mass_group_bound[grp]) - # ...it would be nicer to have an overload for this, but kwargs can't be used for dispatch - - columns = Vector{_gecko_reaction_column}() - coupling_row_reaction = Int[] - coupling_row_gene_product = Int[] - - gids = genes(model) - (lbs, ubs) = bounds(model) - rids = reactions(model) - - gene_name_lookup = Dict(gids .=> 1:length(gids)) - gene_row_lookup = Dict{Int,Int}() - - for i = 1:n_reactions(model) - isozymes = ris_(rids[i]) - if isempty(isozymes) - push!(columns, _gecko_reaction_column(i, 0, 0, 0, lbs[i], ubs[i], [])) - continue - end - - # loop over both directions for all isozymes - for (lb, ub, kcatf, dir) in [ - (-ubs[i], -lbs[i], i -> i.kcat_reverse, -1), - (lbs[i], ubs[i], i -> i.kcat_forward, 1), - ] - # The coefficients in the coupling matrix will be categorized in - # separate rows for negative and positive reactions. Surprisingly, - # we do not need to explicitly remember the bounds, because the - # ones taken from the original model are perfectly okay -- the - # "reverse" direction is unreachable because of individual - # bounds on split reactions, and the "forward" direction is - # properly negated in the reverse case to work nicely with the - # global lower bound. - reaction_coupling_row = - ub > 0 && length(isozymes) > 1 ? begin - push!(coupling_row_reaction, i) - length(coupling_row_reaction) - end : 0 - - # all isozymes in this direction - for (iidx, isozyme) in enumerate(isozymes) - kcat = kcatf(isozyme) - if ub > 0 && kcat > _constants.tolerance - # prepare the coupling with gene product molar - gene_product_coupling = collect( - begin - gidx = gene_name_lookup[gene] - row_idx = if haskey(gene_row_lookup, gidx) - gene_row_lookup[gidx] - else - push!(coupling_row_gene_product, gidx) - gene_row_lookup[gidx] = - length(coupling_row_gene_product) - end - (row_idx, stoich / kcat) - end for (gene, stoich) in isozyme.gene_product_count if - haskey(gene_name_lookup, gene) - ) - - # make a new column - push!( - columns, - _gecko_reaction_column( - i, - iidx, - dir, - reaction_coupling_row, - max(lb, 0), - ub, - gene_product_coupling, - ), - ) - end - end - end - end - - # prepare enzyme capacity constraints - mg_gid_lookup = Dict{String,Vector{String}}() - for gid in gids[coupling_row_gene_product] - mg = gmg_(gid) - if haskey(mg_gid_lookup, mg) - push!(mg_gid_lookup[mg], gid) - else - mg_gid_lookup[mg] = [gid] - end - end - coupling_row_mass_group = Vector{_gecko_capacity}() - for (grp, gs) in mg_gid_lookup - idxs = [gene_row_lookup[x] for x in Int.(indexin(gs, gids))] - mms = gpmm_.(gs) - push!(coupling_row_mass_group, _gecko_capacity(grp, idxs, mms, gmgb_(grp))) - end - - GeckoModel( - [ - _gecko_reaction_column_reactions(columns, model)' * objective(model) - spzeros(length(coupling_row_gene_product)) - ], - columns, - coupling_row_reaction, - collect(zip(coupling_row_gene_product, gpb_.(gids[coupling_row_gene_product]))), - coupling_row_mass_group, - model, - ) -end diff --git a/src/analysis/max_min_driving_force.jl b/src/analysis/max_min_driving_force.jl deleted file mode 100644 index 8bb20f4eb..000000000 --- a/src/analysis/max_min_driving_force.jl +++ /dev/null @@ -1,274 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Perform a max-min driving force analysis on the `model`, as defined by Noor, et al., -"Pathway thermodynamics highlights kinetic obstacles in central metabolism.", PLoS -computational biology, 2014. - -The function uses the supplied `optimizer` and `reaction_standard_gibbs_free_energies`. -Optionally, `flux_solution` can be used to set the directions of each reaction in `model` -(all reactions are assumed to proceed forward and are active by default). The supplied -`flux_solution` should be free of internal cycles i.e. thermodynamically consistent. This -optional input is important if a reaction in `model` normally runs in reverse (negative -flux). Note, reactions in `flux_solution` that are smaller than `small_flux_tol` are also -ignored in the analysis function (for numerical stability). - -The max-min driving force algorithm returns the Gibbs free energy of the reactions, the -concentrations of metabolites and the actual maximum minimum driving force. The optimization -problem solved is: -``` -max min -ΔᵣG -s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) - ΔᵣG ≤ 0 - ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) -``` -where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas constant, T is -the temperature, S is the stoichiometry of the model, and C is the vector of metabolite -concentrations (and their respective lower and upper bounds). - -In case no feasible solution exists, `nothing` is returned. - -Reactions specified in `ignore_reaction_ids` are internally ignored when calculating the -max-min driving force. This should include water and proton importers. - -Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` need to be -specified so that they can be ignored in the calculations. Effectively this assumes an -aqueous environment at constant pH is used. - -`constant_concentrations` is used to fix the concentrations of certain metabolites (such as -CO₂). `concentration_ratios` is used to specify additional constraints on metabolite pair -concentrations (typically, this is done with various cofactors such as the ATP/ADP ratio. -For example, you can fix the concentration of ATP to be always 5× higher than of ADP by -specifying `Dict(("ATP","ADP") => 5.0)` - -`concentration_lb` and `concentration_ub` set the `Cₗ` and `Cᵤ` in the -optimization problems. - -`T` and `R` can be specified in the corresponding units; defaults are K and kJ/K/mol. -""" -function max_min_driving_force( - model::MetabolicModel, - reaction_standard_gibbs_free_energies::Dict{String,Float64}, - optimizer; - flux_solution::Dict{String,Float64} = Dict{String,Float64}(), - proton_ids::Vector{String} = ["h_c", "h_e"], - water_ids::Vector{String} = ["h2o_c", "h2o_e"], - constant_concentrations::Dict{String,Float64} = Dict{String,Float64}(), - concentration_ratios::Dict{Tuple{String,String},Float64} = Dict{ - Tuple{String,String}, - Float64, - }(), - concentration_lb = 1e-9, - concentration_ub = 100e-3, - T = _constants.T, - R = _constants.R, - small_flux_tol = 1e-6, - modifications = [], - ignore_reaction_ids = [], -) - opt_model = Model(optimizer) - - @variables opt_model begin - mmdf - logcs[1:n_metabolites(model)] - dgrs[1:n_reactions(model)] - end - - # set proton log concentration to zero so that it won't impact any calculations (biothermodynamics assumption) - proton_idxs = Int.(indexin(proton_ids, metabolites(model))) - for idx in proton_idxs - JuMP.fix(logcs[idx], 0.0) - end - - # set water concentrations to zero (aqueous condition assumptions) - water_idxs = Int.(indexin(water_ids, metabolites(model))) - for idx in water_idxs - JuMP.fix(logcs[idx], 0.0) - end - - # only consider reactions with supplied thermodynamic data AND have a flux bigger than - # small_flux_tol => finds a thermodynamic profile that explains flux_solution - active_rids = filter( - rid -> - haskey(reaction_standard_gibbs_free_energies, rid) && - abs(get(flux_solution, rid, small_flux_tol / 2)) > small_flux_tol && - !(rid in ignore_reaction_ids), - reactions(model), - ) - active_ridxs = Int.(indexin(active_rids, reactions(model))) - - # give dummy dG0 for reactions that don't have data - dg0s = - [get(reaction_standard_gibbs_free_energies, rid, 0.0) for rid in reactions(model)] - - S = stoichiometry(model) - - @constraint(opt_model, dgrs .== dg0s .+ (R * T) * S' * logcs) - - # thermodynamics should correspond to the fluxes - flux_signs = [sign(get(flux_solution, rid, 1.0)) for rid in reactions(model)] - - # only constrain reactions that have thermo data - @constraint(opt_model, dgrs[active_ridxs] .* flux_signs[active_ridxs] .<= 0) - - # add the absolute bounds - missing_mets = - [mid for mid in keys(constant_concentrations) if !(mid in metabolites(model))] - !isempty(missing_mets) && - throw(DomainError(missing_mets, "metabolite(s) not found in model.")) - for (midx, mid) in enumerate(metabolites(model)) # idx in opt_model (missing ignore_metabolites) - midx in water_idxs && continue - midx in proton_idxs && continue - if haskey(constant_concentrations, mid) - JuMP.fix(logcs[midx], log(constant_concentrations[mid])) - else - # this metabolite needs bounds - @constraint( - opt_model, - log(concentration_lb) <= logcs[midx] <= log(concentration_ub) - ) - end - end - - # add the relative bounds - for ((mid1, mid2), val) in concentration_ratios - idxs = indexin([mid1, mid2], metabolites(model)) # TODO: this is not performant - any(isnothing.(idxs)) && - throw(DomainError((mid1, mid2), "metabolite pair not found in model.")) - @constraint(opt_model, logcs[idxs[1]] == log(val) + logcs[idxs[2]]) - end - - @constraint(opt_model, mmdf .<= -dgrs[active_ridxs] .* flux_signs[active_ridxs]) - - @objective(opt_model, Max, mmdf) - - # apply the modifications, if any - for mod in modifications - mod(model, opt_model) - end - - optimize!(opt_model) - - is_solved(opt_model) || return nothing - - return ( - mmdf = value(opt_model[:mmdf]), - dg_reactions = Dict( - rid => value(opt_model[:dgrs][i]) for (i, rid) in enumerate(reactions(model)) - ), - concentrations = Dict( - mid => exp(value(opt_model[:logcs][i])) for - (i, mid) in enumerate(metabolites(model)) - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Perform a variant of flux variability analysis on a max min driving force type problem. -Arguments are forwarded to [`max_min_driving_force`](@ref). Calls [`screen`](@ref) -internally and possibly distributes computation across `workers`. If -`optimal_objective_value = nothing`, the function first performs regular max min driving -force analysis to find the max min driving force of the model and sets this to -`optimal_objective_value`. Then iteratively maximizes and minimizes the driving force across -each reaction, and then the concentrations while staying close to the original max min -driving force as specified in `bounds`. - -The `bounds` is a user-supplied function that specifies the max min driving force bounds for -the variability optimizations, by default it restricts the flux objective value to the -precise optimum reached in the normal max min driving force analysis. It can return `-Inf` -and `Inf` in first and second pair to remove the limit. Use [`gamma_bounds`](@ref) and -[`objective_bounds`](@ref) for simple bounds. - -Returns a matrix of solutions to [`max_min_driving_force`](@ref) additionally constrained as -described above, where the rows are in the order of the reactions and then the metabolites -of the `model`. For the reaction rows the first column is the maximum dG of that reaction, -and the second column is the minimum dG of that reaction subject to the above constraints. -For the metabolite rows, the first column is the maximum concentration, and the second column -is the minimum concentration subject to the constraints above. -""" -function max_min_driving_force_variability( - model::MetabolicModel, - reaction_standard_gibbs_free_energies::Dict{String,Float64}, - optimizer; - workers = [myid()], - optimal_objective_value = nothing, - bounds = z -> (z, Inf), - modifications = [], - kwargs..., -) - if isnothing(optimal_objective_value) - initsol = max_min_driving_force( - model, - reaction_standard_gibbs_free_energies, - optimizer; - modifications, - kwargs..., - ) - mmdf = initsol.mmdf - else - mmdf = optimal_objective_value - end - - lb, ub = bounds(mmdf) - - dgr_variants = [ - [[_mmdf_add_df_bound(lb, ub), _mmdf_dgr_objective(ridx, sense)]] for - ridx = 1:n_reactions(model), sense in [MAX_SENSE, MIN_SENSE] - ] - concen_variants = [ - [[_mmdf_add_df_bound(lb, ub), _mmdf_concen_objective(midx, sense)]] for - midx = 1:n_metabolites(model), sense in [MAX_SENSE, MIN_SENSE] - ] - - return screen( - model; - args = [dgr_variants; concen_variants], - analysis = (m, args) -> max_min_driving_force( - m, - reaction_standard_gibbs_free_energies, - optimizer; - modifications = [args; modifications], - kwargs..., - ), - workers, - ) -end - -""" -$(TYPEDSIGNATURES) - -Helper function to change the objective to optimizing some dG. -""" -function _mmdf_dgr_objective(ridx, sense) - (model, opt_model) -> begin - @objective(opt_model, sense, opt_model[:dgrs][ridx]) - end -end - -""" -$(TYPEDSIGNATURES) - -Helper function to change the objective to optimizing some concentration. -""" -function _mmdf_concen_objective(midx, sense) - (model, opt_model) -> begin - @objective(opt_model, sense, opt_model[:logcs][midx]) - end -end - -""" -$(TYPEDSIGNATURES) - -Helper function to add a new constraint on the driving force. -""" -function _mmdf_add_df_bound(lb, ub) - (model, opt_model) -> begin - if lb == ub - fix(opt_model[:mmdf], lb; force = true) - else - @constraint(opt_model, lb <= opt_model[:mmdf] <= ub) - end - end -end diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl deleted file mode 100644 index e51e0ea0d..000000000 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ /dev/null @@ -1,102 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Run minimization of metabolic adjustment (MOMA) on `model` with respect to -`flux_ref`, which is a vector of fluxes in the order of `reactions(model)`. -MOMA finds the shortest Euclidian distance between `flux_ref` and `model` with -`modifications`: -``` -min Σᵢ (xᵢ - flux_refᵢ)² -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -Because the problem has a quadratic objective, a QP solver is required. See -"Daniel, Vitkup & Church, Analysis of Optimality in Natural and Perturbed -Metabolic Networks, Proceedings of the National Academy of Sciences, 2002" for -more details. - -Additional arguments are passed to [`flux_balance_analysis`](@ref). - -Returns an optimized model that contains the resultant nearest flux. - -# Example -``` -model = load_model("e_coli_core.json") -flux_ref = flux_balance_analysis_vec(model, Gurobi.Optimizer) -optmodel = minimize_metabolic_adjustment( - model, - flux_ref, - Gurobi.Optimizer; - modifications = [change_constraint("PFL"; lb=0, ub=0)], # find flux of mutant that is closest to the wild type (reference) model - ) -value.(solution[:x]) # extract the flux from the optimizer -``` -""" -minimize_metabolic_adjustment_analysis( - model::MetabolicModel, - flux_ref::Union{Dict{String,Float64},Vector{Float64}}, - optimizer; - modifications = [], - kwargs..., -) = flux_balance_analysis( - model, - optimizer; - modifications = vcat([minimize_metabolic_adjustment(flux_ref)], modifications), - kwargs..., -) - -""" -$(TYPEDSIGNATURES) - -An optimization model modification that implements the MOMA in -[`minimize_metabolic_adjustment_analysis`](@ref). -""" -minimize_metabolic_adjustment(flux_ref::Vector{Float64}) = - (model, opt_model) -> begin - length(opt_model[:x]) == length(flux_ref) || throw( - DomainError( - flux_ref, - "length of the reference flux doesn't match the one in the optimization model", - ), - ) - @objective(opt_model, Min, sum((opt_model[:x] .- flux_ref) .^ 2)) - end - -""" -$(TYPEDSIGNATURES) - -Overload of [`minimize_metabolic_adjustment`](@ref) that works with a -dictionary of fluxes. -""" -minimize_metabolic_adjustment(flux_ref_dict::Dict{String,Float64}) = - (model, opt_model) -> - minimize_metabolic_adjustment([flux_ref_dict[rid] for rid in reactions(model)])( - model, - opt_model, - ) - -""" -$(TYPEDSIGNATURES) - -Perform minimization of metabolic adjustment (MOMA) and return a vector of fluxes in the -same order as the reactions in `model`. Arguments are forwarded to -[`minimize_metabolic_adjustment`](@ref) internally. - -This function is kept for backwards compatibility, use [`flux_vector`](@ref) -instead. -""" -minimize_metabolic_adjustment_analysis_vec(model::MetabolicModel, args...; kwargs...) = - flux_vector(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) - -""" -$(TYPEDSIGNATURES) - -Perform minimization of metabolic adjustment (MOMA) and return a dictionary mapping the -reaction IDs to fluxes. Arguments are forwarded to [`minimize_metabolic_adjustment`](@ref) -internally. - -This function is kept for backwards compatibility, use [`flux_vector`](@ref) -instead. -""" -minimize_metabolic_adjustment_analysis_dict(model::MetabolicModel, args...; kwargs...) = - flux_dict(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) diff --git a/src/analysis/modifications/crowding.jl b/src/analysis/modifications/crowding.jl deleted file mode 100644 index 8bad4c23c..000000000 --- a/src/analysis/modifications/crowding.jl +++ /dev/null @@ -1,36 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Adds a molecular crowding constraint to the optimization problem: `∑ wᵢ × vᵢ ≤ 1` where `wᵢ` -is a weight and `vᵢ` is a flux index in the model's reactions specified in `weights` as `vᵢ -=> wᵢ` pairs. - -See Beg, Qasim K., et al. "Intracellular crowding defines the mode and sequence of -substrate uptake by Escherichia coli and constrains its metabolic activity." Proceedings of -the National Academy of Sciences 104.31 (2007) for more details. -""" -add_crowding_constraints(weights::Dict{Int64,Float64}) = - (model, opt_model) -> begin - idxs = collect(keys(weights)) # order of keys and values is the same - ws = values(weights) - # since fluxes can be positive or negative, need absolute value: ∑ wᵢ × |vᵢ| ≤ 1 - # introduce slack variables to handle this - @variable(opt_model, crowding_slack[1:length(weights)]) - @constraint(opt_model, crowding_slack .>= opt_model[:x][idxs]) - @constraint(opt_model, crowding_slack .>= -opt_model[:x][idxs]) - @constraint(opt_model, sum(w * crowding_slack[i] for (i, w) in enumerate(ws)) <= 1) - end - -""" -$(TYPEDSIGNATURES) - -Variant of [`add_crowding_constraints`](@ref) that takes a dictinary of reactions `ids` -instead of reaction indices mapped to weights. -""" -add_crowding_constraints(weights::Dict{String,Float64}) = - (model, opt_model) -> begin - idxs = indexin(keys(weights), reactions(model)) - nothing in idxs && throw(ArgumentError("Reaction id not found in model.")) - add_crowding_constraints(Dict(zip(Int.(idxs), values(weights))))(model, opt_model) - end diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl deleted file mode 100644 index bcf1c0b89..000000000 --- a/src/analysis/modifications/generic.jl +++ /dev/null @@ -1,67 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Limit the objective value to `tolerance`-times the current objective value, as -with [`objective_bounds`](@ref). -""" -constrain_objective_value(tolerance) = - (_, opt_model) -> begin - lambda_min, lambda_max = objective_bounds(tolerance)(objective_value(opt_model)) - old_objective = objective_function(opt_model) - @constraint(opt_model, lambda_min <= sum(old_objective) <= lambda_max) - end - -""" -$(TYPEDSIGNATURES) - -Change the lower and upper bounds (`lb` and `ub` respectively) of reaction `id` if supplied. -""" -change_constraint(id::String; lb = nothing, ub = nothing) = - (model, opt_model) -> begin - ind = first(indexin([id], reactions(model))) - isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) - set_optmodel_bound!(ind, opt_model, lb = lb, ub = ub) - end - -""" -$(TYPEDSIGNATURES) - -Modification that changes the objective function used in a constraint based -analysis function. `new_objective` can be a single reaction identifier, or an -array of reactions identifiers. - -Optionally, the objective can be weighted by a vector of `weights`, and a -optimization `sense` can be set to either `MAX_SENSE` or `MIN_SENSE`. -""" -change_objective( - new_objective::Union{String,Vector{String}}; - weights = [], - sense = MAX_SENSE, -) = - (model, opt_model) -> begin - - # Construct objective_indices array - if typeof(new_objective) == String - objective_indices = indexin([new_objective], reactions(model)) - else - objective_indices = - [first(indexin([rxnid], reactions(model))) for rxnid in new_objective] - end - - any(isnothing.(objective_indices)) && throw( - DomainError(new_objective, "No matching reaction found for one or more ids."), - ) - - # Initialize weights - opt_weights = spzeros(n_reactions(model)) - - isempty(weights) && (weights = ones(length(objective_indices))) # equal weights - - for (j, i) in enumerate(objective_indices) - opt_weights[i] = weights[j] - end - - v = opt_model[:x] - @objective(opt_model, sense, sum(opt_weights[i] * v[i] for i in objective_indices)) - end diff --git a/src/analysis/modifications/knockout.jl b/src/analysis/modifications/knockout.jl deleted file mode 100644 index b55715e61..000000000 --- a/src/analysis/modifications/knockout.jl +++ /dev/null @@ -1,44 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A modification that zeroes the bounds of all reactions that would be knocked -out by the combination of specified genes (effectively disabling the -reactions). - -A slightly counter-intuitive behavior may occur if knocking out multiple genes: -Because this only changes the reaction bounds, multiple gene knockouts _must_ -be specified in a single call to [`knockout`](@ref), because the modifications -have no way to remember which genes are already knocked out and which not. - -In turn, having a reaction that can be catalyzed either by Gene1 or by Gene2, -specifying `modifications = [knockout(["Gene1", "Gene2"])]` does indeed disable -the reaction, but `modifications = [knockout("Gene1"), knockout("Gene2")]` does -_not_ disable the reaction (although reactions that depend either only on Gene1 -or only on Gene2 are disabled). -""" -knockout(gene_ids::Vector{String}) = - (model, optmodel) -> _do_knockout(model, optmodel, gene_ids) - -""" -$(TYPEDSIGNATURES) - -A helper variant of [`knockout`](@ref) for a single gene. -""" -knockout(gene_id::String) = knockout([gene_id]) - -""" -$(TYPEDSIGNATURES) - -Internal helper for knockouts on generic MetabolicModels. This can be -overloaded so that the knockouts may work differently (more efficiently) with -other models. -""" -function _do_knockout(model::MetabolicModel, opt_model, gene_ids::Vector{String}) - for (rxn_num, rxn_id) in enumerate(reactions(model)) - rga = reaction_gene_association(model, rxn_id) - if !isnothing(rga) && - all([any(in.(gene_ids, Ref(conjunction))) for conjunction in rga]) - set_optmodel_bound!(rxn_num, opt_model, ub = 0, lb = 0) - end - end -end diff --git a/src/analysis/modifications/loopless.jl b/src/analysis/modifications/loopless.jl deleted file mode 100644 index b7578ac74..000000000 --- a/src/analysis/modifications/loopless.jl +++ /dev/null @@ -1,57 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Add quasi-thermodynamic constraints to the model to ensure that no thermodynamically -infeasible internal cycles can occur. Adds the following constraints to the problem: -``` --max_flux_bound × (1 - yᵢ) ≤ xᵢ ≤ max_flux_bound × yᵢ --max_flux_bound × yᵢ + strict_inequality_tolerance × (1 - yᵢ) ≤ Gᵢ -Gᵢ ≤ -strict_inequality_tolerance × yᵢ + max_flux_bound × (1 - yᵢ) -Nᵢₙₜ' × G = 0 -yᵢ ∈ {0, 1} -Gᵢ ∈ ℝ -i ∈ internal reactions -Nᵢₙₜ is the nullspace of the internal stoichiometric matrix -``` -Note, this modification introduces binary variables, so an optimization solver capable of -handing mixed integer problems needs to be used. The arguments `max_flux_bound` and -`strict_inequality_tolerance` implement the "big-M" method of indicator constraints. - -For more details about the algorithm, see `Schellenberger, Lewis, and, Palsson. "Elimination -of thermodynamically infeasible loops in steady-state metabolic models.", Biophysical -journal, 2011`. -""" -add_loopless_constraints(; - max_flux_bound = _constants.default_reaction_bound, # needs to be an order of magnitude bigger, big M method heuristic - strict_inequality_tolerance = _constants.loopless_strict_inequality_tolerance, -) = - (model, opt_model) -> begin - - internal_rxn_idxs = [ - ridx for (ridx, rid) in enumerate(reactions(model)) if - !is_boundary(reaction_stoichiometry(model, rid)) - ] - - N_int = nullspace(Array(stoichiometry(model)[:, internal_rxn_idxs])) # no sparse nullspace function - - y = @variable(opt_model, y[1:length(internal_rxn_idxs)], Bin) - G = @variable(opt_model, G[1:length(internal_rxn_idxs)]) # approx ΔG for internal reactions - - x = opt_model[:x] - for (cidx, ridx) in enumerate(internal_rxn_idxs) - @constraint(opt_model, -max_flux_bound * (1 - y[cidx]) <= x[ridx]) - @constraint(opt_model, x[ridx] <= max_flux_bound * y[cidx]) - - @constraint( - opt_model, - -max_flux_bound * y[cidx] + strict_inequality_tolerance * (1 - y[cidx]) <= G[cidx] - ) - @constraint( - opt_model, - G[cidx] <= - -strict_inequality_tolerance * y[cidx] + max_flux_bound * (1 - y[cidx]) - ) - end - - @constraint(opt_model, N_int' * G .== 0) - end diff --git a/src/analysis/modifications/moment.jl b/src/analysis/modifications/moment.jl deleted file mode 100644 index 31b2e56f6..000000000 --- a/src/analysis/modifications/moment.jl +++ /dev/null @@ -1,120 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A modification that adds enzyme capacity constraints to the problem using a _modified_ -version of the MOMENT algorithm. Requires specific activities, `ksas` [mmol product/g -enzyme/h], for each reaction. Proteins are identified by their associated gene IDs. Adds a -variable vector `y` to the problem corresponding to the protein concentration [g enzyme/gDW -cell] of each gene product in the order of `genes(model)`. The total protein concentration -[g protein/gDW cell] is constrained to be less than or equal to the `protein_mass_fraction`. -Reaction flux constraints are changed to the MOMENT constraints (see below) for all -reactions that have a gene reaction rule, otherwise the flux bounds are left unaltered. - -See Adadi, Roi, et al. "Prediction of microbial growth rate versus biomass yield by a -metabolic network with kinetic parameters." PLoS computational biology (2012) for more -details of the original algorithm. - -Here, a streamlined version of the algorithm is implemented to ensure that the correct units -are used. Specifically, this implementation uses specific activities instead of `kcats`. -Thus, for a reaction that can only proceed forward and is catalyzed by protein `a`, the flux -`x[i]` is bounded by `x[i] <= ksas[i] * y[a]`. If isozymes `a` or `b` catalyse the -reaction, then `x[i] <= ksas[i] * (y[a] + y[b])`. If a reaction is catalyzed by subunits `a` -and `b` then `x[i] <= ksas[i] * min(y[a], y[b])`. These rules are applied recursively in the -model like in the original algorithm. The enzyme capacity constraint is then implemented by -`sum(y) ≤ protein_mass_fraction`. The major benefit of using `ksas` instead of `kcats` is -that active site number and unit issues are prevented. - -# Example -``` -flux_balance_analysis( - ..., - modifications = [ add_moment_constraints(my_kcats, 0.6) ], -) -``` -""" -add_moment_constraints(kcats::Dict{String,Float64}, protein_mass_fraction::Float64) = - (model, opt_model) -> begin - @warn( - "DEPRECATION WARNING: 'add_moment_constraints' will be removed in future versions of COBREXA.jl in favor of a GECKO-based formulation" - ) - - lbs, ubs = get_optmodel_bounds(opt_model) # to assign directions - # get grrs and ignore empty blocks: TODO: fix importing to avoid this ugly conditional see #462 - grrs = Dict( - rid => reaction_gene_association(model, rid) for - rid in reactions(model) if !( - reaction_gene_association(model, rid) == [[""]] || - isnothing(reaction_gene_association(model, rid)) - ) - ) - - # add protein variables - y = @variable(opt_model, y[1:n_genes(model)] >= 0) - - # add variables to deal with enzyme subunits - num_temp = sum(length(values(grr)) for grr in values(grrs)) # number "AND" blocks in grrs - t = @variable(opt_model, [1:num_temp]) # anonymous variable - @constraint(opt_model, t .>= 0) - - #= - Note, not all of t needs to be created, only those with OR GRR rules, however this - adds a lot of complexity to the code - re-implement this method if efficiency becomes - an issue. - =# - - # add capacity constraint - @constraint(opt_model, sum(y) <= protein_mass_fraction) - - k = 1 # counter - kstart = 1 - kend = 1 - x = opt_model[:x] - for (ridx, rid) in enumerate(reactions(model)) - - isnothing(get(grrs, rid, nothing)) && continue # only apply MOMENT constraints to reactions with a valid GRR - grrs[rid] == [[""]] && continue # TODO: remove once #462 is implemented - - # delete original constraints - delete(opt_model, opt_model[:lbs][ridx]) - delete(opt_model, opt_model[:ubs][ridx]) - - #= - For multi-subunit enzymes, the flux is bounded by the minimum concentration of - any subunit in the enzyme. If multiple isozymes with subunits exist, then the - sum of these minima bound the enzyme flux. E.g., suppose that you have a GRR - [[y, z], [w, u, v]], then x <= sum(min(y, z), min(w, u, v)). - - It is possible to reformulate x <= min(y, z) to preserve convexity: - x <= t && t <= y && t <= z where t is a subunit variable. - - The remainder of the code implements these ideas. - =# - - # build up the subunit variables - kstart = k - for grr in grrs[rid] - for gid in grr - gidx = first(indexin([gid], genes(model))) - @constraint(opt_model, t[k] <= y[gidx]) - end - k += 1 - end - kend = k - 1 - - # total enzyme concentration is sum of minimums - isozymes = @expression(opt_model, kcats[rid] * sum(t[kstart:kend])) - - if lbs[ridx] >= 0 && ubs[ridx] > 0 # forward only - @constraint(opt_model, x[ridx] <= isozymes) - @constraint(opt_model, 0 <= x[ridx]) - elseif lbs[ridx] < 0 && ubs[ridx] <= 0 # reverse only - @constraint(opt_model, -isozymes <= x[ridx]) - @constraint(opt_model, x[ridx] <= 0) - elseif lbs[ridx] == ubs[ridx] == 0 # set to zero - @constraint(opt_model, x[ridx] == 0) - else # reversible - @constraint(opt_model, x[ridx] <= isozymes) - @constraint(opt_model, -isozymes <= x[ridx]) - end - end - end diff --git a/src/analysis/modifications/optimizer.jl b/src/analysis/modifications/optimizer.jl deleted file mode 100644 index 41c51bdf8..000000000 --- a/src/analysis/modifications/optimizer.jl +++ /dev/null @@ -1,41 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Change the objective sense of optimization. -Possible arguments are `MAX_SENSE` and `MIN_SENSE`. - -If you want to change the objective and sense at the same time, use -[`change_objective`](@ref) instead to do both at once. -""" -change_sense(objective_sense) = - (_, opt_model) -> set_objective_sense(opt_model, objective_sense) - -""" -$(TYPEDSIGNATURES) - -Change the JuMP optimizer used to run the optimization. - -This may be used to try different approaches for reaching the optimum, and in -problems that may require different optimizers for different parts, such as the -[`parsimonious_flux_balance_analysis`](@ref). -""" -change_optimizer(optimizer) = (_, opt_model) -> set_optimizer(opt_model, optimizer) - -""" -$(TYPEDSIGNATURES) - -Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer -to the JuMP documentation and the documentation of the specific optimizer for -usable keys and values. -""" -change_optimizer_attribute(attribute_key, value) = - (_, opt_model) -> set_optimizer_attribute(opt_model, attribute_key, value) - -""" - silence - -Modification that disable all output from the JuMP optimizer (shortcut for -`set_silent` from JuMP). -""" -const silence = (_, opt_model) -> set_silent(opt_model) diff --git a/src/analysis/parsimonious.jl b/src/analysis/parsimonious.jl new file mode 100644 index 000000000..b8ac0ae4c --- /dev/null +++ b/src/analysis/parsimonious.jl @@ -0,0 +1,81 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Optimize the system of `constraints` to get the optimal `objective` value. Then +try to find a "parsimonious" solution with the same `objective` value, which +optimizes the `parsimonious_objective` (possibly also switching optimization +sense, optimizer, and adding more settings). + +For efficiency, everything is performed on a single instance of JuMP model. + +A simpler version suitable for direct work with metabolic models is available +in [`parsimonious_flux_balance`](@ref). +""" +function parsimonious_optimized_constraints( + constraints::C.ConstraintTreeElem; + objective::C.Value, + settings = [], + parsimonious_objective::C.Value, + parsimonious_optimizer = nothing, + parsimonious_sense = J.MIN_SENSE, + parsimonious_settings = [], + tolerances = [absolute_tolerance_bound(0)], + output = constraints, + kwargs..., +) + + # first solve the optimization problem with the original objective + om = optimization_model(constraints; objective, kwargs...) + for m in settings + m(om) + end + J.optimize!(om) + is_solved(om) || return nothing + + target_objective_value = J.objective_value(om) + + # switch to parsimonizing the solution w.r.t. to the objective value + isnothing(parsimonious_optimizer) || J.set_optimizer(om, parsimonious_optimizer) + for m in parsimonious_settings + m(om) + end + + J.@objective(om, J.MIN_SENSE, C.substitute(parsimonious_objective, om[:x])) + + # try all admissible tolerances + for tolerance in tolerances + (lb, ub) = tolerance(target_objective_value) + J.@constraint( + om, + pfba_tolerance_constraint, + lb <= C.substitute(objective, om[:x]) <= ub + ) + + J.optimize!(om) + is_solved(om) && return C.substitute_values(output, J.value.(om[:x])) + + J.delete(om, pfba_tolerance_constraint) + J.unregister(om, :pfba_tolerance_constraint) + end + + # all tolerances failed + return nothing +end + +export parsimonious_optimized_constraints diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl deleted file mode 100644 index 8d06f4d45..000000000 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ /dev/null @@ -1,114 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Run parsimonious flux balance analysis (pFBA) on the `model`. In short, pFBA -runs two consecutive optimization problems. The first is traditional FBA: -``` -max cᵀx = μ -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -And the second is a quadratic optimization problem: -``` -min Σᵢ xᵢ² -s.t. S x = b - xₗ ≤ x ≤ xᵤ - μ = μ⁰ -``` -Where the optimal solution of the FBA problem, μ⁰, has been added as an -additional constraint. See "Lewis, Nathan E, Hixson, Kim K, Conrad, Tom M, -Lerman, Joshua A, Charusanti, Pep, Polpitiya, Ashoka D, Adkins, Joshua N, -Schramm, Gunnar, Purvine, Samuel O, Lopez-Ferrer, Daniel, Weitz, Karl K, Eils, -Roland, König, Rainer, Smith, Richard D, Palsson, Bernhard Ø, (2010) Omic data -from evolved E. coli are consistent with computed optimal growth from -genome-scale models. Molecular Systems Biology, 6. 390. doi: -accession:10.1038/msb.2010.47" for more details. - -pFBA gets the model optimum by standard FBA (using -[`flux_balance_analysis`](@ref) with `optimizer` and `modifications`), then -finds a minimal total flux through the model that still satisfies the (slightly -relaxed) optimum. This is done using a quadratic problem optimizer. If the -original optimizer does not support quadratic optimization, it can be changed -using the callback in `qp_modifications`, which are applied after the FBA. See -the documentation of [`flux_balance_analysis`](@ref) for usage examples of -modifications. - -Thhe optimum relaxation sequence can be specified in `relax` parameter, it -defaults to multiplicative range of `[1.0, 0.999999, ..., 0.99]` of the original -bound. - -Returns an optimized model that contains the pFBA solution (or an unsolved model -if something went wrong). - -# Example -``` -model = load_model("e_coli_core.json") -optmodel = parsimonious_flux_balance_analysis(model, biomass, Gurobi.Optimizer) -value.(solution[:x]) # extract the flux from the optimizer -``` -""" -function parsimonious_flux_balance_analysis( - model::MetabolicModel, - optimizer; - modifications = [], - qp_modifications = [], - relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99], -) - # Run FBA - opt_model = flux_balance_analysis(model, optimizer; modifications = modifications) - is_solved(opt_model) || return opt_model # FBA failed - - # get the objective - Z = objective_value(opt_model) - original_objective = objective_function(opt_model) - - # prepare the model for pFBA - for mod in qp_modifications - mod(model, opt_model) - end - - # add the minimization constraint for total flux - v = opt_model[:x] # fluxes - @objective(opt_model, Min, sum(dot(v, v))) - - for rb in relax_bounds - lb, ub = objective_bounds(rb)(Z) - @_models_log @info "pFBA step relaxed to [$lb,$ub]" - @constraint(opt_model, pfba_constraint, lb <= original_objective <= ub) - - optimize!(opt_model) - is_solved(opt_model) && break - - delete(opt_model, pfba_constraint) - unregister(opt_model, :pfba_constraint) - end - - return opt_model -end - -""" -$(TYPEDSIGNATURES) - -Perform parsimonious flux balance analysis on `model` using `optimizer`. -Returns a vector of fluxes in the same order as the reactions in `model`. -Arguments are forwarded to [`parsimonious_flux_balance_analysis`](@ref) -internally. - -This function is kept for backwards compatibility, use [`flux_vector`](@ref) -instead. -""" -parsimonious_flux_balance_analysis_vec(model::MetabolicModel, args...; kwargs...) = - flux_vector(model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) - -""" -$(TYPEDSIGNATURES) - -Perform parsimonious flux balance analysis on `model` using `optimizer`. -Returns a dictionary mapping the reaction IDs to fluxes. Arguments are -forwarded to [`parsimonious_flux_balance_analysis`](@ref) internally. - -This function is kept for backwards compatibility, use [`flux_dict`](@ref) -instead. -""" -parsimonious_flux_balance_analysis_dict(model::MetabolicModel, args...; kwargs...) = - flux_dict(model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) diff --git a/src/analysis/sample.jl b/src/analysis/sample.jl new file mode 100644 index 000000000..c12a3d971 --- /dev/null +++ b/src/analysis/sample.jl @@ -0,0 +1,38 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function sample_achr( + constraints::C.ConstraintTree, + points::Matrix{Float64}; + seed, + chains, + collect_iterations, + epsilon, + filter_constraints::C.ConstraintTree, + workers = D.workers(), +) end + +function sample_affine_hr( + constraints::C.ConstraintTree, + points::Matrix{Float64}; + seed, + chains, + collect_iterations, + epsilon, + mix_points, + filter_constraints::C.ConstraintTree, + workers = D.workers(), +) end diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl deleted file mode 100644 index b369b0be6..000000000 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ /dev/null @@ -1,141 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Run a hit-and-run style sampling that starts from `warmup_points` and uses -their affine combinations for generating the run directions to sample the space -delimited by `lbs` and `ubs`. The reaction rate vectors in `warmup_points` -should be organized in columns, i.e. `warmup_points[:,1]` is the first set of -reaction rates. - -There are total `chains` of hit-and-run runs, each on a batch of -`size(warmup_points, 2)` points. The runs are scheduled on `workers`, for good -load balancing `chains` should be ideally much greater than `length(workers)`. - -Each run continues for `maximum(sample_iters)` iterations; the numbers in -`sample_iters` represent the iterations at which the whole "current" batch of -points is collected for output. For example, `sample_iters=[1,4,5]` causes the -process run for 5 iterations, returning the sample batch that was produced by -1st, 4th and last (5th) iteration. - -Returns a matrix of sampled reaction rates (in columns), with all collected -samples horizontally concatenated. The total number of samples (columns) will -be `size(warmup_points,2) * chains * length(sample_iters)`. - -# Example -``` -warmup_points = warmup_from_variability(model, GLPK.Optimizer) -samples = affine_hit_and_run(model, warmup_points, sample_iters = 101:105) - -# convert the result to flux (for models where the distinction matters): -fluxes = reaction_flux(model)' * samples -``` -""" -function affine_hit_and_run( - m::MetabolicModel, - warmup_points::Matrix{Float64}; - sample_iters = 100 .* (1:5), - workers = [myid()], - chains = length(workers), - seed = rand(Int), -) - @assert size(warmup_points, 1) == n_reactions(m) - - lbs, ubs = bounds(m) - C = coupling(m) - cl, cu = coupling_bounds(m) - if isnothing(C) - C = zeros(0, n_reactions(m)) - cl = zeros(0) - cu = zeros(0) - end - save_at.(workers, :cobrexa_hit_and_run_data, Ref((warmup_points, lbs, ubs, C, cl, cu))) - - # sample all chains - samples = hcat( - pmap( - chain -> _affine_hit_and_run_chain( - (@remote cobrexa_hit_and_run_data)..., - sample_iters, - seed + chain, - ), - CachingPool(workers), - 1:chains, - )..., - ) - - # remove warmup points from workers - map(fetch, remove_from.(workers, :cobrexa_hit_and_run_data)) - - return samples -end - -""" -$(TYPEDSIGNATURES) - -Internal helper function for computing a single affine hit-and-run chain. -""" -function _affine_hit_and_run_chain(warmup, lbs, ubs, C, cl, cu, iters, seed) - - rng = StableRNG(seed % UInt) - points = copy(warmup) - d, n_points = size(points) - n_couplings = size(C, 1) - result = Matrix{Float64}(undef, d, n_points * length(iters)) - - # helper for reducing the available run range - function update_range(range, pos, dir, lb, ub) - dl = lb - pos - du = ub - pos - lower, upper = - dir < -_constants.tolerance ? (du, dl) ./ dir : - dir > _constants.tolerance ? (dl, du) ./ dir : (-Inf, Inf) - return (max(range[1], lower), min(range[2], upper)) - end - - iter = 0 - - for (iter_idx, iter_target) in enumerate(iters) - - while iter < iter_target - iter += 1 - - new_points = copy(points) - - for i = 1:n_points - - mix = rand(rng, n_points) .+ _constants.tolerance - dir = points * (mix ./ sum(mix)) - points[:, i] - - # iteratively collect the maximum and minimum possible multiple - # of `dir` added to the current point - run_range = (-Inf, Inf) - for j = 1:d - run_range = - update_range(run_range, points[j, i], dir[j], lbs[j], ubs[j]) - end - - # do the same for coupling - dc = C * dir - pc = C * points[:, i] - for j = 1:n_couplings - run_range = update_range(run_range, pc[j], dc[j], cl[j], cu[j]) - end - - # generate a point in the viable run range and update it - lambda = run_range[1] + rand(rng) * (run_range[2] - run_range[1]) - isfinite(lambda) || continue # avoid divergence - new_points[:, i] = points[:, i] .+ lambda .* dir - - # TODO normally, here we would check if sum(S*new_point) is still - # lower than the tolerance, but we shall trust the computer - # instead. - end - - points = new_points - end - - result[:, n_points*(iter_idx-1)+1:iter_idx*n_points] .= points - end - - result -end diff --git a/src/analysis/sampling/warmup_variability.jl b/src/analysis/sampling/warmup_variability.jl deleted file mode 100644 index 8764bbeb2..000000000 --- a/src/analysis/sampling/warmup_variability.jl +++ /dev/null @@ -1,102 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Generates FVA-like warmup points for samplers, by selecting random points by -minimizing and maximizing reactions. Can not return more than 2 times the -number of reactions in the model. -""" -function warmup_from_variability( - model::MetabolicModel, - optimizer, - n_points::Int, - seed = rand(Int); - kwargs..., -) - nr = n_reactions(model) - - n_points > 2 * nr && throw( - DomainError( - n_points, - "Variability method can not generate more than $(2*nr) points from this model", - ), - ) - - sample = shuffle(StableRNG(seed % UInt), vcat(1:nr, -(1:nr)))[begin:n_points] - warmup_from_variability( - model, - optimizer, - -filter(x -> x < 0, sample), - filter(x -> x > 0, sample); - kwargs..., - ) -end - -""" -$(TYPEDSIGNATURES) - -Generate FVA-like warmup points for samplers, by minimizing and maximizing the -specified reactions. The result is returned as a matrix, each point occupies as -single column in the result. - -!!! warning Limited effect of modifications in `warmup_from_variability` - Modifications of the optimization model applied in `modifications` - parameter that change the semantics of the model have an effect on the - warmup points, but do not automatically carry to the subsequent sampling. - Users are expected to manually transplant any semantic changes to the - actual sampling functions, such as [`affine_hit_and_run`](@ref). -""" -function warmup_from_variability( - model::MetabolicModel, - optimizer, - min_reactions::AbstractVector{Int} = 1:n_reactions(model), - max_reactions::AbstractVector{Int} = 1:n_reactions(model); - modifications = [], - workers::Vector{Int} = [myid()], -)::Matrix{Float64} - - # create optimization problem at workers, apply modifications - save_model = :( - begin - local model = $model - local optmodel = $COBREXA.make_optimization_model(model, $optimizer) - for mod in $modifications - mod(model, optmodel) - end - optmodel - end - ) - - asyncmap(fetch, save_at.(workers, :cobrexa_sampling_warmup_optmodel, Ref(save_model))) - - fluxes = hcat( - dpmap( - rid -> :($COBREXA._maximize_warmup_reaction( - cobrexa_sampling_warmup_optmodel, - $rid, - om -> $COBREXA.JuMP.value.(om[:x]), - )), - CachingPool(workers), - vcat(-min_reactions, max_reactions), - )..., - ) - - # free the data on workers - asyncmap(fetch, remove_from.(workers, :cobrexa_sampling_warmup_optmodel)) - - return fluxes -end - -""" -$(TYPEDSIGNATURES) - -A helper function for finding warmup points from reaction variability. -""" -function _maximize_warmup_reaction(opt_model, rid, ret) - sense = rid > 0 ? MAX_SENSE : MIN_SENSE - var = all_variables(opt_model)[abs(rid)] - - @objective(opt_model, sense, var) - optimize!(opt_model) - - is_solved(opt_model) ? ret(opt_model) : nothing -end diff --git a/src/analysis/screen.jl b/src/analysis/screen.jl new file mode 100644 index 000000000..ef917cd56 --- /dev/null +++ b/src/analysis/screen.jl @@ -0,0 +1,73 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Execute a function with arguments given by `args` on `workers`. + +This is merely a nice shortcut for `Distributed.pmap` running over a +`Distributed.CachingPool` of the given workers. +""" +screen(f, args...; workers = D.workers()) = D.pmap(f, D.CachingPool(workers), args...) + +""" +$(TYPEDSIGNATURES) + +Execute a function arguments from arrays `args` on `workers`, with a pre-cached +JuMP optimization model created from `constraints`, `objective` and `optimizer` +using [`optimization_model`](@ref). `settings` are applied to the optimization +model before first execution of `f`. + +Since the model is cached and never re-created, this may be faster than just +plain [`screen`](@ref) in many use cases. + +The function `f` is supposed to take `length(args)+1` arguments, the first +argument is the JuMP model, and the other arguments are taken from `args` as +with `Distributed.pmap`. While the model may be modified in place, one should +take care to avoid modifications that change results of subsequent invocations +of `f`, as that almost always results in data races and irreproducible +executions. Ideally, all modifications of the model should be either manually +reverted in the invocation of `f`, or the future invocations of `f` must be +able to overwrite them. + +`f` may use [`optimized_model`](@ref) to extract results easily w.r.t. some +given `ConstraintTree`. +""" +function screen_optimization_model( + f, + constraints::C.ConstraintTree, + args...; + objective::Union{Nothing,C.Value} = nothing, + sense = Maximal, + optimizer, + settings = [], + workers = D.workers(), +) + worker_cache = worker_local_data(constraints) do c + om = COBREXA.optimization_model(c; objective, sense, optimizer) + for s in settings + s(om) + end + om + end + + D.pmap( + (as...) -> f(get_worker_local_data(worker_cache), as...), + D.CachingPool(workers), + args..., + ) +end diff --git a/src/analysis/screening.jl b/src/analysis/screening.jl deleted file mode 100644 index a4dd13af4..000000000 --- a/src/analysis/screening.jl +++ /dev/null @@ -1,276 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Internal helper to check the presence and shape of modification and argument -arrays in [`screen`](@ref) and pals. -""" -function _screen_args(argtuple, kwargtuple, modsname) - - mods = get(kwargtuple, modsname, nothing) - args = get(kwargtuple, :args, nothing) - - if isnothing(mods) - if isnothing(args) - throw( - DomainError(args, "at least one of `$modsname` and `args` must be defined"), - ) - end - return NamedTuple{(modsname,)}(Ref([[] for _ in args])) - elseif isnothing(args) - return (args = [() for _ in mods],) - else - if size(mods) != size(args) - throw( - DomainError( - "$(size(mods)) != $(size(args))", - "sizes of `$modsname` and `args` differ", - ), - ) - end - return () - end -end - -""" -$(TYPEDSIGNATURES) - -Take an array of model-modifying function vectors in `variants`, and execute -the function `analysis` on all variants of the `model` specified by `variants`. -The computation is distributed over worker IDs in `workers`. If `args` are -supplied (as an array of the same size as the `variants`), they are forwarded -as arguments to the corresponding analysis function calls. - -The array of variants must contain vectors of single-parameter functions, these -are applied to model in order. The functions must *not* modify the model, but -rather return a modified copy. The copy should be made as shallow as possible, -to increase memory efficiency of the process. Variant generators that modify -the argument model in-place will cause unpredictable results. Refer to the -definition of [`screen_variant`](@ref) for details. - -The function `analysis` will receive a single argument (the modified model), -together with arguments from `args` expanded by `...`. Supply an array of -tuples or vectors to pass in multiple arguments to each function. If the -argument values should be left intact (not expanded to multiple arguments), -they must be wrapped in single-item tuple or vector. - -The modification and analysis functions are transferred to `workers` as-is; all -packages required to run them (e.g. the optimization solvers) must be loaded -there. Typically, you want to use the macro `@everywhere using -MyFavoriteSolver` from `Distributed` package for loading the solvers. - -# Return value - -The results of running `analysis` are collected in to the resulting array, in a -way that preserves the shape of the `variants`, similarly as with `pmap`. - -The results of `analysis` function must be serializable, preferably made only -from pure Julia structures, because they may be transferred over the network -between the computation nodes. For that reason, functions that return whole -JuMP models that contain pointers to allocated C structures (such as -[`flux_balance_analysis`](@ref) used with `GLPK` or `Gurobi` otimizers) will -generally not work in this context. - -Note: this function is a thin argument-handling wrapper around -[`_screen_impl`](@ref). - -# Example -``` -function reverse_reaction(i::Int) - (model::CoreModel) -> begin - mod = copy(model) - mod.S[:,i] .*= -1 # this is unrealistic but sufficient for demonstration - mod - end -end - -m = load_model(CoreModel, "e_coli_core.xml") - -screen(m, - variants = [ - [reverse_reaction(5)], - [reverse_reaction(3), reverse_reaction(6)] - ], - analysis = mod -> mod.S[:,3]) # observe the changes in S - -screen(m, - variants = [ - [reverse_reaction(5)], - [reverse_reaction(3), reverse_reaction(6)] - ], - analysis = mod -> flux_balance_analysis_vec(mod, GLPK.Optimizer)) -``` -""" -screen(args...; kwargs...) = - _screen_impl(args...; kwargs..., _screen_args(args, kwargs, :variants)...) - -""" -$(TYPEDSIGNATURES) - -The actual implementation of [`screen`](@ref). -""" -function _screen_impl( - model::MetabolicModel; - variants::Array{V,N}, - analysis, - args::Array{A,N}, - workers = [myid()], -)::Array where {V<:AbstractVector,A,N} - - asyncmap(fetch, save_at.(workers, :cobrexa_screen_variants_model, Ref(model))) - asyncmap(fetch, save_at.(workers, :cobrexa_screen_variants_analysis_fn, Ref(analysis))) - asyncmap(fetch, get_from.(workers, Ref(:(precache!(cobrexa_screen_variants_model))))) - - res = pmap( - (vars, args)::Tuple -> screen_variant( - (@remote cobrexa_screen_variants_model), - vars, - (@remote cobrexa_screen_variants_analysis_fn), - args, - ), - CachingPool(workers), - zip(variants, args), - ) - - asyncmap(fetch, remove_from.(workers, :cobrexa_screen_variants_model)) - asyncmap(fetch, remove_from.(workers, :cobrexa_screen_variants_analysis_fn)) - - return res -end - -""" -$(TYPEDSIGNATURES) - -Helper function for [`screen`](@ref) that applies all single-argument -functions in `variant` to the `model` (in order from "first" to -"last"), and executes `analysis` on the result. - -Can be used to test model variants locally. -""" -function screen_variant(model::MetabolicModel, variant::Vector, analysis, args = ()) - for fn in variant - model = fn(model) - end - analysis(model, args...) -end - -""" -$(TYPEDSIGNATURES) - -A shortcut for [`screen`](@ref) that only works with model variants. -""" -screen_variants(model, variants, analysis; workers = [myid()]) = - screen(model; variants = variants, analysis = analysis, workers = workers) - -""" -$(TYPEDSIGNATURES) - -A variant of [`optimize_objective`](@ref) directly usable in -[`screen_optmodel_modifications`](@ref). -""" -screen_optimize_objective(_, optmodel)::Maybe{Float64} = optimize_objective(optmodel) - -""" -$(TYPEDSIGNATURES) - -Internal helper for [`screen_optmodel_modifications`](@ref) that creates the -model and applies the modifications. -""" -function _screen_optmodel_prepare(model, optimizer, common_modifications) - precache!(model) - optmodel = make_optimization_model(model, optimizer) - for mod in common_modifications - mod(model, optmodel) - end - return (model, optmodel) -end - -""" -$(TYPEDSIGNATURES) - -Internal helper for [`screen_optmodel_modifications`](@ref) that computes one -item of the screening task. -""" -function _screen_optmodel_item((mods, args)) - (model, optmodel) = @remote cobrexa_screen_optmodel_modifications_data - for mod in mods - mod(model, optmodel) - end - (@remote cobrexa_screen_optmodel_modifications_fn)(model, optmodel, args...) -end - -""" -$(TYPEDSIGNATURES) - -Screen multiple modifications of the same optimization model. - -This function is potentially more efficient than [`screen`](@ref) because it -avoids making variants of the model structure and remaking of the optimization -model. On the other hand, modification functions need to keep the optimization -model in a recoverable state (one that leaves the model usable for the next -modification), which limits the possible spectrum of modifications applied. - -Internally, `model` is distributed to `workers` and transformed into the -optimization model using [`make_optimization_model`](@ref). -`common_modifications` are applied to the models at that point. Next, vectors -of functions in `modifications` are consecutively applied, and the result of -`analysis` function called on model are collected to an array of the same -extent as `modifications`. Calls of `analysis` are optionally supplied with -extra arguments from `args` expanded with `...`, just like in [`screen`](@ref). - -Both the modification functions (in vectors) and the analysis function here -have 2 base parameters (as opposed to 1 with [`screen`](@ref)): first is the -`model` (carried through as-is), second is the prepared JuMP optimization model -that may be modified and acted upon. As an example, you can use modification -[`change_constraint`](@ref) and analysis [`screen_optimize_objective`](@ref). - -Note: This function is a thin argument-handling wrapper around -[`_screen_optmodel_modifications_impl`](@ref). -""" -screen_optmodel_modifications(args...; kwargs...) = _screen_optmodel_modifications_impl( - args...; - kwargs..., - _screen_args(args, kwargs, :modifications)..., -) - -""" -$(TYPEDSIGNATURES) - -The actual implementation of [`screen_optmodel_modifications`](@ref). -""" -function _screen_optmodel_modifications_impl( - model::MetabolicModel, - optimizer; - common_modifications::VF = [], - modifications::Array{V,N}, - args::Array{A,N}, - analysis::Function, - workers = [myid()], -)::Array where {V<:AbstractVector,VF<:AbstractVector,A,N} - - asyncmap( - fetch, - save_at.( - workers, - :cobrexa_screen_optmodel_modifications_data, - Ref( - :($COBREXA._screen_optmodel_prepare( - $model, - $optimizer, - $common_modifications, - )), - ), - ), - ) - asyncmap( - fetch, - save_at.(workers, :cobrexa_screen_optmodel_modifications_fn, Ref(analysis)), - ) - - res = pmap(_screen_optmodel_item, CachingPool(workers), zip(modifications, args)) - - asyncmap(fetch, remove_from.(workers, :cobrexa_screen_optmodel_modifications_data)) - asyncmap(fetch, remove_from.(workers, :cobrexa_screen_optmodel_modifications_fn)) - - return res -end diff --git a/src/analysis/smoment.jl b/src/analysis/smoment.jl deleted file mode 100644 index 179d41597..000000000 --- a/src/analysis/smoment.jl +++ /dev/null @@ -1,68 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Construct a model with a structure given by sMOMENT algorithm; returns a -[`SMomentModel`](@ref) (see the documentation for details). - -# Arguments - -- `reaction_isozyme` parameter is a function that returns a single - [`Isozyme`](@ref) for each reaction, or `nothing` if the reaction is not - enzymatic. If the reaction has multiple isozymes, use - [`smoment_isozyme_speed`](@ref) to select the fastest one, as recommended by - the sMOMENT paper. -- `gene_product_molar_mass` parameter is a function that returns a molar mass - of each gene product as specified by sMOMENT. -- `total_enzyme_capacity` is the maximum "enzyme capacity" in the model. - -Alternatively, all function arguments also accept dictionaries that are used to -provide the same data lookup. -""" -function make_smoment_model( - model::MetabolicModel; - reaction_isozyme::Union{Function,Dict{String,Isozyme}}, - gene_product_molar_mass::Union{Function,Dict{String,Float64}}, - total_enzyme_capacity::Float64, -) - ris_ = - reaction_isozyme isa Function ? reaction_isozyme : - (rid -> get(reaction_isozyme, rid, nothing)) - gpmm_ = - gene_product_molar_mass isa Function ? gene_product_molar_mass : - (gid -> gene_product_molar_mass[gid]) - - columns = Vector{_smoment_column}() - - (lbs, ubs) = bounds(model) - rids = reactions(model) - - for i = 1:n_reactions(model) - isozyme = ris_(rids[i]) - if isnothing(isozyme) - # non-enzymatic reaction (or a totally ignored one) - push!(columns, _smoment_column(i, 0, lbs[i], ubs[i], 0)) - continue - end - - mw = sum(gpmm_(gid) * ps for (gid, ps) in isozyme.gene_product_count) - - if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_reverse > _constants.tolerance - # reaction can run in reverse - push!( - columns, - _smoment_column(i, -1, max(-ubs[i], 0), -lbs[i], mw / isozyme.kcat_reverse), - ) - end - - if max(lbs[i], ubs[i]) > 0 && isozyme.kcat_forward > _constants.tolerance - # reaction can run forward - push!( - columns, - _smoment_column(i, 1, max(lbs[i], 0), ubs[i], mw / isozyme.kcat_forward), - ) - end - end - - return SMomentModel(columns, total_enzyme_capacity, model) -end diff --git a/src/analysis/solver.jl b/src/analysis/solver.jl new file mode 100644 index 000000000..4ca856738 --- /dev/null +++ b/src/analysis/solver.jl @@ -0,0 +1,42 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Make an JuMP model out of `constraints` using [`optimization_model`](@ref) +(most arguments are forwarded there), then apply the `settings`, optimize +the model, and return either `nothing` if the optimization failed, or `output` +substituted with the solved values (`output` defaults to `constraints`. + +For a "nice" version for simpler finding of metabolic model optima, use +[`flux_balance`](@ref). +""" +function optimized_constraints( + constraints::C.ConstraintTree; + settings = [], + output::C.ConstraintTreeElem = constraints, + kwargs..., +) + om = optimization_model(constraints; kwargs...) + for m in settings + m(om) + end + + optimized_model(om; output) +end + +export optimized_constraints diff --git a/src/analysis/variability.jl b/src/analysis/variability.jl new file mode 100644 index 000000000..932cecbd7 --- /dev/null +++ b/src/analysis/variability.jl @@ -0,0 +1,45 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Compute the variability +""" +function constraints_variability( + constraints::C.ConstraintTree, + targets::C.ConstraintTree; + optimizer, + settings = [], + workers = D.workers(), +)::C.Tree{Tuple{Maybe{Float64},Maybe{Float64}}} + + target_array = [dim * dir for dim in tree_deflate(C.value, targets), dir in (-1, 1)] + + result_array = screen_optimization_model( + constraints, + target_array; + optimizer, + settings, + workers, + ) do om, target + J.@objective(om, Maximal, C.substitute(target, om[:x])) + J.optimize!(om) + is_solved(om) ? J.objective_value(om) : nothing + end + + constraint_tree_reinflate(targets, [tuple(a, b) for (a, b) in eachrow(result_array)]) +end diff --git a/src/base/constants.jl b/src/base/constants.jl deleted file mode 100644 index 59be8ba88..000000000 --- a/src/base/constants.jl +++ /dev/null @@ -1,66 +0,0 @@ - -""" -A named tuple that contains the magic values that are used globally for -whatever purposes. -""" -const _constants = ( - default_stoich_show_size = 50_000, - default_reaction_bound = 1e3, - tolerance = 1e-6, - sampling_keep_iters = 100, - sampling_size = 1000, - loopless_strict_inequality_tolerance = 1, - extracellular_suffixes = ["_e", "[e]", "(e)"], - exchange_prefixes = ["EX_", "Exch_", "Ex_", "R_EX_", "R_Ex", "R_Exch_"], - biomass_strings = ["BIOMASS", "biomass", "Biomass"], - keynames = ( - rxns = ["reactions", "rxns", "RXNS", "REACTIONS", "Reactions", "Rxns"], - mets = ["metabolites", "mets", "METS", "METABOLITES", "Metabolites", "Mets"], - genes = ["genes", "GENES", "Genes"], - lbs = ["lbs", "lb", "lowerbounds", "lower_bounds"], - ubs = ["ubs", "ub", "upperbounds", "upper_bounds"], - stochiometry = ["S"], - balance = ["b"], - objective = ["c"], - grrs = ["gene_reaction_rules", "grRules", "rules"], - ids = ["id", "description"], - metformulas = ["metFormula", "metFormulas"], - metcharges = ["metCharge", "metCharges"], - metcompartments = ["metCompartment", "metCompartments", "metComp", "metComps"], - metcomptables = ["comps", "compNames"], - rxnnames = ["rxnNames"], - metnames = ["metNames"], - ), - gene_annotation_checks = ( - "ncbigene", - "ncbigi", - "refseq_locus_tag", - "refseq_name", - "refseq_synonym", - "uniprot", - ), - reaction_annotation_checks = ( - "bigg.reaction", - "biocyc", - "ec-code", - "kegg.reaction", - "metanetx.reaction", - "rhea", - "sabiork", - "seed.reaction", - ), - metabolite_annotation_checks = ( - "kegg.compound", - "bigg.metabolite", - "chebi", - "inchi_key", - "sabiork", - "hmdb", - "seed.compound", - "metanetx.chemical", - "reactome.compound", - "biocyc", - ), - T = 298.15, # Kelvin - R = 8.31446261815324e-3, # kJ/K/mol -) diff --git a/src/base/identifiers.jl b/src/base/identifiers.jl deleted file mode 100644 index 92b147a93..000000000 --- a/src/base/identifiers.jl +++ /dev/null @@ -1,33 +0,0 @@ -""" -This module uses annotation identifiers to classify reactions, metabolites, -genes, etc. If an subject has a matching annotation, then it is assumed that it -is part of the associated class of objects. -""" -module Identifiers -using ..COBREXA.SBOTerms - -const EXCHANGE_REACTIONS = [SBOTerms.EXCHANGE_REACTION] - -const TRANSPORT_REACTIONS = [ - SBOTerms.TRANSPORT_REACTION, - SBOTerms.TRANSCELLULAR_MEMBRANE_INFLUX_REACTION, - SBOTerms.TRANSCELLULAR_MEMBRANE_EFFLUX_REACTION, -] - -const METABOLIC_REACTIONS = [SBOTerms.BIOCHEMICAL_REACTION] - -const BIOMASS_REACTIONS = [SBOTerms.BIOMASS_PRODUCTION] - -const ATP_MAINTENANCE_REACTIONS = [SBOTerms.ATP_MAINTENANCE] - -const PSEUDOREACTIONS = [ - SBOTerms.EXCHANGE_REACTION, - SBOTerms.DEMAND_REACTION, - SBOTerms.BIOMASS_PRODUCTION, - SBOTerms.ATP_MAINTENANCE, - SBOTerms.PSEUDOREACTION, - SBOTerms.SINK_REACTION, -] - -const SPONTANEOUS_REACTIONS = [SBOTerms.SPONTANEOUS_REACTION] -end diff --git a/src/base/logging/log.jl b/src/base/logging/log.jl deleted file mode 100644 index 866846988..000000000 --- a/src/base/logging/log.jl +++ /dev/null @@ -1,58 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -This creates a group of functions that allow masking out topic-related logging -actions. A call that goes as follows: - - @_make_logging_tag XYZ - -creates the following tools: - -- global variable `_XYZ_log_enabled` defaulted to false -- function `log_XYZ` that can be called to turn the logging on/off -- a masking macro `@_XYZ_log` that can be prepended to commands that should - only happen if the logging of tag XYZ is enabled. - -The masking macro is then used as follows: - - @_XYZ_log @info "This is the extra verbose information you wanted!" a b c - -The user can direct logging with these: - - log_XYZ() - log_XYZ(false) - -`doc` should be a name of the stuff that is being printed if the corresponding -log_XYZ() is enabled -- it is used to create a friendly documentation for the -logging switch. In this case it could say `"X, Y and Z-related messages"`. -""" -macro _make_logging_tag(sym::Symbol, doc::String) - enable_flag = Symbol(:_, sym, :_log_enabled) - enable_fun = Symbol(:log_, sym) - log_macro = Symbol(:_, sym, :_log) - # esc() is necessary here because the internal macro processing would - # otherwise bind the variables incorrectly. - esc(:( - begin - $enable_flag = false - - """ - $(string($enable_fun))(enable::Bool=true) - - Enable (default) or disable (by passing `false`) output of $($doc). - """ - $enable_fun(x::Bool = true) = begin - global $enable_flag = x - end - - macro $log_macro(x) - $enable_flag ? x : :nothing - end - end - )) -end - -@_make_logging_tag models "model-related messages" -@_make_logging_tag io "messages and warnings from model input/output" -@_make_logging_tag perf "performance-related tracing information" diff --git a/src/base/macros/change_bounds.jl b/src/base/macros/change_bounds.jl deleted file mode 100644 index 83a479479..000000000 --- a/src/base/macros/change_bounds.jl +++ /dev/null @@ -1,64 +0,0 @@ -""" - @_change_bounds_fn ModelType IdxType [plural] [inplace] begin ... end - -A helper for creating simple bounds-changing function similar to -[`change_bounds`](@ref). -""" -macro _change_bounds_fn(model_type, idx_type, args...) - body = last(args) - typeof(body) == Expr || throw(DomainError(body, "missing function body")) - plural = :plural in args - plural_s = plural ? "s" : "" - inplace = :inplace in args - fname = Symbol(:change_bound, plural_s, inplace ? "!" : "") - idx_var = Symbol( - :rxn, - idx_type == :Int ? "_idx" : - idx_type == :String ? "_id" : - throw(DomainError(idx_type, "unsupported index type for change_bound macro")), - plural_s, - ) - example_idx = - plural ? (idx_type == :Int ? [123, 234] : ["ReactionA", "ReactionC"]) : - (idx_type == :Int ? 123 : "\"ReactionB\"") #= unquoting is hard =# - example_val = plural ? [4.2, 100.1] : 42.3 - missing_default = plural ? :((nothing for _ in $idx_var)) : nothing - - bound_type = Float64 - if plural - idx_type = AbstractVector{eval(idx_type)} - bound_type = AbstractVector{bound_type} - end - - docstring = """ - $fname( - model::$model_type, - $idx_var::$idx_type; - lower = $missing_default, - upper = $missing_default, - ) - - Change the specified reaction flux bound$(plural_s) in the model - $(inplace ? "in-place" : "and return the modified model"). - - # Example - ``` - $(inplace ? "new_model = " : "")$fname(model, $example_idx, lower=$(-0.5 .* example_val), upper=$example_val) - ``` - """ - - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fname( - model::$model_type, - $idx_var::$idx_type; - lower = $missing_default, - upper = $missing_default, - ) = $body - ), - ) -end diff --git a/src/base/macros/is_xxx_reaction.jl b/src/base/macros/is_xxx_reaction.jl deleted file mode 100644 index b8601df86..000000000 --- a/src/base/macros/is_xxx_reaction.jl +++ /dev/null @@ -1,49 +0,0 @@ - -""" -@_is_reaction_fn(anno_id, identifier) - -A helper for creating functions like `is_exchange_reaction`. -""" -macro _is_reaction_fn(anno_id, identifiers) - - fname = Symbol(:is_, anno_id, :_reaction) - grammar = any(startswith.(anno_id, ["a", "e", "i", "o", "u"])) ? "an" : "a" - - body = quote - begin - anno = reaction_annotations(model, reaction_id) - for key in annotation_keys - if haskey(anno, key) - any(in.($identifiers, Ref(anno[key]))) && return true - end - end - return false - end - end - - docstring = """ - $fname( - model::MetabolicModel, - reaction_id::String; - annotation_keys = ["sbo", "SBO"], - ) - - Check if a reaction is annotated as $(grammar) $(anno_id) reaction. Uses - `$identifiers` internally, which includes SBO identifiers. In - the reaction annotations, use the keys in `annotation_keys` to look for entries. - Returns false if no hits or if no keys are found. - """ - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fname( - model::MetabolicModel, - reaction_id::String; - annotation_keys = ["sbo", "SBO"], - ) = $body - ), - ) -end diff --git a/src/base/macros/model_wrapper.jl b/src/base/macros/model_wrapper.jl deleted file mode 100644 index 66de84db6..000000000 --- a/src/base/macros/model_wrapper.jl +++ /dev/null @@ -1,72 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -A helper backend for [`@_inherit_model_methods`](@ref) and -[`@_inherit_model_methods_fn`](@ref). -""" -function _inherit_model_methods_impl( - source, - mtype::Symbol, - arglist, - access, - fwdlist, - fns..., -) - Expr( - :block, - ( - begin - header = Expr(:call, fn, :(model::$mtype), arglist.args...) - call = Expr(:call, fn, access(:model), fwdlist.args...) - esc( - Expr( - :macrocall, - Symbol("@doc"), - source, - """ - $header - - Evaluates [`$fn`](@ref) on the model contained in $mtype. - """, - Expr(:(=), header, Expr(:block, source, call)), - ), - ) - end for fn in fns - )..., - ) -end - -""" -$(TYPEDSIGNATURES) - -Generates trivial accessor functions listed in `fns` for a model that is -wrapped in type `mtype` as field `member`. -""" -macro _inherit_model_methods(mtype::Symbol, arglist, member::Symbol, fwdlist, fns...) - _inherit_model_methods_impl( - __source__, - mtype, - arglist, - sym -> :($sym.$member), - fwdlist, - fns..., - ) -end - -""" -$(TYPEDSIGNATURES) - -A more generic version of [`@_inherit_model_methods`](@ref) that accesses the -"inner" model using an accessor function name. -""" -macro _inherit_model_methods_fn(mtype::Symbol, arglist, accessor, fwdlist, fns...) - _inherit_model_methods_impl( - __source__, - mtype, - arglist, - sym -> :($accessor($sym)), - fwdlist, - fns..., - ) -end diff --git a/src/base/macros/remove_item.jl b/src/base/macros/remove_item.jl deleted file mode 100644 index 0caf50c55..000000000 --- a/src/base/macros/remove_item.jl +++ /dev/null @@ -1,41 +0,0 @@ - -""" - @ _remove_fn objname ModelType IndexType [plural] [inplace] begin ... end - -A helper for creating functions that follow the `remove_objname` template, such -as [`remove_metabolites`](@ref) and [`remove_reaction`](@ref). -""" -macro _remove_fn(objname, model_type, idx_type, args...) - body = last(args) - typeof(body) == Expr || throw(DomainError(body, "missing function body")) - plural = :plural in args - plural_s = plural ? "s" : "" - inplace = :inplace in args - fname = Symbol(:remove_, objname, plural_s, inplace ? "!" : "") - idx_var = Symbol( - objname, - idx_type == :Int ? "_idx" : - idx_type == :String ? "_id" : - throw(DomainError(idx_type, "unsupported index type for _remove_fn macro")), - plural_s, - ) - - if plural - idx_type = AbstractVector{eval(idx_type)} - end - - docstring = """ - $fname(model::$model_type, $idx_var::$idx_type) - - Remove $objname$plural_s from the model of type `$model_type` - $(inplace ? "in-place" : "and return the modified model"). - """ - - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :($fname(model::$model_type, $idx_var::$idx_type) = $body), - ) -end diff --git a/src/base/macros/serialized.jl b/src/base/macros/serialized.jl deleted file mode 100644 index 0883e5a91..000000000 --- a/src/base/macros/serialized.jl +++ /dev/null @@ -1,25 +0,0 @@ -""" - @_serialized_change_unwrap function - -Creates a simple wrapper structure that calls the `function` transparently on -the internal precached model. The internal type is returned (otherwise this -would break the consistency of serialization). -""" -macro _serialized_change_unwrap(fn::Symbol) - docstring = """ - $fn(model::Serialized, ...) - - Calls [`$fn`](@ref) of the internal serialized model type. - Returns the modified un-serialized model. - """ - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fn(model::Serialized, args...; kwargs...) = - $fn(unwrap_serialized(model), args...; kwargs...) - ), - ) -end diff --git a/src/base/ontologies/SBOTerms.jl b/src/base/ontologies/SBOTerms.jl deleted file mode 100644 index 6a3e2a4f3..000000000 --- a/src/base/ontologies/SBOTerms.jl +++ /dev/null @@ -1,84 +0,0 @@ -""" -This module contains SBO terms recognized by COBREXA. For the full ontology, see -https://github.com/EBI-BioModels/SBO/blob/master/SBO_OBO.obo. - -If an SBO term appears here it *may* be used in a function; if an SBO term does -not appear here, then it is *not* used in a COBREXA function. - -These terms are used in `Identifiers.jl` which groups them as appropriate for -use in functions that classify reactions, metabolites, etc. -""" -module SBOTerms -const FLUX_BALANCE_FRAMEWORK = "SBO:0000624" -const RESOURCE_BALANCE_FRAMEWORK = "SBO:0000692" -const CONSTRAINT_BASED_FRAMEWORK = "SBO:0000693" - -const PRODUCT = "SBO:0000011" -const CONCENTRATION_OF_PRODUCT = "SBO:0000512" -const SIDE_PRODUCT = "SBO:0000603" - -const SUBSTRATE = "SBO:0000015" -const CONCENTRATION_OF_SUBSTRATE = "SBO:0000515" -const SIDE_SUBSTRATE = "SBO:0000604" - -const ENZYME = "SBO:0000014" -const TOTAL_CONCENTRATION_OF_ENZYME = "SBO:0000300" - -const TRANSCRIPTION = "SBO:0000183" -const TRANSLATION = "SBO:0000184" - -const GENE = "SBO:0000243" -const METABOLITE = "SBO:0000299" -const MACROMOLECULE = "SBO:0000245" -const SIMPLE_CHEMICAL = "SBO:0000247" -const RIBONUCLEIC_ACID = "SBO:0000250" -const DEOXYRIBONUCLEIC_ACID = "SBO:0000251" -const TRANSFER_RNA = "SBO:0000313" -const RIBOSOMAL_RNA = "SBO:0000314" -const MESSENGER_RNA = "SBO:0000278" -const TRANSPORTER = "SBO:0000284" -const PROTEIN_COMPLEX = "SBO:0000297" - -const MOLECULAR_MASS = "SBO:0000647" -const CATALYTIC_RATE_CONSTANT = "SBO:0000025" # turnover number synonym -const CAPACITY = "SBO:0000661" -const MICHAELIS_CONSTANT = "SBO:0000027" -const MICHAELIS_CONSTANT_FOR_PRODUCT = "SBO:0000323" -const MICHAELIS_CONSTANT_FOR_SUBSTRATE = "SBO:0000322" -const INHIBITORY_CONSTANT = "SBO:0000261" - -const STOICHIOMETRIC_COEFFICIENT = "SBO:0000481" -const AND = "SBO:0000173" -const OR = "SBO:0000174" - -const PH = "SBO:0000304" -const IONIC_STRENGTH = "SBO:0000623" - -const THERMODYNAMIC_TEMPERATURE = "SBO:0000147" -const STANDARD_GIBBS_FREE_ENERGY_OF_REACTION = "SBO:0000583" -const GIBBS_FREE_ENERGY_OF_REACTION = "SBO:0000617" -const STANDARD_GIBBS_FREE_ENERGY_OF_FORMATION = "SBO:0000582" -const TRANSFORMED_STANDARD_GIBBS_FREE_ENERGY_CHANGE_OF_REACTION = "SBO:0000620" -const TRANSFORMED_GIBBS_FREE_ENERGY_CHANGE_OF_REACTION = "SBO:0000622" -const TRANSFORMED_STANDARD_GIBBS_FREE_ENERGY_OF_FORMATION = "SBO:0000621" - -const BIOCHEMICAL_OR_TRANSPORT_REACTION = "SBO:0000167" -const BIOCHEMICAL_REACTION = "SBO:0000176" -const TRANSPORT_REACTION = "SBO:0000655" -const TRANSCELLULAR_MEMBRANE_INFLUX_REACTION = "SBO:0000587" -const TRANSCELLULAR_MEMBRANE_EFFLUX_REACTION = "SBO:0000588" -const FLUX_BOUND = "SBO:0000625" -const DEFAULT_FLUX_BOUND = "SBO:0000626" -const EXCHANGE_REACTION = "SBO:0000627" -const DEMAND_REACTION = "SBO:0000628" -const BIOMASS_PRODUCTION = "SBO:0000629" -const ATP_MAINTENANCE = "SBO:0000630" -const PSEUDOREACTION = "SBO:0000631" -const SINK_REACTION = "SBO:0000632" -const SPONTANEOUS_REACTION = "SBO:0000672" - -const ACTIVE_TRANSPORT = "SBO:0000657" -const PASSIVE_TRANSPORT = "SBO:0000658" - -const SUBSYSTEM = "SBO:0000633" -end diff --git a/src/base/solver.jl b/src/base/solver.jl deleted file mode 100644 index 7d112e7d8..000000000 --- a/src/base/solver.jl +++ /dev/null @@ -1,133 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Convert `MetabolicModel`s to a JuMP model, place objectives and the equality -constraint. - -Here coupling means inequality constraints coupling multiple variables together. -""" -function make_optimization_model(model::MetabolicModel, optimizer; sense = MAX_SENSE) - - precache!(model) - - m, n = size(stoichiometry(model)) - xl, xu = bounds(model) - - optimization_model = Model(optimizer) - @variable(optimization_model, x[1:n]) - @objective(optimization_model, sense, objective(model)' * x) - @constraint(optimization_model, mb, stoichiometry(model) * x .== balance(model)) # mass balance - @constraint(optimization_model, lbs, xl .<= x) # lower bounds - @constraint(optimization_model, ubs, x .<= xu) # upper bounds - - C = coupling(model) # empty if no coupling - isempty(C) || begin - cl, cu = coupling_bounds(model) - @constraint(optimization_model, c_lbs, cl .<= C * x) # coupling lower bounds - @constraint(optimization_model, c_ubs, C * x .<= cu) # coupling upper bounds - end - - return optimization_model -end - -""" -$(TYPEDSIGNATURES) - -Return `true` if `opt_model` solved successfully (solution is optimal or locally -optimal). Return `false` if any other termination status is reached. -Termination status is defined in the documentation of `JuMP`. -""" -is_solved(opt_model) = termination_status(opt_model) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] - -""" -$(TYPEDSIGNATURES) - -Shortcut for running JuMP `optimize!` on a model and returning the objective -value, if solved. -""" -function optimize_objective(opt_model)::Maybe{Float64} - optimize!(opt_model) - solved_objective_value(opt_model) -end - -""" -$(TYPEDSIGNATURES) - -Returns vectors of the lower and upper bounds of `opt_model` constraints, where -`opt_model` is a JuMP model constructed by e.g. -[`make_optimization_model`](@ref) or [`flux_balance_analysis`](@ref). -""" -get_optmodel_bounds(opt_model) = ( - [-normalized_rhs(lb) for lb in opt_model[:lbs]], - [normalized_rhs(ub) for ub in opt_model[:ubs]], -) - -""" -$(TYPEDSIGNATURES) - -Helper function to set the bounds of a variable in the model. Internally calls -`set_normalized_rhs` from JuMP. If the bounds are set to `nothing`, they will -not be changed. -""" -function set_optmodel_bound!( - vidx, - opt_model; - lb::Maybe{Real} = nothing, - ub::Maybe{Real} = nothing, -) - isnothing(lb) || set_normalized_rhs(opt_model[:lbs][vidx], -lb) - isnothing(ub) || set_normalized_rhs(opt_model[:ubs][vidx], ub) -end - -""" -$(TYPEDSIGNATURES) - -Returns the current objective value of a model, if solved. - -# Example -``` -solved_objective_value(flux_balance_analysis(model, ...)) -``` -""" -solved_objective_value(opt_model)::Maybe{Float64} = - is_solved(opt_model) ? objective_value(opt_model) : nothing - -""" -$(TYPEDSIGNATURES) - -Returns a vector of fluxes of the model, if solved. - -# Example -``` -flux_vector(flux_balance_analysis(model, ...)) -``` -""" -flux_vector(model::MetabolicModel, opt_model)::Maybe{Vector{Float64}} = - is_solved(opt_model) ? reaction_flux(model)' * value.(opt_model[:x]) : nothing - -""" -$(TYPEDSIGNATURES) - -Returns the fluxes of the model as a reaction-keyed dictionary, if solved. - -# Example -``` -flux_dict(model, flux_balance_analysis(model, ...)) -``` -""" -flux_dict(model::MetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = - is_solved(opt_model) ? - Dict(fluxes(model) .=> reaction_flux(model)' * value.(opt_model[:x])) : nothing - -""" -$(TYPEDSIGNATURES) - -A pipeable variant of `flux_dict`. - -# Example -``` -flux_balance_analysis(model, ...) |> flux_dict(model) -``` -""" -flux_dict(model::MetabolicModel) = opt_model -> flux_dict(model, opt_model) diff --git a/src/base/types/CoreModel.jl b/src/base/types/CoreModel.jl deleted file mode 100644 index 9133fda6a..000000000 --- a/src/base/types/CoreModel.jl +++ /dev/null @@ -1,176 +0,0 @@ - -""" -$(TYPEDEF) - -A "bare bones" core linear optimization problem of the form, with reaction and -metabolite names. -``` -min c^T x -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` - -# Fields -$(TYPEDFIELDS) -""" -mutable struct CoreModel <: MetabolicModel - S::SparseMat - b::SparseVec - c::SparseVec - xl::Vector{Float64} - xu::Vector{Float64} - rxns::Vector{String} - mets::Vector{String} - grrs::Vector{Maybe{GeneAssociation}} - - function CoreModel( - S::MatType, - b::VecType, - c::VecType, - xl::VecType, - xu::VecType, - rxns::StringVecType, - mets::StringVecType, - grrs::Vector{Maybe{GeneAssociation}} = Vector{Maybe{GeneAssociation}}( - nothing, - length(rxns), - ), - ) - all([length(b), length(mets)] .== size(S, 1)) || - throw(DimensionMismatch("inconsistent number of metabolites")) - - all( - [length(c), length(xl), length(xu), length(rxns), length(grrs)] .== size(S, 2), - ) || throw(DimensionMismatch("inconsistent number of reactions")) - - new(sparse(S), sparse(b), sparse(c), collect(xl), collect(xu), rxns, mets, grrs) - end -end - -""" -$(TYPEDSIGNATURES) - -Get the reactions in a `CoreModel`. -""" -reactions(a::CoreModel)::Vector{String} = a.rxns - -""" -$(TYPEDSIGNATURES) - -Metabolites in a `CoreModel`. -""" -metabolites(a::CoreModel)::Vector{String} = a.mets - -""" -$(TYPEDSIGNATURES) - -`CoreModel` stoichiometry matrix. -""" -stoichiometry(a::CoreModel)::SparseMat = a.S - -""" -$(TYPEDSIGNATURES) - -`CoreModel` flux bounds. -""" -bounds(a::CoreModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) - -""" -$(TYPEDSIGNATURES) - -`CoreModel` target flux balance. -""" -balance(a::CoreModel)::SparseVec = a.b - -""" -$(TYPEDSIGNATURES) - -`CoreModel` objective vector. -""" -objective(a::CoreModel)::SparseVec = a.c - -""" -$(TYPEDSIGNATURES) - -Collect all genes contained in the [`CoreModel`](@ref). The call is expensive -for large models, because the vector is not stored and instead gets rebuilt -each time this function is called. -""" -function genes(a::CoreModel)::Vector{String} - res = Set{String}() - for grr in a.grrs - isnothing(grr) && continue - for gs in grr - for g in gs - push!(res, g) - end - end - end - sort(collect(res)) -end - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. -""" -reaction_stoichiometry(m::CoreModel, rid::String)::Dict{String,Float64} = - Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, first(indexin([rid], m.rxns))])...)) - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction at index `ridx`. -""" -reaction_stoichiometry(m::CoreModel, ridx)::Dict{String,Float64} = - Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, ridx])...)) - -""" -$(TYPEDSIGNATURES) - -Retrieve a vector of all gene associations in a [`CoreModel`](@ref), in the -same order as `reactions(model)`. -""" -reaction_gene_association_vec(model::CoreModel)::Vector{Maybe{GeneAssociation}} = model.grrs - -""" -$(TYPEDSIGNATURES) - -Retrieve the [`GeneAssociation`](@ref) from [`CoreModel`](@ref) by reaction -index. -""" -reaction_gene_association(model::CoreModel, ridx::Int)::Maybe{GeneAssociation} = - model.grrs[ridx] - -""" -$(TYPEDSIGNATURES) - -Retrieve the [`GeneAssociation`](@ref) from [`CoreModel`](@ref) by reaction ID. -""" -reaction_gene_association(model::CoreModel, rid::String)::Maybe{GeneAssociation} = - model.grrs[first(indexin([rid], model.rxns))] - -""" -$(TYPEDSIGNATURES) - -Make a `CoreModel` out of any compatible model type. -""" -function Base.convert(::Type{CoreModel}, m::M) where {M<:MetabolicModel} - if typeof(m) == CoreModel - return m - end - - (xl, xu) = bounds(m) - CoreModel( - stoichiometry(m), - balance(m), - objective(m), - xl, - xu, - reactions(m), - metabolites(m), - Vector{Maybe{GeneAssociation}}([ - reaction_gene_association(m, id) for id in reactions(m) - ]), - ) -end diff --git a/src/base/types/CoreModelCoupled.jl b/src/base/types/CoreModelCoupled.jl deleted file mode 100644 index 048ffa730..000000000 --- a/src/base/types/CoreModelCoupled.jl +++ /dev/null @@ -1,103 +0,0 @@ - -""" -$(TYPEDEF) - -A matrix-based wrap that adds reaction coupling matrix to the inner model. A -flux `x` feasible in this model must satisfy: -``` - cₗ ≤ C x ≤ cᵤ -``` - -# Fields -$(TYPEDFIELDS) -""" -mutable struct CoreCoupling{M} <: ModelWrapper where {M<:MetabolicModel} - lm::M - C::SparseMat - cl::Vector{Float64} - cu::Vector{Float64} - - function CoreCoupling( - lm::M, - C::MatType, - cl::VecType, - cu::VecType, - ) where {M<:MetabolicModel} - length(cu) == length(cl) || - throw(DimensionMismatch("`cl` and `cu` need to have the same size")) - size(C) == (length(cu), n_reactions(lm)) || - throw(DimensionMismatch("wrong dimensions of `C`")) - - new{M}(lm, sparse(C), collect(cl), collect(cu)) - end -end - -""" -$(TYPEDSIGNATURES) - -Get the internal [`CoreModel`](@ref) out of [`CoreCoupling`](@ref). -""" -unwrap_model(a::CoreCoupling) = a.lm - -""" -$(TYPEDSIGNATURES) - -Coupling constraint matrix for a `CoreCoupling`. -""" -coupling(a::CoreCoupling)::SparseMat = vcat(coupling(a.lm), a.C) - -""" -$(TYPEDSIGNATURES) - -The number of coupling constraints in a `CoreCoupling`. -""" -n_coupling_constraints(a::CoreCoupling)::Int = n_coupling_constraints(a.lm) + size(a.C, 1) - -""" -$(TYPEDSIGNATURES) - -Coupling bounds for a `CoreCoupling`. -""" -coupling_bounds(a::CoreCoupling)::Tuple{Vector{Float64},Vector{Float64}} = - vcat.(coupling_bounds(a.lm), (a.cl, a.cu)) - -""" -$(TYPEDSIGNATURES) - -Make a `CoreCoupling` out of any compatible model type. -""" -function Base.convert( - ::Type{CoreCoupling{M}}, - mm::MetabolicModel; - clone_coupling = true, -) where {M} - if mm isa CoreCoupling{M} - mm - elseif mm isa CoreCoupling - CoreCoupling(convert(M, mm.lm), mm.C, mm.cl, mm.cu) - elseif clone_coupling - (cl, cu) = coupling_bounds(mm) - CoreCoupling(convert(M, mm), coupling(mm), cl, cu) - else - CoreCoupling(convert(M, mm), spzeros(0, n_reactions(mm)), spzeros(0), spzeros(0)) - end -end - -""" - const CoreModelCoupled = CoreCoupling{CoreModel} - -A matrix-based linear model with additional coupling constraints in the form: -``` - cₗ ≤ C x ≤ cᵤ -``` - -Internally, the model is implemented using [`CoreCoupling`](@ref) that contains a single [`CoreModel`](@ref). -""" -const CoreModelCoupled = CoreCoupling{CoreModel} - -CoreModelCoupled(lm::CoreModel, C::MatType, cl::VecType, cu::VecType) = - CoreCoupling(lm, sparse(C), collect(cl), collect(cu)) - -# these are special for CoreModel-ish models -@_inherit_model_methods CoreModelCoupled () lm () reaction_gene_association_vec -@_inherit_model_methods CoreModelCoupled (ridx::Int,) lm (ridx,) reaction_stoichiometry diff --git a/src/base/types/FluxSummary.jl b/src/base/types/FluxSummary.jl deleted file mode 100644 index 13f136225..000000000 --- a/src/base/types/FluxSummary.jl +++ /dev/null @@ -1,137 +0,0 @@ -""" -$(TYPEDEF) - -A struct used to store summary information about the solution -of a constraint based analysis result. - -# Fields -$(TYPEDFIELDS) -""" -struct FluxSummary - biomass_fluxes::OrderedDict{String,Float64} - import_fluxes::OrderedDict{String,Float64} - export_fluxes::OrderedDict{String,Float64} - unbounded_fluxes::OrderedDict{String,Float64} -end - -""" -$(TYPEDSIGNATURES) - -A default empty constructor for `FluxSummary`. -""" -function FluxSummary() - FluxSummary( - OrderedDict{String,Float64}(), - OrderedDict{String,Float64}(), - OrderedDict{String,Float64}(), - OrderedDict{String,Float64}(), - ) -end - -""" -$(TYPEDSIGNATURES) - -Summarize a dictionary of fluxes into small, useful representation of the most -important information contained. Useful for pretty-printing and quickly -exploring the results. Internally this function uses -[`looks_like_biomass_reaction`](@ref) and -[`looks_like_exchange_reaction`](@ref). The corresponding keyword arguments -passed to these functions. Use this if your model has non-standard ids for -reactions. Fluxes smaller than `small_flux_bound` are not stored, while fluxes -larger than `large_flux_bound` are only stored if `keep_unbounded` is `true`. - -# Example -``` -julia> sol = flux_dict(flux_balance_analysis(model, Tulip.Optimizer)) -julia> fr = flux_summary(sol) -Biomass: - BIOMASS_Ecoli_core_w_GAM: 0.8739 -Import: - EX_o2_e: -21.7995 - EX_glc__D_e: -10.0 - EX_nh4_e: -4.7653 - EX_pi_e: -3.2149 -Export: - EX_h_e: 17.5309 - EX_co2_e: 22.8098 - EX_h2o_e: 29.1758 -``` -""" -function flux_summary( - flux_result::Maybe{Dict{String,Float64}}; - exclude_exchanges = false, - exchange_prefixes = _constants.exchange_prefixes, - biomass_strings = _constants.biomass_strings, - exclude_biomass = false, - small_flux_bound = 1.0 / _constants.default_reaction_bound^2, - large_flux_bound = _constants.default_reaction_bound, - keep_unbounded = false, -) - isnothing(flux_result) && return FluxSummary() - - rxn_ids = collect(keys(flux_result)) - ex_rxns = filter( - x -> looks_like_exchange_reaction( - x, - exclude_biomass = exclude_biomass, - biomass_strings = biomass_strings, - exchange_prefixes = exchange_prefixes, - ), - rxn_ids, - ) - bmasses = filter( - x -> looks_like_biomass_reaction( - x; - exclude_exchanges = exclude_exchanges, - exchange_prefixes = exchange_prefixes, - biomass_strings = biomass_strings, - ), - rxn_ids, - ) - - ex_fluxes = [flux_result[k] for k in ex_rxns] - bmass_fluxes = [flux_result[k] for k in bmasses] - - idx_srt_fluxes = sortperm(ex_fluxes) - import_fluxes = [ - idx for - idx in idx_srt_fluxes if -large_flux_bound < ex_fluxes[idx] <= -small_flux_bound - ] - export_fluxes = [ - idx for - idx in idx_srt_fluxes if small_flux_bound < ex_fluxes[idx] <= large_flux_bound - ] - - if keep_unbounded - lower_unbounded = - [idx for idx in idx_srt_fluxes if ex_fluxes[idx] <= -large_flux_bound] - upper_unbounded = - [idx for idx in idx_srt_fluxes if ex_fluxes[idx] >= large_flux_bound] - return FluxSummary( - OrderedDict(k => v for (k, v) in zip(bmasses, bmass_fluxes)), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[import_fluxes], ex_fluxes[import_fluxes]) - ), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[export_fluxes], ex_fluxes[export_fluxes]) - ), - OrderedDict( - k => v for (k, v) in zip( - [ex_rxns[lower_unbounded]; ex_rxns[upper_unbounded]], - [ex_fluxes[lower_unbounded]; ex_fluxes[upper_unbounded]], - ) - ), - ) - else - return FluxSummary( - OrderedDict(k => v for (k, v) in zip(bmasses, bmass_fluxes)), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[import_fluxes], ex_fluxes[import_fluxes]) - ), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[export_fluxes], ex_fluxes[export_fluxes]) - ), - OrderedDict{String,Float64}(), - ) - end -end diff --git a/src/base/types/FluxVariabilitySummary.jl b/src/base/types/FluxVariabilitySummary.jl deleted file mode 100644 index c1b451096..000000000 --- a/src/base/types/FluxVariabilitySummary.jl +++ /dev/null @@ -1,100 +0,0 @@ -""" -$(TYPEDEF) - -Stores summary information about the result of a flux variability analysis. - -# Fields -$(TYPEDFIELDS) -""" -struct FluxVariabilitySummary - biomass_fluxes::Dict{String,Vector{Maybe{Float64}}} - exchange_fluxes::Dict{String,Vector{Maybe{Float64}}} -end - -""" -$(TYPEDSIGNATURES) - -A default empty constructor for [`FluxVariabilitySummary`](@ref). -""" -function FluxVariabilitySummary() - FluxVariabilitySummary( - Dict{String,Vector{Maybe{Float64}}}(), - Dict{String,Vector{Maybe{Float64}}}(), - ) -end - -""" -$(TYPEDSIGNATURES) - -Summarize a dictionary of flux dictionaries obtained eg. from -[`flux_variability_analysis_dict`](@ref). The simplified summary representation -is useful for pretty-printing and easily showing the most important results. - -Internally this function uses [`looks_like_biomass_reaction`](@ref) and -[`looks_like_exchange_reaction`](@ref). The corresponding keyword arguments are -passed to these functions. Use this if your model has an uncommon naming of -reactions. - -# Example -``` -julia> sol = flux_variability_analysis_dict(model, Gurobi.Optimizer; bounds = objective_bounds(0.99)) -julia> flux_res = flux_variability_summary(sol) -Biomass Lower bound Upper bound - BIOMASS_Ecoli_core_w_GAM: 0.8652 0.8652 -Exchange - EX_h2o_e: 28.34 28.34 - EX_co2_e: 22.0377 22.0377 - EX_o2_e: -22.1815 -22.1815 - EX_h_e: 17.3556 17.3556 - EX_glc__D_e: -10.0 -10.0 - EX_nh4_e: -4.8448 -4.8448 - EX_pi_e: -3.2149 -3.2149 - EX_for_e: 0.0 0.0 - ... ... ... -``` -""" -function flux_variability_summary( - flux_result::Tuple{Dict{String,Dict{String,Float64}},Dict{String,Dict{String,Float64}}}; - exclude_exchanges = false, - exchange_prefixes = _constants.exchange_prefixes, - biomass_strings = _constants.biomass_strings, - exclude_biomass = false, -) - isnothing(flux_result) && return FluxVariabilitySummary() - - rxn_ids = keys(flux_result[1]) - ex_rxns = filter( - x -> looks_like_exchange_reaction( - x, - exclude_biomass = exclude_biomass, - biomass_strings = biomass_strings, - exchange_prefixes = exchange_prefixes, - ), - rxn_ids, - ) - bmasses = filter( - x -> looks_like_biomass_reaction( - x; - exclude_exchanges = exclude_exchanges, - exchange_prefixes = exchange_prefixes, - biomass_strings = biomass_strings, - ), - rxn_ids, - ) - - biomass_fluxes = Dict{String,Vector{Maybe{Float64}}}() - for rxn_id in bmasses - lb = isnothing(flux_result[1][rxn_id]) ? nothing : flux_result[1][rxn_id][rxn_id] - ub = isnothing(flux_result[2][rxn_id]) ? nothing : flux_result[2][rxn_id][rxn_id] - biomass_fluxes[rxn_id] = [lb, ub] - end - - ex_rxn_fluxes = Dict{String,Vector{Maybe{Float64}}}() - for rxn_id in ex_rxns - lb = isnothing(flux_result[1][rxn_id]) ? nothing : flux_result[1][rxn_id][rxn_id] - ub = isnothing(flux_result[2][rxn_id]) ? nothing : flux_result[2][rxn_id][rxn_id] - ex_rxn_fluxes[rxn_id] = [lb, ub] - end - - return FluxVariabilitySummary(biomass_fluxes, ex_rxn_fluxes) -end diff --git a/src/base/types/Gene.jl b/src/base/types/Gene.jl deleted file mode 100644 index 909c60411..000000000 --- a/src/base/types/Gene.jl +++ /dev/null @@ -1,20 +0,0 @@ -""" -$(TYPEDEF) - -# Fields -$(TYPEDFIELDS) -""" -mutable struct Gene - id::String - name::Maybe{String} - notes::Notes - annotations::Annotations -end - -""" -$(TYPEDSIGNATURES) - -A convenient constructor for a `Gene`. -""" -Gene(id = ""; name = nothing, notes = Notes(), annotations = Annotations()) = - Gene(String(id), name, notes, annotations) diff --git a/src/base/types/HDF5Model.jl b/src/base/types/HDF5Model.jl deleted file mode 100644 index c6b710733..000000000 --- a/src/base/types/HDF5Model.jl +++ /dev/null @@ -1,75 +0,0 @@ - -""" -$(TYPEDEF) - -A model that is stored in HDF5 format. The model data is never really pulled -into memory, but instead mmap'ed as directly as possible into the Julia -structures. This makes reading the `HDF5Model`s extremely fast, at the same -time the (uncached) `HDF5Model`s can be sent around efficiently among -distributed nodes just like [`Serialized`](@ref) models, provided the nodes -share a common storage. - -All HDF5Models must have the backing disk storage. To create one, use -[`save_h5_model`](@ref) or [`save_model`](@ref) with `.h5` file extension. To -create a temporary model that behaves like a model "in memory", save it to a -temporary file. For related reasons, you can not use `convert` models to -`HDF5Model` format, because the conversion would impliy having the model saved -somewhere. - -# Fields -$(TYPEDFIELDS) -""" -mutable struct HDF5Model <: MetabolicModel - h5::Maybe{HDF5.File} - filename::String - - HDF5Model(filename::String) = new(nothing, filename) -end - -function precache!(model::HDF5Model)::Nothing - if isnothing(model.h5) - model.h5 = h5open(model.filename, "r") - end - nothing -end - -function n_reactions(model::HDF5Model)::Int - precache!(model) - length(model.h5["reactions"]) -end - -function reactions(model::HDF5Model)::Vector{String} - precache!(model) - # TODO is there any reasonable method to mmap strings from HDF5? - read(model.h5["reactions"]) -end - -function n_metabolites(model::HDF5Model)::Int - precache!(model) - length(model.h5["metabolites"]) -end - -function metabolites(model::HDF5Model)::Vector{String} - precache!(model) - read(model.h5["metabolites"]) -end - -function stoichiometry(model::HDF5Model)::SparseMat - precache!(model) - _h5_read_sparse(SparseMat, model.h5["stoichiometry"]) -end - -function bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} - precache!(model) - (HDF5.readmmap(model.h5["lower_bounds"]), HDF5.readmmap(model.h5["upper_bounds"])) -end - -function balance(model::HDF5Model)::SparseVec - precache!(model) - _h5_read_sparse(SparseVec, model.h5["balance"]) -end - -function objective(model::HDF5Model)::SparseVec - precache!(model) - _h5_read_sparse(SparseVec, model.h5["objective"]) -end diff --git a/src/base/types/Isozyme.jl b/src/base/types/Isozyme.jl deleted file mode 100644 index b7dc6cb9b..000000000 --- a/src/base/types/Isozyme.jl +++ /dev/null @@ -1,13 +0,0 @@ -""" -$(TYPEDEF) - -Information about isozyme composition and activity. - -# Fields -$(TYPEDFIELDS) -""" -mutable struct Isozyme - gene_product_count::Dict{String,Int} - kcat_forward::Float64 - kcat_reverse::Float64 -end diff --git a/src/base/types/JSONModel.jl b/src/base/types/JSONModel.jl deleted file mode 100644 index 03ab30a5d..000000000 --- a/src/base/types/JSONModel.jl +++ /dev/null @@ -1,350 +0,0 @@ -""" -$(TYPEDEF) - -A struct used to store the contents of a JSON model, i.e. a model read from a -file ending with `.json`. These model files typically store all the model data -in arrays of JSON objects (represented in Julia as vectors of dictionaries). - -Usually, not all of the fields of the input JSON can be easily represented when -converting to other models, care should be taken to avoid losing information. - -Direct work with the `json` structure is not very efficient; the model -structure therefore caches some of the internal structure in the extra fields. -The single-parameter [`JSONModel`](@ref) constructor creates these caches -correctly from the `json`. The model structure is designed as read-only, and -changes in `json` invalidate the cache. - -# Example -```` -model = load_json_model("some_model.json") -model.json # see the actual underlying JSON -reactions(model) # see the list of reactions -```` - -# Fields -$(TYPEDFIELDS) -""" -struct JSONModel <: MetabolicModel - json::Dict{String,Any} - rxn_index::Dict{String,Int} - rxns::Vector{Any} - met_index::Dict{String,Int} - mets::Vector{Any} - gene_index::Dict{String,Int} - genes::Vector{Any} -end - -_json_rxn_name(r, i) = string(get(r, "id", "rxn$i")) -_json_met_name(m, i) = string(get(m, "id", "met$i")) -_json_gene_name(g, i) = string(get(g, "id", "gene$i")) - -JSONModel(json::Dict{String,Any}) = begin - rkey = _guesskey(keys(json), _constants.keynames.rxns) - isnothing(rkey) && throw(DomainError(keys(json), "JSON model has no reaction keys")) - rs = json[rkey] - - mkey = _guesskey(keys(json), _constants.keynames.mets) - ms = json[mkey] - isnothing(mkey) && throw(DomainError(keys(json), "JSON model has no metabolite keys")) - - gkey = _guesskey(keys(json), _constants.keynames.genes) - gs = isnothing(gkey) ? [] : json[gkey] - - JSONModel( - json, - Dict(_json_rxn_name(r, i) => i for (i, r) in enumerate(rs)), - rs, - Dict(_json_met_name(m, i) => i for (i, m) in enumerate(ms)), - ms, - Dict(_json_gene_name(g, i) => i for (i, g) in enumerate(gs)), - gs, - ) -end - -function _parse_annotations(x)::Annotations - Annotations([k => if typeof(v) == String - [v] - else - convert(Vector{String}, v) - end for (k, v) in x]) -end - -_parse_notes(x)::Notes = _parse_annotations(x) - -""" -$(TYPEDSIGNATURES) - -Extract reaction names (stored as `.id`) from JSON model. -""" -reactions(model::JSONModel) = [_json_rxn_name(r, i) for (i, r) in enumerate(model.rxns)] - -""" -$(TYPEDSIGNATURES) - -Extract metabolite names (stored as `.id`) from JSON model. -""" -metabolites(model::JSONModel) = [_json_met_name(m, i) for (i, m) in enumerate(model.mets)] - -""" -$(TYPEDSIGNATURES) - -Extract gene names from a JSON model. -""" -genes(model::JSONModel) = [_json_gene_name(g, i) for (i, g) in enumerate(model.genes)] - -""" -$(TYPEDSIGNATURES) - -Get the stoichiometry. Assuming the information is stored in reaction object -under key `.metabolites`. -""" -function stoichiometry(model::JSONModel) - rxn_ids = reactions(model) - met_ids = metabolites(model) - - n_entries = 0 - for r in model.rxns - for _ in r["metabolites"] - n_entries += 1 - end - end - - MI = Vector{Int}() - RI = Vector{Int}() - SV = Vector{Float64}() - sizehint!(MI, n_entries) - sizehint!(RI, n_entries) - sizehint!(SV, n_entries) - - for (i, rid) in enumerate(rxn_ids) - r = model.rxns[model.rxn_index[rid]] - for (mid, coeff) in r["metabolites"] - haskey(model.met_index, mid) || throw( - DomainError( - met_id, - "Unknown metabolite found in stoichiometry of $(rxn_ids[i])", - ), - ) - - push!(MI, model.met_index[mid]) - push!(RI, i) - push!(SV, coeff) - end - end - return SparseArrays.sparse(MI, RI, SV, length(met_ids), length(rxn_ids)) -end - -""" -$(TYPEDSIGNATURES) - -Get the bounds for reactions, assuming the information is stored in -`.lower_bound` and `.upper_bound`. -""" -bounds(model::JSONModel) = ( - [get(rxn, "lower_bound", -_constants.default_reaction_bound) for rxn in model.rxns], - [get(rxn, "upper_bound", _constants.default_reaction_bound) for rxn in model.rxns], -) - -""" -$(TYPEDSIGNATURES) - -Collect `.objective_coefficient` keys from model reactions. -""" -objective(model::JSONModel) = - sparse([float(get(rxn, "objective_coefficient", 0.0)) for rxn in model.rxns]) - -""" -$(TYPEDSIGNATURES) - -Parses the `.gene_reaction_rule` from reactions. -""" -reaction_gene_association(model::JSONModel, rid::String) = _maybemap( - _parse_grr, - get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), -) - -""" -$(TYPEDSIGNATURES) - -Parses the `.subsystem` out from reactions. -""" -reaction_subsystem(model::JSONModel, rid::String) = - get(model.rxns[model.rxn_index[rid]], "subsystem", nothing) - -""" -$(TYPEDSIGNATURES) - -Parse and return the metabolite `.formula` -""" -metabolite_formula(model::JSONModel, mid::String) = - _maybemap(_parse_formula, get(model.mets[model.met_index[mid]], "formula", nothing)) - -""" -$(TYPEDSIGNATURES) - -Return the metabolite `.charge` -""" -metabolite_charge(model::JSONModel, mid::String) = - get(model.mets[model.met_index[mid]], "charge", 0) - -""" -$(TYPEDSIGNATURES) - -Return the metabolite `.compartment` -""" -metabolite_compartment(model::JSONModel, mid::String) = - get(model.mets[model.met_index[mid]], "compartment", nothing) - -""" -$(TYPEDSIGNATURES) - -Gene annotations from the [`JSONModel`](@ref). -""" -gene_annotations(model::JSONModel, gid::String)::Annotations = _maybemap( - _parse_annotations, - get(model.genes[model.gene_index[gid]], "annotation", nothing), -) - -""" -$(TYPEDSIGNATURES) - -Gene notes from the [`JSONModel`](@ref). -""" -gene_notes(model::JSONModel, gid::String)::Notes = - _maybemap(_parse_notes, get(model.genes[model.gene_index[gid]], "notes", nothing)) - -""" -$(TYPEDSIGNATURES) - -Reaction annotations from the [`JSONModel`](@ref). -""" -reaction_annotations(model::JSONModel, rid::String)::Annotations = _maybemap( - _parse_annotations, - get(model.rxns[model.rxn_index[rid]], "annotation", nothing), -) - -""" -$(TYPEDSIGNATURES) - -Reaction notes from the [`JSONModel`](@ref). -""" -reaction_notes(model::JSONModel, rid::String)::Notes = - _maybemap(_parse_notes, get(model.rxns[model.rxn_index[rid]], "notes", nothing)) - -""" -$(TYPEDSIGNATURES) - -Metabolite annotations from the [`JSONModel`](@ref). -""" -metabolite_annotations(model::JSONModel, mid::String)::Annotations = _maybemap( - _parse_annotations, - get(model.mets[model.met_index[mid]], "annotation", nothing), -) - -""" -$(TYPEDSIGNATURES) - -Metabolite notes from the [`JSONModel`](@ref). -""" -metabolite_notes(model::JSONModel, mid::String)::Notes = - _maybemap(_parse_notes, get(model.mets[model.met_index[mid]], "notes", nothing)) - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. -""" -reaction_stoichiometry(model::JSONModel, rid::String)::Dict{String,Float64} = - model.rxns[model.rxn_index[rid]]["metabolites"] - -""" -$(TYPEDSIGNATURES) - -Return the name of reaction with ID `rid`. -""" -reaction_name(model::JSONModel, rid::String) = - get(model.rxns[model.rxn_index[rid]], "name", nothing) - -""" -$(TYPEDSIGNATURES) - -Return the name of metabolite with ID `mid`. -""" -metabolite_name(model::JSONModel, mid::String) = - get(model.mets[model.met_index[mid]], "name", nothing) - -""" -$(TYPEDSIGNATURES) - -Return the name of gene with ID `gid`. -""" -gene_name(model::JSONModel, gid::String) = - get(model.genes[model.gene_index[gid]], "name", nothing) - -""" -$(TYPEDSIGNATURES) - -Convert any [`MetabolicModel`](@ref) to [`JSONModel`](@ref). -""" -function Base.convert(::Type{JSONModel}, mm::MetabolicModel) - if typeof(mm) == JSONModel - return mm - end - - rxn_ids = reactions(mm) - met_ids = metabolites(mm) - gene_ids = genes(mm) - S = stoichiometry(mm) - lbs, ubs = bounds(mm) - ocs = objective(mm) - - json = Dict{String,Any}() - json["id"] = "model" # default - - json[first(_constants.keynames.genes)] = [ - Dict([ - "id" => gid, - "name" => gene_name(mm, gid), - "annotation" => gene_annotations(mm, gid), - "notes" => gene_notes(mm, gid), - ],) for gid in gene_ids - ] - - json[first(_constants.keynames.mets)] = [ - Dict([ - "id" => mid, - "name" => metabolite_name(mm, mid), - "formula" => _maybemap(_unparse_formula, metabolite_formula(mm, mid)), - "charge" => metabolite_charge(mm, mid), - "compartment" => metabolite_compartment(mm, mid), - "annotation" => metabolite_annotations(mm, mid), - "notes" => metabolite_notes(mm, mid), - ]) for mid in met_ids - ] - - json[first(_constants.keynames.rxns)] = [ - begin - res = Dict{String,Any}() - res["id"] = rid - res["name"] = reaction_name(mm, rid) - res["subsystem"] = reaction_subsystem(mm, rid) - res["annotation"] = reaction_annotations(mm, rid) - res["notes"] = reaction_notes(mm, rid) - - grr = reaction_gene_association(mm, rid) - if !isnothing(grr) - res["gene_reaction_rule"] = _unparse_grr(String, grr) - end - - res["lower_bound"] = lbs[ri] - res["upper_bound"] = ubs[ri] - res["objective_coefficient"] = ocs[ri] - I, V = findnz(S[:, ri]) - res["metabolites"] = - Dict{String,Float64}([met_ids[ii] => vv for (ii, vv) in zip(I, V)]) - res - end for (ri, rid) in enumerate(rxn_ids) - ] - - return JSONModel(json) -end diff --git a/src/base/types/MATModel.jl b/src/base/types/MATModel.jl deleted file mode 100644 index 958d9d3be..000000000 --- a/src/base/types/MATModel.jl +++ /dev/null @@ -1,280 +0,0 @@ -""" -$(TYPEDEF) - -Wrapper around the models loaded in dictionaries from the MATLAB representation. - -# Fields -$(TYPEDFIELDS) -""" -struct MATModel <: MetabolicModel - mat::Dict{String,Any} -end - -n_metabolites(m::MATModel)::Int = size(m.mat["S"], 1) -n_reactions(m::MATModel)::Int = size(m.mat["S"], 2) - -""" -$(TYPEDSIGNATURES) - -Extracts reaction names from `rxns` key in the MAT file. -""" -function reactions(m::MATModel)::Vector{String} - if haskey(m.mat, "rxns") - reshape(m.mat["rxns"], n_reactions(m)) - else - "rxn" .* string.(1:n_reactions(m)) - end -end - -""" -$(TYPEDSIGNATURES) - -Guesses whether C in the MAT file is stored in A=[S;C]. -""" -_mat_has_squashed_coupling(mat) = - haskey(mat, "A") && haskey(mat, "b") && length(mat["b"]) == size(mat["A"], 1) - - -""" -$(TYPEDSIGNATURES) - -Extracts metabolite names from `mets` key in the MAT file. -""" -function metabolites(m::MATModel)::Vector{String} - nm = n_metabolites(m) - if haskey(m.mat, "mets") - reshape(m.mat["mets"], length(m.mat["mets"]))[begin:nm] - else - "met" .* string.(1:n_metabolites(m)) - end -end - -""" -$(TYPEDSIGNATURES) - -Extract the stoichiometry matrix, stored under key `S`. -""" -stoichiometry(m::MATModel) = sparse(m.mat["S"]) - -""" -$(TYPEDSIGNATURES) - -Extracts bounds from the MAT file, saved under `lb` and `ub`. -""" -bounds(m::MATModel) = ( - reshape(get(m.mat, "lb", fill(-Inf, n_reactions(m), 1)), n_reactions(m)), - reshape(get(m.mat, "ub", fill(Inf, n_reactions(m), 1)), n_reactions(m)), -) - -""" -$(TYPEDSIGNATURES) - -Extracts balance from the MAT model, defaulting to zeroes if not present. -""" -function balance(m::MATModel) - b = get(m.mat, "b", spzeros(n_metabolites(m), 1)) - if _mat_has_squashed_coupling(m.mat) - b = b[1:n_metabolites(m), :] - end - sparse(reshape(b, n_metabolites(m))) -end - -""" -$(TYPEDSIGNATURES) - -Extracts the objective from the MAT model (defaults to zeroes). -""" -objective(m::MATModel) = - sparse(reshape(get(m.mat, "c", zeros(n_reactions(m), 1)), n_reactions(m))) - -""" -$(TYPEDSIGNATURES) - -Extract coupling matrix stored, in `C` key. -""" -coupling(m::MATModel) = - _mat_has_squashed_coupling(m.mat) ? sparse(m.mat["A"][n_reactions(m)+1:end, :]) : - sparse(get(m.mat, "C", zeros(0, n_reactions(m)))) - -""" -$(TYPEDSIGNATURES) - -Extracts the coupling constraints. Currently, there are several accepted ways to store these in MATLAB models; this takes the constraints from vectors `cl` and `cu`. -""" -function coupling_bounds(m::MATModel) - nc = n_coupling_constraints(m) - if _mat_has_squashed_coupling(m.mat) - ( - sparse(fill(-Inf, nc)), - sparse(reshape(m.mat["b"], length(m.mat["b"]))[n_reactions(m)+1:end]), - ) - else - ( - sparse(reshape(get(m.mat, "cl", fill(-Inf, nc, 1)), nc)), - sparse(reshape(get(m.mat, "cu", fill(Inf, nc, 1)), nc)), - ) - end -end - -""" -$(TYPEDSIGNATURES) - -Extracts the possible gene list from `genes` key. -""" -function genes(m::MATModel) - x = get(m.mat, "genes", []) - reshape(x, length(x)) -end - -""" -$(TYPEDSIGNATURES) - -Extracts the associations from `grRules` key, if present. -""" -function reaction_gene_association(m::MATModel, rid::String) - if haskey(m.mat, "grRules") - grr = m.mat["grRules"][findfirst(==(rid), reactions(m))] - typeof(grr) == String ? _parse_grr(grr) : nothing - else - nothing - end -end - -""" -$(TYPEDSIGNATURES) - -Extract metabolite formula from key `metFormula` or `metFormulas`. -""" -metabolite_formula(m::MATModel, mid::String) = _maybemap( - x -> _parse_formula(x[findfirst(==(mid), metabolites(m))]), - gets(m.mat, nothing, _constants.keynames.metformulas), -) - -""" -$(TYPEDSIGNATURES) - -Extract metabolite charge from `metCharge` or `metCharges`. -""" -function metabolite_charge(m::MATModel, mid::String)::Maybe{Int} - met_charge = _maybemap( - x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, _constants.keynames.metcharges), - ) - _maybemap(Int, isnan(met_charge) ? nothing : met_charge) -end - -""" -$(TYPEDSIGNATURES) - -Extract metabolite compartment from `metCompartment` or `metCompartments`. -""" -function metabolite_compartment(m::MATModel, mid::String) - res = _maybemap( - x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, _constants.keynames.metcompartments), - ) - # if the metabolite is an integer or a (very integerish) float, it is an - # index to a table of metabolite names (such as in the yeast GEM) - typeof(res) <: Real || return res - return _maybemap( - table -> table[Int(res)], - gets(m.mat, nothing, _constants.keynames.metcomptables), - ) -end - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. -""" -function reaction_stoichiometry(m::MATModel, rid::String)::Dict{String,Float64} - ridx = first(indexin([rid], m.mat["rxns"])) - reaction_stoichiometry(m, ridx) -end - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction at index `ridx`. -""" -function reaction_stoichiometry(m::MATModel, ridx)::Dict{String,Float64} - met_inds = findall(m.mat["S"][:, ridx] .!= 0.0) - Dict(m.mat["mets"][met_ind] => m.mat["S"][met_ind, ridx] for met_ind in met_inds) -end - -# NOTE: There's no useful standard on how and where to store notes and -# annotations in MATLAB models. We therefore leave it very open for the users, -# who can easily support any annotation scheme using a custom wrapper. -# Even the (simple) assumptions about grRules, formulas and charges that we use -# here are very likely completely incompatible with >50% of the MATLAB models -# out there. - -""" -$(TYPEDSIGNATURES) - -Convert any metabolic model to `MATModel`. -""" -function Base.convert(::Type{MATModel}, m::MetabolicModel) - if typeof(m) == MATModel - return m - end - - lb, ub = bounds(m) - cl, cu = coupling_bounds(m) - nr = n_reactions(m) - nm = n_metabolites(m) - return MATModel( - Dict( - "S" => stoichiometry(m), - "rxns" => reactions(m), - "mets" => metabolites(m), - "lb" => Vector(lb), - "ub" => Vector(ub), - "b" => Vector(balance(m)), - "c" => Vector(objective(m)), - "C" => coupling(m), - "cl" => Vector(cl), - "cu" => Vector(cu), - "genes" => genes(m), - "grRules" => - _default.( - "", - _maybemap.( - x -> _unparse_grr(String, x), - reaction_gene_association.(Ref(m), reactions(m)), - ), - ), - "metFormulas" => - _default.( - "", - _maybemap.( - _unparse_formula, - metabolite_formula.(Ref(m), metabolites(m)), - ), - ), - "metCharges" => _default.(0, metabolite_charge.(Ref(m), metabolites(m))), - "metCompartments" => - _default.("", metabolite_compartment.(Ref(m), metabolites(m))), - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Extract reaction name from `rxnNames`. -""" -reaction_name(m::MATModel, rid::String) = _maybemap( - x -> x[findfirst(==(rid), reactions(m))], - gets(m.mat, nothing, _constants.keynames.rxnnames), -) - -""" -$(TYPEDSIGNATURES) - -Extract metabolite name from `metNames`. -""" -metabolite_name(m::MATModel, mid::String) = _maybemap( - x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, _constants.keynames.metnames), -) diff --git a/src/base/types/MetabolicModel.jl b/src/base/types/MetabolicModel.jl deleted file mode 100644 index 7474b1e61..000000000 --- a/src/base/types/MetabolicModel.jl +++ /dev/null @@ -1,361 +0,0 @@ - -# -# IMPORTANT -# -# This file provides a list of "officially supported" accessors that should -# work with all subtypes of [`MetabolicModel`](@ref). Keep this synced with the -# automatically derived methods for [`ModelWrapper`](@ref). -# - -_missing_impl_error(m, a) = throw(MethodError(m, a)) - -""" -$(TYPEDSIGNATURES) - -Return a vector of reaction identifiers in a model. The vector precisely -corresponds to the columns in [`stoichiometry`](@ref) matrix. - -For technical reasons, the "reactions" may sometimes not be true reactions but -various virtual and helper pseudo-reactions that are used in the metabolic -modeling, such as metabolite exchanges, separate forward and reverse reactions, -supplies of enzymatic and genetic material and virtual cell volume, etc. To -simplify the view of the model contents use [`reaction_flux`](@ref). -""" -function reactions(a::MetabolicModel)::Vector{String} - _missing_impl_error(reactions, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Return a vector of metabolite identifiers in a model. The vector precisely -corresponds to the rows in [`stoichiometry`](@ref) matrix. - -As with [`reactions`](@ref)s, some metabolites in models may be virtual, -representing purely technical equality constraints. -""" -function metabolites(a::MetabolicModel)::Vector{String} - _missing_impl_error(metabolites, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Get the number of reactions in a model. -""" -function n_reactions(a::MetabolicModel)::Int - length(reactions(a)) -end - -""" -$(TYPEDSIGNATURES) - -Get the number of metabolites in a model. -""" -function n_metabolites(a::MetabolicModel)::Int - length(metabolites(a)) -end - -""" -$(TYPEDSIGNATURES) - -Get the sparse stoichiometry matrix of a model. A feasible solution `x` of a -model `m` is defined as satisfying the equations: - -- `stoichiometry(m) * x .== balance(m)` -- `x .>= lbs` -- `y .<= ubs` -- `(lbs, ubs) == bounds(m) -""" -function stoichiometry(a::MetabolicModel)::SparseMat - _missing_impl_error(stoichiometry, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Get the lower and upper solution bounds of a model. -""" -function bounds(a::MetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} - _missing_impl_error(bounds, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Get the sparse balance vector of a model. -""" -function balance(a::MetabolicModel)::SparseVec - return spzeros(n_metabolites(a)) -end - -""" -$(TYPEDSIGNATURES) - -Get the objective vector of the model. Analysis functions, such as -[`flux_balance_analysis`](@ref), are supposed to maximize `dot(objective, x)` -where `x` is a feasible solution of the model. -""" -function objective(a::MetabolicModel)::SparseVec - _missing_impl_error(objective, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -In some models, the [`reactions`](@ref) that correspond to the columns of -[`stoichiometry`](@ref) matrix do not fully represent the semantic contents of -the model; for example, fluxes may be split into forward and reverse reactions, -reactions catalyzed by distinct enzymes, etc. Together with -[`reaction_flux`](@ref) (and [`n_fluxes`](@ref)) this specifies how the -flux is decomposed into individual reactions. - -By default (and in most models), fluxes and reactions perfectly correspond. -""" -function fluxes(a::MetabolicModel)::Vector{String} - reactions(a) -end - -function n_fluxes(a::MetabolicModel)::Int - n_reactions(a) -end - -""" -$(TYPEDSIGNATURES) - -Retrieve a sparse matrix that describes the correspondence of a solution of the -linear system to the fluxes (see [`fluxes`](@ref) for rationale). Returns a -sparse matrix of size `(n_reactions(a), n_fluxes(a))`. For most models, this is -an identity matrix. -""" -function reaction_flux(a::MetabolicModel)::SparseMat - nr = n_reactions(a) - nf = n_fluxes(a) - nr == nf || _missing_impl_error(reaction_flux, (a,)) - spdiagm(fill(1, nr)) -end - -""" -$(TYPEDSIGNATURES) - -Get a matrix of coupling constraint definitions of a model. By default, there -is no coupling in the models. -""" -function coupling(a::MetabolicModel)::SparseMat - return spzeros(0, n_reactions(a)) -end - -""" -$(TYPEDSIGNATURES) - -Get the number of coupling constraints in a model. -""" -function n_coupling_constraints(a::MetabolicModel)::Int - size(coupling(a), 1) -end - -""" -$(TYPEDSIGNATURES) - -Get the lower and upper bounds for each coupling bound in a model, as specified -by `coupling`. By default, the model does not have any coupling bounds. -""" -function coupling_bounds(a::MetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} - return (spzeros(0), spzeros(0)) -end - -""" -$(TYPEDSIGNATURES) - -Return identifiers of all genes contained in the model. By default, there are -no genes. - -In SBML, these are usually called "gene products" but we write `genes` for -simplicity. -""" -function genes(a::MetabolicModel)::Vector{String} - return [] -end - -""" -$(TYPEDSIGNATURES) - -Return the number of genes in the model (as returned by [`genes`](@ref)). If -you just need the number of the genes, this may be much more efficient than -calling [`genes`](@ref) and measuring the array. -""" -function n_genes(a::MetabolicModel)::Int - return length(genes(a)) -end - -""" -$(TYPEDSIGNATURES) - -Returns the sets of genes that need to be present so that the reaction can work -(technically, a DNF on gene availability, with positive atoms only). - -For simplicity, `nothing` may be returned, meaning that the reaction always -takes place. (in DNF, that would be equivalent to returning `[[]]`.) -""" -function reaction_gene_association( - a::MetabolicModel, - reaction_id::String, -)::Maybe{GeneAssociation} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return the subsystem of reaction `reaction_id` in `model` if it is assigned. If not, -return `nothing`. -""" -function reaction_subsystem(model::MetabolicModel, reaction_id::String)::Maybe{String} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid` in the model. The dictionary -maps the metabolite IDs to their stoichiometric coefficients. -""" -function reaction_stoichiometry(m::MetabolicModel, rid::String)::Dict{String,Float64} - mets = metabolites(m) - Dict( - mets[k] => v for - (k, v) in zip(findnz(stoichiometry(m)[:, first(indexin([rid], reactions(m)))])...) - ) -end - -""" -$(TYPEDSIGNATURES) - -Return the formula of metabolite `metabolite_id` in `model`. -Return `nothing` in case the formula is not known or irrelevant. -""" -function metabolite_formula( - model::MetabolicModel, - metabolite_id::String, -)::Maybe{MetaboliteFormula} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return the charge associated with metabolite `metabolite_id` in `model`. -Returns `nothing` if charge not present. -""" -function metabolite_charge(model::MetabolicModel, metabolite_id::String)::Maybe{Int} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return the compartment of metabolite `metabolite_id` in `model` if it is assigned. If not, -return `nothing`. -""" -function metabolite_compartment(model::MetabolicModel, metabolite_id::String)::Maybe{String} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return standardized names that may help identifying the reaction. The -dictionary assigns vectors of possible identifiers to identifier system names, -e.g. `"Reactome" => ["reactomeID123"]`. -""" -function reaction_annotations(a::MetabolicModel, reaction_id::String)::Annotations - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return standardized names that may help to reliably identify the metabolite. The -dictionary assigns vectors of possible identifiers to identifier system names, -e.g. `"ChEMBL" => ["123"]` or `"PubChem" => ["CID123", "CID654645645"]`. -""" -function metabolite_annotations(a::MetabolicModel, metabolite_id::String)::Annotations - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return standardized names that identify the corresponding gene or product. The -dictionary assigns vectors of possible identifiers to identifier system names, -e.g. `"PDB" => ["PROT01"]`. -""" -function gene_annotations(a::MetabolicModel, gene_id::String)::Annotations - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with reaction `reaction_id` in `model`. -""" -function reaction_notes(model::MetabolicModel, reaction_id::String)::Notes - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with metabolite `reaction_id` in `model`. -""" -function metabolite_notes(model::MetabolicModel, metabolite_id::String)::Notes - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with the gene `gene_id` in `model`. -""" -function gene_notes(model::MetabolicModel, gene_id::String)::Notes - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return the name of reaction with ID `rid`. -""" -reaction_name(model::MetabolicModel, rid::String) = nothing - -""" -$(TYPEDSIGNATURES) - -Return the name of metabolite with ID `mid`. -""" -metabolite_name(model::MetabolicModel, mid::String) = nothing - -""" -$(TYPEDSIGNATURES) - -Return the name of gene with ID `gid`. -""" -gene_name(model::MetabolicModel, gid::String) = nothing - -""" -$(TYPEDSIGNATURES) - -Do whatever is feasible to get the model into a state that can be read from -as-quickly-as-possible. This may include e.g. generating helper index -structures and loading delayed parts of the model from disk. The model should -be modified "transparently" in-place. Analysis functions call this right before -applying modifications or converting the model to the optimization model using -[`make_optimization_model`](@ref); usually on the same machine where the -optimizers (and, generally, the core analysis algorithms) will run. The calls -are done in a good hope that the performance will be improved. - -By default, it should be safe to do nothing. -""" -function precache!(a::MetabolicModel)::Nothing - nothing -end diff --git a/src/base/types/Metabolite.jl b/src/base/types/Metabolite.jl deleted file mode 100644 index 79bb0330a..000000000 --- a/src/base/types/Metabolite.jl +++ /dev/null @@ -1,30 +0,0 @@ -""" -$(TYPEDEF) - -# Fields -$(TYPEDFIELDS) -""" -mutable struct Metabolite - id::String - name::Maybe{String} - formula::Maybe{String} - charge::Maybe{Int} - compartment::Maybe{String} - notes::Notes - annotations::Annotations -end - -""" -$(TYPEDSIGNATURES) - -A constructor for `Metabolite`s. -""" -Metabolite( - id = ""; - name = nothing, - formula = nothing, - charge = nothing, - compartment = nothing, - notes = Notes(), - annotations = Annotations(), -) = Metabolite(String(id), name, formula, charge, compartment, notes, annotations) diff --git a/src/base/types/ModelWrapper.jl b/src/base/types/ModelWrapper.jl deleted file mode 100644 index e1988f1ae..000000000 --- a/src/base/types/ModelWrapper.jl +++ /dev/null @@ -1,23 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -A simple helper to pick the single w -""" -function unwrap_model(a::ModelWrapper) - _missing_impl_error(unwrap_model, (a,)) -end - -# -# IMPORTANT -# -# The list of inherited functions must be synced with the methods available for [`MetabolicModel`](@ref). -# - -@_inherit_model_methods_fn ModelWrapper () unwrap_model () reactions metabolites stoichiometry bounds balance objective fluxes n_fluxes reaction_flux coupling n_coupling_constraints coupling_bounds genes n_genes precache! - -@_inherit_model_methods_fn ModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_association reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes - -@_inherit_model_methods_fn ModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes - -@_inherit_model_methods_fn ModelWrapper (gid::String,) unwrap_model (gid,) gene_annotations gene_notes diff --git a/src/base/types/Reaction.jl b/src/base/types/Reaction.jl deleted file mode 100644 index b87e1c45c..000000000 --- a/src/base/types/Reaction.jl +++ /dev/null @@ -1,85 +0,0 @@ -""" -$(TYPEDEF) - -A structure for representing a single reaction in a [`StandardModel`](@ref). - -# Fields -$(TYPEDFIELDS) -""" -mutable struct Reaction - id::String - name::Maybe{String} - metabolites::Dict{String,Float64} - lb::Float64 - ub::Float64 - grr::Maybe{GeneAssociation} - subsystem::Maybe{String} - notes::Notes - annotations::Annotations - objective_coefficient::Float64 -end - -""" -$(TYPEDSIGNATURES) - -A constructor for Reaction that only takes a reaction `id` and -assigns default/uninformative values to all the fields that are not -explicitely assigned. -""" -function Reaction( - id = ""; - name = nothing, - metabolites = Dict{String,Float64}(), - lb = -_constants.default_reaction_bound, - ub = _constants.default_reaction_bound, - grr = nothing, - subsystem = nothing, - notes = Notes(), - annotations = Annotations(), - objective_coefficient = 0.0, -) - mets = Dict(k => float(v) for (k, v) in metabolites) - return Reaction( - id, - name, - mets, - lb, - ub, - grr, - subsystem, - notes, - annotations, - objective_coefficient, - ) -end - -""" -$(TYPEDSIGNATURES) - -Convenience constructor for `Reaction`. The reaction equation is specified using -`metabolites`, which is a dictionary mapping metabolite ids to stoichiometric -coefficients. The direcion of the reaction is set through `dir` which can take -`:bidirectional`, `:forward`, and `:reverse` as values. Finally, the -`default_bound` is the value taken to mean infinity in the context of constraint -based models, often this is set to a very high flux value like 1000. -""" -function Reaction( - id::String, - metabolites, - dir = :bidirectional; - default_bound = _constants.default_reaction_bound, -) - if dir == :forward - lb = 0.0 - ub = default_bound - elseif dir == :reverse - lb = -default_bound - ub = 0.0 - elseif dir == :bidirectional - lb = -default_bound - ub = default_bound - else - throw(DomainError(dir, "unsupported direction")) - end - Reaction(id; metabolites = metabolites, lb = lb, ub = ub) -end diff --git a/src/base/types/ReactionStatus.jl b/src/base/types/ReactionStatus.jl deleted file mode 100644 index 90940711b..000000000 --- a/src/base/types/ReactionStatus.jl +++ /dev/null @@ -1,13 +0,0 @@ -""" -$(TYPEDEF) - -Used for concise reporting of modeling results. - -# Fields -$(TYPEDFIELDS) -""" -mutable struct ReactionStatus - already_present::Bool - index::Int - info::String -end diff --git a/src/base/types/SBMLModel.jl b/src/base/types/SBMLModel.jl deleted file mode 100644 index e07e7a31b..000000000 --- a/src/base/types/SBMLModel.jl +++ /dev/null @@ -1,482 +0,0 @@ -""" -$(TYPEDEF) - -Thin wrapper around the model from SBML.jl library. Allows easy conversion from -SBML to any other model format. - -# Fields -$(TYPEDFIELDS) -""" -struct SBMLModel <: MetabolicModel - sbml::SBML.Model - reaction_ids::Vector{String} - reaction_idx::Dict{String,Int} - metabolite_ids::Vector{String} - metabolite_idx::Dict{String,Int} - gene_ids::Vector{String} - active_objective::String -end - -""" -$(TYPEDEF) - -Construct the SBML model and add the necessary cached indexes, possibly choosing an active objective. -""" -function SBMLModel(sbml::SBML.Model, active_objective::String = "") - rxns = sort(collect(keys(sbml.reactions))) - mets = sort(collect(keys(sbml.species))) - genes = sort(collect(keys(sbml.gene_products))) - - SBMLModel( - sbml, - rxns, - Dict(rxns .=> eachindex(rxns)), - mets, - Dict(mets .=> eachindex(mets)), - genes, - active_objective, - ) -end - -""" -$(TYPEDSIGNATURES) - -Get reactions from a [`SBMLModel`](@ref). -""" -reactions(model::SBMLModel)::Vector{String} = model.reaction_ids - -""" -$(TYPEDSIGNATURES) - -Get metabolites from a [`SBMLModel`](@ref). -""" -metabolites(model::SBMLModel)::Vector{String} = model.metabolite_ids - -""" -$(TYPEDSIGNATURES) - -Efficient counting of reactions in [`SBMLModel`](@ref). -""" -n_reactions(model::SBMLModel)::Int = length(model.reaction_ids) - -""" -$(TYPEDSIGNATURES) - -Efficient counting of metabolites in [`SBMLModel`](@ref). -""" -n_metabolites(model::SBMLModel)::Int = length(model.metabolite_ids) - -""" -$(TYPEDSIGNATURES) - -Recreate the stoichiometry matrix from the [`SBMLModel`](@ref). -""" -function stoichiometry(model::SBMLModel)::SparseMat - - # find the vector size for preallocation - nnz = 0 - for (_, r) in model.sbml.reactions - for _ in r.reactants - nnz += 1 - end - for _ in r.products - nnz += 1 - end - end - - Rows = Int[] - Cols = Int[] - Vals = Float64[] - sizehint!(Rows, nnz) - sizehint!(Cols, nnz) - sizehint!(Vals, nnz) - - row_idx = Dict(k => i for (i, k) in enumerate(model.metabolite_ids)) - for (ridx, rid) in enumerate(model.reaction_ids) - r = model.sbml.reactions[rid] - for sr in r.reactants - push!(Rows, model.metabolite_idx[sr.species]) - push!(Cols, ridx) - push!(Vals, isnothing(sr.stoichiometry) ? -1.0 : -sr.stoichiometry) - end - for sr in r.products - push!(Rows, model.metabolite_idx[sr.species]) - push!(Cols, ridx) - push!(Vals, isnothing(sr.stoichiometry) ? 1.0 : sr.stoichiometry) - end - end - return sparse(Rows, Cols, Vals, n_metabolites(model), n_reactions(model)) -end - -""" -$(TYPEDSIGNATURES) - -Get the lower and upper flux bounds of model [`SBMLModel`](@ref). Throws `DomainError` in -case if the SBML contains mismatching units. -""" -function bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} - # There are multiple ways in SBML to specify a lower/upper bound. There are - # the "global" model bounds that we completely ignore now because no one - # uses them. In reaction, you can specify the bounds using "LOWER_BOUND" - # and "UPPER_BOUND" parameters, but also there may be a FBC plugged-in - # parameter name that refers to the parameters. We extract these, using - # the units from the parameters. For unbounded reactions we use -Inf or Inf - # as a default. - - common_unit = "" - - function get_bound(rid, fld, param, default) - rxn = model.sbml.reactions[rid] - param_name = SBML.mayfirst(getfield(rxn, fld), param) - param = get( - rxn.kinetic_parameters, - param_name, - get(model.sbml.parameters, param_name, default), - ) - unit = SBML.mayfirst(param.units, "") - if unit != "" - if common_unit != "" - if unit != common_unit - throw( - DomainError( - units_in_sbml, - "The SBML file uses multiple units; loading would need conversion", - ), - ) - end - else - common_unit = unit - end - end - return param.value - end - - return ( - get_bound.( - model.reaction_ids, - :lower_bound, - "LOWER_BOUND", - Ref(SBML.Parameter(value = -Inf)), - ), - get_bound.( - model.reaction_ids, - :upper_bound, - "UPPER_BOUND", - Ref(SBML.Parameter(value = Inf)), - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Balance vector of a [`SBMLModel`](@ref). This is always zero. -""" -balance(model::SBMLModel)::SparseVec = spzeros(n_metabolites(model)) - -""" -$(TYPEDSIGNATURES) - -Objective of the [`SBMLModel`](@ref). -""" -function objective(model::SBMLModel)::SparseVec - res = sparsevec([], [], n_reactions(model)) - - objective = get(model.sbml.objectives, model.active_objective, nothing) - if isnothing(objective) && length(model.sbml.objectives) == 1 - objective = first(values(model.sbml.objectives)) - end - if !isnothing(objective) - direction = objective.type == "maximize" ? 1.0 : -1.0 - for (rid, coef) in objective.flux_objectives - res[model.reaction_idx[rid]] = float(direction * coef) - end - else - # old-style objectives - for (rid, r) in model.sbml.reactions - oc = get(r.kinetic_parameters, "OBJECTIVE_COEFFICIENT", nothing) - isnothing(oc) || (res[model.reaction_idx[rid]] = float(oc.value)) - end - end - return res -end - -""" -$(TYPEDSIGNATURES) - -Get genes of a [`SBMLModel`](@ref). -""" -genes(model::SBMLModel)::Vector{String} = model.gene_ids - -""" -$(TYPEDSIGNATURES) - -Get number of genes in [`SBMLModel`](@ref). -""" -n_genes(model::SBMLModel)::Int = length(model.gene_ids) - -""" -$(TYPEDSIGNATURES) - -Retrieve the [`GeneAssociation`](@ref) from [`SBMLModel`](@ref). -""" -reaction_gene_association(model::SBMLModel, rid::String)::Maybe{GeneAssociation} = - _maybemap(_parse_grr, model.sbml.reactions[rid].gene_product_association) - -""" -$(TYPEDSIGNATURES) - -Get [`MetaboliteFormula`](@ref) from a chosen metabolite from [`SBMLModel`](@ref). -""" -metabolite_formula(model::SBMLModel, mid::String)::Maybe{MetaboliteFormula} = - _maybemap(_parse_formula, model.sbml.species[mid].formula) - -""" -$(TYPEDSIGNATURES) - -Get the compartment of a chosen metabolite from [`SBMLModel`](@ref). -""" -metabolite_compartment(model::SBMLModel, mid::String) = model.sbml.species[mid].compartment - -""" -$(TYPEDSIGNATURES) - -Get charge of a chosen metabolite from [`SBMLModel`](@ref). -""" -metabolite_charge(model::SBMLModel, mid::String)::Maybe{Int} = - model.sbml.species[mid].charge - -function _parse_sbml_identifiers_org_uri(uri::String)::Tuple{String,String} - m = match(r"^http://identifiers.org/([^/]+)/(.*)$", uri) - isnothing(m) ? ("RESOURCE_URI", uri) : (m[1], m[2]) -end - -function _sbml_import_cvterms(sbo::Maybe{String}, cvs::Vector{SBML.CVTerm})::Annotations - res = Annotations() - isnothing(sbo) || (res["sbo"] = [sbo]) - for cv in cvs - cv.biological_qualifier == :is || continue - for (id, val) in _parse_sbml_identifiers_org_uri.(cv.resource_uris) - push!(get!(res, id, []), val) - end - end - return res -end - -function _sbml_export_cvterms(annotations::Annotations)::Vector{SBML.CVTerm} - isempty(annotations) && return [] - length(annotations) == 1 && haskey(annotations, "sbo") && return [] - [ - SBML.CVTerm( - biological_qualifier = :is, - resource_uris = [ - id == "RESOURCE_URI" ? val : "http://identifiers.org/$id/$val" for - (id, vals) in annotations if id != "sbo" for val in vals - ], - ), - ] -end - -function _sbml_export_sbo(annotations::Annotations)::Maybe{String} - haskey(annotations, "sbo") || return nothing - if length(annotations["sbo"]) != 1 - @_io_log @error "Data loss: SBO term is not unique for SBML export" annotations["sbo"] - return - end - return annotations["sbo"][1] -end - -function _sbml_import_notes(notes::Maybe{String})::Notes - isnothing(notes) ? Notes() : Notes("" => [notes]) -end - -function _sbml_export_notes(notes::Notes)::Maybe{String} - isempty(notes) || @_io_log @error "Data loss: notes not exported to SBML" notes - nothing -end - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. -""" -function reaction_stoichiometry(m::SBMLModel, rid::String)::Dict{String,Float64} - s = Dict{String,Float64}() - default1(x) = isnothing(x) ? 1 : x - for sr in m.sbml.reactions[rid].reactants - s[sr.species] = get(s, sr.species, 0.0) - default1(sr.stoichiometry) - end - for sr in m.sbml.reactions[rid].products - s[sr.species] = get(s, sr.species, 0.0) + default1(sr.stoichiometry) - end - return s -end - -""" -$(TYPEDSIGNATURES) - -Return the name of reaction with ID `rid`. -""" -reaction_name(model::SBMLModel, rid::String) = model.sbml.reactions[rid].name - -""" -$(TYPEDSIGNATURES) - -Return the name of metabolite with ID `mid`. -""" -metabolite_name(model::SBMLModel, mid::String) = model.sbml.species[mid].name - -""" -$(TYPEDSIGNATURES) - -Return the name of gene with ID `gid`. -""" -gene_name(model::SBMLModel, gid::String) = model.sbml.gene_products[gid].name - -""" -$(TYPEDSIGNATURES) - -Return the annotations of reaction with ID `rid`. -""" -reaction_annotations(model::SBMLModel, rid::String) = - _sbml_import_cvterms(model.sbml.reactions[rid].sbo, model.sbml.reactions[rid].cv_terms) - -""" -$(TYPEDSIGNATURES) - -Return the annotations of metabolite with ID `mid`. -""" -metabolite_annotations(model::SBMLModel, mid::String) = - _sbml_import_cvterms(model.sbml.species[mid].sbo, model.sbml.species[mid].cv_terms) - -""" -$(TYPEDSIGNATURES) - -Return the annotations of gene with ID `gid`. -""" -gene_annotations(model::SBMLModel, gid::String) = _sbml_import_cvterms( - model.sbml.gene_products[gid].sbo, - model.sbml.gene_products[gid].cv_terms, -) - -""" -$(TYPEDSIGNATURES) - -Return the notes about reaction with ID `rid`. -""" -reaction_notes(model::SBMLModel, rid::String) = - _sbml_import_notes(model.sbml.reactions[rid].notes) - -""" -$(TYPEDSIGNATURES) - -Return the notes about metabolite with ID `mid`. -""" -metabolite_notes(model::SBMLModel, mid::String) = - _sbml_import_notes(model.sbml.species[mid].notes) - -""" -$(TYPEDSIGNATURES) - -Return the notes about gene with ID `gid`. -""" -gene_notes(model::SBMLModel, gid::String) = - _sbml_import_notes(model.sbml.gene_products[gid].notes) - -""" -$(TYPEDSIGNATURES) - -Convert any metabolic model to [`SBMLModel`](@ref). -""" -function Base.convert(::Type{SBMLModel}, mm::MetabolicModel) - if typeof(mm) == SBMLModel - return mm - end - - mets = metabolites(mm) - rxns = reactions(mm) - stoi = stoichiometry(mm) - (lbs, ubs) = bounds(mm) - comps = _default.("compartment", metabolite_compartment.(Ref(mm), mets)) - compss = Set(comps) - - metid(x) = startswith(x, "M_") ? x : "M_$x" - rxnid(x) = startswith(x, "R_") ? x : "R_$x" - gprid(x) = startswith(x, "G_") ? x : "G_$x" - - return SBMLModel( - SBML.Model( - compartments = Dict( - comp => SBML.Compartment(constant = true) for comp in compss - ), - species = Dict( - metid(mid) => SBML.Species( - name = metabolite_name(mm, mid), - compartment = _default("compartment", comps[mi]), - formula = _maybemap(_unparse_formula, metabolite_formula(mm, mid)), - charge = metabolite_charge(mm, mid), - constant = false, - boundary_condition = false, - only_substance_units = false, - sbo = _sbml_export_sbo(metabolite_annotations(mm, mid)), - notes = _sbml_export_notes(metabolite_notes(mm, mid)), - metaid = metid(mid), - cv_terms = _sbml_export_cvterms(metabolite_annotations(mm, mid)), - ) for (mi, mid) in enumerate(mets) - ), - reactions = Dict( - rxnid(rid) => SBML.Reaction( - name = reaction_name(mm, rid), - reactants = [ - SBML.SpeciesReference( - species = metid(mets[i]), - stoichiometry = -stoi[i, ri], - constant = true, - ) for - i in SparseArrays.nonzeroinds(stoi[:, ri]) if stoi[i, ri] <= 0 - ], - products = [ - SBML.SpeciesReference( - species = metid(mets[i]), - stoichiometry = stoi[i, ri], - constant = true, - ) for - i in SparseArrays.nonzeroinds(stoi[:, ri]) if stoi[i, ri] > 0 - ], - kinetic_parameters = Dict( - "LOWER_BOUND" => SBML.Parameter(value = lbs[ri]), - "UPPER_BOUND" => SBML.Parameter(value = ubs[ri]), - ), - lower_bound = "LOWER_BOUND", - upper_bound = "UPPER_BOUND", - gene_product_association = _maybemap( - x -> _unparse_grr(SBML.GeneProductAssociation, x), - reaction_gene_association(mm, rid), - ), - reversible = true, - sbo = _sbml_export_sbo(reaction_annotations(mm, rid)), - notes = _sbml_export_notes(reaction_notes(mm, rid)), - metaid = rxnid(rid), - cv_terms = _sbml_export_cvterms(reaction_annotations(mm, rid)), - ) for (ri, rid) in enumerate(rxns) - ), - gene_products = Dict( - gprid(gid) => SBML.GeneProduct( - label = gid, - name = gene_name(mm, gid), - sbo = _sbml_export_sbo(gene_annotations(mm, gid)), - notes = _sbml_export_notes(gene_notes(mm, gid)), - metaid = gprid(gid), - cv_terms = _sbml_export_cvterms(gene_annotations(mm, gid)), - ) for gid in genes(mm) - ), - active_objective = "objective", - objectives = Dict( - "objective" => SBML.Objective( - "maximize", - Dict(rid => oc for (rid, oc) in zip(rxns, objective(mm)) if oc != 0), - ), - ), - ), - ) -end diff --git a/src/base/types/Serialized.jl b/src/base/types/Serialized.jl deleted file mode 100644 index aef1c887c..000000000 --- a/src/base/types/Serialized.jl +++ /dev/null @@ -1,41 +0,0 @@ - -""" -$(TYPEDEF) - -A meta-model that represents a model that is serialized on the disk. The -internal model will be loaded on-demand by using any accessor, or by calling -[`precache!`](@ref) directly. - -# Fields -$(TYPEDFIELDS) -""" -mutable struct Serialized{M} <: ModelWrapper where {M<:MetabolicModel} - m::Maybe{M} - filename::String - - Serialized{T}(filename::String) where {T} = new{T}(nothing, filename) - Serialized(model::T, filename::String) where {T<:MetabolicModel} = - new{T}(model, filename) -end - -""" -$(TYPEDSIGNATURES) - -Unwrap the serialized model (precaching it transparently). -""" -function unwrap_model(m::Serialized) - precache!(m) - m.m -end - -""" -$(TYPEDSIGNATURES) - -Load the `Serialized` model from disk in case it's not alreadly loaded. -""" -function precache!(model::Serialized)::Nothing - if isnothing(model.m) - model.m = deserialize(model.filename) - end - nothing -end diff --git a/src/base/types/StandardModel.jl b/src/base/types/StandardModel.jl deleted file mode 100644 index 211a1e088..000000000 --- a/src/base/types/StandardModel.jl +++ /dev/null @@ -1,379 +0,0 @@ -""" -$(TYPEDEF) - -`StandardModel` is used to store a constraint based metabolic model with -meta-information. Meta-information is defined as annotation details, which -include gene-reaction-rules, formulas, etc. - -This model type seeks to keep as much meta-information as possible, as opposed -to `CoreModel` and `CoreModelCoupled`, which keep the bare neccessities only. -When merging models and keeping meta-information is important, use this as the -model type. If meta-information is not important, use the more efficient core -model types. See [`CoreModel`](@ref) and [`CoreModelCoupled`](@ref) for -comparison. - -In this model, reactions, metabolites, and genes are stored in ordered -dictionaries indexed by each struct's `id` field. For example, -`model.reactions["rxn1_id"]` returns a `Reaction` where the field `id` equals -`"rxn1_id"`. This makes adding and removing reactions efficient. - -Note that the stoichiometric matrix (or any other core data, e.g. flux bounds) -is not stored directly as in `CoreModel`. When this model type is used in -analysis functions, these core data structures are built from scratch each time -an analysis function is called. This can cause performance issues if you run -many small analysis functions sequentially. Consider using the core model -types if performance is critical. - -See also: [`Reaction`](@ref), [`Metabolite`](@ref), [`Gene`](@ref) - -# Example -``` -model = load_model(StandardModel, "my_model.json") -keys(model.reactions) -``` - -# Fields -$(TYPEDFIELDS) -""" -mutable struct StandardModel <: MetabolicModel - id::String - reactions::OrderedDict{String,Reaction} - metabolites::OrderedDict{String,Metabolite} - genes::OrderedDict{String,Gene} - - StandardModel( - id = ""; - reactions = OrderedDict{String,Reaction}(), - metabolites = OrderedDict{String,Metabolite}(), - genes = OrderedDict{String,Gene}(), - ) = new(id, reactions, metabolites, genes) -end - -# MetabolicModel interface follows -""" -$(TYPEDSIGNATURES) - -Return a vector of reaction id strings contained in `model`. -The order of reaction ids returned here matches the order used to construct the -stoichiometric matrix. -""" -reactions(model::StandardModel)::StringVecType = collect(keys(model.reactions)) - -""" -$(TYPEDSIGNATURES) - -Return the number of reactions contained in `model`. -""" -n_reactions(model::StandardModel)::Int = length(model.reactions) - - -""" -$(TYPEDSIGNATURES) - -Return a vector of metabolite id strings contained in `model`. -The order of metabolite strings returned here matches the order used to construct -the stoichiometric matrix. -""" -metabolites(model::StandardModel)::StringVecType = collect(keys(model.metabolites)) - -""" -$(TYPEDSIGNATURES) - -Return the number of metabolites in `model`. -""" -n_metabolites(model::StandardModel)::Int = length(model.metabolites) - -""" -$(TYPEDSIGNATURES) - -Return a vector of gene id strings in `model`. -""" -genes(model::StandardModel)::StringVecType = collect(keys(model.genes)) - -""" -$(TYPEDSIGNATURES) - -Return the number of genes in `model`. -""" -n_genes(model::StandardModel)::Int = length(model.genes) - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometric matrix associated with `model` in sparse format. -""" -function stoichiometry(model::StandardModel)::SparseMat - n_entries = 0 - for (_, r) in model.reactions - for _ in r.metabolites - n_entries += 1 - end - end - - MI = Vector{Int}() - RI = Vector{Int}() - SV = Vector{Float64}() - sizehint!(MI, n_entries) - sizehint!(RI, n_entries) - sizehint!(SV, n_entries) - - # establish the ordering - rxns = reactions(model) - met_idx = Dict(mid => i for (i, mid) in enumerate(metabolites(model))) - - # fill the matrix entries - for (ridx, rid) in enumerate(rxns) - for (mid, coeff) in model.reactions[rid].metabolites - haskey(met_idx, mid) || throw( - DomainError( - mid, - "Metabolite $(mid) not found in model but occurs in stoichiometry of $(rid)", - ), - ) - push!(MI, met_idx[mid]) - push!(RI, ridx) - push!(SV, coeff) - end - end - return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), n_reactions(model)) -end - -""" -$(TYPEDSIGNATURES) - -Return the lower bounds for all reactions in `model` in sparse format. -""" -lower_bounds(model::StandardModel)::Vector{Float64} = - sparse([model.reactions[rxn].lb for rxn in reactions(model)]) - -""" -$(TYPEDSIGNATURES) - -Return the upper bounds for all reactions in `model` in sparse format. -Order matches that of the reaction ids returned in `reactions()`. -""" -upper_bounds(model::StandardModel)::Vector{Float64} = - sparse([model.reactions[rxn].ub for rxn in reactions(model)]) - -""" -$(TYPEDSIGNATURES) - -Return the lower and upper bounds, respectively, for reactions in `model`. -Order matches that of the reaction ids returned in `reactions()`. -""" -bounds(model::StandardModel)::Tuple{Vector{Float64},Vector{Float64}} = - (lower_bounds(model), upper_bounds(model)) - -""" -$(TYPEDSIGNATURES) - -Return the balance of the linear problem, i.e. b in Sv = 0 where S is the stoichiometric matrix -and v is the flux vector. -""" -balance(model::StandardModel)::SparseVec = spzeros(length(model.metabolites)) - -""" -$(TYPEDSIGNATURES) - -Return sparse objective vector for `model`. -""" -objective(model::StandardModel)::SparseVec = - sparse([model.reactions[rid].objective_coefficient for rid in keys(model.reactions)]) - -""" -$(TYPEDSIGNATURES) - -Return the gene reaction rule in string format for reaction with `id` in `model`. -Return `nothing` if not available. -""" -reaction_gene_association(model::StandardModel, id::String)::Maybe{GeneAssociation} = - _maybemap(identity, model.reactions[id].grr) - -""" -$(TYPEDSIGNATURES) - -Return the formula of reaction `id` in `model`. -Return `nothing` if not present. -""" -metabolite_formula(model::StandardModel, id::String)::Maybe{MetaboliteFormula} = - _maybemap(_parse_formula, model.metabolites[id].formula) - -""" -$(TYPEDSIGNATURES) - -Return the charge associated with metabolite `id` in `model`. -Return nothing if not present. -""" -metabolite_charge(model::StandardModel, id::String)::Maybe{Int} = - model.metabolites[id].charge - -""" -$(TYPEDSIGNATURES) - -Return compartment associated with metabolite `id` in `model`. -Return `nothing` if not present. -""" -metabolite_compartment(model::StandardModel, id::String)::Maybe{String} = - model.metabolites[id].compartment - -""" -$(TYPEDSIGNATURES) - -Return the subsystem associated with reaction `id` in `model`. -Return `nothing` if not present. -""" -reaction_subsystem(model::StandardModel, id::String)::Maybe{String} = - model.reactions[id].subsystem - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with metabolite `id` in `model`. -Return an empty Dict if not present. -""" -metabolite_notes(model::StandardModel, id::String)::Maybe{Notes} = - model.metabolites[id].notes - -""" -$(TYPEDSIGNATURES) - -Return the annotation associated with metabolite `id` in `model`. -Return an empty Dict if not present. -""" -metabolite_annotations(model::StandardModel, id::String)::Maybe{Annotations} = - model.metabolites[id].annotations - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with gene `id` in `model`. -Return an empty Dict if not present. -""" -gene_notes(model::StandardModel, id::String)::Maybe{Notes} = model.genes[id].notes - -""" -$(TYPEDSIGNATURES) - -Return the annotation associated with gene `id` in `model`. -Return an empty Dict if not present. -""" -gene_annotations(model::StandardModel, id::String)::Maybe{Annotations} = - model.genes[id].annotations - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with reaction `id` in `model`. -Return an empty Dict if not present. -""" -reaction_notes(model::StandardModel, id::String)::Maybe{Notes} = model.reactions[id].notes - -""" -$(TYPEDSIGNATURES) - -Return the annotation associated with reaction `id` in `model`. -Return an empty Dict if not present. -""" -reaction_annotations(model::StandardModel, id::String)::Maybe{Annotations} = - model.reactions[id].annotations - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. -""" -reaction_stoichiometry(m::StandardModel, rid::String)::Dict{String,Float64} = - m.reactions[rid].metabolites - -""" -$(TYPEDSIGNATURES) - -Return the name of reaction with ID `id`. -""" -reaction_name(m::StandardModel, rid::String) = m.reactions[rid].name - -""" -$(TYPEDSIGNATURES) - -Return the name of metabolite with ID `id`. -""" -metabolite_name(m::StandardModel, mid::String) = m.metabolites[mid].name - -""" -$(TYPEDSIGNATURES) - -Return the name of gene with ID `id`. -""" -gene_name(m::StandardModel, gid::String) = m.genes[gid].name - -""" -$(TYPEDSIGNATURES) - -Convert any `MetabolicModel` into a `StandardModel`. -Note, some data loss may occur since only the generic interface is used during -the conversion process. -""" -function Base.convert(::Type{StandardModel}, model::MetabolicModel) - if typeof(model) == StandardModel - return model - end - - id = "" # TODO: add accessor to get model ID - modelreactions = OrderedDict{String,Reaction}() - modelmetabolites = OrderedDict{String,Metabolite}() - modelgenes = OrderedDict{String,Gene}() - - gids = genes(model) - metids = metabolites(model) - rxnids = reactions(model) - - for gid in gids - modelgenes[gid] = Gene( - gid; - name = gene_name(model, gid), - notes = gene_notes(model, gid), - annotations = gene_annotations(model, gid), - ) - end - - for mid in metids - modelmetabolites[mid] = Metabolite( - mid; - name = metabolite_name(model, mid), - charge = metabolite_charge(model, mid), - formula = _maybemap(_unparse_formula, metabolite_formula(model, mid)), - compartment = metabolite_compartment(model, mid), - notes = metabolite_notes(model, mid), - annotations = metabolite_annotations(model, mid), - ) - end - - S = stoichiometry(model) - lbs, ubs = bounds(model) - ocs = objective(model) - for (i, rid) in enumerate(rxnids) - rmets = Dict{String,Float64}() - for (j, stoich) in zip(findnz(S[:, i])...) - rmets[metids[j]] = stoich - end - modelreactions[rid] = Reaction( - rid; - name = reaction_name(model, rid), - metabolites = rmets, - lb = lbs[i], - ub = ubs[i], - grr = reaction_gene_association(model, rid), - objective_coefficient = ocs[i], - notes = reaction_notes(model, rid), - annotations = reaction_annotations(model, rid), - subsystem = reaction_subsystem(model, rid), - ) - end - - return StandardModel( - id; - reactions = modelreactions, - metabolites = modelmetabolites, - genes = modelgenes, - ) -end diff --git a/src/base/types/abstract/Maybe.jl b/src/base/types/abstract/Maybe.jl deleted file mode 100644 index 7b5df0a39..000000000 --- a/src/base/types/abstract/Maybe.jl +++ /dev/null @@ -1,25 +0,0 @@ - -""" - Maybe{T} = Union{Nothing, T} - -A nice name for "nullable" type. -""" -const Maybe{T} = Union{Nothing,T} - -""" -$(TYPEDSIGNATURES) - -Fold the `Maybe{T}` down to `T` by defaulting. -""" -function _default(d::T, x::Maybe{T})::T where {T} - isnothing(x) ? d : x -end - -""" -$(TYPEDSIGNATURES) - -Apply a function to `x` only if it is not `nothing`. -""" -function _maybemap(f, x::Maybe)::Maybe - isnothing(x) ? nothing : f(x) -end diff --git a/src/base/types/abstract/MetabolicModel.jl b/src/base/types/abstract/MetabolicModel.jl deleted file mode 100644 index 8b1fc4b77..000000000 --- a/src/base/types/abstract/MetabolicModel.jl +++ /dev/null @@ -1,66 +0,0 @@ - -""" - abstract type MetabolicModel end - -A helper supertype of everything usable as a linear-like model for COBREXA -functions. - -If you want your model type to work with COBREXA, add the `MetabolicModel` as -its supertype, and implement the accessor functions. Accessors -[`reactions`](@ref), [`metabolites`](@ref), [`stoichiometry`](@ref), -[`bounds`](@ref) and [`objective`](@ref) must be implemented; others are not -mandatory and default to safe "empty" values. -""" -abstract type MetabolicModel end - -""" - abstract type ModelWrapper <: MetabolicModel end - -A helper supertype of all "wrapper" types that contain precisely one other -[`MetabolicModel`](@ref). -""" -abstract type ModelWrapper <: MetabolicModel end - -const SparseMat = SparseMatrixCSC{Float64,Int} -const SparseVec = SparseVector{Float64,Int} -const MatType = AbstractMatrix{Float64} -const VecType = AbstractVector{Float64} -const StringVecType = AbstractVector{String} - -""" - GeneAssociation = Vector{Vector{String}} - -An association to genes, represented as a logical formula in a positive -disjunctive normal form (DNF). (The 2nd-level vectors of strings are connected -by "and" to form conjunctions, and the 1st-level vectors of these conjunctions -are connected by "or" to form the DNF.) -""" -const GeneAssociation = Vector{Vector{String}} - -""" - MetaboliteFormula = Dict{String,Int} - -Dictionary of atoms and their abundances in a molecule. -""" -const MetaboliteFormula = Dict{String,Int} - -""" - Annotations = Dict{String,Vector{String}} - -Dictionary used to store (possible multiple) standardized annotations of -something, such as a [`Metabolite`](@ref) and a [`Reaction`](@ref). - -# Example -``` -Annotations("PubChem" => ["CID12345", "CID54321"]) -``` -""" -const Annotations = Dict{String,Vector{String}} - -""" - Notes = Dict{String,Vector{String}} - -Free-form notes about something (e.g. a [`Gene`](@ref)), categorized by -"topic". -""" -const Notes = Dict{String,Vector{String}} diff --git a/src/base/types/wrappers/GeckoModel.jl b/src/base/types/wrappers/GeckoModel.jl deleted file mode 100644 index dace278d7..000000000 --- a/src/base/types/wrappers/GeckoModel.jl +++ /dev/null @@ -1,261 +0,0 @@ -""" -$(TYPEDEF) - -A helper type for describing the contents of [`GeckoModel`](@ref)s. - -# Fields -$(TYPEDFIELDS) -""" -struct _gecko_reaction_column - reaction_idx::Int - isozyme_idx::Int - direction::Int - reaction_coupling_row::Int - lb::Float64 - ub::Float64 - gene_product_coupling::Vector{Tuple{Int,Float64}} -end - -""" -$(TYPEDEF) - -A helper struct that contains the gene product capacity terms organized by -the grouping type, e.g. metabolic or membrane groups etc. - -# Fields -$(TYPEDFIELDS) -""" -struct _gecko_capacity - group_id::String - gene_product_idxs::Vector{Int} - gene_product_molar_masses::Vector{Float64} - group_upper_bound::Float64 -end - -""" -$(TYPEDEF) - -A model with complex enzyme concentration and capacity bounds, as described in -*Sánchez, Benjamín J., et al. "Improving the phenotype predictions of a yeast -genome-scale metabolic model by incorporating enzymatic constraints." Molecular -systems biology 13.8 (2017): 935.* - -Use [`make_gecko_model`](@ref) or [`with_gecko`](@ref) to construct this kind -of model. - -The model wraps another "internal" model, and adds following modifications: -- enzymatic reactions with known enzyme information are split into multiple - forward and reverse variants for each isozyme, -- reaction coupling is added to ensure the groups of isozyme reactions obey the - global reaction flux bounds from the original model, -- gene concentrations specified by each reaction and its gene product stoichiometry, - can constrained by the user to reflect measurements, such as - from mass spectrometry, -- additional coupling is added to simulate total masses of different proteins - grouped by type (e.g., membrane-bound and free-floating proteins), which can - be again constrained by the user (this is slightly generalized from original - GECKO algorithm, which only considers a single group of indiscernible - proteins). - -The structure contains fields `columns` that describe the contents of the -stoichiometry matrix columns, `coupling_row_reaction`, -`coupling_row_gene_product` and `coupling_row_mass_group` that describe -correspondence of the coupling rows to original model and determine the -coupling bounds (note: the coupling for gene product is actually added to -stoichiometry, not in [`coupling`](@ref)), and `inner`, which is the original -wrapped model. The `objective` of the model includes also the extra columns for -individual genes, as held by `coupling_row_gene_product`. - -Implementation exposes the split reactions (available as `reactions(model)`), -but retains the original "simple" reactions accessible by [`fluxes`](@ref). -The related constraints are implemented using [`coupling`](@ref) and -[`coupling_bounds`](@ref). - -# Fields -$(TYPEDFIELDS) -""" -struct GeckoModel <: ModelWrapper - objective::SparseVec - columns::Vector{_gecko_reaction_column} - coupling_row_reaction::Vector{Int} - coupling_row_gene_product::Vector{Tuple{Int,Tuple{Float64,Float64}}} - coupling_row_mass_group::Vector{_gecko_capacity} - - inner::MetabolicModel -end - -unwrap_model(model::GeckoModel) = model.inner - -""" -$(TYPEDSIGNATURES) - -Return a stoichiometry of the [`GeckoModel`](@ref). The enzymatic reactions are -split into unidirectional forward and reverse ones, each of which may have -multiple variants per isozyme. -""" -function stoichiometry(model::GeckoModel) - irrevS = stoichiometry(model.inner) * COBREXA._gecko_reaction_column_reactions(model) - enzS = COBREXA._gecko_gene_product_coupling(model) - [ - irrevS spzeros(size(irrevS, 1), size(enzS, 1)) - -enzS I(size(enzS, 1)) - ] -end - -""" -$(TYPEDSIGNATURES) - -Return the objective of the [`GeckoModel`](@ref). Note, the objective is with -respect to the internal variables, i.e. [`reactions(model)`](@ref), which are -the unidirectional reactions and the genes involved in enzymatic reactions that -have kinetic data. -""" -objective(model::GeckoModel) = model.objective - -""" -$(TYPEDSIGNATURES) - -Returns the internal reactions in a [`GeckoModel`](@ref) (these may be split -to forward- and reverse-only parts with different isozyme indexes; reactions -IDs are mangled accordingly with suffixes). -""" -function reactions(model::GeckoModel) - inner_reactions = reactions(model.inner) - mangled_reactions = [ - _gecko_reaction_name( - inner_reactions[col.reaction_idx], - col.direction, - col.isozyme_idx, - ) for col in model.columns - ] - [mangled_reactions; genes(model)] -end - -""" -$(TYPEDSIGNATURES) - -Returns the number of all irreversible reactions in `model` as well as the -number of gene products that take part in enzymatic reactions. -""" -n_reactions(model::GeckoModel) = length(model.columns) + n_genes(model) - -""" -$(TYPEDSIGNATURES) - -Return variable bounds for [`GeckoModel`](@ref). -""" -function bounds(model::GeckoModel) - lbs = [ - [col.lb for col in model.columns] - [lb for (_, (lb, _)) in model.coupling_row_gene_product] - ] - ubs = [ - [col.ub for col in model.columns] - [ub for (_, (_, ub)) in model.coupling_row_gene_product] - ] - (lbs, ubs) -end - -""" -$(TYPEDSIGNATURES) - -Get the mapping of the reaction rates in [`GeckoModel`](@ref) to the original -fluxes in the wrapped model. -""" -function reaction_flux(model::GeckoModel) - rxnmat = _gecko_reaction_column_reactions(model)' * reaction_flux(model.inner) - [ - rxnmat - spzeros(n_genes(model), size(rxnmat, 2)) - ] -end - -""" -$(TYPEDSIGNATURES) - -Return the coupling of [`GeckoModel`](@ref). That combines the coupling of the -wrapped model, coupling for split (arm) reactions, and the coupling for the total -enzyme capacity. -""" -function coupling(model::GeckoModel) - innerC = coupling(model.inner) * _gecko_reaction_column_reactions(model) - rxnC = _gecko_reaction_coupling(model) - enzcap = _gecko_mass_group_coupling(model) - [ - innerC spzeros(size(innerC, 1), n_genes(model)) - rxnC spzeros(size(rxnC, 1), n_genes(model)) - spzeros(length(model.coupling_row_mass_group), length(model.columns)) enzcap - ] -end - -""" -$(TYPEDSIGNATURES) - -Count the coupling constraints in [`GeckoModel`](@ref) (refer to -[`coupling`](@ref) for details). -""" -n_coupling_constraints(model::GeckoModel) = - n_coupling_constraints(model.inner) + - length(model.coupling_row_reaction) + - length(model.coupling_row_mass_group) - -""" -$(TYPEDSIGNATURES) - -The coupling bounds for [`GeckoModel`](@ref) (refer to [`coupling`](@ref) for -details). -""" -function coupling_bounds(model::GeckoModel) - (iclb, icub) = coupling_bounds(model.inner) - (ilb, iub) = bounds(model.inner) - return ( - vcat( - iclb, - ilb[model.coupling_row_reaction], - [0.0 for _ in model.coupling_row_mass_group], - ), - vcat( - icub, - iub[model.coupling_row_reaction], - [grp.group_upper_bound for grp in model.coupling_row_mass_group], - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Return the balance of the reactions in the inner model, concatenated with a vector of -zeros representing the enzyme balance of a [`GeckoModel`](@ref). -""" -balance(model::GeckoModel) = - [balance(model.inner); spzeros(length(model.coupling_row_gene_product))] - -""" -$(TYPEDSIGNATURES) - -Return the number of genes that have enzymatic constraints associated with them. -""" -n_genes(model::GeckoModel) = length(model.coupling_row_gene_product) - -""" -$(TYPEDSIGNATURES) - -Return the gene ids of genes that have enzymatic constraints associated with them. -""" -genes(model::GeckoModel) = - genes(model.inner)[[idx for (idx, _) in model.coupling_row_gene_product]] - -""" -$(TYPEDSIGNATURES) - -Return the ids of all metabolites, both real and pseudo, for a [`GeckoModel`](@ref). -""" -metabolites(model::GeckoModel) = [metabolites(model.inner); genes(model) .* "#gecko"] - -""" -$(TYPEDSIGNATURES) - -Return the number of metabolites, both real and pseudo, for a [`GeckoModel`](@ref). -""" -n_metabolites(model::GeckoModel) = n_metabolites(model.inner) + n_genes(model) diff --git a/src/base/types/wrappers/SMomentModel.jl b/src/base/types/wrappers/SMomentModel.jl deleted file mode 100644 index 98f8041f7..000000000 --- a/src/base/types/wrappers/SMomentModel.jl +++ /dev/null @@ -1,150 +0,0 @@ - -""" -$(TYPEDEF) - -A helper type that describes the contents of [`SMomentModel`](@ref)s. - -# Fields -$(TYPEDFIELDS) -""" -struct _smoment_column - reaction_idx::Int # number of the corresponding reaction in the inner model - direction::Int # 0 if "as is" and unique, -1 if reverse-only part, 1 if forward-only part - lb::Float64 # must be 0 if the reaction is unidirectional (if direction!=0) - ub::Float64 - capacity_required::Float64 # must be 0 for bidirectional reactions (if direction==0) -end - -""" -$(TYPEDEF) - -An enzyme-capacity-constrained model using sMOMENT algorithm, as described by -*Bekiaris, Pavlos Stephanos, and Steffen Klamt, "Automatic construction of -metabolic models with enzyme constraints" BMC bioinformatics, 2020*. - -Use [`make_smoment_model`](@ref) or [`with_smoment`](@ref) to construct the -models. - -The model is constructed as follows: -- stoichiometry of the original model is retained as much as possible, but - enzymatic reations are split into forward and reverse parts (marked by a - suffix like `...#forward` and `...#reverse`), -- coupling is added to simulate a virtual metabolite "enzyme capacity", which - is consumed by all enzymatic reactions at a rate given by enzyme mass divided - by the corresponding kcat, -- the total consumption of the enzyme capacity is constrained to a fixed - maximum. - -The `SMomentModel` structure contains a worked-out representation of the -optimization problem atop a wrapped [`MetabolicModel`](@ref), in particular the -separation of certain reactions into unidirectional forward and reverse parts, -an "enzyme capacity" required for each reaction, and the value of the maximum -capacity constraint. Original coupling in the inner model is retained. - -In the structure, the field `columns` describes the correspondence of stoichiometry -columns to the stoichiometry and data of the internal wrapped model, and -`total_enzyme_capacity` is the total bound on the enzyme capacity consumption -as specified in sMOMENT algorithm. - -This implementation allows easy access to fluxes from the split reactions -(available in `reactions(model)`), while the original "simple" reactions from -the wrapped model are retained as [`fluxes`](@ref). All additional constraints -are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). - -# Fields -$(TYPEDFIELDS) -""" -struct SMomentModel <: ModelWrapper - columns::Vector{_smoment_column} - total_enzyme_capacity::Float64 - - inner::MetabolicModel -end - -unwrap_model(model::SMomentModel) = model.inner - -""" -$(TYPEDSIGNATURES) - -Return a stoichiometry of the [`SMomentModel`](@ref). The enzymatic reactions -are split into unidirectional forward and reverse ones. -""" -stoichiometry(model::SMomentModel) = - stoichiometry(model.inner) * _smoment_column_reactions(model) - -""" -$(TYPEDSIGNATURES) - -Reconstruct an objective of the [`SMomentModel`](@ref). -""" -objective(model::SMomentModel) = _smoment_column_reactions(model)' * objective(model.inner) - -""" -$(TYPEDSIGNATURES) - -Returns the internal reactions in a [`SMomentModel`](@ref) (these may be split -to forward- and reverse-only parts; reactions IDs are mangled accordingly with -suffixes). -""" -reactions(model::SMomentModel) = - let inner_reactions = reactions(model.inner) - [ - _smoment_reaction_name(inner_reactions[col.reaction_idx], col.direction) for - col in model.columns - ] - end - -""" -$(TYPEDSIGNATURES) - -The number of reactions (including split ones) in [`SMomentModel`](@ref). -""" -n_reactions(model::SMomentModel) = length(model.columns) - -""" -$(TYPEDSIGNATURES) - -Return the variable bounds for [`SMomentModel`](@ref). -""" -bounds(model::SMomentModel) = - ([col.lb for col in model.columns], [col.ub for col in model.columns]) - -""" -$(TYPEDSIGNATURES) - -Get the mapping of the reaction rates in [`SMomentModel`](@ref) to the original -fluxes in the wrapped model. -""" -reaction_flux(model::SMomentModel) = - _smoment_column_reactions(model)' * reaction_flux(model.inner) - -""" -$(TYPEDSIGNATURES) - -Return the coupling of [`SMomentModel`](@ref). That combines the coupling of -the wrapped model, coupling for split reactions, and the coupling for the total -enzyme capacity. -""" -coupling(model::SMomentModel) = vcat( - coupling(model.inner) * _smoment_column_reactions(model), - [col.capacity_required for col in model.columns]', -) - -""" -$(TYPEDSIGNATURES) - -Count the coupling constraints in [`SMomentModel`](@ref) (refer to -[`coupling`](@ref) for details). -""" -n_coupling_constraints(model::SMomentModel) = n_coupling_constraints(model.inner) + 1 - -""" -$(TYPEDSIGNATURES) - -The coupling bounds for [`SMomentModel`](@ref) (refer to [`coupling`](@ref) for -details). -""" -coupling_bounds(model::SMomentModel) = - let (iclb, icub) = coupling_bounds(model.inner) - (vcat(iclb, [0.0]), vcat(icub, [model.total_enzyme_capacity])) - end diff --git a/src/base/utils/Annotation.jl b/src/base/utils/Annotation.jl deleted file mode 100644 index 9832d0d4c..000000000 --- a/src/base/utils/Annotation.jl +++ /dev/null @@ -1,61 +0,0 @@ - -_annotations(m::Metabolite) = m.annotations -_annotations(r::Reaction) = r.annotations -_annotations(g::Gene) = g.annotations - -""" -$(TYPEDSIGNATURES) - -Extract annotations from a dictionary of items `xs` and build an index that -maps annotation "kinds" (e.g. `"PubChem"`) to the mapping from the annotations -(e.g. `"COMPOUND_12345"`) to item IDs that carry the annotations. - -Function `annotations` is used to access the `Annotations` object in the -dictionary values. - -This is extremely useful for finding items by annotation data. -""" -function annotation_index( - xs::AbstractDict{String}; - annotations = _annotations, -)::Dict{String,Dict{String,Set{String}}} - res = Dict{String,Dict{String,Set{String}}}() - for (n, ax) in xs - a = annotations(ax) - for (k, anns) in a - if !haskey(res, k) - res[k] = Dict{String,Set{String}}() - end - for v in anns - if !haskey(res[k], v) - res[k][v] = Set([n]) - else - push!(res[k][v], n) - end - end - end - end - res -end - -""" -$(TYPEDSIGNATURES) - -Find items (genes, metabolites, ...) from the annotation index that are -identified non-uniquely by at least one of their annotations. - -This often indicates that the items are duplicate or miscategorized. -""" -function ambiguously_identified_items( - index::Dict{String,Dict{String,Set{String}}}, -)::Set{String} - res = Set{String}() - for (_, idents) in index - for (_, items) in idents - if length(items) > 1 - push!(res, items...) - end - end - end - res -end diff --git a/src/base/utils/CoreModel.jl b/src/base/utils/CoreModel.jl deleted file mode 100644 index 94476477b..000000000 --- a/src/base/utils/CoreModel.jl +++ /dev/null @@ -1,19 +0,0 @@ -Base.isequal(model1::CoreModel, model2::CoreModel) = - isequal(model1.S, model2.S) && - isequal(model1.b, model2.b) && - isequal(model1.c, model2.c) && - isequal(model1.xl, model2.xl) && - isequal(model1.xu, model2.xu) && - isequal(model1.rxns, model2.rxns) && - isequal(model1.mets, model2.mets) - -Base.copy(model::CoreModel) = - CoreModel(model.S, model.b, model.c, model.xl, model.xu, model.rxns, model.mets) - -Base.isequal(model1::CoreModelCoupled, model2::CoreModelCoupled) = - isequal(model1.lm, model2.lm) && - isequal(model1.C, model2.C) && - isequal(model1.cl, model2.cl) && - isequal(model1.cu, model2.cu) - -Base.copy(model::CoreModelCoupled) = CoreModelCoupled(model.lm, model.C, model.cl, model.cu) diff --git a/src/base/utils/HDF5Model.jl b/src/base/utils/HDF5Model.jl deleted file mode 100644 index 97934ef22..000000000 --- a/src/base/utils/HDF5Model.jl +++ /dev/null @@ -1,32 +0,0 @@ - -_h5_mmap_nonempty(x) = length(x) > 0 ? HDF5.readmmap(x) : HDF5.read(x) - -function _h5_write_sparse(g::HDF5.Group, v::SparseVector) - write(g, "n", v.n) - write(g, "nzind", v.nzind) - write(g, "nzval", v.nzval) -end - -function _h5_read_sparse(::Type{X}, g::HDF5.Group) where {X<:SparseVector} - n = read(g["n"]) - nzind = _h5_mmap_nonempty(g["nzind"]) - nzval = _h5_mmap_nonempty(g["nzval"]) - SparseVector{eltype(nzval),eltype(nzind)}(n, nzind, nzval) -end - -function _h5_write_sparse(g::HDF5.Group, m::SparseMatrixCSC) - write(g, "m", m.m) - write(g, "n", m.n) - write(g, "colptr", m.colptr) - write(g, "rowval", m.rowval) - write(g, "nzval", m.nzval) -end - -function _h5_read_sparse(::Type{X}, g::HDF5.Group) where {X<:SparseMatrixCSC} - m = read(g["m"]) - n = read(g["n"]) - colptr = _h5_mmap_nonempty(g["colptr"]) - rowval = _h5_mmap_nonempty(g["rowval"]) - nzval = _h5_mmap_nonempty(g["nzval"]) - SparseMatrixCSC{eltype(nzval),eltype(colptr)}(m, n, colptr, rowval, nzval) -end diff --git a/src/base/utils/Reaction.jl b/src/base/utils/Reaction.jl deleted file mode 100644 index 011b732b9..000000000 --- a/src/base/utils/Reaction.jl +++ /dev/null @@ -1,138 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Check if `rxn` already exists in `rxns` but has another `id`. -If `only_metabolites` is `true` then only the metabolite `id`s are checked. -Otherwise, compares metabolite `id`s and the absolute value of their stoichiometric coefficients to those of `rxn`. -If `rxn` has the same reaction equation as another reaction in `rxns`, the return the `id`. -Otherwise return `nothing`. - -See also: [`reaction_mass_balanced`](@ref) -""" -function check_duplicate_reaction( - crxn::Reaction, - rxns::OrderedDict{String,Reaction}; - only_metabolites = true, -) - for (k, rxn) in rxns - if rxn.id != crxn.id # skip if same ID - if only_metabolites # only check if metabolites are the same - if issetequal(keys(crxn.metabolites), keys(rxn.metabolites)) - return k - end - else # also check the stoichiometric coefficients - reaction_checker = true - for (kk, vv) in rxn.metabolites # get reaction stoich - if abs(get(crxn.metabolites, kk, 0)) != abs(vv) # if at least one stoich doesn't match - reaction_checker = false - break - end - end - if reaction_checker && - issetequal(keys(crxn.metabolites), keys(rxn.metabolites)) - return k - end - end - end - end - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return true if the reaction denoted by `rxn_dict` is a boundary reaction, otherwise return false. -Checks if on boundary by inspecting the number of metabolites in `rxn_dict`. -Boundary reactions have only one metabolite, e.g. an exchange reaction, or a sink/demand reaction. -""" -function is_boundary(rxn_dict::Dict{String,Float64})::Bool - length(keys(rxn_dict)) == 1 -end - -is_boundary(model::MetabolicModel, rxn_id::String) = - is_boundary(reaction_stoichiometry(model, rxn_id)) - -is_boundary(rxn::Reaction) = is_boundary(rxn.metabolites) - -is_boundary(model::StandardModel, rxn::Reaction) = is_boundary(rxn) # for consistency with functions below - -""" -$(TYPEDSIGNATURES) - -Returns a dictionary mapping the stoichiometry of atoms through a single reaction. Uses the -metabolite information in `model` to determine the mass balance. Accepts a reaction -dictionary, a reaction string id or a `Reaction` as an argument for `rxn`. - -See also: [`reaction_mass_balanced`](@ref) -""" -function reaction_atom_balance(model::MetabolicModel, reaction_dict::Dict{String,Float64}) - atom_balances = Dict{String,Float64}() - for (met, stoich_rxn) in reaction_dict - adict = metabolite_formula(model, met) - isnothing(adict) && - throw(ErrorException("Metabolite $met does not have a formula assigned to it.")) - for (atom, stoich_molecule) in adict - atom_balances[atom] = - get(atom_balances, atom, 0.0) + stoich_rxn * stoich_molecule - end - end - return atom_balances -end - -function reaction_atom_balance(model::MetabolicModel, rxn_id::String) - reaction_atom_balance(model, reaction_stoichiometry(model, rxn_id)) -end - -reaction_atom_balance(model::StandardModel, rxn::Reaction) = - reaction_atom_balance(model, rxn.id) - -""" -$(TYPEDSIGNATURES) - -Checks if `rxn` is atom balanced. Returns a boolean for whether the reaction is balanced, -and the associated balance of atoms for convenience (useful if not balanced). Calls -`reaction_atom_balance` internally. - -See also: [`check_duplicate_reaction`](@ref), [`reaction_atom_balance`](@ref) -""" -reaction_mass_balanced(model::StandardModel, rxn_id::String) = - all(values(reaction_atom_balance(model, rxn_id)) .== 0) - -reaction_mass_balanced(model::StandardModel, rxn::Reaction) = - reaction_mass_balanced(model, rxn.id) - -reaction_mass_balanced(model::StandardModel, reaction_dict::Dict{String,Float64}) = - all(values(reaction_atom_balance(model, reaction_dict)) .== 0) - -""" -$(TYPEDSIGNATURES) - -Return the reaction equation as a string. The metabolite strings can be manipulated by -setting `format_id`. - -# Example -``` -julia> req = Dict("coa_c" => -1, "for_c" => 1, "accoa_c" => 1, "pyr_c" => -1) -julia> stoichiometry_string(req) -"coa_c + pyr_c = for_c + accoa_c" - -julia> stoichiometry_string(req; format_id = x -> x[1:end-2]) -"coa + pyr = for + accoa" -``` -""" -function stoichiometry_string(req; format_id = x -> x) - count_prefix(n) = abs(n) == 1 ? "" : string(abs(n), " ") - substrates = - join((string(count_prefix(n), format_id(met)) for (met, n) in req if n < 0), " + ") - products = - join((string(count_prefix(n), format_id(met)) for (met, n) in req if n >= 0), " + ") - return substrates * " = " * products -end - -""" -$(TYPEDSIGNATURES) - -Alternative of [`stoichiometry_string`](@ref) take takes a `Reaction` as an argument. -""" -stoichiometry_string(rxn::Reaction; kwargs...) = - stoichiometry_string(rxn.metabolites; kwargs...) diff --git a/src/base/utils/Serialized.jl b/src/base/utils/Serialized.jl deleted file mode 100644 index 577d101c4..000000000 --- a/src/base/utils/Serialized.jl +++ /dev/null @@ -1,33 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Serialize the `model` to file `filename`, returning a [`Serialized`](@ref) -model that can be loaded back transparently by [`precache!`](@ref). The result -does _not_ contain the actual model data that are deferred to the disk; it may -thus be used to save memory, or send the model efficiently to remote workers -within distributed shared-storage environments. - -The benefit of using this over "raw" `Serialization.serialize` is that the -resulting `Serialized` model will reload itself automatically with -[`precache!`](@ref) at first usage, which needs to be done manually when using -the `Serialization` package directly. -""" -function serialize_model( - model::MM, - filename::String, -)::Serialized{MM} where {MM<:MetabolicModel} - open(f -> serialize(f, model), filename, "w") - Serialized{MM}(filename) -end - -""" -$(TYPEDSIGNATURES) - -Specialization of [`serialize_model`](@ref) that prevents nested serialization -of already-serialized models. -""" -function serialize_model(model::Serialized, filename::String) - precache!(model) - serialize_model(model.m, filename) -end diff --git a/src/base/utils/StandardModel.jl b/src/base/utils/StandardModel.jl deleted file mode 100644 index 3da400441..000000000 --- a/src/base/utils/StandardModel.jl +++ /dev/null @@ -1,50 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`StandardModel`](@ref) -""" -Base.copy(m::StandardModel) = StandardModel( - m.id, - reactions = m.reactions, - metabolites = m.metabolites, - genes = m.genes, -) - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`Reaction`](@ref) -""" -Base.copy(r::Reaction) = Reaction( - r.id; - metabolites = r.metabolites, - lb = r.lb, - ub = r.ub, - grr = r.grr, - subsystem = r.subsystem, - notes = r.notes, - annotations = r.annotations, - objective_coefficient = r.objective_coefficient, -) - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`Metabolite`](@ref) -""" -Base.copy(m::Metabolite) = Metabolite( - m.id; - formula = m.formula, - charge = m.charge, - compartment = m.compartment, - notes = m.notes, - annotations = m.annotations, -) - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`Gene`](@ref) -""" -Base.copy(g::Gene) = Gene(g.id; notes = g.notes, annotations = g.annotations) diff --git a/src/base/utils/bounds.jl b/src/base/utils/bounds.jl deleted file mode 100644 index 5ca724795..000000000 --- a/src/base/utils/bounds.jl +++ /dev/null @@ -1,23 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A bounds-generating function for [`flux_variability_analysis`](@ref) that -limits the objective value to be at least `gamma*Z₀`, as usual in COBRA -packages. Use as the `bounds` argument: -``` -flux_variability_analysis(model, some_optimizer; bounds = gamma_bounds(0.9)) -``` -""" -gamma_bounds(gamma) = z -> (gamma * z, Inf) - -""" -$(TYPEDSIGNATURES) - -A bounds-generating function for [`flux_variability_analysis`](@ref) that -limits the objective value to a small multiple of Z₀. Use as `bounds` argument, -similarly to [`gamma_bounds`](@ref). -""" -objective_bounds(tolerance) = z -> begin - vs = (z * tolerance, z / tolerance) - (minimum(vs), maximum(vs)) -end diff --git a/src/base/utils/chemical_formulas.jl b/src/base/utils/chemical_formulas.jl deleted file mode 100644 index d4c081619..000000000 --- a/src/base/utils/chemical_formulas.jl +++ /dev/null @@ -1,26 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Parse a formula in format `C2H6O` into a [`MetaboliteFormula`](@ref), which is -basically a dictionary of atom counts in the molecule. -""" -function _parse_formula(f::String)::MetaboliteFormula - res = Dict{String,Int}() - pattern = @r_str "([A-Z][a-z]*)([1-9][0-9]*)?" - - for m in eachmatch(pattern, f) - res[m.captures[1]] = isnothing(m.captures[2]) ? 1 : parse(Int, m.captures[2]) - end - - return res -end - -""" -$(TYPEDSIGNATURES) - -Format [`MetaboliteFormula`](@ref) to `String`. -""" -function _unparse_formula(f::MetaboliteFormula)::String - return join(["$elem$n" for (elem, n) in f]) -end diff --git a/src/base/utils/enzymes.jl b/src/base/utils/enzymes.jl deleted file mode 100644 index cf86b71b7..000000000 --- a/src/base/utils/enzymes.jl +++ /dev/null @@ -1,56 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Return a dictionary mapping protein molar concentrations to their ids. The -argument `opt_model` is a solved optimization problem, typically returned by -[`flux_balance_analysis`](@ref). See [`flux_dict`](@ref) for the corresponding -function that returns a dictionary of solved fluxes. -""" -gene_product_dict(model::GeckoModel, opt_model) = - is_solved(opt_model) ? - Dict(genes(model) .=> value.(opt_model[:x])[(length(model.columns)+1):end]) : nothing - -""" -$(TYPEDSIGNATURES) - -A pipe-able variant of [`gene_product_dict`](@ref). -""" -gene_product_dict(model::GeckoModel) = x -> gene_product_dict(model, x) - -""" -$(TYPEDSIGNATURES) - -Extract the mass utilization in mass groups from a solved [`GeckoModel`](@ref). -""" -gene_product_mass_group_dict(model::GeckoModel, opt_model) = - is_solved(opt_model) ? - Dict( - grp.group_id => dot( - value.(opt_model[:x])[length(model.columns).+grp.gene_product_idxs], - grp.gene_product_molar_masses, - ) for grp in model.coupling_row_mass_group - ) : nothing - -""" -$(TYPEDSIGNATURES) - -A pipe-able variant of [`gene_product_mass_group_dict`](@ref). -""" -gene_product_mass_group_dict(model::GeckoModel) = - x -> gene_product_mass_group_dict(model, x) - -""" -$(TYPEDSIGNATURES) - -Extract the total mass utilization in a solved [`SMomentModel`](@ref). -""" -gene_product_mass(model::SMomentModel, opt_model) = - is_solved(opt_model) ? - sum((col.capacity_required for col in model.columns) .* value.(opt_model[:x])) : nothing - -""" -$(TYPEDSIGNATURES) - -A pipe-able variant of [`gene_product_mass`](@ref). -""" -gene_product_mass(model::SMomentModel) = x -> gene_product_mass(model, x) diff --git a/src/base/utils/fluxes.jl b/src/base/utils/fluxes.jl deleted file mode 100644 index cf23a3f9d..000000000 --- a/src/base/utils/fluxes.jl +++ /dev/null @@ -1,69 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Return two dictionaries of metabolite `id`s mapped to reactions that consume or -produce them, given the flux distribution supplied in `flux_dict`. -""" -function metabolite_fluxes(model::MetabolicModel, flux_dict::Dict{String,Float64}) - S = stoichiometry(model) - rids = reactions(model) - mids = metabolites(model) - - producing = Dict{String,Dict{String,Float64}}() - consuming = Dict{String,Dict{String,Float64}}() - for (row, mid) in enumerate(mids) - for (col, rid) in enumerate(rids) - mf = flux_dict[rid] * S[row, col] - if mf < 0 # consuming rxn - if haskey(consuming, mid) - consuming[mid][rid] = mf - else - consuming[mid] = Dict(rid => mf) - end - elseif mf >= 0 # producing rxn - if haskey(producing, mid) - producing[mid][rid] = mf - else - producing[mid] = Dict(rid => mf) - end - end - end - end - return consuming, producing -end - -""" -$(TYPEDSIGNATURES) - -Return a dictionary mapping the flux of atoms across a flux solution given by -`reaction_fluxes` using the reactions in `model` to determine the appropriate stoichiometry. - -Note, this function ignores metabolites with no formula assigned to them, no error message -will be displayed. - -Note, if a model is mass balanced there should be not net flux of any atom. By removing -reactions from the flux_solution you can investigate how that impacts the mass balances. - -# Example -``` -# Find flux of Carbon through all metabolic reactions except the biomass reaction -delete!(fluxes, "BIOMASS_Ecoli_core_w_GAM") -atom_fluxes(model, fluxes)["C"] -``` -""" -function atom_fluxes(model::MetabolicModel, reaction_fluxes::Dict{String,Float64}) - rids = reactions(model) - atom_flux = Dict{String,Float64}() - for (ridx, rid) in enumerate(rids) - haskey(reaction_fluxes, rid) || continue - rflux = reaction_fluxes[rid] - for (mid, mstoi) in reaction_stoichiometry(model, rid) - atoms = metabolite_formula(model, mid) - isnothing(atoms) && continue # missing formulas are ignored - for (atom, abundance) in atoms - atom_flux[atom] = get(atom_flux, atom, 0.0) + rflux * mstoi * abundance - end - end - end - return atom_flux -end diff --git a/src/base/utils/gecko.jl b/src/base/utils/gecko.jl deleted file mode 100644 index bbaf4c822..000000000 --- a/src/base/utils/gecko.jl +++ /dev/null @@ -1,93 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Internal helper for systematically naming reactions in [`GeckoModel`](@ref). -""" -_gecko_reaction_name(original_name::String, direction::Int, isozyme_idx::Int) = - direction == 0 ? original_name : - direction > 0 ? "$original_name#forward#$isozyme_idx" : - "$original_name#reverse#$isozyme_idx" - -""" -$(TYPEDSIGNATURES) - -Retrieve a utility mapping between reactions and split reactions; rows -correspond to "original" reactions, columns correspond to "split" reactions. -""" -_gecko_reaction_column_reactions(model::GeckoModel) = - _gecko_reaction_column_reactions(model.columns, model.inner) - -""" -$(TYPEDSIGNATURES) - -Helper method that doesn't require the whole [`GeckoModel`](@ref). -""" -_gecko_reaction_column_reactions(columns, inner) = sparse( - [col.reaction_idx for col in columns], - 1:length(columns), - [col.direction >= 0 ? 1 : -1 for col in columns], - n_reactions(inner), - length(columns), -) - -""" -$(TYPEDSIGNATURES) - -Compute the part of the coupling for [`GeckoModel`](@ref) that limits the -"arm" reactions (which group the individual split unidirectional reactions). -""" -_gecko_reaction_coupling(model::GeckoModel) = - let tmp = [ - (col.reaction_coupling_row, i, col.direction) for - (i, col) in enumerate(model.columns) if col.reaction_coupling_row != 0 - ] - sparse( - [row for (row, _, _) in tmp], - [col for (_, col, _) in tmp], - [val for (_, _, val) in tmp], - length(model.coupling_row_reaction), - length(model.columns), - ) - end - -""" -$(TYPEDSIGNATURES) - -Compute the part of the coupling for GeckoModel that limits the amount of each -kind of protein available. -""" -_gecko_gene_product_coupling(model::GeckoModel) = - let - tmp = [ - (row, i, val) for (i, col) in enumerate(model.columns) for - (row, val) in col.gene_product_coupling - ] - sparse( - [row for (row, _, _) in tmp], - [col for (_, col, _) in tmp], - [val for (_, _, val) in tmp], - length(model.coupling_row_gene_product), - length(model.columns), - ) - end - -""" -$(TYPEDSIGNATURES) - -Compute the part of the coupling for [`GeckoModel`](@ref) that limits the total -mass of each group of gene products. -""" -function _gecko_mass_group_coupling(model::GeckoModel) - tmp = [ # mm = molar mass, mg = mass group, i = row idx, j = col idx - (i, j, mm) for (i, mg) in enumerate(model.coupling_row_mass_group) for - (j, mm) in zip(mg.gene_product_idxs, mg.gene_product_molar_masses) - ] - sparse( - [i for (i, _, _) in tmp], - [j for (_, j, _) in tmp], - [mm for (_, _, mm) in tmp], - length(model.coupling_row_mass_group), - n_genes(model), - ) -end diff --git a/src/base/utils/gene_associations.jl b/src/base/utils/gene_associations.jl deleted file mode 100644 index 109317900..000000000 --- a/src/base/utils/gene_associations.jl +++ /dev/null @@ -1,137 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Parse `SBML.GeneProductAssociation` structure to the simpler GeneAssociation. -The input must be (implicitly) in a positive DNF. -""" -function _parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociation - parse_ref(x) = - typeof(x) == SBML.GPARef ? [x.gene_product] : - begin - @_models_log @warn "Could not parse a part of gene association, ignoring: $x" - String[] - end - parse_and(x) = - typeof(x) == SBML.GPAAnd ? vcat([parse_and(i) for i in x.terms]...) : parse_ref(x) - parse_or(x) = - typeof(x) == SBML.GPAOr ? vcat([parse_or(i) for i in x.terms]...) : [parse_and(x)] - return parse_or(gpa) -end - -""" -$(TYPEDSIGNATURES) - -Convert a GeneAssociation to the corresponding `SBML.jl` structure. -""" -function _unparse_grr( - ::Type{SBML.GeneProductAssociation}, - x::GeneAssociation, -)::SBML.GeneProductAssociation - SBML.GPAOr([SBML.GPAAnd([SBML.GPARef(j) for j in i]) for i in x]) -end - -""" -$(TYPEDSIGNATURES) - -Parse a DNF gene association rule in format `(YIL010W and YLR043C) or (YIL010W -and YGR209C)` to `GeneAssociation. Also accepts `OR`, `|`, `||`, `AND`, `&`, -and `&&`. - -# Example -``` -julia> _parse_grr("(YIL010W and YLR043C) or (YIL010W and YGR209C)") -2-element Array{Array{String,1},1}: - ["YIL010W", "YLR043C"] - ["YIL010W", "YGR209C"] -``` -""" -_parse_grr(s::String)::Maybe{GeneAssociation} = _maybemap(_parse_grr, _parse_grr_to_sbml(s)) - -""" -$(TYPEDSIGNATURES) - -Internal helper for parsing the string GRRs into SBML data structures. More -general than [`_parse_grr`](@ref). -""" -function _parse_grr_to_sbml(str::String)::Maybe{SBML.GeneProductAssociation} - s = str - toks = String[] - m = Nothing - while !isnothing( - begin - m = match(r"( +|[a-zA-Z0-9_-]+|[^ a-zA-Z0-9_()-]+|[(]|[)])(.*)", s) - end, - ) - tok = strip(m.captures[1]) - !isempty(tok) && push!(toks, tok) - s = m.captures[2] - end - - fail() = throw(DomainError(str, "Could not parse GRR")) - - # shunting yard - ops = Symbol[] - vals = SBML.GeneProductAssociation[] - fold(sym, op) = - while !isempty(ops) && last(ops) == sym - r = pop!(vals) - l = pop!(vals) - pop!(ops) - push!(vals, op([l, r])) - end - for tok in toks - if tok in ["and", "AND", "&", "&&"] - push!(ops, :and) - elseif tok in ["or", "OR", "|", "||"] - fold(:and, SBML.GPAAnd) - push!(ops, :or) - elseif tok == "(" - push!(ops, :paren) - elseif tok == ")" - fold(:and, SBML.GPAAnd) - fold(:or, SBML.GPAOr) - if isempty(ops) || last(ops) != :paren - fail() - else - pop!(ops) - end - else - push!(vals, SBML.GPARef(tok)) - end - end - - fold(:and, SBML.GPAAnd) - fold(:or, SBML.GPAOr) - - if !isempty(ops) || length(vals) > 1 - fail() - end - - if isempty(vals) - nothing - else - first(vals) - end -end - -""" -$(TYPEDSIGNATURES) - -Converts a nested string gene reaction array back into a gene reaction rule -string. - -# Example -``` -julia> _unparse_grr(String, [["YIL010W", "YLR043C"], ["YIL010W", "YGR209C"]]) -"(YIL010W and YLR043C) or (YIL010W and YGR209C)" -``` -""" -function _unparse_grr(::Type{String}, grr::GeneAssociation)::String - grr_strings = String[] - for gr in grr - push!(grr_strings, "(" * join([g for g in gr], " and ") * ")") - end - grr_string = join(grr_strings, " or ") - return grr_string -end diff --git a/src/base/utils/guesskey.jl b/src/base/utils/guesskey.jl deleted file mode 100644 index 183e82656..000000000 --- a/src/base/utils/guesskey.jl +++ /dev/null @@ -1,36 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Unfortunately, many model types that contain dictionares do not have -standardized field names, so we need to try a few possibilities and guess the -best one. The keys used to look for valid field names should be ideally -specified as constants in `src/base/constants.jl`. -""" -function _guesskey(avail, possibilities) - x = intersect(possibilities, avail) - - if isempty(x) - @debug "could not find any of keys: $possibilities" - return nothing - end - - if length(x) > 1 - @debug "Possible ambiguity between keys: $x" - end - return x[1] -end - -""" -$(TYPEDSIGNATURES) - -Return `fail` if key in `keys` is not in `collection`, otherwise -return `collection[key]`. Useful if may different keys need to be -tried due to non-standardized model formats. -""" -function gets(collection, fail, keys) - for key in keys - haskey(collection, key) && return collection[key] - end - return fail -end diff --git a/src/base/utils/looks_like.jl b/src/base/utils/looks_like.jl deleted file mode 100644 index 69f2d93d9..000000000 --- a/src/base/utils/looks_like.jl +++ /dev/null @@ -1,149 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A predicate that matches reaction identifiers that look like -exchange or biomass reactions, given the usual naming schemes in common model -repositories. Exchange reactions are identified based on matching prefixes in -the set `exchange_prefixes` and biomass reactions are identified by looking for -occurences of `biomass_strings` in the reaction id. - -Also see [`find_exchange_reactions`](@ref). - -# Note -While `looks_like_exchange_reaction` is useful for heuristically finding a -reaction, it is preferable to use standardized terms for finding reactions (e.g. -SBO terms). See [`is_exchange_reaction`](@ref) for a more systematic -alternative. - -# Example -``` -findall(looks_like_exchange_reaction, reactions(model)) # returns indices -filter(looks_like_exchange_reaction, reactions(model)) # returns Strings - -# to use the optional arguments you need to expand the function's arguments -# using an anonymous function -findall(x -> looks_like_exchange_reaction(x; exclude_biomass=true), reactions(model)) # returns indices -filter(x -> looks_like_exchange_reaction(x; exclude_biomass=true), reactions(model)) # returns Strings -``` -""" -function looks_like_exchange_reaction( - rxn_id::String; - exclude_biomass = false, - biomass_strings = _constants.biomass_strings, - exchange_prefixes = _constants.exchange_prefixes, -)::Bool - any(startswith(rxn_id, x) for x in exchange_prefixes) && - !(exclude_biomass && any(occursin(x, rxn_id) for x in biomass_strings)) -end - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding exchange reaction indexes in a model; arguments are -forwarded to [`looks_like_exchange_reaction`](@ref). -""" -find_exchange_reactions(m::MetabolicModel; kwargs...) = - findall(id -> looks_like_exchange_reaction(id; kwargs...), reactions(m)) - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding exchange reaction identifiers in a model; arguments are -forwarded to [`looks_like_exchange_reaction`](@ref). -""" -find_exchange_reaction_ids(m::MetabolicModel; kwargs...) = - filter(id -> looks_like_exchange_reaction(id, kwargs...), reactions(m)) - -""" -$(TYPEDSIGNATURES) - -A predicate that matches reaction identifiers that look like biomass reactions. -Biomass reactions are identified by looking for occurences of `biomass_strings` -in the reaction id. If `exclude_exchanges` is set, the strings that look like -exchanges (from [`looks_like_exchange_reaction`](@ref)) will not match. - -# Note -While `looks_like_biomass_reaction` is useful for heuristically finding a -reaction, it is preferable to use standardized terms for finding reactions (e.g. -SBO terms). See [`is_biomass_reaction`](@ref) for a more systematic -alternative. - -# Example -``` -filter(looks_like_biomass_reaction, reactions(model)) # returns strings -findall(looks_like_biomass_reaction, reactions(model)) # returns indices -``` -""" -function looks_like_biomass_reaction( - rxn_id::String; - exclude_exchanges = false, - exchange_prefixes = _constants.exchange_prefixes, - biomass_strings = _constants.biomass_strings, -)::Bool - any(occursin(x, rxn_id) for x in biomass_strings) && - !(exclude_exchanges && any(startswith(rxn_id, x) for x in exchange_prefixes)) -end - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding biomass reaction indexes in a model; arguments are -forwarded to [`looks_like_biomass_reaction`](@ref). -""" -find_biomass_reactions(m::MetabolicModel; kwargs...) = - findall(id -> looks_like_biomass_reaction(id; kwargs...), reactions(m)) - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding biomass reaction identifiers in a model; arguments are -forwarded to [`looks_like_biomass_reaction`](@ref). -""" -find_biomass_reaction_ids(m::MetabolicModel; kwargs...) = - filter(id -> looks_like_biomass_reaction(id; kwargs...), reactions(m)) - -""" -$(TYPEDSIGNATURES) - -A predicate that matches metabolite identifiers that look like they are extracellular -metabolites. Extracellular metabolites are identified by `extracellular_suffixes` at the end of the -metabolite id. - -# Example -``` -filter(looks_like_extracellular_metabolite, metabolites(model)) # returns strings -findall(looks_like_extracellular_metabolite, metabolites(model)) # returns indices -``` -""" -function looks_like_extracellular_metabolite( - met_id::String; - extracellular_suffixes = _constants.extracellular_suffixes, -)::Bool - any(endswith(met_id, x) for x in extracellular_suffixes) -end - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding extracellular metabolite indexes in a model; arguments are -forwarded to [`looks_like_extracellular_metabolite`](@ref). -""" -find_extracellular_metabolites(m::MetabolicModel; kwargs...) = - findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolites(m)) - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding extracellular metabolite identifiers in a model; arguments are -forwarded to [`looks_like_extracellular_metabolite`](@ref). -""" -find_extracellular_metabolite_ids(m::MetabolicModel; kwargs...) = - findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolites(m)) - -@_is_reaction_fn "exchange" Identifiers.EXCHANGE_REACTIONS -@_is_reaction_fn "transport" Identifiers.TRANSPORT_REACTIONS -@_is_reaction_fn "biomass" Identifiers.BIOMASS_REACTIONS -@_is_reaction_fn "atp_maintenance" Identifiers.ATP_MAINTENANCE_REACTIONS -@_is_reaction_fn "pseudo" Identifiers.PSEUDOREACTIONS -@_is_reaction_fn "metabolic" Identifiers.METABOLIC_REACTIONS -@_is_reaction_fn "spontaneous" Identifiers.SPONTANEOUS_REACTIONS diff --git a/src/base/utils/smoment.jl b/src/base/utils/smoment.jl deleted file mode 100644 index 4bbf4783c..000000000 --- a/src/base/utils/smoment.jl +++ /dev/null @@ -1,55 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Internal helper for systematically naming reactions in [`SMomentModel`](@ref). -""" -_smoment_reaction_name(original_name::String, direction::Int) = - direction == 0 ? original_name : - direction > 0 ? "$original_name#forward" : "$original_name#reverse" - -""" -$(TYPEDSIGNATURES) - -Retrieve a utility mapping between reactions and split reactions; rows -correspond to "original" reactions, columns correspond to "split" reactions. -""" -_smoment_column_reactions(model::SMomentModel) = sparse( - [col.reaction_idx for col in model.columns], - 1:length(model.columns), - [col.direction >= 0 ? 1 : -1 for col in model.columns], - n_reactions(model.inner), - length(model.columns), -) - -""" -$(TYPEDSIGNATURES) - -Compute a "score" for picking the most viable isozyme for -[`make_smoment_model`](@ref), based on maximum kcat divided by relative mass of -the isozyme. This is used because sMOMENT algorithm can not handle multiple -isozymes for one reaction. -""" -smoment_isozyme_speed(isozyme::Isozyme, gene_product_molar_mass) = - max(isozyme.kcat_forward, isozyme.kcat_reverse) / sum( - count * gene_product_molar_mass(gene) for - (gene, count) in isozyme.gene_product_count - ) - -""" -$(TYPEDSIGNATURES) - -A piping- and argmax-friendly overload of [`smoment_isozyme_speed`](@ref). - -# Example -``` -gene_mass_function = gid -> 1.234 - -best_isozyme_for_smoment = argmax( - smoment_isozyme_speed(gene_mass_function), - my_isozyme_vector, -) -``` -""" -smoment_isozyme_speed(gene_product_molar_mass::Function) = - isozyme -> smoment_isozyme_speed(isozyme, gene_product_molar_mass) diff --git a/src/builders/compare.jl b/src/builders/compare.jl new file mode 100644 index 000000000..168645693 --- /dev/null +++ b/src/builders/compare.jl @@ -0,0 +1,71 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +A constraint that makes sure that the difference from `a` to `b` is within the +`difference_bound`. For example, `difference_constraint(-1, 1, 2)` will always +be valid. Any type of `ConstraintTree.Bound` can be supplied. +""" +difference_constraint(a, b, difference_bound) = + C.Constraint(C.value(b) - C.value(a), difference_bound) + +export difference_constraint + +""" +$(TYPEDSIGNATURES) + +A constraint that makes sure that the values of `a` and `b` are the same. +""" +equal_value_constraint(a, b) = difference_constraint(a, b, 0) + +export equal_value_constraint + +""" +$(TYPEDSIGNATURES) + +A constriant tree that makes sure that all values in `tree` are the same as the +value of `a`. + +Names in the output `ConstraintTree` match the names in the `tree`. +""" +all_equal_constraints(a, tree::C.ConstraintTree) = + C.map(tree) do b + equal_value_constraint(a, b) + end + +export all_equal_constraints + +""" +$(TYPEDSIGNATURES) + +A constraint that makes sure that the value of `a` is greater than or equal to +the the value of `b`. +""" +greater_or_equal_constraint(a, b) = difference_constraint(a, b, C.Between(-Inf, 0)) + +export greater_or_equal_constraint + +""" +$(TYPEDSIGNATURES) + +A constraint that makes sure that the value of `a` is less than or equal to the +the value of `b`. +""" +less_or_equal_constraint(a, b) = difference_constraint(a, b, C.Between(0, Inf)) + +export less_or_equal_constraint diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl new file mode 100644 index 000000000..e0c8ed828 --- /dev/null +++ b/src/builders/enzymes.jl @@ -0,0 +1,115 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Create a `ConstraintTree` with variables for isozyme contributions to reaction +fluxes. The tree has 2 levels: the first contains all reaction flux IDs that +have isozymes, the second contains the isozyme IDs for each reaction flux. + +`fluxes` should be anything that can be iterated to give reaction flux IDs. + +`flux_isozymes` is a function that, for a given reaction flux ID, returns +anything iterable that contains the isozyme IDs for the given reaction flux. +Returning an empty iterable prevents allocating the subtree for the given flux. +""" +isozyme_amount_variables(fluxes, flux_isozymes) = sum( + ( + f^C.variables(keys = flux_isozymes(f), bounds = C.Between(0, Inf)) for + f in fluxes if !isempty(flux_isozymes(f)) + ), + init = C.ConstraintTree(), +) + +export isozyme_amount_variables + +""" +$(TYPEDSIGNATURES) + +A constraint tree that sums up partial contributions of reaction isozymes to +the fluxes of reactions. + +For practical purposes, both fluxes and isozymes are here considered to be +unidirectional, i.e., one would typically apply this twice to constraint both +"forward" and "reverse" fluxes. + +Function `kcat` should retugn the kcat value for a given reaction and isozyme +(IDs of which respectively form the 2 parameters for each call). +""" +function isozyme_flux_constraints( + isozyme_amounts::C.ConstraintTree, + fluxes::C.ConstraintTree, + kcat, +) + C.ConstraintTree( + rid => C.Constraint( + sum(kcat(rid, iid) * i.value for (iid, i) in ri if !isnothing(kcat(rid, iid))) - fluxes[rid].value, + 0.0, + ) for (rid, ri) in isozyme_amounts if haskey(fluxes, rid) + ) +end + +export isozyme_flux_constraints + +""" +$(TYPEDSIGNATURES) + +A constraint tree that binds the isozyme amounts to gene product amounts +accordingly to their multiplicities (aka. stoichiometries, protein units, ...) +given by `isozyme_stoichiometry`. + +Values in `gene_product_amounts` should describe the gene product allocations. + +`isozyme_amount_trees` is an iterable that contains `ConstraintTree`s that +describe the allocated isozyme amounts (such as created by +[`isozyme_amount_variables`](@ref). One may typically pass in both forward- and +reverse-direction amounts at once, but it is possible to use a single tree, +e.g., in a uni-tuple: `tuple(my_tree)`. + +`isozyme_stoichiometry` gets called with a reaction and isozyme ID as given by +the isozyme amount trees. It may return `nothing` in case there's no +information. +""" +function gene_product_isozyme_constraints( + gene_product_amounts::C.ConstraintTree, + isozymes_amount_trees, + isozyme_stoichiometry, +) + res = C.ConstraintTree() + # This needs to invert the stoichiometry mapping, + # so we patch up a fresh new CT in place. + for iss in isozymes_amount_trees + for (rid, is) in iss + for (iid, i) in is + gpstoi = isozyme_stoichiometry(rid, iid) + isnothing(gpstoi) && continue + for (gp, stoi) in gpstoi + haskey(gene_product_amounts, gp) || continue + if haskey(res, gp) + res[gp].value += i.value * stoi + else + res[gp] = + C.Constraint(i.value * stoi - gene_product_amounts[gp].value, 0) + end + end + end + end + end + res +end + +export gene_product_isozyme_constraints diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl new file mode 100644 index 000000000..910895a8a --- /dev/null +++ b/src/builders/fbc.jl @@ -0,0 +1,154 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +A constraint tree that models the content of the given instance of +`AbstractFBCModel`. + +The constructed tree contains subtrees `fluxes` (with the reaction-defining +"variables") and `flux_stoichiometry` (with the metabolite-balance-defining +constraints), and a single constraint `objective` thad describes the objective +function of the model. + +Optionally if `interface` is specified, an "interface block" will be created +within the constraint tree for later use as a "module" in creating bigger +models (such as communities) using [`join_module_constraints`](@ref). The +possible parameter values include: +- `nothing` -- default, no interface is created +- `:sbo` -- the interface gets created from model's SBO annotations) +- `:identifier_prefixes` -- the interface is guesstimated from commonly + occurring adhoc reaction ID prefixes used in contemporary models +- `:boundary` -- the interface is created from all reactions that either only + consume or only produce metabolites + +Output interface name can be set via `interface_name`. + +See [`Configuration`](@ref) for fine-tuning the default interface creation. +""" +function flux_balance_constraints( + model::A.AbstractFBCModel; + interface::Maybe{Symbol} = nothing, + interface_name = :interface, +) + rxn_strings = A.reactions(model) + rxns = Symbol.(rxn_strings) + mets = Symbol.(A.metabolites(model)) + lbs, ubs = A.bounds(model) + stoi = A.stoichiometry(model) + bal = A.balance(model) + obj = A.objective(model) + + constraints = C.ConstraintTree( + :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * + :flux_stoichiometry^C.ConstraintTree( + met => C.Constraint( + value = C.LinearValue(SparseArrays.sparse(row)), + bound = C.EqualTo(b), + ) for (met, row, b) in zip(mets, eachrow(stoi), bal) + ) * + :objective^C.Constraint(C.LinearValue(SparseArrays.sparse(obj))), + ) + + add_interface(sym, flt) = + any(flt) && ( + constraints *= + interface_name^sym^C.ConstraintTree( + r => constraints.fluxes[r] for r in rxns[flt] + ) + ) + if interface == :sbo + sbod(sbos, rid) = any(in(sbos), get(A.reaction_annotations(model, rid), "sbo", [])) + add_interface(:exchanges, sbod.(Ref(configuration.exchange_sbos), rxn_strings)) + add_interface(:biomass, sbod.(Ref(configuration.biomass_sbos), rxn_strings)) + add_interface( + :atp_maintenance, + sbod.(Ref(configuration.atp_maintenance_sbos), rxn_strings), + ) + add_interface(:demand, sbod.(Ref(configuration.demand_sbos), rxn_strings)) + elseif interface == :identifier_prefixes + prefixed(ps, s) = any(p -> startswith(s, p), ps) + add_interface( + :exchanges, + prefixed.(Ref(configuration.exchange_id_prefixes), rxn_strings), + ) + add_interface( + :biomass, + prefixed.(Ref(configuration.biomass_id_prefixes), rxn_strings), + ) + add_interface( + :atp_maintenance, + in.(rxn_strings, Ref(configuration.atp_maintenance_ids)), + ) + elseif interface == :boundary + add_interface( + :boundary, + [(all(col .<= 0) | all(col .>= 0)) for col in eachcol(stoi)], + ) + elseif interface == nothing + # nothing :] + else + throw(DomainError(interface, "unknown interface specifier")) + end + + return constraints +end + +export flux_balance_constraints + +""" +$(TYPEDSIGNATURES) + +Build log-concentration-stoichiometry constraints for the `model`, as used e.g. +by [`max_min_driving_force_analysis`](@ref). + +The output constraint tree contains a log-concentration variable for each +metabolite in subtree `log_concentrations`. Individual reactions' total +reactant log concentrations (i.e., all log concentrations of actual reactants +minus all log concentrations of products) have their own variables in +`reactant_log_concentrations`. + +Function `concentration_bound` may return a bound for the log-concentration of +a given metabolite (compatible with `ConstraintTrees.Bound`), or `nothing`. +""" +function log_concentration_constraints( + model::A.AbstractFBCModel; + concentration_bound = _ -> nothing, +) + rxns = Symbol.(A.reactions(model)) + mets = Symbol.(A.metabolites(model)) + stoi = A.stoichiometry(model) + + constraints = + :log_concentrations^C.variables(keys = mets, bounds = concentration_bound.(mets)) + + cs = C.ConstraintTree() + + for (midx, ridx, coeff) in zip(SparseArrays.findnz(stoi)...) + rid = rxns[ridx] + value = constraints.log_concentrations[mets[midx]].value * coeff + if haskey(cs, rid) + cs[rid].value += value + else + cs[rid] = C.Constraint(; value) + end + end + + return constraints * :reactant_log_concentrations^cs +end + +export log_concentration_constraints diff --git a/src/builders/interface.jl b/src/builders/interface.jl new file mode 100644 index 000000000..8620ebcf9 --- /dev/null +++ b/src/builders/interface.jl @@ -0,0 +1,101 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Join multiple constraint tree modules with interfaces into a bigger module with +an interface. + +Modules are like usual constraint trees, but contain an explicitly declared +`interface` part, marked properly in arguments using e.g. a tuple (the +parameters should form a dictionary constructor that would generally look such +as `:module_name => (module, module.interface)`; the second tuple member may +also be specified just by name as e.g. `:interface`, or omitted while relying +on `default_interface`). + +Interface parts get merged and constrained to create a new interface; networks +are intact with disjoint variable sets. + +Compatible modules with ready-made interfaces may be created e.g. by +[`flux_balance_constraints`](@ref). + +`ignore` may be used to selectively ignore parts of interfaces given the +"module name" identifier and constraint path in the interface (these form 2 +parameters passed to `ignore`). Similarly, `bound` may be used to specify +bounds for the new interface, if required. +""" +function interface_constraints( + ps::Pair...; + default_interface = :interface, + out_interface = default_interface, + out_balance = Symbol(out_interface, :_balance), + ignore = (_, _) -> false, + bound = _ -> nothing, +) + + prep(id::String, x) = prep(Symbol(id), x) + prep(id::Symbol, mod::C.ConstraintTree) = prep(id, (mod, default_interface)) + prep(id::Symbol, (mod, multiplier)::Tuple{C.ConstraintTree,<:Real}) = + prep(id, (mod, default_interface, multiplier)) + prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,Symbol}) = + prep(id, (mod, mod[interface])) + prep(id::Symbol, (mod, interface, multiplier)::Tuple{C.ConstraintTree,Symbol,<:Real}) = + prep(id, (mod, mod[interface], multiplier)) + prep( + id::Symbol, + (mod, interface, multiplier)::Tuple{C.ConstraintTree,C.ConstraintTreeElem,<:Real}, + ) = prep(id, (mod, C.map(c -> c * multiplier, interface))) + prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTreeElem,C.ConstraintTreeElem}) = + (id^(:network^mod * :interface^interface)) + prep_pair((a, b)) = prep(a, b) + + # first, collect everything into one huge network + # (while also renumbering the interfaces) + modules = sum(prep_pair.(ps); init = C.ConstraintTree()) + + # TODO maybe split the interface creation into a separate function + # (BUT- people shouldn't really need it since they should have all of their + # interfacing stuff in interface subtrees anyway, right?) + + # fold a union of all non-ignored interface keys + interface_sum = foldl(modules, init = C.ConstraintTree()) do accs, (id, ms) + C.imerge(accs, ms.interface) do path, acc, m + ignore(id, path) ? missing : + ismissing(acc) ? C.Constraint(value = m.value) : + C.Constraint(value = acc.value + m.value) + end + end + + # extract the plain networks and add variables for the new interfaces + constraints = + C.ConstraintTree(id => (m.network) for (id, m) in modules) + + out_interface^C.variables_ifor((path, _) -> bound(path), interface_sum) + + # join everything with the interface balance and return + constraints * out_balance^C.zip(interface_sum, constraints[out_interface]) do sum, out + C.Constraint(sum.value - out.value, 0) + end +end + +""" +$(TYPEDSIGNATURES) + +Overload of [`interface_constraints`](@ref) for general key-value containers. +""" +interface_constraints(kv; kwargs...) = interface_constraints(kv...; kwargs...) + +export interface_constraints diff --git a/src/builders/knockouts.jl b/src/builders/knockouts.jl new file mode 100644 index 000000000..fd29a70eb --- /dev/null +++ b/src/builders/knockouts.jl @@ -0,0 +1,55 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Make a `ConstraintTree` that knocks out fluxes given by the predicate +`knockout_test`. The predicate function is called with a single parameter (the +key of the flux in tree `fluxes`) and must return a boolean. Returning `true` +means that the corresponding flux (usually a reaction flux) will be knocked +out. + +Use [`fbc_gene_knockout_constraints`](@ref) to apply gene knockouts easily to +models with `AbstractFBCModel` interface. +""" +knockout_constraints(knockout_test, fluxes::C.ConstraintTree) = C.ConstraintTree( + id => C.Constraint(C.value(flux), 0) for (id, flux) in fluxes if knockout_test(id) +) + +""" +$(TYPEDSIGNATURES) + +Make a `ConstraintTree` that simulates a gene knockout of `knockout_genes` in +the `model` and disables corresponding `fluxes` accordingly. + +Keys of the fluxes must correspond to the reaction identifiers in the `model`. + +`knockout_genes` may be any collection that support element tests using `in`. +Since the test is done many times, a `Set` is a preferred contained for longer +lists of genes. +""" +fbc_gene_knockout_constraints(; + fluxes::C.ConstraintTree, + knockout_genes, + model::A.AbstractFBCModel, +) = + knockout_constraints(fluxes) do rid + maybemap( + !, + A.reaction_gene_products_available(model, string(rid), !in(knockout_genes)), + ) + end diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl new file mode 100644 index 000000000..de539be7d --- /dev/null +++ b/src/builders/loopless.jl @@ -0,0 +1,94 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Construct the loopless constraint system that binds `fluxes` of all +`internal_reactions` to direction of `loopless_direction_indicators` and +connects them to `loopless_driving_forces`. The solution is bounded to lie in +`internal_nullspace` (which is a sufficient algebraic condition for +loop-less-ness). + +The indicators must be discrete variables, valued `1` if the reaction flux goes +forward, or `0` if the reaction flux is reversed. + +The simplest (but by no means the fastest) way to obtain a good +`internal_nullspace` is to use `LinearAlgebra.nullspace` with the internal +reactions' stoichiometry matrix. Rows of `internal_nullspace` must correspond +to `internal_reactions`. + +`flux_infinity_bound` is used as the maximal bound for fluxes (for constraints +that connect them to indicator variables); it should optimally be greater than +the maximum possible absolute value of any flux in the original model. + +`driving_force_nonzero_bound` and `driving_force_infinity_bound` are similarly +used to limit the individual reaction's driving forces. +""" +loopless_constraints(; + fluxes::C.ConstraintTree, + loopless_direction_indicators::C.ConstraintTree, + loopless_driving_forces::C.ConstraintTree, + internal_reactions::Vector{Symbol}, + internal_nullspace::Matrix, + flux_infinity_bound, + driving_force_nonzero_bound, + driving_force_infinity_bound, +) = C.ConstraintTree( + :flux_direction_lower_bounds => C.ConstraintTree( + r => C.Constraint( + value = fluxes[r].value + + flux_infinity_bound * (1 - loopless_direction_indicators[r].value), + bound = C.Between(0, Inf), + ) for r in internal_reactions + ), + :flux_direction_upper_bounds => C.ConstraintTree( + r => C.Constraint( + value = fluxes[r].value - + flux_infinity_bound * loopless_direction_indicators[r].value, + bound = C.Between(-Inf, 0), + ) for r in internal_reactions + ), + :driving_force_lower_bounds => C.ConstraintTree( + r => C.Constraint( + value = loopless_driving_forces[r].value - + driving_force_nonzero_bound * loopless_direction_indicators[r].value + + driving_force_infinity_bound * + (1 - loopless_direction_indicators[r].value), + bound = C.Between(0, Inf), + ) for r in internal_reactions + ), + :driving_force_upper_bounds => C.ConstraintTree( + r => C.Constraint( + value = loopless_driving_forces[r].value + + driving_force_nonzero_bound * + (1 - loopless_direction_indicators[r].value) - + driving_force_infinity_bound * loopless_direction_indicators[r].value, + bound = C.Between(-Inf, 0), + ) for r in internal_reactions + ), + :loopless_nullspace => C.ConstraintTree( + Symbol(:nullspace_base_, i) => C.Constraint( + value = sum( + coeff * loopless_driving_forces[r].value for + (coeff, r) in zip(col, internal_reactions) + ), + bound = C.EqualTo(0), + ) for (i, col) in enumerate(eachcol(internal_nullspace)) + ), +) + +export loopless_constraints diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl new file mode 100644 index 000000000..a9568c306 --- /dev/null +++ b/src/builders/objectives.jl @@ -0,0 +1,58 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Construct a `ConstraintTrees.Value` out of squared sum of all values directly +present in a given constraint tree. +""" +squared_sum_value(x::C.ConstraintTree) = squared_sum_error_value(x, _ -> 0.0) + +""" +$(TYPEDSIGNATURES) + +Construct a `ConstraintTrees.Value` out of a sum of all values directly present +in a given constraint tree. +""" +function sum_value(x...) + res = zero(C.LinearValue) + for ct in x + C.map(ct) do c + res += c.value + end + end + res +end + +""" +$(TYPEDSIGNATURES) + +Construct a `ConstraintTrees.Value` out of squared error (in the RMSE-like +squared-error sense) between the values in the constraint tree and the +reference `target`. + +`target` is a function that takes a symbol (key) and returns either a `Float64` +reference value, or `nothing` if the error of given key should not be +considered. +""" +squared_sum_error_value(constraints::C.ConstraintTree, target) = sum( + ( + C.squared(C.value(c) - t) for + (t, c) in ((target(k), c) for (k, c) in constraints) if !isnothing(t) + ), + init = zero(C.LinearValue), +) diff --git a/src/builders/scale.jl b/src/builders/scale.jl new file mode 100644 index 000000000..230663a91 --- /dev/null +++ b/src/builders/scale.jl @@ -0,0 +1,41 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Linearly scale all bounds in a constraint tree by the `factor`. This actually +changes the model semantics, and may not work in surprising/improper ways with +some constraint systems, esp. the MILP and QP ones. + +See also [`scale_constraints`](@ref). +""" +scale_bounds(tree::C.ConstraintTree, factor) = + C.map(tree) do c + isnothing(c.bound) ? c : C.Constraint(value = c.value, bound = factor * c.bound) + end + +""" +$(TYPEDSIGNATURES) + +Linearly scale all constraints in a constraint tree by the `factor`. + +See also [`scale_bounds`](@ref). +""" +scale_constraints(tree::C.ConstraintTree, factor) = + C.map(tree) do c + c * factor + end diff --git a/src/builders/unsigned.jl b/src/builders/unsigned.jl new file mode 100644 index 000000000..2762b3502 --- /dev/null +++ b/src/builders/unsigned.jl @@ -0,0 +1,59 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +A constraint tree that bound the values present in `signed` to be sums of pairs +of `positive` and `negative` contributions to the individual values. + +Keys in the result are the same as the keys of `signed` constraints. + +Typically, this can be used to create "unidirectional" fluxes +together with [`unsigned_negative_contribution_variables`](@ref) and +[`unsigned_positive_contribution_variables`](@ref). +""" +sign_split_constraints(; + positive::C.ConstraintTree, + negative::C.ConstraintTree, + signed::C.ConstraintTree, +) = + C.zip(positive, negative, signed, C.Constraint) do p, n, s + equal_value_constraint(s.value + n.value, p.value) + end +#TODO the construction needs an example in the docs. + +export sign_split_constraints + +positive_bound_contribution(b::C.EqualTo) = b.equal_to >= 0 ? b : C.EqualTo(0.0) +positive_bound_contribution(b::C.Between) = + b.lower >= 0 && b.upper >= 0 ? b : + b.lower <= 0 && b.upper <= 0 ? C.EqualTo(0) : + C.Between(max(0, b.lower), max(0, b.upper)) +positive_bound_contribution(b::Switch) = + let upper_bound = max(b.a, b.b) + upper_bound > 0 ? C.Between(0.0, upper_bound) : C.EqualTo(0.0) + end + +unsigned_positive_contribution_variables(cs::C.ConstraintTree) = + C.variables_for(c -> positive_bound_contribution(c.bound), cs) + +export unsigned_positive_contribution_variables + +unsigned_negative_contribution_variables(cs::C.ConstraintTree) = + C.variables_for(c -> positive_bound_contribution(-c.bound), cs) + +export unsigned_negative_contribution_variables diff --git a/src/config.jl b/src/config.jl new file mode 100644 index 000000000..5f707dc2a --- /dev/null +++ b/src/config.jl @@ -0,0 +1,83 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDEF) + +Global configuration options for various COBREXA functions, mainly for various +non-interesting function parameters that are too inconvenient to be passed +around manually. + +Changing the configuration values at runtime is possible via the global +[`configuration`](@ref) variable. + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct Configuration + + """ + Prefixes that [`flux_balance_constraints`](@ref) uses for guessing + which reactions are exchanges. + """ + exchange_id_prefixes::Vector{String} = ["EX_", "R_EX_"] + + """ + Prefixes that [`flux_balance_constraints`](@ref) uses for guessing + which reactions are biomass reactions. + """ + biomass_id_prefixes::Vector{String} = ["BIOMASS_", "R_BIOMASS_"] + + """ + Reaction identifiers that [`flux_balance_constraints`](@ref) considers to + be ATP maintenance reactions. + """ + atp_maintenance_ids::Vector{String} = ["ATPM", "R_ATPM"] + + """ + SBO numbers that label exchange reactions for + [`flux_balance_constraints`](@ref). + """ + exchange_sbos::Vector{String} = ["SBO:0000627"] + + """ + SBO numbers that label biomass production reactions for + [`flux_balance_constraints`](@ref). + """ + biomass_sbos::Vector{String} = ["SBO:0000629"] + + """ + SBO numbers that label ATP maintenance reactions for + [`flux_balance_constraints`](@ref). + """ + atp_maintenance_sbos::Vector{String} = ["SBO:0000630"] + + """ + SBO numbers that label metabolite demand reactions for + [`flux_balance_constraints`](@ref). + """ + demand_sbos::Vector{String} = ["SBO:0000628"] +end + +""" + const configuration + +The configuration object. You can change the contents of configuration to +override the default behavior of some of the functions. + +The available options are described by struct [`Configuration`](@ref). +""" +const configuration = Configuration() diff --git a/src/frontend/balance.jl b/src/frontend/balance.jl new file mode 100644 index 000000000..583ce9926 --- /dev/null +++ b/src/frontend/balance.jl @@ -0,0 +1,37 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Compute an optimal objective-optimizing solution of the given `model`. + +Most arguments are forwarded to [`optimized_constraints`](@ref). + +Returns a tree with the optimization solution of the same shape as +given by [`flux_balance_constraints`](@ref). +""" +function flux_balance_analysis(model::A.AbstractFBCModel, optimizer; kwargs...) + constraints = flux_balance_constraints(model) + optimized_constraints( + constraints; + objective = constraints.objective.value, + optimizer, + kwargs..., + ) +end + +export flux_balance_analysis diff --git a/src/frontend/community.jl b/src/frontend/community.jl new file mode 100644 index 000000000..c07dd4ab2 --- /dev/null +++ b/src/frontend/community.jl @@ -0,0 +1,25 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function community_flux_balance_analysis( + model_abundances::Vector{Tuple{A.AbstractFBCModel,Float64}}, + optimizer; + kwargs..., +) + # TODO f this gets complicated, make a specialized community_constraints + # builder or so. But ideally this is just module loading + 1 big join + + # optimizer run. +end diff --git a/src/frontend/envelope.jl b/src/frontend/envelope.jl new file mode 100644 index 000000000..a952a38b9 --- /dev/null +++ b/src/frontend/envelope.jl @@ -0,0 +1,83 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Find the objective production envelope of the `model` in the dimensions given +by `reactions`. + +This runs a variability analysis of constraints to determine an applicable +range for the dimensions, then splits the dimensions to equal-sized breaks (of +count `breaks` for each dimension, i.e. total `breaks ^ length(reactions)` +individual "multidimensional breaks") thus forming a grid, and returns an array +of fluxes through the model objective with the individual reactions fixed to +flux as given by the grid. + +`optimizer` and `settings` are used to construct the optimization models. + +The computation is distributed to the specified `workers`, defaulting to all +available workers. +""" +function objective_production_envelope( + model::A.AbstractFBCModel, + reactions::Vector{String}; + breaks = 10, + optimizer, + settings = [], + workers = D.workers(), +) + constraints = flux_balance_constraints(model) + rs = Symbol.(reactions) + + envelope_bounds = constraints_variability( + constraints, + (r => constraints.fluxes[r] for r in rs); + optimizer, + settings, + workers, + ) + + #TODO check for nothings in the bounds + + bss = [break_interval(envelope_bounds[r]..., breaks) for r in rs] + + return ( + breaks = reactions .=> bss, + objective_values = constraints_objective_envelope( + constraints, + (constraints.fluxes[r] => bs for (r, bs) in zip(rs, bss))...; + objective = model.objective.value, + sense = Maximal, + optimizer, + settings, + workers, + ), + ) + + # this converts nicely to a dataframe, but I'm not a total fan. + #= + xss = Iterators.product(bss) + @assert length(result) == length(xss) + xss = reshape(xss, tuple(length(xss))) + + return (; + (r => [xs[i] for xs in xss] for (i, r) in enumerate(rs))..., + objective_value_name => reshape(result, tuple(length(result))), + ) + # TODO think about it + =# +end diff --git a/src/frontend/enzymes.jl b/src/frontend/enzymes.jl new file mode 100644 index 000000000..14bd8896e --- /dev/null +++ b/src/frontend/enzymes.jl @@ -0,0 +1,140 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDEF) + +A simple struct storing information about the isozyme composition, including +subunit stoichiometry and turnover numbers. Use with +[`enzyme_constrained_flux_balance_analysis`](@ref). + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct Isozyme + gene_product_stoichiometry::Dict{String,Float64} + kcat_forward::Maybe{Float64} = nothing + kcat_reverse::Maybe{Float64} = nothing +end + +export Isozyme + +""" +$(TYPEDSIGNATURES) + +Run a basic enzyme-constrained flux balance analysis on `model`. The enzyme +model is parameterized by `reaction_isozymes`, which is a mapping of reaction +identifiers to [`Isozyme`](@ref) descriptions. + +Additionally, one typically wants to supply `gene_product_molar_masses` to +describe the weights of enzyme building material, and `capacity` which limits +the mass of enzymes in the whole model. + +`capacity` may be a single number, which sets the limit for "all described +enzymes". Alternatively, `capacity` may be a vector of identifier-genes-limit +triples that make a constraint (identified by the given identifier) that limits +the listed genes to the given limit. +""" +function enzyme_constrained_flux_balance_analysis( + model::A.AbstractFBCModel; + reaction_isozymes::Dict{String,Dict{String,Isozyme}}, + gene_product_molar_masses::Dict{String,Float64}, + capacity::Union{Vector{Tuple{String,Vector{String},Float64}},Float64}, + optimizer, + settings = [], +) + constraints = flux_balance_constraints(model) + + # might be nice to omit some conditionally (e.g. slash the direction if one + # kcat is nothing) + isozyme_amounts = isozyme_amount_variables( + Symbol.(keys(reaction_isozymes)), + rid -> Symbol.(keys(reaction_isozymes[string(rid)])), + ) + + # allocate variables for everything (nb. += wouldn't associate right here) + constraints = + constraints + + :fluxes_forward^unsigned_positive_contribution_variables(constraints.fluxes) + + :fluxes_reverse^unsigned_negative_contribution_variables(constraints.fluxes) + + :isozyme_forward_amounts^isozyme_amounts + + :isozyme_reverse_amounts^isozyme_amounts + + :gene_product_amounts^C.variables( + keys = Symbol.(A.genes(model)), + bounds = C.Between(0, Inf), + ) + + # connect all parts with constraints + constraints = + constraints * + :directional_flux_balance^sign_split_constraints( + positive = constraints.fluxes_forward, + negative = constraints.fluxes_reverse, + signed = constraints.fluxes, + ) * + :isozyme_flux_forward_balance^isozyme_flux_constraints( + constraints.isozyme_forward_amounts, + constraints.fluxes_forward, + (rid, isozyme) -> maybemap( + x -> x.kcat_forward, + maybeget(reaction_isozymes, string(rid), string(isozyme)), + ), + ) * + :isozyme_flux_reverse_balance^isozyme_flux_constraints( + constraints.isozyme_reverse_amounts, + constraints.fluxes_reverse, + (rid, isozyme) -> maybemap( + x -> x.kcat_reverse, + maybeget(reaction_isozymes, string(rid), string(isozyme)), + ), + ) * + :gene_product_isozyme_balance^gene_product_isozyme_constraints( + constraints.gene_product_amounts, + (constraints.isozyme_forward_amounts, constraints.isozyme_reverse_amounts), + (rid, isozyme) -> maybemap( + x -> [(Symbol(k), v) for (k, v) in x.gene_product_stoichiometry], + maybeget(reaction_isozymes, string(rid), string(isozyme)), + ), + ) * + :gene_product_capacity^( + capacity isa Float64 ? + C.Constraint( + value = sum( + gpa.value * gene_product_molar_masses[String(gp)] for + (gp, gpa) in constraints.gene_product_amounts + ), + bound = C.Between(0, capacity), + ) : + C.ConstraintTree( + Symbol(id) => C.Constraint( + value = sum( + constraints.gene_product_amounts[Symbol(gp)].value * + gene_product_molar_masses[gp] for gp in gps + ), + bound = C.Between(0, limit), + ) for (id, gps, limit) in capacity_limits + ) + ) + + optimized_constraints( + constraints; + objective = constraints.objective.value, + optimizer, + settings, + ) +end + +export enzyme_constrained_flux_balance_analysis diff --git a/src/frontend/knockout.jl b/src/frontend/knockout.jl new file mode 100644 index 000000000..f03f13ff5 --- /dev/null +++ b/src/frontend/knockout.jl @@ -0,0 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function gene_knockouts(model::A.AbstractFBCModel, genes, n; kwargs...) + # TODO just use screen to do this right +end diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl new file mode 100644 index 000000000..afdf6817b --- /dev/null +++ b/src/frontend/loopless.jl @@ -0,0 +1,77 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Perform a flux balance analysis with added quasi-thermodynamic constraints that +ensure that thermodynamically infeasible internal cycles can not occur. The +method is closer described by: Schellenberger, Lewis, and, Palsson. +"Elimination of thermodynamically infeasible loops in steady-state metabolic +models.", Biophysical journal, 2011`. + +The loopless condition comes with a performance overhead: the computation needs +to find the null space of the stoichiometry matrix (essentially inverting it); +and the subsequently created optimization problem contains binary variables for +each internal reaction, thus requiring a MILP solver and a potentially +exponential solving time. + +The arguments `driving_force_max_bound` and `driving_force_nonzero_bound` set +the bounds (possibly negated ones) on the virtual "driving forces" (G_i in the +paper). +""" +function loopless_flux_balance_analysis( + model::A.AbstractFBCModel; + flux_infinity_bound = 10000.0, + driving_force_nonzero_bound = 1.0, + driving_force_infinity_bound = 1000.0, + settings = [], + optimizer, +) + + constraints = flux_balance_constraints(model) + + rxns = A.reactions(model) + stoi = A.stoichiometry(model) + internal_mask = count(stoi .!= 0; dims = 1)[begin, :] .> 1 + internal_reactions = Symbol.(rxns[internal_mask]) + + constraints = + constraints + + :loopless_directions^C.variables(keys = internal_reactions, bounds = Switch(0, 1)) + + :loopless_driving_forces^C.variables(keys = internal_reactions) + + constraints *= + :loopless_constraints^loopless_constraints(; + fluxes = constraints.fluxes, + loopless_direction_indicators = constraints.loopless_directions, + loopless_driving_forces = constraints.loopless_driving_forces, + internal_reactions, + internal_nullspace = LinearAlgebra.nullspace(Matrix(stoi[:, internal_mask])), + flux_infinity_bound, + driving_force_nonzero_bound, + driving_force_infinity_bound, + ) + + optimized_constraints( + constraints; + objective = constraints.objective.value, + optimizer, + settings, + ) +end + +export loopless_flux_balance_analysis diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl new file mode 100644 index 000000000..069383734 --- /dev/null +++ b/src/frontend/mmdf.jl @@ -0,0 +1,205 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Perform a max-min driving force analysis using `optimizer` on the `model` with +supplied reaction standard Gibbs energies in `reaction_standard_gibbs_free_energies`. + +The method was described by by: Noor, et al., "Pathway thermodynamics highlights +kinetic obstacles in central metabolism.", PLoS computational biology, 2014. + +`reference_flux` sets the directions of each reaction in `model`. The scale of +the values is not important, only the direction is examined (w.r.t. +`reference_flux_atol` tolerance). Ideally, the supplied `reference_flux` should +be completely free of internal cycles, which enables the thermodynamic +consistency. To get the cycle-free flux, you can use +[`loopless_flux_balance_analysis`](@ref) (computationally complex but gives +very good fluxes), [`parsimonious_flux_balance_analysis`](@ref) or +[`linear_parsimonious_flux_balance_analysis`](@ref) (computationally simplest +but the consistency is not guaranteed). + +Internally, [`log_concentration_constraints`](@ref) is used to lay out the +base structure of the problem. + +Following arguments are set optionally: +- `water_metabolites`, `proton_metabolites` and `ignored_metabolites` allow to + completely ignore constraints on a part of the metabolite set, which is + explicitly recommended especially for water and protons (since the analyses + generally assume aqueous environment of constant pH) +- `constant_concentrations` can be used to fix the concentrations of the + metabolites +- `concentration_lower_bound` and `concentration_upper_bound` set the default + concentration bounds for all other metabolites +- `concentration ratios` is a dictionary that assigns a tuple of + metabolite-metabolite-concentration ratio constraint names; e.g. ATP/ADP + ratio can be fixed to five-times-more-ATP by setting `concentration_ratios = + Dict("adenosin_ratio" => ("atp", "adp", 5.0))` +- `T` and `R` default to the usual required thermodynamic constraints in the + usual units (K and kJ/K/mol, respectively). These multiply the + log-concentrations to obtain the actual Gibbs energies and thus driving + forces. +""" +function max_min_driving_force_analysis( + model::A.AbstractFBCModel, + reaction_standard_gibbs_free_energies::Dict{String,Float64}; + reference_flux = Dict{String,Float64}(), + concentration_ratios = Dict{String,Tuple{String,String,Float64}}(), + constant_concentrations = Dict{String,Float64}(), + ignored_metabolites = [], + proton_metabolites = [], + water_metabolites = [], + concentration_lower_bound = 1e-9, # M + concentration_upper_bound = 1e-1, # M + T = 298.15, # Kelvin + R = 8.31446261815324e-3, # kJ/K/mol + reference_flux_atol = 1e-6, + check_ignored_reactions = missing, + settings = [], + optimizer, +) + + # First let's just check if all the identifiers are okay because we use + # quite a lot of these; the rest of the function may be a bit cleaner with + # this checked properly. + + model_reactions = Set(A.reactions(model)) + model_metabolites = Set(A.metabolites(model)) + + all(in(model_reactions), keys(reaction_standard_gibbs_free_energies)) || throw( + DomainError( + reaction_standard_gibbs_free_energies, + "unknown reactions referenced by reaction_standard_gibbs_free_energies", + ), + ) + all(x -> haskey(reaction_standard_gibbs_free_energies, x), keys(reference_flux)) || + throw(DomainError(reference_flux, "some reactions have no reference flux")) + all(in(model_reactions), keys(reference_flux)) || throw( + DomainError( + reaction_standard_gibbs_free_energies, + "unknown reactions referenced by reference_flux", + ), + ) + all(in(model_metabolites), keys(constant_concentrations)) || throw( + DomainError( + constant_concentrations, + "unknown metabolites referenced by constant_concentrations", + ), + ) + all( + in(model_metabolites), + (m for (_, (x, y, _)) in concentration_ratios for m in (x, y)), + ) || throw( + DomainError( + concentration_ratios, + "unknown metabolites referenced by concentration_ratios", + ), + ) + all(in(model_metabolites), proton_metabolites) || throw( + DomainError( + concentration_ratios, + "unknown metabolites referenced by proton_metabolites", + ), + ) + all(in(model_metabolites), water_metabolites) || throw( + DomainError( + concentration_ratios, + "unknown metabolites referenced by water_metabolites", + ), + ) + all(in(model_metabolites), ignored_metabolites) || throw( + DomainError( + concentration_ratios, + "unknown metabolites referenced by ignored_metabolites", + ), + ) + if !ismissing(check_ignored_reactions) && ( + all( + x -> !haskey(reaction_standard_gibbs_free_energies, x), + check_ignored_reactions, + ) || ( + union( + Set(check_ignored_reactions), + Set(keys(reaction_standard_gibbs_free_energies)), + ) != model_reactions + ) + ) + throw(AssertionError("check_ignored_reactions validation failed")) + end + + # that was a lot of checking. + + default_concentration_bound = + C.Between(log(concentration_lower_bound), log(concentration_upper_bound)) + + no_concentration_metabolites = union( + Set(Symbol.(water_metabolites)), + Set(Symbol.(proton_metabolites)), + Set(Symbol.(ignored_metabolites)), + ) + + constraints = + log_concentration_constraints( + model, + concentration_bound = met -> if met in no_concentration_metabolites + C.EqualTo(0) + else + mid = String(met) + if haskey(constant_concentrations, mid) + C.EqualTo(log(constant_concentrations[mid])) + else + default_concentration_bound + end + end, + ) + :min_driving_force^C.variable() + + driving_forces = C.ConstraintTree( + let r = Symbol(rid), + rf = reference_flux[rid], + df = dGr0 + R * T * constraints.reactant_log_concentrations[r].value + + r => if isapprox(rf, 0.0, atol = reference_flux_atol) + C.Constraint(df, C.EqualTo(0)) + else + C.Constraint(rf > 0 ? -df : df, C.Between(0, Inf)) + end + end for (rid, dGr0) in reaction_standard_gibbs_free_energies + ) + + constraints = + constraints * + :driving_forces^driving_forces * + :min_driving_force_thresholds^C.map(driving_forces) do c + less_or_equal_constraint(constraints.min_driving_force, c) + end * + :concentration_ratio_constraints^C.ConstraintTree( + Symbol(cid) => difference_constraint( + constraints.log_concentrations[Symbol(m1)], + constraints.log_concentrations[Symbol(m2)], + log(ratio), + ) for (cid, (m1, m2, ratio)) in concentration_ratios + ) + + optimized_constraints( + constraints; + objective = constraints.min_driving_force.value, + optimizer, + settings, + ) +end + +export max_min_driving_force_analysis diff --git a/src/frontend/moma.jl b/src/frontend/moma.jl new file mode 100644 index 000000000..c93860315 --- /dev/null +++ b/src/frontend/moma.jl @@ -0,0 +1,169 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Find a feasible solution of the "minimal metabolic adjustment analysis" (MOMA) +for the `model`, which is the "closest" feasible solution to the given +`reference_fluxes`, in the sense of squared-sum error distance. The minimized +squared distance (the objective) is present in the result tree as +`minimal_adjustment_objective`. + +This is often used for models with smaller feasible region than the reference +models (typically handicapped by a knockout, nutritional deficiency or a +similar perturbation). MOMA solution then gives an expectable "easiest" +adjustment of the organism towards a somewhat working state. + +Reference fluxes that do not exist in the model are ignored (internally, the +objective is constructed via [`squared_sum_error_value`](@ref)). + +Additional parameters are forwarded to [`optimized_constraints`](@ref). +""" +function minimization_of_metabolic_adjustment( + model::A.AbstractFBCModel, + reference_fluxes::Dict{Symbol,Float64}, + optimizer; + kwargs..., +) + constraints = flux_balance_constraints(model) + objective = + squared_sum_error_value(constraints.fluxes, x -> get(reference_fluxes, x, nothing)) + optimized_constraints( + constraints * :minimal_adjustment_objective^C.Constraint(objective); + optimizer, + objective, + sense = Minimal, + kwargs..., + ) +end + +""" +$(TYPEDSIGNATURES) + +A slightly easier-to-use version of +[`minimization_of_metabolic_adjustment`](@ref) that computes the +reference flux as the optimal solution of the [`reference_model`](@ref). The +reference flux is calculated using `reference_optimizer` and +`reference_modifications`, which default to the `optimizer` and `settings`. + +Leftover arguments are passed to the overload of +[`minimization_of_metabolic_adjustment`](@ref) that accepts the +reference flux dictionary. +""" +function minimization_of_metabolic_adjustment( + model::A.AbstractFBCModel, + reference_model::A.AbstractFBCModel, + optimizer; + reference_optimizer = optimizer, + settings = [], + reference_settings = settings, + kwargs..., +) + reference_constraints = flux_balance_constraints(reference_model) + reference_fluxes = optimized_constraints( + reference_constraints; + optimizer = reference_optimizer, + settings = reference_settings, + output = reference_constraints.fluxes, + ) + isnothing(reference_fluxes) && return nothing + minimization_of_metabolic_adjustment( + model, + reference_fluxes, + optimizer; + settings, + kwargs..., + ) +end + +export minimization_of_metabolic_adjustment + +""" +$(TYPEDSIGNATURES) + +Like [`minimization_of_metabolic_adjustment`](@ref) but optimizes the +L1 norm. This typically produces a sufficiently good result with less +resources, depending on the situation. See documentation of +[`linear_parsimonious_flux_balance`](@ref) for some of the +considerations. +""" +function linear_minimization_of_metabolic_adjustment( + model::A.AbstractFBCModel, + reference_fluxes::Dict{Symbol,Float64}, + optimizer; + kwargs..., +) + constraints = flux_balance_constraints(model) + + difference = C.zip(ct.fluxes, C.Tree(reference_fluxes)) do orig, ref + C.Constraint(orig.value - ref) + end + + difference_split_variables = + C.variables(keys = keys(difference), bounds = C.Between(0, Inf)) + constraints += :reference_positive_diff^difference_split_variables + constraints += :reference_negative_diff^difference_split_variables + + # `difference` actually doesn't need to go to the CT, but we include it + # anyway to calm the curiosity of good neighbors. + constraints *= :reference_diff^difference + constraints *= + :reference_directional_diff_balance^sign_split_constraints( + positive = constraints.reference_positive_diff, + negative = constraints.reference_negative_diff, + signed = difference, + ) + + objective = + sum_value(constraints.reference_positive_diff, constraints.reference_negative_diff) + + optimized_constraints( + constraints * :linear_minimal_adjustment_objective^C.Constraint(objective); + optimizer, + objective, + sense = Minimal, + kwargs..., + ) +end + +function linear_minimization_of_metabolic_adjustment( + model::A.AbstractFBCModel, + reference_model::A.AbstractFBCModel, + optimizer; + reference_optimizer = optimizer, + settings = [], + reference_settings = settings, + kwargs..., +) + reference_constraints = flux_balance_constraints(reference_model) + reference_fluxes = optimized_constraints( + reference_constraints; + optimizer = reference_optimizer, + settings = reference_settings, + output = reference_constraints.fluxes, + ) + isnothing(reference_fluxes) && return nothing + linear_minimization_of_metabolic_adjustment( + model, + reference_fluxes, + optimizer; + settings, + kwargs..., + ) +end + +export linear_minimization_of_metabolic_adjustment diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl new file mode 100644 index 000000000..b94597ab5 --- /dev/null +++ b/src/frontend/parsimonious.jl @@ -0,0 +1,97 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Compute a parsimonious flux solution for the given `model`. In short, the +objective value of the parsimonious solution should be the same as the one from +[`flux_balance_analysis`](@ref), except the squared sum of reaction fluxes is minimized. +If there are multiple possible fluxes that achieve a given objective value, +parsimonious flux thus represents the "minimum energy" one, thus arguably more +realistic. The optimized squared distance is present in the result as +`parsimonious_objective`. + +Most arguments are forwarded to [`parsimonious_optimized_constraints`](@ref), +with some (objectives) filled in automatically to fit the common processing of +FBC models, and some (`tolerances`) provided with more practical defaults. + +Similarly to the [`flux_balance_analysis`](@ref), returns a tree with the optimization +solutions of the shape as given by [`flux_balance_constraints`](@ref). +""" +function parsimonious_flux_balance_analysis( + model::A.AbstractFBCModel, + optimizer; + tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]), + kwargs..., +) + constraints = flux_balance_constraints(model) + parsimonious_objective = squared_sum_value(constraints.fluxes) + parsimonious_optimized_constraints( + constraints * :parsimonious_objective^C.Constraint(parsimonious_objective); + optimizer, + objective = constraints.objective.value, + parsimonious_objective, + tolerances, + kwargs..., + ) +end + +export parsimonious_flux_balance_analysis + +""" +$(TYPEDSIGNATURES) + +Like [`parsimonious_flux_balance_analysis`](@ref), but uses a L1 metric for +solving the parsimonious problem. + +In turn, the solution is often faster, does not require a solver capable of +quadratic objectives, and has many beneficial properties of the usual +parsimonious solutions (such as the general lack of unnecessary loops). On the +other hand, like with plain flux balance analysis there is no strong guarantee +of uniqueness of the solution. +""" +function linear_parsimonious_flux_balance_analysis( + model::A.AbstractFBCModel, + optimizer; + tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]), + kwargs..., +) + constraints = flux_balance_constraints(model) + constraints = + constraints + + :fluxes_forward^unsigned_positive_contribution_variables(ct.fluxes) + + :fluxes_reverse^unsigned_negative_contribution_variables(ct.fluxes) + constraints *= + :directional_flux_balance^sign_split_constraints( + positive = ct.fluxes_forward, + negative = ct.fluxes_reverse, + signed = ct.fluxes, + ) + + parsimonious_objective = sum_value(ct.fluxes_forward, ct.fluxes_reverse) + + parsimonious_optimized_constraints( + constraints * :parsimonious_objective^C.Constraint(parsimonious_objective); + optimizer, + objective = constraints.objective.value, + parsimonious_objective, + tolerances, + kwargs..., + ) +end + +export linear_parsimonious_flux_balance_analysis diff --git a/src/frontend/sample.jl b/src/frontend/sample.jl new file mode 100644 index 000000000..b97f0f438 --- /dev/null +++ b/src/frontend/sample.jl @@ -0,0 +1,23 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function flux_sampling_achr(model::A.AbstractFBCModel; optimizer, kwargs...) + #TODO +end + +function flux_sampling_affine_hr(model::A.AbstractFBCModel; optimizer, kwargs...) + #TODO probably share a lot of the frontend with the above thing +end diff --git a/src/frontend/variability.jl b/src/frontend/variability.jl new file mode 100644 index 000000000..0719b67ed --- /dev/null +++ b/src/frontend/variability.jl @@ -0,0 +1,61 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Perform a Flux Variability Analysis (FVA) on the `model`, and return a +dictionary of flux ranges where the model is able to perform optimally. The +optimality tolerance can be specified with objective_bound using e.g. +[`relative_tolerance_bound`](@ref) or [`absolute_tolerance_bound`](@ref); the +default is 99% relative tolerance. + +Parameters `optimizer` and `settings` are used as with +[`optimized_constraints`](@ref). `workers` may be used to enable parallel or +distributed processing; the execution defaults to all available workers. +""" +function flux_variability_analysis( + model::A.AbstractFBCModel; + objective_bound = relative_tolerance_bound(0.99), + optimizer, + settings, + workers = D.workers(), +) + constraints = flux_balance_constraints(model) + + objective = constraints.objective_value + + objective_flux = optimized_constraints( + constraints; + objective = constraints.objective.value, + output = constraints.objective, + optimizer, + settings, + ) + + isnothing(objective_flux) && return nothing + + constraint_variability( + constraints * + :objective_bound^C.Constraint(objective, objective_bound(objective_flux)), + constraints.fluxes; + optimizer, + settings, + workers, + ) +end + +export flux_variability_analysis diff --git a/src/io.jl b/src/io.jl new file mode 100644 index 000000000..64c58d56b --- /dev/null +++ b/src/io.jl @@ -0,0 +1,61 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Load a FBC model representation while guessing the correct model type. Uses +`AbstractFBCModels.load`. + +This overload almost always involves a search over types; do not use it in +environments where performance is critical. +""" +function load_model(path::String) + A.load(path) +end + +""" +$(TYPEDSIGNATURES) + +Load a FBC model representation. Uses `AbstractFBCModels.load`. +""" +function load_model(model_type::Type{T}, path::String) where {T<:A.AbstractFBCModel} + A.load(model_type, path) +end + +export load_model + +""" +$(TYPEDSIGNATURES) + +Save a FBC model representation. Uses `AbstractFBCModels.save`. +""" +function save_model(model::T, path::String) where {T<:A.AbstractFBCModel} + A.save(model, path) +end + +export save_model + +""" +$(TYPEDSIGNATURES) + +Safely download a model with a known hash. All arguments are forwarded to +`AbstractFBCModels.download_data_file` -- see the documentation in the +AbstractFBCModels package for details. +""" +download_model(args...; kwargs...) = A.download_data_file(args...; kwargs...) + +export download_model diff --git a/src/io/h5.jl b/src/io/h5.jl deleted file mode 100644 index 5d7785305..000000000 --- a/src/io/h5.jl +++ /dev/null @@ -1,54 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Return a HDF5Model associated with the given file. Does not actually load -anything (for efficiency) -- use [`precache!`](@ref) to start pulling data into -the memory. -""" -function load_h5_model(file_name::String)::HDF5Model - return HDF5Model(file_name) -end - -""" -$(TYPEDSIGNATURES) - -Converts and writes a metabolic model to disk in the HDF5 format. - -Additionally returns an (uncached) [`HDF5Model`](@ref) that represents the -contents of the saved file. Because all HDF5-based models need to be backed by -disk storage, writing the data to disk (using this function) is the only way to -make new HDF5 models. -""" -function save_h5_model(model::MetabolicModel, file_name::String)::HDF5Model - rxns = reactions(model) - rxnp = sortperm(rxns) - mets = metabolites(model) - metp = sortperm(mets) - h5open(file_name, "w") do f - write(f, "metabolites", mets[metp]) - write(f, "reactions", rxns[rxnp]) - _h5_write_sparse(create_group(f, "balance"), balance(model)[metp]) - _h5_write_sparse(create_group(f, "objective"), objective(model)[rxnp]) - _h5_write_sparse(create_group(f, "stoichiometry"), stoichiometry(model)[metp, rxnp]) - let (lbs, ubs) = bounds(model) - write(f, "lower_bounds", lbs[rxnp]) - write(f, "upper_bounds", ubs[rxnp]) - end - end - # TODO: genes, grrs, compartments. Perhaps chemistry and others? - HDF5Model(file_name) -end - -""" -$(TYPEDSIGNATURES) - -Close (and un-cache) the [`HDF5Model`](@ref) data. This allows the associated -file to be opened for writing again. -""" -function Base.close(model::HDF5Model) - if !isnothing(model.h5) - close(model.h5) - model.h5 = nothing - end -end diff --git a/src/io/io.jl b/src/io/io.jl deleted file mode 100644 index d90a895f8..000000000 --- a/src/io/io.jl +++ /dev/null @@ -1,82 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Generic function for loading models that chooses a specific loader function -based on the `extension` argument (e.g., `".xml"` chooses loading of the SBML -model format), or throws an error. By default the extension from `file_name` -is used. - -Currently, these model types are supported: - -- SBML models (`*.xml`, loaded with [`load_sbml_model`](@ref)) -- JSON models (`*.json`, loaded with [`load_json_model`](@ref)) -- MATLAB models (`*.mat`, loaded with [`load_mat_model`](@ref)) -- HDF5 models (`*.h5`, loaded with [`load_h5_model`](@ref)) -""" -function load_model( - file_name::String; - extension = last(splitext(file_name)), -)::MetabolicModel - - if extension == ".json" - return load_json_model(file_name) - elseif extension == ".xml" - return load_sbml_model(file_name) - elseif extension == ".mat" - return load_mat_model(file_name) - elseif extension == ".h5" - return load_h5_model(file_name) - else - throw(DomainError(extension, "Unknown file extension")) - end -end - -""" -$(TYPEDSIGNATURES) - -Helper function that loads the model using [`load_model`](@ref) and returns it -converted to `type`. - -# Example: - - load_model(CoreModel, "mySBMLModel.xml") -""" -function load_model( - type::Type{T}, - file_name::String; - extension = last(splitext(file_name)), -)::T where {T<:MetabolicModel} - convert(type, load_model(file_name; extension)) -end - -""" -$(TYPEDSIGNATURES) - -Generic function for saving models that chooses a specific writer function from -the `extension` argument (such as `".xml"` for SBML format), or throws an -error. By default the extension from `file_name` is used. - -Currently, these model types are supported: - -- SBML models (`*.xml`, saved with [`save_sbml_model`](@ref)) -- JSON models (`*.json`, saved with [`save_json_model`](@ref)) -- MATLAB models (`*.mat`, saved with [`save_mat_model`](@ref)) -- HDF5 models (`*.h5`, saved with [`save_h5_model`](@ref)) -""" -function save_model( - model::MetabolicModel, - file_name::String; - extension = last(splitext(file_name)), -) - if extension == ".json" - return save_json_model(model, file_name) - elseif extension == ".xml" - return save_sbml_model(model, file_name) - elseif extension == ".mat" - return save_mat_model(model, file_name) - elseif extension == ".h5" - return save_h5_model(model, file_name) - else - throw(DomainError(extension, "Unknown file extension")) - end -end diff --git a/src/io/json.jl b/src/io/json.jl deleted file mode 100644 index 918267a5c..000000000 --- a/src/io/json.jl +++ /dev/null @@ -1,26 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Load and return a JSON-formatted model that is stored in `file_name`. -""" -function load_json_model(filename::String)::JSONModel - return JSONModel(JSON.parsefile(filename)) -end - -""" -$(TYPEDSIGNATURES) - -Save a [`JSONModel`](@ref) in `model` to a JSON file `file_name`. - -In case the `model` is not `JSONModel`, it will be converted automatically. -""" -function save_json_model(model::MetabolicModel, file_name::String) - m = - typeof(model) == JSONModel ? model : - begin - @_io_log @warn "Automatically converting $(typeof(model)) to JSONModel for saving, information may be lost." - convert(JSONModel, model) - end - - open(f -> JSON.print(f, m.json), file_name, "w") -end diff --git a/src/io/mat.jl b/src/io/mat.jl deleted file mode 100644 index 84b205868..000000000 --- a/src/io/mat.jl +++ /dev/null @@ -1,33 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Load and return a MATLAB file `file_name` that contains a COBRA-compatible -model. -""" -function load_mat_model(file_name::String)::MATModel - model_pair = first(matread(file_name)) - @_io_log @info "Loading MAT: taking a model with ID $(model_pair.first)" - return MATModel(model_pair.second) -end - -""" -$(TYPEDSIGNATURES) - -Save a [`MATModel`](@ref) in `model` to a MATLAB file `file_name` in a format -compatible with other MATLAB-based COBRA software. - -In case the `model` is not `MATModel`, it will be converted automatically. - -`model_name` is the identifier name for the whole model written to the MATLAB -file; defaults to just "model". -""" -function save_mat_model(model::MetabolicModel, file_path::String; model_name = "model") - m = - typeof(model) == MATModel ? model : - begin - @_io_log @warn "Automatically converting $(typeof(model)) to MATModel for saving, information may be lost." - convert(MATModel, model) - end - matwrite(file_path, Dict(model_name => m.mat)) -end diff --git a/src/io/sbml.jl b/src/io/sbml.jl deleted file mode 100644 index 574a60300..000000000 --- a/src/io/sbml.jl +++ /dev/null @@ -1,25 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Load and return a SBML XML model in `file_name`. -""" -function load_sbml_model(file_name::String)::SBMLModel - return SBMLModel(SBML.readSBML(file_name)) -end - -""" -$(TYPEDSIGNATURES) - -Write a given SBML model to `file_name`. -""" -function save_sbml_model(model::MetabolicModel, file_name::String) - m = - typeof(model) == SBMLModel ? model : - begin - @_io_log @warn "Automatically converting $(typeof(model)) to SBMLModel for saving, information may be lost." - convert(SBMLModel, model) - end - - SBML.writeSBML(m.sbml, file_name) -end diff --git a/src/io/show/FluxSummary.jl b/src/io/show/FluxSummary.jl deleted file mode 100644 index 259036045..000000000 --- a/src/io/show/FluxSummary.jl +++ /dev/null @@ -1,48 +0,0 @@ -function _pad_spaces(slen::Int, maxlen::Int) - " "^max(0, maxlen - slen + 1) -end - -_pad_spaces(str::String, maxlen::Int) = _pad_spaces(length(str), maxlen) - -function Base.show(io::IO, ::MIME"text/plain", flux_res::FluxSummary) - longest_biomass_len = - maximum(length(k) for k in keys(flux_res.biomass_fluxes); init = 0) - longest_import_len = maximum(length(k) for k in keys(flux_res.import_fluxes); init = 0) - longest_export_len = maximum(length(k) for k in keys(flux_res.export_fluxes); init = 0) - - if !isempty(flux_res.unbounded_fluxes) - longest_unbounded_len = - maximum([length(k) for k in keys(flux_res.unbounded_fluxes)]) - word_pad = max( - longest_biomass_len, - longest_export_len, - longest_import_len, - longest_unbounded_len, - ) - else - word_pad = max(longest_biomass_len, longest_export_len, longest_import_len) - end - - println(io, "Biomass") - for (k, v) in flux_res.biomass_fluxes - println(io, " ", k, ":", _pad_spaces(k, word_pad), round(v, digits = 4)) - end - - println(io, "Import") - for (k, v) in flux_res.import_fluxes - println(io, " ", k, ":", _pad_spaces(k, word_pad), round(v, digits = 4)) - end - - println(io, "Export") - for (k, v) in flux_res.export_fluxes - println(io, " ", k, ":", _pad_spaces(k, word_pad), round(v, digits = 4)) - end - - if !isempty(flux_res.unbounded_fluxes) - println(io, "Unbounded") - for (k, v) in flux_res.unbounded_fluxes - println(io, " ", k, ":", _pad_spaces(k, word_pad), round(v, digits = 4)) - end - end - return nothing -end diff --git a/src/io/show/FluxVariabilitySummary.jl b/src/io/show/FluxVariabilitySummary.jl deleted file mode 100644 index ba07fda5e..000000000 --- a/src/io/show/FluxVariabilitySummary.jl +++ /dev/null @@ -1,68 +0,0 @@ -function Base.show(io::IO, ::MIME"text/plain", flux_res::FluxVariabilitySummary) - - longest_biomass_len = - maximum(length(k) for k in keys(flux_res.biomass_fluxes); init = 0) - longest_exchange_len = - maximum(length(k) for k in keys(flux_res.exchange_fluxes); init = 0) - word_pad_len = max(longest_biomass_len, longest_exchange_len) - - longest_biomass_flux_len = maximum( - length(string(round(v[1], digits = 4))) for v in values(flux_res.biomass_fluxes); - init = 0, - ) - longest_exchange_flux_len = max( - maximum( - length(string(round(v[1], digits = 4))) for - v in values(flux_res.exchange_fluxes); - init = 0, - ), - length("Lower bound "), - ) - number_pad_len = max(longest_biomass_flux_len, longest_exchange_flux_len) - - println( - io, - "Biomass", - _pad_spaces(length("Biomass"), word_pad_len + 3), - "Lower bound ", - _pad_spaces("Lower bound ", number_pad_len), - "Upper bound", - ) - for (k, v) in flux_res.biomass_fluxes - lb = isnothing(v[1]) ? " " : string(round(v[1], digits = 4)) - ub = isnothing(v[2]) ? " " : string(round(v[1], digits = 4)) - println( - io, - " ", - k, - ":", - _pad_spaces(k, word_pad_len), - lb, - _pad_spaces(lb, number_pad_len), - ub, - ) - end - - println(io, "Exchange") - ex_ids = collect(keys(flux_res.exchange_fluxes)) - vs = [ - abs(flux_res.exchange_fluxes[k][1]) + abs(flux_res.exchange_fluxes[k][2]) for - k in ex_ids - ] - idxs = sortperm(vs, rev = true) - for k in ex_ids[idxs] - v = flux_res.exchange_fluxes[k] - lb = isnothing(v[1]) ? " " : string(round(v[1], digits = 4)) - ub = isnothing(v[2]) ? " " : string(round(v[2], digits = 4)) - println( - io, - " ", - k, - ":", - _pad_spaces(k, word_pad_len), - lb, - _pad_spaces(lb, number_pad_len), - ub, - ) - end -end diff --git a/src/io/show/Gene.jl b/src/io/show/Gene.jl deleted file mode 100644 index 8d030b238..000000000 --- a/src/io/show/Gene.jl +++ /dev/null @@ -1,5 +0,0 @@ -function Base.show(io::IO, ::MIME"text/plain", g::Gene) - for fname in fieldnames(Gene) - _pretty_print_keyvals(io, "Gene.$(string(fname)): ", getfield(g, fname)) - end -end diff --git a/src/io/show/MetabolicModel.jl b/src/io/show/MetabolicModel.jl deleted file mode 100644 index b600208be..000000000 --- a/src/io/show/MetabolicModel.jl +++ /dev/null @@ -1,15 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Pretty printing of everything metabolic-modelish. -""" -function Base.show(io::IO, ::MIME"text/plain", m::MetabolicModel) - _pretty_print_keyvals(io, "", "Metabolic model of type $(typeof(m))") - if n_reactions(m) <= _constants.default_stoich_show_size - println(io, stoichiometry(m)) - else # too big to display nicely - println(io, "S = [...]") - end - _pretty_print_keyvals(io, "Number of reactions: ", string(n_reactions(m))) - _pretty_print_keyvals(io, "Number of metabolites: ", string(n_metabolites(m))) -end diff --git a/src/io/show/Metabolite.jl b/src/io/show/Metabolite.jl deleted file mode 100644 index 9f116fd21..000000000 --- a/src/io/show/Metabolite.jl +++ /dev/null @@ -1,10 +0,0 @@ -function Base.show(io::IO, ::MIME"text/plain", m::Metabolite) - for fname in fieldnames(Metabolite) - if fname == :charge - c = isnothing(getfield(m, fname)) ? nothing : string(getfield(m, fname)) - _pretty_print_keyvals(io, "Metabolite.$(string(fname)): ", c) - else - _pretty_print_keyvals(io, "Metabolite.$(string(fname)): ", getfield(m, fname)) - end - end -end diff --git a/src/io/show/Reaction.jl b/src/io/show/Reaction.jl deleted file mode 100644 index b5e340615..000000000 --- a/src/io/show/Reaction.jl +++ /dev/null @@ -1,55 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Nicely format a substance list. -""" -function _pretty_substances(ss::Vector{String})::String - if isempty(ss) - "∅" - elseif length(ss) > 5 - join([ss[1], ss[2], "...", ss[end-1], ss[end]], " + ") - else - join(ss, " + ") - end -end - -function Base.show(io::IO, ::MIME"text/plain", r::Reaction) - if r.ub > 0.0 && r.lb < 0.0 - arrow = " ↔ " - elseif r.ub <= 0.0 && r.lb < 0.0 - arrow = " ← " - elseif r.ub > 0.0 && r.lb >= 0.0 - arrow = " → " - else - arrow = " →|← " # blocked reaction - end - substrates = - ["$(-v) $k" for (k, v) in Iterators.filter(((_, v)::Pair -> v < 0), r.metabolites)] - products = - ["$v $k" for (k, v) in Iterators.filter(((_, v)::Pair -> v >= 0), r.metabolites)] - - for fname in fieldnames(Reaction) - if fname == :metabolites - _pretty_print_keyvals( - io, - "Reaction.$(string(fname)): ", - _pretty_substances(substrates) * arrow * _pretty_substances(products), - ) - elseif fname == :grr - _pretty_print_keyvals( - io, - "Reaction.$(string(fname)): ", - _maybemap(x -> _unparse_grr(String, x), r.grr), - ) - elseif fname in (:lb, :ub, :objective_coefficient) - _pretty_print_keyvals( - io, - "Reaction.$(string(fname)): ", - string(getfield(r, fname)), - ) - else - _pretty_print_keyvals(io, "Reaction.$(string(fname)): ", getfield(r, fname)) - end - end -end diff --git a/src/io/show/Serialized.jl b/src/io/show/Serialized.jl deleted file mode 100644 index 598e2ceff..000000000 --- a/src/io/show/Serialized.jl +++ /dev/null @@ -1,12 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Show the [`Serialized`](@ref) model without unnecessarily loading it. -""" -function Base.show(io::IO, ::MIME"text/plain", m::Serialized{M}) where {M} - print( - io, - "Serialized{$M} saved in \"$(m.filename)\" ($(isnothing(m.m) ? "not loaded" : "loaded"))", - ) -end diff --git a/src/io/show/pretty_printing.jl b/src/io/show/pretty_printing.jl deleted file mode 100644 index 8df6f6462..000000000 --- a/src/io/show/pretty_printing.jl +++ /dev/null @@ -1,45 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Nicely prints keys and values. -""" -_pretty_print_keyvals(io, def::String, payload; kwargs...) = - _pretty_print_keyvals(io, def, isnothing(payload) ? "---" : string(payload); kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specialization of `_pretty_print_keyvals` for plain strings. -""" -function _pretty_print_keyvals(io, def::String, payload::String) - print(io, def) - if isempty(payload) - println(io, "---") - else - println(io, payload) - end -end - -""" -$(TYPEDSIGNATURES) - -Specialization of `_pretty_print_keyvals` for dictionaries. -""" -function _pretty_print_keyvals(io, def::String, payload::Dict) - - print(io, def) - if isempty(payload) - println(io, "---") - else - println(io, "") - for (k, v) in payload - if length(v) > 2 && length(v[1]) < 20 - println(io, "\t", k, ": ", v[1], ", ..., ", v[end]) - elseif length(v[1]) > 20 # basically for envipath annotations... or long notes - println(io, "\t", k, ": ", v[1][1:20], "...") - else - println(io, "\t", k, ": ", v) - end - end - end -end diff --git a/src/misc/bounds.jl b/src/misc/bounds.jl new file mode 100644 index 000000000..b809a4bf4 --- /dev/null +++ b/src/misc/bounds.jl @@ -0,0 +1,37 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Make a function that returns absolute tolerance bounds, i.e. `value - +tolerance` and `value + tolerance` in a tuple, in the increasing order. +""" +absolute_tolerance_bound(tolerance) = x -> begin + bound = (x - tolerance, x + tolerance) + (minimum(bound), maximum(bound)) +end + +""" +$(TYPEDSIGNATURES) + +Make a function that returns relative tolerance bounds, i.e. `value / +tolerance` and `value * tolerance` in a tuple, in the increasing order. +""" +relative_tolerance_bound(tolerance) = x -> begin + bound = (x * tolerance, x / tolerance) + (minimum(bound), maximum(bound)) +end diff --git a/src/misc/breaks.jl b/src/misc/breaks.jl new file mode 100644 index 000000000..2cf41467b --- /dev/null +++ b/src/misc/breaks.jl @@ -0,0 +1,25 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Break an interval into `breaks` (count) breaks. + +Used for computing breaks in [`objective_production_envelope`](@ref). +""" +break_interval(lower, upper, breaks::Int) = + lower .+ (upper - lower) .* ((1:s) .- 1) ./ max(breaks - 1, 1) diff --git a/src/misc/maybe.jl b/src/misc/maybe.jl new file mode 100644 index 000000000..166b2f6fd --- /dev/null +++ b/src/misc/maybe.jl @@ -0,0 +1,32 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Helper for getting stuff from dictionaries where keys may be easily missing. +""" +maybeget(_::Nothing, _...) = nothing +maybeget(x, k, ks...) = haskey(x, k) ? maybeget(x[k], ks...) : nothing +maybeget(x) = x + +""" +$(TYPEDSIGNATURES) + +Helper for applying functions to stuff that might be `nothing`. +""" +maybemap(f, _::Nothing) = nothing +maybemap(f, x) = f(x) diff --git a/src/misc/settings.jl b/src/misc/settings.jl new file mode 100644 index 000000000..1418f27e7 --- /dev/null +++ b/src/misc/settings.jl @@ -0,0 +1,64 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Change the objective sense of optimization. Accepted arguments include +[`Minimal`](@ref), [`Maximal`](@ref), and [`Feasible`](@ref). +""" +set_objective_sense(objective_sense) = + opt_model -> J.set_objective_sense(opt_model, objective_sense) + +export set_objective_sense + +""" +$(TYPEDSIGNATURES) + +Change the JuMP optimizer used to run the optimization. +""" +set_optimizer(optimizer) = opt_model -> J.set_optimizer(opt_model, optimizer) + +export set_optimizer + +""" +$(TYPEDSIGNATURES) + +Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer +to the JuMP documentation and the documentation of the specific optimizer for +usable keys and values. +""" +set_optimizer_attribute(attribute_key, value) = + opt_model -> J.set_optimizer_attribute(opt_model, attribute_key, value) + +export set_optimizer_attribute + +""" + silence + +Modification that disable all output from the JuMP optimizer (shortcut for +`set_silent` from JuMP). +""" +silence(opt_model) = J.set_silent(opt_model) + +""" +$(TYPEDSIGNATURES) + +Portable way to set a time limit in seconds for the optimizer computation. +""" +set_time_limit_sec(limit) = opt_model -> J.set_time_limit_sec(opt_model, limit) + +export silence diff --git a/src/misc/trees.jl b/src/misc/trees.jl new file mode 100644 index 000000000..b7235743a --- /dev/null +++ b/src/misc/trees.jl @@ -0,0 +1,53 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#TODO these are likely hot candidates to be moved to CTs + +""" +$(TYPEDSIGNATURES) + +Extract all elements of a `ConstraintTrees.Tree` in order and return them in a +`Vector` transformed by `f`. If the order is not modified, one can re-insert a +vector of modified elements into the same-shaped tree using +[`tree_reinflate`](@ref). +""" +function tree_deflate(f, x::C.Tree{T})::Vector{T} where {T} + count = 0 + C.traverse(x) do _ + count += 1 + end + res = Vector{T}(undef, count) + i = 1 + C.traverse(x) do c + res[i] = f(c) + i += 1 + end + res +end + +""" +$(TYPEDSIGNATURES) + +Insert a `Vector` of elements into the "values" of a `ConstraintTrees.Tree`. +The order of elements is given by [`tree_deflate`](@ref). +""" +function tree_reinflate(x::C.Tree, elems::Vector{T})::C.Tree{T} where {T} + i = 0 + C.map(x) do _ + i += 1 + elems[i] + end +end diff --git a/src/reconstruction/CoreModel.jl b/src/reconstruction/CoreModel.jl deleted file mode 100644 index 57ad17a76..000000000 --- a/src/reconstruction/CoreModel.jl +++ /dev/null @@ -1,440 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Add `rxns` to `model` efficiently. The model must already contain the metabolites used by -`rxns` in the model. -""" -function add_reactions!(model::CoreModel, rxns::Vector{Reaction}) - I = Int64[] # rows - J = Int64[] # cols - V = Float64[] # values - cs = zeros(length(rxns)) - lbs = zeros(length(rxns)) - ubs = zeros(length(rxns)) - for (j, rxn) in enumerate(rxns) - req = rxn.metabolites - for (i, v) in zip(indexin(keys(req), metabolites(model)), values(req)) - push!(J, j) - push!(I, i) - push!(V, v) - end - push!(model.rxns, rxn.id) - lbs[j] = rxn.lb - ubs[j] = rxn.ub - cs[j] = rxn.objective_coefficient - end - Sadd = sparse(I, J, V, n_metabolites(model), length(rxns)) - model.S = [model.S Sadd] - model.c = dropzeros([model.c; cs]) - model.xu = ubs - model.xl = lbs - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Add `rxn` to `model`. The model must already contain the metabolites used by -`rxn` in the model. -""" -add_reaction!(model::CoreModel, rxn::Reaction) = add_reactions!(model, [rxn]) - -""" -$(TYPEDSIGNATURES) - -Add reaction(s) to a `CoreModel` model `m`. -""" -function add_reactions( - m::CoreModel, - s::VecType, - b::VecType, - c::AbstractFloat, - xl::AbstractFloat, - xu::AbstractFloat; - check_consistency = false, -) - return add_reactions( - m, - sparse(reshape(s, (length(s), 1))), - sparse(b), - sparse([c]), - sparse([xl]), - sparse([xu]), - check_consistency = check_consistency, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::CoreModel, - s::VecType, - b::VecType, - c::AbstractFloat, - xl::AbstractFloat, - xu::AbstractFloat, - rxn::String, - mets::StringVecType; - check_consistency = false, -) - return add_reactions( - m, - sparse(reshape(s, (length(s), 1))), - sparse(b), - sparse([c]), - sparse([xl]), - sparse([xu]), - [rxn], - mets, - check_consistency = check_consistency, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::CoreModel, - Sp::MatType, - b::VecType, - c::VecType, - xl::VecType, - xu::VecType; - check_consistency = false, -) - rxns = ["r$x" for x = length(m.rxns)+1:length(m.rxns)+length(xu)] - mets = ["m$x" for x = length(m.mets)+1:length(m.mets)+size(Sp)[1]] - return add_reactions( - m, - Sp, - b, - c, - xl, - xu, - rxns, - mets, - check_consistency = check_consistency, - ) -end - -""" -$(TYPEDSIGNATURES) - -Add all reactions from `m2` to `m1`. -""" -function add_reactions(m1::CoreModel, m2::CoreModel; check_consistency = false) - return add_reactions( - m1, - m2.S, - m2.b, - m2.c, - m2.xl, - m2.xu, - m2.rxns, - m2.mets, - check_consistency = check_consistency, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::CoreModel, - Sp::MatType, - b::VecType, - c::VecType, - xl::VecType, - xu::VecType, - rxns::StringVecType, - mets::StringVecType; - check_consistency = false, -) - Sp = sparse(Sp) - b = sparse(b) - c = sparse(c) - xl = collect(xl) - xu = collect(xu) - - all([length(b), length(mets)] .== size(Sp, 1)) || - throw(DimensionMismatch("inconsistent number of metabolites")) - all(length.([c, xl, xu, rxns]) .== size(Sp, 2)) || - throw(DimensionMismatch("inconsistent number of reactions")) - - new_reactions = findall(Bool[!(rxn in m.rxns) for rxn in rxns]) - new_metabolites = findall(Bool[!(met in m.mets) for met in mets]) - - if check_consistency - (newReactions1, newMetabolites1) = verify_consistency( - m, - Sp, - b, - c, - xl, - xu, - rxns, - mets, - new_reactions, - new_metabolites, - ) - end - - new_mets = vcat(m.mets, mets[new_metabolites]) - - zero_block = spzeros(length(new_metabolites), n_reactions(m)) - ext_s = vcat(sparse(m.S), zero_block) - - mapping = [findfirst(isequal(met), new_mets) for met in mets] - (I, J, elements) = findnz(sparse(Sp[:, new_reactions])) - ext_sp = spzeros(length(new_mets), length(new_reactions)) - for (k, i) in enumerate(I) - new_i = mapping[i] - ext_sp[new_i, J[k]] = elements[k] - end - - new_s = hcat(ext_s, ext_sp) - newb = vcat(m.b, b[new_metabolites]) - newc = vcat(m.c, c[new_reactions]) - newxl = vcat(m.xl, xl[new_reactions]) - newxu = vcat(m.xu, xu[new_reactions]) - new_rxns = vcat(m.rxns, rxns[new_reactions]) - new_lp = CoreModel(new_s, newb, newc, newxl, newxu, new_rxns, new_mets) - - if check_consistency - return (new_lp, new_reactions, new_metabolites) - else - return new_lp - end -end - -""" -$(TYPEDSIGNATURES) - -Check the consistency of given reactions with existing reactions in `m`. - -TODO: work in progress, doesn't return consistency status. -""" -function verify_consistency( - m::CoreModel, - Sp::M, - b::V, - c::V, - xl::B, - xu::B, - names::K, - mets::K, - new_reactions, - new_metabolites, -) where {M<:MatType,V<:VecType,B<:VecType,K<:StringVecType} - - if !isempty(new_reactions) - statuses = Vector{ReactionStatus}(undef, length(names)) - for (i, name) in enumerate(names) - rxn_index = findfirst(isequal(name), m.rxns) - reaction = Sp[:, i] - stoich_index = findfirst(Bool[reaction == m.S[:, j] for j = 1:size(m.S, 2)]) - if isnothing(rxn_index) & isnothing(stoich_index) - statuses[i] = ReactionStatus(false, 0, "new") - end - - if !isnothing(rxn_index) & isnothing(stoich_index) - statuses[i] = ReactionStatus(true, 0, "same name") - end - - if isnothing(rxn_index) & !isnothing(stoich_index) - statuses[i] = ReactionStatus(true, 0, "same stoichiometry") - end - - if !isnothing(rxn_index) & !isnothing(stoich_index) - statuses[i] = ReactionStatus(true, 0, "same name, same stoichiometry") - end - end - end - - return (new_reactions, new_metabolites) -end - -@_change_bounds_fn CoreModel Int inplace begin - isnothing(lower) || (model.xl[rxn_idx] = lower) - isnothing(upper) || (model.xu[rxn_idx] = upper) - nothing -end - -@_change_bounds_fn CoreModel Int inplace plural begin - for (i, l, u) in zip(rxn_idxs, lower, upper) - change_bound!(model, i, lower = l, upper = u) - end -end - -@_change_bounds_fn CoreModel Int begin - change_bounds(model, [rxn_idx], lower = [lower], upper = [upper]) -end - -@_change_bounds_fn CoreModel Int plural begin - n = copy(model) - n.xl = copy(n.xl) - n.xu = copy(n.xu) - change_bounds!(n, rxn_idxs, lower = lower, upper = upper) - n -end - -@_change_bounds_fn CoreModel String inplace begin - change_bounds!(model, [rxn_id], lower = [lower], upper = [upper]) -end - -@_change_bounds_fn CoreModel String inplace plural begin - change_bounds!( - model, - Vector{Int}(indexin(rxn_ids, reactions(model))), - lower = lower, - upper = upper, - ) -end - -@_change_bounds_fn CoreModel String begin - change_bounds(model, [rxn_id], lower = [lower], upper = [upper]) -end - -@_change_bounds_fn CoreModel String plural begin - change_bounds( - model, - Int.(indexin(rxn_ids, reactions(model))), - lower = lower, - upper = upper, - ) -end - -@_remove_fn reaction CoreModel Int inplace begin - remove_reactions!(model, [reaction_idx]) -end - -@_remove_fn reaction CoreModel Int inplace plural begin - mask = .!in.(1:n_reactions(model), Ref(reaction_idxs)) - model.S = model.S[:, mask] - model.c = model.c[mask] - model.xl = model.xl[mask] - model.xu = model.xu[mask] - model.rxns = model.rxns[mask] - nothing -end - -@_remove_fn reaction CoreModel Int begin - remove_reactions(model, [reaction_idx]) -end - -@_remove_fn reaction CoreModel Int plural begin - n = copy(model) - remove_reactions!(n, reaction_idxs) - return n -end - -@_remove_fn reaction CoreModel String inplace begin - remove_reactions!(model, [reaction_id]) -end - -@_remove_fn reaction CoreModel String inplace plural begin - remove_reactions!(model, Int.(indexin(reaction_ids, reactions(model)))) -end - -@_remove_fn reaction CoreModel String begin - remove_reactions(model, [reaction_id]) -end - -@_remove_fn reaction CoreModel String plural begin - remove_reactions(model, Int.(indexin(reaction_ids, reactions(model)))) -end - -@_remove_fn metabolite CoreModel Int inplace begin - remove_metabolites!(model, [metabolite_idx]) -end - -@_remove_fn metabolite CoreModel Int plural inplace begin - remove_reactions!( - model, - [ - ridx for ridx = 1:n_reactions(model) if - any(in.(findnz(model.S[:, ridx])[1], Ref(metabolite_idxs))) - ], - ) - mask = .!in.(1:n_metabolites(model), Ref(metabolite_idxs)) - model.S = model.S[mask, :] - model.b = model.b[mask] - model.mets = model.mets[mask] - nothing -end - -@_remove_fn metabolite CoreModel Int begin - remove_metabolites(model, [metabolite_idx]) -end - -@_remove_fn metabolite CoreModel Int plural begin - n = deepcopy(model) #everything gets changed anyway - remove_metabolites!(n, metabolite_idxs) - return n -end - -@_remove_fn metabolite CoreModel String inplace begin - remove_metabolites!(model, [metabolite_id]) -end - -@_remove_fn metabolite CoreModel String inplace plural begin - remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolites(model)))) -end - -@_remove_fn metabolite CoreModel String begin - remove_metabolites(model, [metabolite_id]) -end - -@_remove_fn metabolite CoreModel String plural begin - remove_metabolites(model, Int.(indexin(metabolite_ids, metabolites(model)))) -end - -""" -$(TYPEDSIGNATURES) - -Change the objective to reactions at given indexes, optionally specifying their -`weights` in the same order. By default, all set weights are 1. -""" -function change_objective!( - model::CoreModel, - rxn_idxs::Vector{Int}; - weights = ones(length(rxn_idxs)), -) - model.c = spzeros(length(model.c)) - model.c[rxn_idxs] .= weights - nothing -end - -""" -$(TYPEDSIGNATURES) - -Change objective function of a CoreModel to a single `1` at reaction index -`rxn_idx`. -""" -change_objective!(model::CoreModel, rxn_idx::Int) = change_objective!(model, [rxn_idx]) - -""" -$(TYPEDSIGNATURES) - -Change objective of given reaction IDs, optionally specifying objective -`weights` in the same order as `rxn_ids`. By default, all set weights are 1. -""" -function change_objective!( - model::CoreModel, - rxn_ids::Vector{String}; - weights = ones(length(rxn_ids)), -) - idxs = indexin(rxn_ids, reactions(model)) - any(isnothing(idx) for idx in idxs) && - throw(DomainError(rxn_ids, "Some reaction ids not found in the model")) - change_objective!(model, Int.(idxs); weights) -end - -""" -$(TYPEDSIGNATURES) - -Change objective function of a CoreModel to a single `1` at the given reaction -ID. -""" -change_objective!(model::CoreModel, rxn_id::String) = change_objective!(model, [rxn_id]) diff --git a/src/reconstruction/CoreModelCoupled.jl b/src/reconstruction/CoreModelCoupled.jl deleted file mode 100644 index 147cfab89..000000000 --- a/src/reconstruction/CoreModelCoupled.jl +++ /dev/null @@ -1,380 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Add reaction(s) to a `CoreModelCoupled` model `m`. - -""" -function add_reactions( - m::CoreModelCoupled, - s::V1, - b::V2, - c::AbstractFloat, - xl::AbstractFloat, - xu::AbstractFloat; - check_consistency = false, -) where {V1<:VecType,V2<:VecType} - new_lm = add_reactions(m.lm, s, b, c, xl, xu, check_consistency = check_consistency) - return CoreModelCoupled( - new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), - m.cl, - m.cu, - ) -end - -""" -$(TYPEDSIGNATURES) - -""" -function add_reactions( - m::CoreModelCoupled, - s::V1, - b::V2, - c::AbstractFloat, - xl::AbstractFloat, - xu::AbstractFloat, - rxn::String, - mets::K; - check_consistency = false, -) where {V1<:VecType,V2<:VecType,K<:StringVecType} - new_lm = add_reactions( - m.lm, - s, - b, - c, - xl, - xu, - rxn, - mets, - check_consistency = check_consistency, - ) - return CoreModelCoupled( - new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), - m.cl, - m.cu, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::CoreModelCoupled, - Sp::M, - b::V, - c::V, - xl::V, - xu::V; - check_consistency = false, -) where {M<:MatType,V<:VecType} - new_lm = add_reactions(m.lm, Sp, b, c, xl, xu, check_consistency = check_consistency) - return CoreModelCoupled( - new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), - m.cl, - m.cu, - ) -end - -""" -$(TYPEDSIGNATURES) - -Add all reactions from `m2` to `m1`. -""" -function add_reactions(m1::CoreModelCoupled, m2::CoreModel; check_consistency = false) - new_lm = add_reactions(m1.lm, m2, check_consistency = check_consistency) - return CoreModelCoupled( - new_lm, - hcat(m1.C, spzeros(size(m1.C, 1), n_reactions(new_lm) - n_reactions(m1.lm))), - m1.cl, - m1.cu, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::CoreModelCoupled, - Sp::M, - b::V, - c::V, - xl::V, - xu::V, - rxns::K, - mets::K; - check_consistency = false, -) where {M<:MatType,V<:VecType,K<:StringVecType} - new_lm = add_reactions( - m.lm, - Sp, - b, - c, - xl, - xu, - rxns, - mets, - check_consistency = check_consistency, - ) - return CoreModelCoupled( - new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), - m.cl, - m.cu, - ) -end - -""" -$(TYPEDSIGNATURES) - -Add constraints of the following form to CoreCoupling and return the modified -model. - -The arguments are same as for in-place [`add_coupling_constraints!`](@ref). -""" -function add_coupling_constraints(m::CoreCoupling, args...) - new_lp = deepcopy(m) - add_coupling_constraints!(new_lp, args...) - return new_lp -end - -""" -$(TYPEDSIGNATURES) - -Add coupling constraints to a plain [`CoreModel`](@ref) (returns a -[`CoreModelCoupled`](@ref)). -""" -add_coupling_constraints(m::CoreModel, args...) = CoreModelCoupled(m, args...) - -""" -$(TYPEDSIGNATURES) - -Overload for adding a single coupling constraint. -""" -function add_coupling_constraints!( - m::CoreCoupling, - c::VecType, - cl::AbstractFloat, - cu::AbstractFloat, -) - return add_coupling_constraints!(m, sparse(reshape(c, (1, length(c)))), [cl], [cu]) -end - -""" -$(TYPEDSIGNATURES) - -In-place add a single coupling constraint in form -``` - cₗ ≤ C x ≤ cᵤ -``` -""" -function add_coupling_constraints!( - m::CoreCoupling, - C::MatType, - cl::V, - cu::V, -) where {V<:VecType} - - all([length(cu), length(cl)] .== size(C, 1)) || - throw(DimensionMismatch("mismatched numbers of constraints")) - size(C, 2) == n_reactions(m) || - throw(DimensionMismatch("mismatched number of reactions")) - - m.C = vcat(m.C, sparse(C)) - m.cl = vcat(m.cl, collect(cl)) - m.cu = vcat(m.cu, collect(cu)) - nothing -end - -""" -$(TYPEDSIGNATURES) - -Remove coupling constraints from the linear model, and return the modified -model. Arguments are the same as for in-place version -[`remove_coupling_constraints!`](@ref). -""" -function remove_coupling_constraints(m::CoreCoupling, args...) - new_model = deepcopy(m) - remove_coupling_constraints!(new_model, args...) - return new_model -end - -""" -$(TYPEDSIGNATURES) - -Removes a single coupling constraints from a [`CoreCoupling`](@ref) in-place. -""" -remove_coupling_constraints!(m::CoreCoupling, constraint::Int) = - remove_coupling_constraints!(m, [constraint]) - - -""" -$(TYPEDSIGNATURES) - -Removes a set of coupling constraints from a [`CoreCoupling`](@ref) -in-place. -""" -function remove_coupling_constraints!(m::CoreCoupling, constraints::Vector{Int}) - to_be_kept = filter(!in(constraints), 1:n_coupling_constraints(m)) - m.C = m.C[to_be_kept, :] - m.cl = m.cl[to_be_kept] - m.cu = m.cu[to_be_kept] - nothing -end - -""" -$(TYPEDSIGNATURES) - -Change the lower and/or upper bounds (`cl` and `cu`) for the given list of -coupling constraints. -""" -function change_coupling_bounds!( - model::CoreCoupling, - constraints::Vector{Int}; - cl::V = Float64[], - cu::V = Float64[], -) where {V<:VecType} - found = (constraints .>= 1) .& (constraints .<= n_coupling_constraints(model)) - red_constraints = constraints[found] - - length(red_constraints) == length(unique(red_constraints)) || - error("`constraints` appears to contain duplicates") - if !isempty(cl) - length(constraints) == length(cl) || - throw(DimensionMismatch("`constraints` size doesn't match with `cl`")) - model.cl[red_constraints] = cl[found] - end - - if !isempty(cu) - length(constraints) == length(cu) || - throw(DimensionMismatch("`constraints` size doesn't match with `cu`")) - model.cu[red_constraints] = cu[found] - end - nothing -end - -# TODO see if some of these can be derived from ModelWrapper -@_change_bounds_fn CoreCoupling Int inplace begin - change_bound!(model.lm, rxn_idx, lower = lower, upper = upper) -end - -@_change_bounds_fn CoreCoupling Int inplace plural begin - change_bounds!(model.lm, rxn_idxs, lower = lower, upper = upper) -end - -@_change_bounds_fn CoreCoupling String inplace begin - change_bound!(model.lm, rxn_id, lower = lower, upper = upper) -end - -@_change_bounds_fn CoreCoupling String inplace plural begin - change_bounds!(model.lm, rxn_ids, lower = lower, upper = upper) -end - -@_change_bounds_fn CoreCoupling Int begin - n = copy(model) - n.lm = change_bound(model.lm, rxn_idx, lower = lower, upper = upper) - n -end - -@_change_bounds_fn CoreCoupling Int plural begin - n = copy(model) - n.lm = change_bounds(model.lm, rxn_idxs, lower = lower, upper = upper) - n -end - -@_change_bounds_fn CoreCoupling String begin - n = copy(model) - n.lm = change_bound(model.lm, rxn_id, lower = lower, upper = upper) - n -end - -@_change_bounds_fn CoreCoupling String plural begin - n = copy(model) - n.lm = change_bounds(model.lm, rxn_ids, lower = lower, upper = upper) - n -end - -@_remove_fn reaction CoreCoupling Int inplace begin - remove_reactions!(model, [reaction_idx]) -end - -@_remove_fn reaction CoreCoupling Int inplace plural begin - orig_rxns = reactions(model.lm) - remove_reactions!(model.lm, reaction_idxs) - model.C = model.C[:, in.(orig_rxns, Ref(Set(reactions(model.lm))))] - nothing -end - -@_remove_fn reaction CoreCoupling Int begin - remove_reactions(model, [reaction_idx]) -end - -@_remove_fn reaction CoreCoupling Int plural begin - n = copy(model) - n.lm = remove_reactions(n.lm, reaction_idxs) - n.C = n.C[:, in.(reactions(model.lm), Ref(Set(reactions(n.lm))))] - return n -end - -@_remove_fn reaction CoreCoupling String inplace begin - remove_reactions!(model, [reaction_id]) -end - -@_remove_fn reaction CoreCoupling String inplace plural begin - remove_reactions!(model, Int.(indexin(reaction_ids, reactions(model)))) -end - -@_remove_fn reaction CoreCoupling String begin - remove_reactions(model, [reaction_id]) -end - -@_remove_fn reaction CoreCoupling String plural begin - remove_reactions(model, Int.(indexin(reaction_ids, reactions(model)))) -end - -@_remove_fn metabolite CoreCoupling Int inplace begin - remove_metabolites!(model, [metabolite_idx]) -end - -@_remove_fn metabolite CoreCoupling Int plural inplace begin - orig_rxns = reactions(model.lm) - model.lm = remove_metabolites(model.lm, metabolite_idxs) - model.C = model.C[:, in.(orig_rxns, Ref(Set(reactions(model.lm))))] - nothing -end - -@_remove_fn metabolite CoreCoupling Int begin - remove_metabolites(model, [metabolite_idx]) -end - -@_remove_fn metabolite CoreCoupling Int plural begin - n = copy(model) - n.lm = remove_metabolites(n.lm, metabolite_idxs) - return n -end - -@_remove_fn metabolite CoreCoupling String inplace begin - remove_metabolites!(model, [metabolite_id]) -end - -@_remove_fn metabolite CoreCoupling String inplace plural begin - remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolites(model)))) -end - -@_remove_fn metabolite CoreCoupling String begin - remove_metabolites(model, [metabolite_id]) -end - -@_remove_fn metabolite CoreCoupling String plural begin - remove_metabolites(model, Int.(indexin(metabolite_ids, metabolites(model)))) -end - -""" -$(TYPEDSIGNATURES) - -Forwards arguments to [`change_objective!`](@ref) of the internal model. -""" -function change_objective!(model::CoreCoupling, args...; kwargs...) - change_objective!(model.lm, args...; kwargs...) -end diff --git a/src/reconstruction/Reaction.jl b/src/reconstruction/Reaction.jl deleted file mode 100644 index 3fe897e1b..000000000 --- a/src/reconstruction/Reaction.jl +++ /dev/null @@ -1,74 +0,0 @@ -""" -$(TYPEDEF) - -A small helper type for constructing reactions inline - -# Fields -$(TYPEDFIELDS) -""" -struct _Stoichiometry - s::Dict{String,Float64} -end - -const _Stoichiometrizable = Union{Metabolite,_Stoichiometry} - -Base.convert(::Type{_Stoichiometry}, ::Nothing) = _Stoichiometry(Dict()) -Base.convert(::Type{_Stoichiometry}, m::Metabolite) = _Stoichiometry(Dict(m.id => 1.0)) - -Base.:*(a::Real, m::Metabolite) = _Stoichiometry(Dict(m.id => a)) - -""" -$(TYPEDSIGNATURES) - -Shorthand for `metabolite1 + metabolite2`. Add 2 groups of [`Metabolite`](@ref)s -together to form reactions inline. Use with `+`, `*`, [`→`](@ref) and similar -operators. -""" -function Base.:+(a::_Stoichiometrizable, b::_Stoichiometrizable) - ad = convert(_Stoichiometry, a).s - bd = convert(_Stoichiometry, b).s - _Stoichiometry( - Dict( - mid => get(ad, mid, 0.0) + get(bd, mid, 0.0) for - mid in union(keys(ad), keys(bd)) - ), - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function _make_reaction_dict(r, p) - rd = convert(_Stoichiometry, r).s - pd = convert(_Stoichiometry, p).s - return Dict{String,Float64}( - mid => get(pd, mid, 0.0) - get(rd, mid, 0.0) for mid in union(keys(rd), keys(pd)) - ) -end - -""" -$(TYPEDSIGNATURES) - -Shorthand for `substrates → products`. Make a forward-only [`Reaction`](@ref) -from `substrates` and `products`. -""" -→(substrates::Maybe{_Stoichiometrizable}, products::Maybe{_Stoichiometrizable}) = - Reaction("", _make_reaction_dict(substrates, products), :forward) - -""" -$(TYPEDSIGNATURES) - -Shorthand for `substrates ← products`. Make a reverse-only [`Reaction`](@ref) -from `substrates` and `products`. -""" -←(substrates::Maybe{_Stoichiometrizable}, products::Maybe{_Stoichiometrizable}) = - Reaction("", _make_reaction_dict(substrates, products), :reverse) - -""" -$(TYPEDSIGNATURES) - -Shorthand for `substrates ↔ products`. Make a bidirectional (reversible) -[`Reaction`](@ref) from `substrates` and `products`. -""" -↔(substrates::Maybe{_Stoichiometrizable}, products::Maybe{_Stoichiometrizable}) = - Reaction("", _make_reaction_dict(substrates, products), :bidirectional) diff --git a/src/reconstruction/SerializedModel.jl b/src/reconstruction/SerializedModel.jl deleted file mode 100644 index 8f9d66f2c..000000000 --- a/src/reconstruction/SerializedModel.jl +++ /dev/null @@ -1,20 +0,0 @@ - -# this just generates the necessary wrappers - -@_serialized_change_unwrap add_reactions -@_serialized_change_unwrap change_bound -@_serialized_change_unwrap change_bounds -@_serialized_change_unwrap remove_metabolite -@_serialized_change_unwrap remove_metabolites -@_serialized_change_unwrap remove_reaction -@_serialized_change_unwrap remove_reactions - -""" -$(TYPEDSIGNATURES) - -Returns the model stored in the serialized structure. -""" -function unwrap_serialized(model::Serialized) - precache!(model) - model.m -end diff --git a/src/reconstruction/StandardModel.jl b/src/reconstruction/StandardModel.jl deleted file mode 100644 index 89c77f8eb..000000000 --- a/src/reconstruction/StandardModel.jl +++ /dev/null @@ -1,260 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Add `rxns` to `model` based on reaction `id`. -""" -function add_reactions!(model::StandardModel, rxns::Vector{Reaction}) - for rxn in rxns - model.reactions[rxn.id] = rxn - end - nothing -end - -""" -$(TYPEDSIGNATURES) - -Add `rxn` to `model` based on reaction `id`. -""" -add_reaction!(model::StandardModel, rxn::Reaction) = add_reactions!(model, [rxn]) - -""" -$(TYPEDSIGNATURES) - -Add `mets` to `model` based on metabolite `id`. -""" -function add_metabolites!(model::StandardModel, mets::Vector{Metabolite}) - for met in mets - model.metabolites[met.id] = met - end - nothing -end - -""" -$(TYPEDSIGNATURES) - -Add `met` to `model` based on metabolite `id`. -""" -add_metabolite!(model::StandardModel, met::Metabolite) = add_metabolites!(model, [met]) - -""" -$(TYPEDSIGNATURES) - -Add `genes` to `model` based on gene `id`. -""" -function add_genes!(model::StandardModel, genes::Vector{Gene}) - for gene in genes - model.genes[gene.id] = gene - end - nothing -end - -""" -$(TYPEDSIGNATURES) - -Add `gene` to `model` based on gene `id`. -""" -add_gene!(model::StandardModel, gene::Gene) = add_genes!(model, [gene]) - -""" -$(TYPEDSIGNATURES) - -Shortcut to add multiple reactions and their lower and upper bounds - -Call variants -------------- -``` -@add_reactions! model begin - reaction_name, reaction -end - -@add_reactions! model begin - reaction_name, reaction, lower_bound -end - -@add_reactions! model begin - reaction_name, reaction, lower_bound, upper_bound -end -``` - -Examples --------- -``` -@add_reactions! model begin - "v1", nothing → A, 0, 500 - "v2", A ↔ B + C, -500 - "v3", B + C → nothing -end -``` -""" -macro add_reactions!(model::Symbol, ex::Expr) - model = esc(model) - all_reactions = Expr(:block) - for line in MacroTools.striplines(ex).args - args = line.args - id = esc(args[1]) - reaction = esc(args[2]) - push!(all_reactions.args, :(r = $reaction)) - push!(all_reactions.args, :(r.id = $id)) - if length(args) == 3 - lb = args[3] - push!(all_reactions.args, :(r.lb = $lb)) - elseif length(args) == 4 - lb = args[3] - ub = args[4] - push!(all_reactions.args, :(r.lb = $lb)) - push!(all_reactions.args, :(r.ub = $ub)) - end - push!(all_reactions.args, :(add_reaction!($model, r))) - end - return all_reactions -end - -""" -$(TYPEDSIGNATURES) - -Remove all genes with `ids` from `model`. If `knockout_reactions` is true, then also -constrain reactions that require the genes to function to carry zero flux. - -# Example -``` -remove_genes!(model, ["g1", "g2"]) -``` -""" -function remove_genes!( - model::StandardModel, - gids::Vector{String}; - knockout_reactions::Bool = false, -) - if knockout_reactions - rm_reactions = String[] - for (rid, r) in model.reactions - if !isnothing(r.grr) && - all(any(in.(gids, Ref(conjunction))) for conjunction in r.grr) - push!(rm_reactions, rid) - end - end - pop!.(Ref(model.reactions), rm_reactions) - end - pop!.(Ref(model.genes), gids) - nothing -end - -""" -$(TYPEDSIGNATURES) - -Remove gene with `id` from `model`. If `knockout_reactions` is true, then also -constrain reactions that require the genes to function to carry zero flux. - -# Example -``` -remove_gene!(model, "g1") -``` -""" -remove_gene!(model::StandardModel, gid::String; knockout_reactions::Bool = false) = - remove_genes!(model, [gid]; knockout_reactions = knockout_reactions) - - -@_change_bounds_fn StandardModel String inplace begin - isnothing(lower) || (model.reactions[rxn_id].lb = lower) - isnothing(upper) || (model.reactions[rxn_id].ub = upper) - nothing -end - -@_change_bounds_fn StandardModel String inplace plural begin - for (i, l, u) in zip(rxn_ids, lower, upper) - change_bound!(model, i, lower = l, upper = u) - end -end - -@_change_bounds_fn StandardModel String begin - change_bounds(model, [rxn_id], lower = [lower], upper = [upper]) -end - -@_change_bounds_fn StandardModel String plural begin - n = copy(model) - n.reactions = copy(model.reactions) - for i in rxn_ids - n.reactions[i] = copy(n.reactions[i]) - end - change_bounds!(n, rxn_ids, lower = lower, upper = upper) - return n -end - -@_remove_fn reaction StandardModel String inplace begin - if !(reaction_id in reactions(model)) - @_models_log @info "Reaction $reaction_id not found in model." - else - delete!(model.reactions, reaction_id) - end - nothing -end - -@_remove_fn reaction StandardModel String inplace plural begin - remove_reaction!.(Ref(model), reaction_ids) - nothing -end - -@_remove_fn reaction StandardModel String begin - remove_reactions(model, [reaction_id]) -end - -@_remove_fn reaction StandardModel String plural begin - n = copy(model) - n.reactions = copy(model.reactions) - remove_reactions!(n, reaction_ids) - return n -end - -@_remove_fn metabolite StandardModel String inplace begin - remove_metabolites!(model, [metabolite_id]) -end - -@_remove_fn metabolite StandardModel String inplace plural begin - !all(in.(metabolite_ids, Ref(metabolites(model)))) && - @_models_log @info "Some metabolites not found in model." - remove_reactions!( - model, - [ - rid for (rid, rn) in model.reactions if - any(haskey.(Ref(rn.metabolites), metabolite_ids)) - ], - ) - delete!.(Ref(model.metabolites), metabolite_ids) - nothing -end - -@_remove_fn metabolite StandardModel String begin - remove_metabolites(model, [metabolite_id]) -end - -@_remove_fn metabolite StandardModel String plural begin - n = copy(model) - n.reactions = copy(model.reactions) - n.metabolites = copy(model.metabolites) - remove_metabolites!(n, metabolite_ids) - return n -end - -""" -$(TYPEDSIGNATURES) - -Change the objective for `model` to reaction(s) with `rxn_ids`, optionally specifying their `weights`. By default, -assume equal weights. If no objective exists in model, sets objective. -""" -function change_objective!( - model::StandardModel, - rxn_ids::Vector{String}; - weights = ones(length(rxn_ids)), -) - all(!haskey(model.reactions, rid) for rid in rxn_ids) && - throw(DomainError(rxn_ids, "Some reaction ids were not found in model.")) - - for (_, rxn) in model.reactions # reset to zero - rxn.objective_coefficient = 0.0 - end - for (k, rid) in enumerate(rxn_ids) - model.reactions[rid].objective_coefficient = weights[k] - end -end - -change_objective!(model::StandardModel, rxn_id::String) = change_objective!(model, [rxn_id]) diff --git a/src/reconstruction/community.jl b/src/reconstruction/community.jl deleted file mode 100644 index a2ca155a5..000000000 --- a/src/reconstruction/community.jl +++ /dev/null @@ -1,542 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Add an objective column to the `community` model with optional id `objective_id`. Supply a -dictionary mapping the string names of the objective metabolites to their weights in -`objective_mets_weights`. Note, the weights are negated inside the function so that positive -weights are seen as reagents/substrates, NOT products in the reaction equation. - -# Example -``` -add_community_objective!(model, Dict("met1"=>1.0, "met2"=>2.0)) -``` - -See also: [`update_community_objective!`](@ref) -""" -function add_community_objective!( - community::CoreModel, - objective_mets_weights::Dict{String,Float64}; - objective_id = "community_biomass", -) - obj_inds = indexin(keys(objective_mets_weights), metabolites(community)) - nothing in obj_inds && throw(ArgumentError("Some objective metabolite(s) not found.")) - - nr, _ = size(community.S) - objcol = spzeros(nr) - objcol[obj_inds] .= -collect(values(objective_mets_weights)) - - # extend model by one reaction - community.S = hcat(community.S, objcol) - community.xl = [community.xl; 0.0] - community.xu = [community.xu; _constants.default_reaction_bound] - community.rxns = [community.rxns; objective_id] - community.c = spzeros(size(community.S, 2)) - community.c[end] = 1.0 - - return nothing # stop annoying return value -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`add_community_objective!`] that takes a `StandardModel` community model as input. -""" -function add_community_objective!( - community::StandardModel, - objective_mets_weights::Dict{String,Float64}; - objective_id = "community_biomass", -) - nothing in indexin(keys(objective_mets_weights), metabolites(community)) && - throw(ArgumentError("Some objective metabolite(s) not found.")) - - rdict = Dict(k => -float(v) for (k, v) in objective_mets_weights) - rxn = Reaction(objective_id) - rxn.metabolites = rdict - rxn.lb = 0.0 - rxn.ub = _constants.default_reaction_bound - rxn.objective_coefficient = 1.0 - community.reactions[rxn.id] = rxn - - return nothing # stop annoying return value -end - -""" -$(TYPEDSIGNATURES) - -Update the weights for the objective column with id `objective_id` in `community` using -`objective_mets_weights`, which maps metabolite ids to weights. The current weights are -reset to 0 before being updated to the supplied weights. Note, the weights are negated -inside the function so that the objective metabolites are seen as reagents/substrates, NOT -products in the reaction equation. - -# Example -``` -update_community_objective!(model, "community_biomass", Dict("met1"=>1.0, "met2"=>2.0)) -``` - -See also: [`add_community_objective!`](@ref) -""" -function update_community_objective!( - community::CoreModel, - objective_id::String, - objective_mets_weights::Dict{String,Float64}, -) - obj_inds = indexin(keys(objective_mets_weights), metabolites(community)) - nothing in obj_inds && throw(ArgumentError("Some objective metabolite(s) not found.")) - - objective_column_index = first(indexin([objective_id], reactions(community))) - community.S[:, objective_column_index] .= 0.0 # reset - community.S[obj_inds, objective_column_index] .= - -collect(values(objective_mets_weights)) - dropzeros!(community.S) - community.c = spzeros(size(community.S, 2)) - community.c[objective_column_index] = 1.0 - community.xl[objective_column_index] = 0.0 - community.xu[objective_column_index] = _constants.default_reaction_bound - - return nothing # stop annoying return value -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`update_community_objective!`] that takes a `StandardModel` community model as input. -""" -function update_community_objective!( - community::StandardModel, - objective_id::String, - objective_mets_weights::Dict{String,Float64}, -) - delete!(community.reactions, objective_id) - add_community_objective!(community, objective_mets_weights; objective_id = objective_id) - - return nothing # stop annoying return value -end - -""" -$(TYPEDSIGNATURES) - -Return a `CoreModel` representing the community model of `models` joined through their -exchange reactions and metabolites in the dictionary `exchange_rxn_mets`, which maps -exchange reactions to their associated metabolite. These exchange reactions and metabolites -link model metabolites to environmental metabolites and reactions. Optionally specify -`model_names` to append a specific name to each reaction and metabolite of an organism for -easier reference (default is `species_i` for each model index i in `models`). Note, the -bounds of the environmental variables are all set to zero. Thus, to run a simulation you -need to constrain them appropriately. All the other bounds are inherited from the models -used to construct the community model. - -If `biomass_ids` is supplied, then a community model is returned that has an extra reaction -added to the end of the stoichiometric matrix (last column) that can be assigned as the -objective reaction. It also creates biomass "metabolites" that can be used in this objective -reaction. In the returned mode, these biomass metabolites are produced by the reaction -corresponding to `biomass_ids` in each model respectively. Note, this reaction is -unspecified, further action needs to be taken to specify it, e.g. assign weights to the last -column of the stoichiometric matrix in the rows corresponding to the biomass metabolites. - -To further clarify how this `join` works. Suppose you have 2 organisms with stoichiometric -matrices S₁ and S₂ and you want to link them with `exchange_rxn_mets = Dict(er₁ => em₁, er₂ -=> em₂, er₃ => em₃, ...)`. Then a new community stoichiometric matrix is constructed that -looks like this: -``` - _ er₁ er₂ er₃ ... b_ - |S₁ | - | S₂ | - em₁| | -S = em₂| | - em₃| | - ...| | - bm₁| | - bm₂|_ _| - -``` -The exchange reactions in each model get linked to environmental metabolites, `emᵢ`, and -these get linked to environmental exchanges, `erᵢ`. These `erᵢ` behave like normal single -organism exchange reactions. When `biomass_ids` are supplied, each model's biomass reaction -produces a pseudo-metabolite (`bmᵢ`). These can be weighted in column `b`, called the -`community_biomass` reaction in the community model, if desired. Refer to the tutorial if -this is unclear. - -# Example -``` -m1 = load_model(core_model_path) -m2 = load_model(CoreModel, core_model_path) - -# need to list ALL the exchanges that will form part of the entire model -exchange_rxn_mets = Dict(k => first(keys(reaction_stoichiometry(m1, ex_rxn))) - for filter(looks_like_exchange_reaction, reactions(m1))) - -biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] - -community = join_with_exchanges( - CoreModel, - [m1, m2], - exchange_rxn_mets; - biomass_ids = biomass_ids, -) -``` -""" -function join_with_exchanges( - ::Type{CoreModel}, - models::Vector{M}, - exchange_rxn_mets::Dict{String,String}; - biomass_ids = String[], - model_names = String[], -) where {M<:MetabolicModel} - - exchange_rxn_ids = keys(exchange_rxn_mets) - exchange_met_ids = values(exchange_rxn_mets) - add_biomass_objective = isempty(biomass_ids) ? false : true - - # get offsets to construct community S - reaction_lengths = [n_reactions(model) for model in models] - metabolite_lengths = [n_metabolites(model) for model in models] - reaction_offset = [0; cumsum(reaction_lengths[1:end-1])] - metabolite_offset = [0; cumsum(metabolite_lengths[1:end-1])] - - # get each model's S matrix (needed for the size calculations) - stoichs = [stoichiometry(model) for model in models] - nnzs = [findnz(stoich) for stoich in stoichs] # nonzero indices per model - - # size calculations - column_add = add_biomass_objective ? 1 : 0 # objective rxn - row_add = add_biomass_objective ? length(models) : 0 # biomass as metabolites - nnz_add = add_biomass_objective ? (1 + length(models)) : 0 - nnz_total = - sum(length(first(nnz)) for nnz in nnzs) + - length(models) * length(exchange_rxn_ids) + - length(exchange_met_ids) + - nnz_add - n_reactions_metabolic = sum(reaction_lengths) - n_reactions_total = n_reactions_metabolic + length(exchange_rxn_ids) + column_add - n_metabolites_metabolic = sum(metabolite_lengths) - n_metabolites_total = n_metabolites_metabolic + length(exchange_met_ids) + row_add - - # Create empty storage vectors - lbs = spzeros(n_reactions_total) - ubs = spzeros(n_reactions_total) - rxns = Array{String,1}(undef, n_reactions_total) - mets = Array{String,1}(undef, n_metabolites_total) - I = Array{Int,1}(undef, nnz_total) - J = Array{Int,1}(undef, nnz_total) - V = Array{Float64,1}(undef, nnz_total) - - # build metabolic components, block diagonals - kstart = 1 - for i = 1:length(models) - kend = kstart + length(nnzs[i][3]) - 1 - rng = kstart:kend - I[rng] .= nnzs[i][1] .+ metabolite_offset[i] - J[rng] .= nnzs[i][2] .+ reaction_offset[i] - V[rng] .= nnzs[i][3] - kstart += length(nnzs[i][3]) - end - - # build environmental - exchange links - for i = 1:length(models) - exchange_rxn_inds = indexin(exchange_rxn_ids, reactions(models[i])) - exchange_met_inds = indexin(exchange_met_ids, metabolites(models[i])) - for (n, (ex_rxn, ex_met)) in enumerate(zip(exchange_rxn_inds, exchange_met_inds)) # each exchange rxn has one exchange met - isnothing(ex_rxn) && continue - isnothing(ex_met) && continue - # connect environmental metabolite with exchange metabolite - I[kstart] = n_metabolites_metabolic + n - J[kstart] = ex_rxn + reaction_offset[i] - V[kstart] = -stoichs[i][ex_met, ex_rxn] # ex is normally negative, make positive - kstart += 1 - end - end - - # # build diagonal environmental exchanges - for i = 1:length(exchange_rxn_ids) - I[kstart] = n_metabolites_metabolic + i - J[kstart] = n_reactions_metabolic + i - V[kstart] = -1.0 - kstart += 1 - end - - if add_biomass_objective - n_before_biomass_row = n_metabolites_metabolic + length(exchange_met_ids) - for i = 1:length(models) - biomass_ind = first(indexin([biomass_ids[i]], reactions(models[i]))) - I[kstart] = i + n_before_biomass_row - J[kstart] = biomass_ind + reaction_offset[i] - V[kstart] = 1.0 - kstart += 1 - end - end - - S = sparse( - I[1:kstart-1], - J[1:kstart-1], - V[1:kstart-1], - n_metabolites_total, - n_reactions_total, - ) # could be that some microbes don't have all the exchanges, hence kstart-1 - - _reaction_offsets = cumsum(reaction_lengths) - _metabolite_offsets = cumsum(metabolite_lengths) - for i = 1:length(models) - species = isempty(model_names) ? "species_$(i)_" : model_names[i] * "_" - tlbs, tubs = bounds(models[i]) - lbs[reaction_offset[i]+1:_reaction_offsets[i]] .= tlbs - ubs[reaction_offset[i]+1:_reaction_offsets[i]] .= tubs - rxns[reaction_offset[i]+1:_reaction_offsets[i]] = species .* reactions(models[i]) - mets[metabolite_offset[i]+1:_metabolite_offsets[i]] = - species .* metabolites(models[i]) - end - mets[_metabolite_offsets[end]+1:_metabolite_offsets[end]+length(exchange_met_ids)] .= - exchange_met_ids - rxns[_reaction_offsets[end]+1:_reaction_offsets[end]+length(exchange_rxn_ids)] .= - exchange_rxn_ids - - if add_biomass_objective - rxns[end] = "community_biomass" - for i = 1:length(models) - species = isempty(model_names) ? "species_$(i)_" : model_names[i] * "_" - mets[end-length(biomass_ids)+i] = species .* biomass_ids[i] - end - end - - return CoreModel(S, spzeros(size(S, 1)), spzeros(size(S, 2)), lbs, ubs, rxns, mets) -end - -""" -$(TYPEDSIGNATURES) - -A variant of [`join_with_exchanges`](@ref) that returns a `StandardModel`. -""" -function join_with_exchanges( - ::Type{StandardModel}, - models::Vector{M}, - exchange_rxn_mets::Dict{String,String}; - biomass_ids = [], - model_names = [], -)::StandardModel where {M<:MetabolicModel} - - community = StandardModel() - rxns = OrderedDict{String,Reaction}() - mets = OrderedDict{String,Metabolite}() - genes = OrderedDict{String,Gene}() - sizehint!(rxns, sum(n_reactions(m) for m in models) + length(keys(exchange_rxn_mets))) - sizehint!( - mets, - sum(n_metabolites(m) for m in models) + length(values(exchange_rxn_mets)), - ) - sizehint!(genes, sum(n_genes(m) for m in models)) - - for (i, model) in enumerate(models) - species = isempty(model_names) ? "species_$(i)" : model_names[i] # underscore gets added in add_model_with_exchanges! - biomass_id = isempty(biomass_ids) ? nothing : biomass_ids[i] - add_model_with_exchanges!( - community, - model, - exchange_rxn_mets; - model_name = species, - biomass_id = biomass_id, - ) - end - - if !isempty(biomass_ids) - bm = Dict{String,Float64}() # unassigned - community.reactions["community_biomass"] = - Reaction("community_biomass", bm, :forward) - end - - # Add environmental exchange reactions and metabolites.TODO: add annotation details - for (rid, mid) in exchange_rxn_mets - community.reactions[rid] = Reaction(rid) - community.reactions[rid].metabolites = Dict{String,Float64}(mid => -1.0) - community.reactions[rid].lb = 0.0 - community.reactions[rid].ub = 0.0 - community.metabolites[mid] = Metabolite(mid) - community.metabolites[mid].id = mid - end - - return community -end - -""" -$(TYPEDSIGNATURES) - -Add `model` to `community`, which is a pre-existing community model with exchange reactions -and metabolites in the dictionary `exchange_rxn_mets`. The `model_name` is appended to each -reaction and metabolite, see [`join_with_exchanges`](@ref). If `biomass_id` is specified -then a biomass metabolite for `model` is also added to the resulting model. The column -corresponding to the `biomass_id` reaction then produces this new biomass metabolite with -unit coefficient. The exchange reactions and metabolites in `exchange_rxn_mets` must already -exist in `community`. Always returns a new community model because it is more efficient than -resizing all the matrices. - -No in-place variant for `CoreModel`s exists yet. - -# Example -``` -community = add_model_with_exchanges(community, - model, - exchange_rxn_mets; - model_name="species_2", - biomass_id="BIOMASS_Ecoli_core_w_GAM") -``` -""" -function add_model_with_exchanges( - community::CoreModel, - model::MetabolicModel, - exchange_rxn_mets::Dict{String,String}; - model_name = "unknown_species", - biomass_id = nothing, -) - - exchange_rxn_ids = keys(exchange_rxn_mets) - exchange_met_ids = values(exchange_rxn_mets) - exchange_met_community_inds = indexin(exchange_met_ids, metabolites(community)) - exchange_rxn_community_inds = indexin(exchange_rxn_ids, reactions(community)) - if any(isnothing.(exchange_met_community_inds)) || - any(isnothing.(exchange_rxn_community_inds)) - throw( - DomainError( - "exchange metabolite/reaction not found.", - "Exchange metabolites/reactions must already be contained in the community model.", - ), - ) - end - - n_cmodel_rows, n_cmodel_cols = size(stoichiometry(community)) - n_model_rows, n_model_cols = size(stoichiometry(model)) - # A note on the variable names here.Suppose M is some sparse matrix, then I - # = row indices, J = column indices and V = values at the associated - # indices. So I[a] = i, J[a]=j and then M[i,j] = V[a] - Iadd, Jadd, Vadd = findnz(stoichiometry(model)) - - # shift to fit into community - Iadd .+= n_cmodel_rows - Jadd .+= n_cmodel_cols - - # when adding a single model not that many reactions, push! okay? - exchange_rxn_model_idxs = indexin(exchange_rxn_ids, reactions(model)) - for i = 1:length(exchange_rxn_ids) - isnothing(exchange_rxn_model_idxs[i]) && continue - push!(Iadd, exchange_met_community_inds[i]) # already present ex met in community model - push!(Jadd, n_cmodel_cols + exchange_rxn_model_idxs[i]) # new column hence the offset - push!(Vadd, 1.0) - end - - biomass_met = 0.0 - if biomass_id != "" # add biomass metabolite - biomass_rxn = first(indexin([biomass_id], reactions(model))) - push!(Iadd, n_model_rows + n_cmodel_rows + 1) - push!(Jadd, biomass_rxn + n_cmodel_cols) - push!(Vadd, 1.0) - biomass_met = 1 - end - - n_metabolites_total = n_model_rows + n_cmodel_rows + biomass_met - n_reactions_total = n_cmodel_cols + n_model_cols - - I, J, V = findnz(stoichiometry(community)) - I = [I; Iadd] - J = [J; Jadd] - V = [V; Vadd] - S = sparse(I, J, V, n_metabolites_total, n_reactions_total) - - # A note on the variables here. The bounds are vectors of upper and lower - # bounds for each reaction. So lbs = [lb_1, lb_2, lb_i, ...], ubs = [ub_1, - # ub_2, ub_i, ...] for reaction i. See the bounds function for more - # information - lbsadd, ubsadd = bounds(model) - lbs, ubs = bounds(community) - lbs = [lbs; lbsadd] - ubs = [ubs; ubsadd] - - rxnsadd = "$(model_name)_" .* reactions(model) - if !isnothing(biomass_id) - metsadd = ["$(model_name)_" .* metabolites(model); "$(model_name)_" * biomass_id] - else - metsadd = "$(model_name)_" .* metabolites(model) - end - rxns = [reactions(community); rxnsadd] - mets = [metabolites(community); metsadd] - - # adds to the original community data, could possibly reset? - I, V = findnz(balance(community)) - b = sparsevec(I, V, n_metabolites_total) - I, V = findnz(objective(community)) - c = sparsevec(I, V, n_reactions_total) - - return CoreModel(S, b, c, lbs, ubs, rxns, mets) -end - -""" -$(TYPEDSIGNATURES) - -The `StandardModel` variant of [`add_model_with_exchanges`](@ref), but is in-place. -""" -function add_model_with_exchanges!( - community::StandardModel, - model::MetabolicModel, - exchange_rxn_mets::Dict{String,String}; - model_name = "unknown_species", - biomass_id = nothing, -) - stdm = model isa StandardModel ? deepcopy(model) : convert(StandardModel, model) - model_name = model_name * "_" - - for met in values(stdm.metabolites) - met.id = model_name * met.id - community.metabolites[met.id] = met - end - - for rxn in values(stdm.reactions) - # rename reaction string - rxn.metabolites = Dict(model_name * k => v for (k, v) in rxn.metabolites) - # change direction of exchange - if rxn.id in keys(exchange_rxn_mets) - rxn.metabolites[model_name*exchange_rxn_mets[rxn.id]] = 1.0 # exchanges should be negative originally. TODO: test if they are? - end - # add biomass metabolite if applicable - if rxn.id == biomass_id - rxn.metabolites[model_name*rxn.id] = 1.0 # produces one biomass - community.metabolites[model_name*rxn.id] = Metabolite(model_name * rxn.id) - end - # add environmental connection - if rxn.id in keys(exchange_rxn_mets) - rxn.metabolites[exchange_rxn_mets[rxn.id]] = -1.0 - end - # reset objective - rxn.objective_coefficient = 0.0 - rxn.id = model_name * rxn.id - # add to community model - community.reactions[rxn.id] = rxn - end - - for gene in values(stdm.genes) - gene.id = model_name * gene.id - community.genes[gene.id] = gene - end - - return nothing -end - -""" -$(TYPEDSIGNATURES) - -The `StandardModel` variant of [`add_model_with_exchanges`](@ref). Makes a deepcopy of -`community` and calls the inplace variant of this function on that copy. -""" -function add_model_with_exchanges( - community::StandardModel, - model::MetabolicModel, - exchange_rxn_mets::Dict{String,String}; - model_name = "unknown_species", - biomass_id = nothing, -) - new_comm = deepcopy(community) - add_model_with_exchanges!( - new_comm, - model, - exchange_rxn_mets; - model_name = model_name, - biomass_id = biomass_id, - ) - return new_comm -end diff --git a/src/reconstruction/enzymes.jl b/src/reconstruction/enzymes.jl deleted file mode 100644 index 6f22cf800..000000000 --- a/src/reconstruction/enzymes.jl +++ /dev/null @@ -1,17 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Specifies a model variant which adds extra semantics of the sMOMENT algorithm, -giving a [`SMomentModel`](@ref). The arguments are forwarded to -[`make_smoment_model`](@ref). Intended for usage with [`screen`](@ref). -""" -with_smoment(; kwargs...) = model -> make_smoment_model(model; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant which adds extra semantics of the Gecko algorithm, -giving a [`GeckoModel`](@ref). The arguments are forwarded to -[`make_gecko_model`](@ref). Intended for usage with [`screen`](@ref). -""" -with_gecko(; kwargs...) = model -> make_gecko_model(model; kwargs...) diff --git a/src/reconstruction/gapfill_minimum_reactions.jl b/src/reconstruction/gapfill_minimum_reactions.jl deleted file mode 100644 index e3828e8fb..000000000 --- a/src/reconstruction/gapfill_minimum_reactions.jl +++ /dev/null @@ -1,210 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Find a minimal set of reactions from `universal_reactions` that should be added -to `model` so that the model has a feasible solution with bounds on its -objective function given in `objective_bounds`. Weights of the added reactions -may be specified in `weights` to prefer adding reactions with lower weights. - -Internally, this builds and solves a mixed integer program, following -the method of Reed et al. (Reed, Jennifer L., et al. "Systems approach to -refining genome annotation." *Proceedings of the National Academy of Sciences* -(2006)). - -The function returns a solved JuMP optimization model, with the boolean -reaction inclusion indicators in variable vector `y`. Use -[`gapfilled_mask`](@ref) or [`gapfilled_rids`](@ref) to collect the reaction -information in Julia datatypes. - -To reduce the uncertainty in the MILP solver (and likely reduce the -complexity), you may put a limit on the size of the added reaction set in -`maximum_new_reactions`. - -# Common pitfalls - -If [`gapfill_minimum_reactions`](@ref) is supposed to generate any reasonable -output, the input model *MUST NOT* be feasible, otherwise there is "no work to -do" and no reactions are added. Notably, an inactive model (the flux is zero) -is considered to be feasible. If this is the case, [`gapfilled_rids`](@ref) -will return an empty vector (as opposed to `nothing`). - -To prevent this, you may need to modify the model to disallow the trivial -solutions (for example by putting a lower bound on reactions that you expect to -be working in the solved model, in a similar manner like how the ATP -maintenance reaction is bounded in E. Coli "core" model). The -`objective_bounds` parameter makes this easier by directly placing a bound on -the objective value of the model, which typically forces the model to be -active. - -The `maximum_new_reactions` parameter may have critical impact on performance -in some solvers, because (in a general worst case) there is -`2^maximum_new_reactions` model variants to be examined. Putting a hard limit -on the reaction count may serve as a heuristic that helps the solver not to -waste too much time solving impractically complex subproblems. -""" -function gapfill_minimum_reactions( - model::MetabolicModel, - universal_reactions::Vector{Reaction}, - optimizer; - objective_bounds = (_constants.tolerance, _constants.default_reaction_bound), - maximum_new_reactions = length(universal_reactions), - weights = fill(1.0, length(universal_reactions)), - modifications = [], -) - precache!(model) - - # constraints from universal reactions that can fill gaps - univs = _universal_stoichiometry(universal_reactions, metabolites(model)) - - # add space for additional metabolites and glue with the universal reaction - # stoichiometry - extended_stoichiometry = [[ - stoichiometry(model) - spzeros(length(univs.new_mids), n_reactions(model)) - ] univs.stoichiometry] - - # make the model anew (we can't really use make_optimization_model because - # we need the balances and several other things completely removed. Could - # be solved either by parametrizing make_optimization_model or by making a - # tiny temporary wrapper for this. - # keep this in sync with src/base/solver.jl, except for adding balances. - opt_model = Model(optimizer) - @variable(opt_model, x[1:n_reactions(model)]) - xl, xu = bounds(model) - @constraint(opt_model, lbs, xl .<= x) - @constraint(opt_model, ubs, x .<= xu) - - C = coupling(model) - isempty(C) || begin - cl, cu = coupling_bounds(model) - @constraint(opt_model, c_lbs, cl .<= C * x) - @constraint(opt_model, c_ubs, C * x .<= cu) - end - - # add the variables for new stuff - @variable(opt_model, ux[1:length(universal_reactions)]) # fluxes from universal reactions - @variable(opt_model, y[1:length(universal_reactions)], Bin) # indicators - - # combined metabolite balances - @constraint( - opt_model, - extended_stoichiometry * [x; ux] .== - [balance(model); zeros(length(univs.new_mids))] - ) - - # objective bounds - @constraint(opt_model, objective_bounds[1] <= objective(model)' * x) - @constraint(opt_model, objective_bounds[2] >= objective(model)' * x) - - # flux bounds of universal reactions with indicators - @constraint(opt_model, ulb, univs.lbs .* y .<= ux) - @constraint(opt_model, uub, univs.ubs .* y .>= ux) - - # minimize the total number of indicated reactions - @objective(opt_model, Min, weights' * y) - - # limit the number of indicated reactions - # (prevents the solver from exploring too far) - @constraint(opt_model, sum(y) <= maximum_new_reactions) - - # apply all modifications - for mod in modifications - mod(model, opt_model) - end - - optimize!(opt_model) - - return opt_model -end - -""" -$(TYPEDSIGNATURES) - -Get a `BitVector` of added reactions from the model solved by -[`gapfill_minimum_reactions`](@ref). The bit indexes correspond to the indexes -of `universal_reactions` given to the gapfilling function. In case the model is -not solved, this returns `nothing`. - -If this function returns a zero vector (instead of `nothing`), it is very -likely that the original model was already feasible and you may need to -constraint it more. Refer to "pitfalls" section in the documentation of -[`gapfill_minimum_reactions`](@ref) for more details. - -# Example - - gapfill_minimum_reactions(myModel, myReactions, Tulip.Optimizer) |> gapfilled_mask -""" -gapfilled_mask(opt_model)::BitVector = - is_solved(opt_model) ? value.(opt_model[:y]) .> 0 : nothing - -""" -$(TYPEDSIGNATURES) - -Utility to extract a short vector of IDs of the reactions added by the -gapfilling algorithm. Use with `opt_model` returned from -[`gapfill_minimum_reactions`](@ref). - -If this function returns an empty vector (instead of `nothing`), it is very -likely that the original model was already feasible and you may need to -constraint it more. Refer to "pitfalls" section in the documentation of -[`gapfill_minimum_reactions`](@ref) for more details. -""" -gapfilled_rids(opt_model, universal_reactions::Vector{Reaction}) = - let v = gapfilled_mask(opt_model) - isnothing(v) ? nothing : [rxn.id for rxn in universal_reactions[v]] - end - -""" -$(TYPEDSIGNATURES) - -Overload of [`gapfilled_rids`](@ref) that can be piped easily. - -# Example - - gapfill_minimum_reactions(myModel, myReactions, Tulip.Optimizer) |> gapfilled_rids(myReactions) -""" -gapfilled_rids(universal_reactions::Vector{Reaction}) = - opt_model -> gapfilled_rids(opt_model, universal_reactions) - -""" -$(TYPEDSIGNATURES) - -A helper function that constructs the stoichiometric matrix of a set of -`universal_reactions`. The order of the metabolites is determined with -`mids`, so that this stoichiometric matrix can be combined with -another one. -""" -function _universal_stoichiometry(urxns::Vector{Reaction}, mids::Vector{String}) - - # traversal over all elements in stoichiometry of universal_reactions - stoiMap(f) = [ - f(ridx, mid, stoi) for (ridx, rxn) in enumerate(urxns) for - (mid, stoi) in rxn.metabolites - ] - - # make an index and find new metabolites - met_id_lookup = Dict(mids .=> eachindex(mids)) - - new_mids = - collect(Set(filter(x -> !haskey(met_id_lookup, x), stoiMap((_, mid, _) -> mid)))) - all_mids = vcat(mids, new_mids) - - # remake the index with all metabolites - met_id_lookup = Dict(all_mids .=> eachindex(all_mids)) - - # build the result - return ( - stoichiometry = float.( - sparse( - stoiMap((_, mid, _) -> met_id_lookup[mid]), - stoiMap((ridx, _, _) -> ridx), - stoiMap((_, _, stoi) -> stoi), - length(all_mids), - length(urxns), - ), - ), - lbs = [rxn.lb for rxn in urxns], - ubs = [rxn.ub for rxn in urxns], - new_mids = new_mids, - ) -end diff --git a/src/reconstruction/modifications/generic.jl b/src/reconstruction/modifications/generic.jl deleted file mode 100644 index 383366500..000000000 --- a/src/reconstruction/modifications/generic.jl +++ /dev/null @@ -1,56 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that has a new bound set. Forwards arguments to -[`change_bound`](@ref). Intended for usage with [`screen`](@ref). -""" -with_changed_bound(args...; kwargs...) = m -> change_bound(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that has new bounds set. Forwards arguments to -[`change_bounds`](@ref). Intended for usage with [`screen`](@ref). -""" -with_changed_bounds(args...; kwargs...) = m -> change_bounds(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant without a certain metabolite. Forwards arguments to -[`remove_metabolite`](@ref). Intended to be used with [`screen`](@ref). -""" -with_removed_metabolite(args...; kwargs...) = m -> remove_metabolite(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Plural version of [`with_removed_metabolite`](@ref), calls -[`remove_metabolites`](@ref) internally. -""" -with_removed_metabolites(args...; kwargs...) = - m -> remove_metabolites(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with reactions added. Forwards the arguments to -[`add_reactions`](@ref). Intended to be used with [`screen`](@ref). -""" -with_added_reactions(args...; kwargs...) = m -> add_reactions(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant without a certain reaction. Forwards arguments to -[`remove_reaction`](@ref). Intended to be used with [`screen`](@ref). -""" -with_removed_reaction(args...; kwargs...) = m -> remove_reaction(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Plural version of [`with_removed_reaction`](@ref), calls -[`remove_reactions`](@ref) internally. -""" -with_removed_reactions(args...; kwargs...) = m -> remove_reactions(m, args...; kwargs...) diff --git a/src/solver.jl b/src/solver.jl new file mode 100644 index 000000000..fee6ac07d --- /dev/null +++ b/src/solver.jl @@ -0,0 +1,151 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDEF) + +Representation of a "binary switch" bound for `ConstraintTree`s. The value is +constrained to be either the value of field `a` or of field `b`; both fields +are `Float64`s. Upon translation to JuMP, the switches create an extra boolean +variable, and the value is constrained to equal `a + boolean_var * (b-a)`. + +Switches can be offset by adding real numbers, negated, and multiplied and +divided by scalar constraints. For optimizing some special cases, multiplying +by exact zero returns an equality bound to zero. + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct Switch <: C.Bound + "One choice" + a::Float64 + + "The other choice" + b::Float64 +end + +export Switch + +Base.:-(x::Switch) = Switch(-s.a, -s.b) +Base.:+(x::Real, s::Switch) = b + a +Base.:+(s::Switch, x::Real) = Switch(s.a + x, s.b + x) +Base.:*(x::Real, s::Switch) = b * a +Base.:*(s::Switch, x::Real) = x == 0 ? C.EqualTo(0) : Switch(s.a * x, s.b * x) +Base.:/(s::Switch, x::Real) = Switch(s.a / x, s.b / x) + +""" +$(TYPEDSIGNATURES) + +Construct a JuMP `Model` that describes the precise constraint system into the +JuMP `Model` created for solving in `optimizer`, with a given optional +`objective` and optimization `sense` chosen from [`Maximal`](@ref), +[`Minimal`](@ref) and [`Feasible`](@ref). +""" +function optimization_model( + cs::C.ConstraintTreeElem; + objective::Union{Nothing,C.Value} = nothing, + optimizer, + sense = Maximal, +) + model = J.Model(optimizer) + + J.@variable(model, x[1:C.var_count(cs)]) + isnothing(objective) || J.@objective(model, sense, C.substitute(objective, x)) + + # constraints + function add_constraint(v::C.Value, b::C.EqualTo) + J.@constraint(model, C.substitute(v, x) == b.equal_to) + end + function add_constraint(v::C.Value, b::C.Between) + vx = C.substitute(v, x) + isinf(b.lower) || J.@constraint(model, vx >= b.lower) + isinf(b.upper) || J.@constraint(model, vx <= b.upper) + end + function add_constraint(v::C.Value, b::Switch) + boolean = J.@variable(model, binary = true) + J.@constraint(model, C.substitute(v, x) == b.a + boolean * (b.b - b.a)) + end + add_constraint(::C.Value, _::Nothing) = nothing + function add_constraint(c::C.Constraint) + add_constraint(c.value, c.bound) + end + function add_constraint(c::C.ConstraintTree) + add_constraint.(values(c)) + end + + add_constraint(cs) + + return model +end + +export optimization_model + +""" +$(TYPEDSIGNATURES) + +`true` if `opt_model` solved successfully (solution is optimal or +locally optimal). `false` if any other termination status is reached. +""" +is_solved(opt_model::J.Model) = + J.termination_status(opt_model) in [J.MOI.OPTIMAL, J.MOI.LOCALLY_SOLVED] + +export is_solved + +""" + Minimal + +Objective sense for finding the minimal value of the objective. + +Same as `JuMP.MIN_SENSE`. +""" +const Minimal = J.MIN_SENSE +export Minimal + +""" + Maximal + +Objective sense for finding the maximal value of the objective. + +Same as `JuMP.MAX_SENSE`. +""" +const Maximal = J.MAX_SENSE +export Maximal + +""" + Maximal + +Objective sense for finding the any feasible value of the objective. + +Same as `JuMP.FEASIBILITY_SENSE`. +""" +const Feasible = J.FEASIBILITY_SENSE +export Feasible + +""" +$(TYPEDSIGNATURES) + +Like [`optimized_constraints`](@ref), but works directly with a given JuMP +model `om` without applying any settings or creating the optimization model. + +To run the process manually, you can use [`optimization_model`](@ref) to +convert the constraints into a suitable JuMP optimization model. +""" +function optimized_model(om; output::C.ConstraintTreeElem) + J.optimize!(om) + is_solved(om) ? C.substitute_values(output, J.value.(om[:x])) : nothing +end + +export optimized_model diff --git a/src/types.jl b/src/types.jl new file mode 100644 index 000000000..4aa608ac3 --- /dev/null +++ b/src/types.jl @@ -0,0 +1,22 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" + Maybe{X} + +Type of optional values. +""" +const Maybe{X} = Union{Nothing,X} diff --git a/src/worker_data.jl b/src/worker_data.jl new file mode 100644 index 000000000..e17bbc07d --- /dev/null +++ b/src/worker_data.jl @@ -0,0 +1,63 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDEF) + +Helper struct that provides access to local data that are unboxed and cached +directly on distributed workers. + +Use with [`get_worker_local_data`](@ref) and `Distributed.CachingPool`. + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct worker_local_data + "The data that is transferred to the remote worker" + transfer_data::Any + "The data that is cached on the remote worker" + local_data::Union{Some,Nothing} + """ + The function that converts the transferred data to locally-cached data on + the remote worker + """ + transform::Function + + """ + $(TYPEDSIGNATURES) + + Conveniently create a data pack to cache on the remote workers. `f` + receives a single input (the transfer data `x`) and should produce the + worker-local data. + """ + worker_local_data(f, x) = new(x, nothing, f) +end + +""" +$(TYPEDSIGNATURES) + +"Unwrap" the [`worker_local_data`](@ref) on a remote worker to get the +`local_data` out. If required, executes the `transform` function. + +Local copies of `transfer_data` are forgotten after the function executes. +""" +function get_worker_local_data(x::worker_local_data) + if isnothing(x.local_data) + x.local_data = Some(x.transform(x.transfer_data)) + x.transfer_data = nothing + end + some(x.local_data) +end diff --git a/test/analysis/envelopes.jl b/test/analysis/envelopes.jl deleted file mode 100644 index 967a1f063..000000000 --- a/test/analysis/envelopes.jl +++ /dev/null @@ -1,40 +0,0 @@ - -@testset "Envelopes" begin - m = load_model(model_paths["e_coli_core.xml"]) - - rxns = [1, 2, 3] - - lat = collect.(envelope_lattice(m, rxns; samples = 3)) - @test lat == [[-1000.0, 0.0, 1000.0], [-1000.0, 0.0, 1000.0], [-1000.0, 0.0, 1000.0]] - @test lat == collect.(envelope_lattice(m, reactions(m)[rxns]; samples = 3)) - - vals = - objective_envelope( - m, - reactions(m)[rxns], - Tulip.Optimizer; - lattice_args = (samples = 3, ranges = [(-5, 0), (-5, 0), (-5, 5)]), - workers = W, - ).values - - @test size(vals) == (3, 3, 3) - @test count(isnothing, vals) == 15 - @test isapprox( - filter(!isnothing, vals), - [ - 0.5833914451564178, - 0.5722033617617296, - 0.673404874265479, - 0.5610152783118744, - 0.6606736594947443, - 0.7593405739802911, - 0.7020501075121626, - 0.689318892439569, - 0.787985806919745, - 0.6765876776826879, - 0.7752545924613183, - 0.8739215069575214, - ], - atol = TEST_TOLERANCE, - ) -end diff --git a/test/analysis/fba_with_crowding.jl b/test/analysis/fba_with_crowding.jl deleted file mode 100644 index 7ecb187b4..000000000 --- a/test/analysis/fba_with_crowding.jl +++ /dev/null @@ -1,25 +0,0 @@ -@testset "FBA with crowding constraints" begin - model = load_model(model_paths["e_coli_core.json"]) - rid_weight = Dict( - rid => 0.004 for rid in reactions(model) if - !looks_like_biomass_reaction(rid) && !looks_like_exchange_reaction(rid) - ) - - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_optimizer_attribute("IPM_IterationsLimit", 1000), - add_crowding_constraints(rid_weight), - change_constraint("EX_glc__D_e"; lb = -6), - ], - ) - - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 0.491026987015203, - atol = TEST_TOLERANCE, - ) - - @test isapprox(sol["EX_ac_e"], 0.7084745257320869, atol = TEST_TOLERANCE) -end diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl deleted file mode 100644 index 51215f321..000000000 --- a/test/analysis/flux_balance_analysis.jl +++ /dev/null @@ -1,119 +0,0 @@ -@testset "Flux balance analysis with CoreModel" begin - cp = test_simpleLP() - lp = flux_balance_analysis(cp, Tulip.Optimizer) - @test termination_status(lp) == MOI.OPTIMAL - sol = JuMP.value.(lp[:x]) - @test sol ≈ [1.0, 2.0] - - # test the maximization of the objective - cp = test_simpleLP2() - lp = flux_balance_analysis(cp, Tulip.Optimizer) - @test termination_status(lp) == MOI.OPTIMAL - sol = JuMP.value.(lp[:x]) - @test sol ≈ [-1.0, 2.0] - - # test with a more biologically meaningfull model - cp = load_model(CoreModel, model_paths["iJR904.mat"]) - expected_optimum = 0.9219480950504393 - - lp = flux_balance_analysis(cp, Tulip.Optimizer) - @test termination_status(lp) == MOI.OPTIMAL - sol = JuMP.value.(lp[:x]) - @test isapprox(objective_value(lp), expected_optimum, atol = TEST_TOLERANCE) - @test isapprox(cp.c' * sol, expected_optimum, atol = TEST_TOLERANCE) - - # test the "nicer output" variants - fluxes_vec = flux_balance_analysis_vec(cp, Tulip.Optimizer) - @test all(fluxes_vec .== sol) - fluxes_dict = flux_balance_analysis_dict(cp, Tulip.Optimizer) - rxns = reactions(cp) - @test all([fluxes_dict[rxns[i]] == sol[i] for i in eachindex(rxns)]) -end - -@testset "Flux balance analysis with StandardModel" begin - - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lb = -12, ub = -12), - change_sense(MAX_SENSE), - change_optimizer_attribute("IPM_IterationsLimit", 110), - ], - ) - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 1.0572509997013568, - atol = TEST_TOLERANCE, - ) - - pfl_frac = 0.8 - biomass_frac = 0.2 - sol_multi = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_objective( - ["BIOMASS_Ecoli_core_w_GAM", "PFL"]; - weights = [biomass_frac, pfl_frac], - ), - ], - ) - @test isapprox( - biomass_frac * sol_multi["BIOMASS_Ecoli_core_w_GAM"] + pfl_frac * sol_multi["PFL"], - 31.999999998962604, - atol = TEST_TOLERANCE, - ) - - @test_throws DomainError flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_constraint("gbbrsh"; lb = -12, ub = -12)], - ) - @test_throws DomainError flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_objective("gbbrsh")], - ) - @test_throws DomainError flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_objective(["BIOMASS_Ecoli_core_w_GAM"; "gbbrsh"])], - ) -end - -@testset "Flux balance analysis with CoreModelCoupled" begin - - model = load_model(CoreModel, model_paths["e_coli_core.json"]) - - # assume coupling constraints of the form: - # -γ ≤ vᵢ/μ ≤ γ - # I.e., enforces that the ratio between any reaction flux - # and the growth rate is bounded by γ. - γ = 40 - - # construct coupling bounds - nr = n_reactions(model) - biomass_index = first(indexin(["BIOMASS_Ecoli_core_w_GAM"], reactions(model))) - - Cf = sparse(1.0I, nr, nr) - Cf[:, biomass_index] .= -γ - - Cb = sparse(1.0I, nr, nr) - Cb[:, biomass_index] .= γ - - C = [Cf; Cb] - - clb = spzeros(2 * nr) - clb[1:nr] .= -1000.0 - cub = spzeros(2 * nr) - cub[nr+1:end] .= 1000 - - cmodel = CoreModelCoupled(model, C, clb, cub) # construct - - dc = flux_balance_analysis_dict(cmodel, Tulip.Optimizer) - @test isapprox(dc["BIOMASS_Ecoli_core_w_GAM"], 0.665585699298256, atol = TEST_TOLERANCE) -end diff --git a/test/analysis/flux_variability_analysis.jl b/test/analysis/flux_variability_analysis.jl deleted file mode 100644 index 8d384ce5b..000000000 --- a/test/analysis/flux_variability_analysis.jl +++ /dev/null @@ -1,95 +0,0 @@ -@testset "Flux variability analysis" begin - cp = test_simpleLP() - optimizer = Tulip.Optimizer - fluxes = flux_variability_analysis(cp, optimizer) - - @test size(fluxes) == (2, 2) - @test fluxes ≈ [ - 1.0 1.0 - 2.0 2.0 - ] - - rates = reaction_variability_analysis(cp, optimizer) - @test fluxes == rates - - fluxes = flux_variability_analysis(cp, [2], optimizer) - - @test size(fluxes) == (1, 2) - @test isapprox(fluxes, [2 2], atol = TEST_TOLERANCE) - - # a special testcase for slightly sub-optimal FVA (gamma<1) - cp = CoreModel( - [-1.0 -1.0 -1.0], - [0.0], - [1.0, 0.0, 0.0], - [0.0, 0.0, -1.0], - 1.0 * ones(3), - ["r$x" for x = 1:3], - ["m1"], - ) - fluxes = flux_variability_analysis(cp, optimizer) - @test isapprox( - fluxes, - [ - 1.0 1.0 - 0.0 0.0 - -1.0 -1.0 - ], - atol = TEST_TOLERANCE, - ) - fluxes = flux_variability_analysis(cp, optimizer; bounds = gamma_bounds(0.5)) - @test isapprox( - fluxes, - [ - 0.5 1.0 - 0.0 0.5 - -1.0 -0.5 - ], - atol = TEST_TOLERANCE, - ) - fluxes = flux_variability_analysis(cp, optimizer; bounds = _ -> (0, Inf)) - @test isapprox( - fluxes, - [ - 0.0 1.0 - 0.0 1.0 - -1.0 0.0 - ], - atol = TEST_TOLERANCE, - ) - - @test isempty(flux_variability_analysis(cp, Vector{Int}(), Tulip.Optimizer)) - @test_throws DomainError flux_variability_analysis(cp, [-1], Tulip.Optimizer) - @test_throws DomainError flux_variability_analysis(cp, [99999999], Tulip.Optimizer) -end - -@testset "Parallel FVA" begin - cp = test_simpleLP() - - fluxes = flux_variability_analysis(cp, [1, 2], Tulip.Optimizer; workers = W) - @test isapprox( - fluxes, - [ - 1.0 1.0 - 2.0 2.0 - ], - atol = TEST_TOLERANCE, - ) -end - -@testset "Flux variability analysis with StandardModel" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - mins, maxs = flux_variability_analysis_dict( - model, - Tulip.Optimizer; - bounds = objective_bounds(0.99), - modifications = [ - change_optimizer_attribute("IPM_IterationsLimit", 500), - change_constraint("EX_glc__D_e"; lb = -10, ub = -10), - change_constraint("EX_o2_e"; lb = 0.0, ub = 0.0), - ], - ) - - @test isapprox(maxs["EX_ac_e"]["EX_ac_e"], 8.5185494, atol = TEST_TOLERANCE) - @test isapprox(mins["EX_ac_e"]["EX_ac_e"], 7.4483887, atol = TEST_TOLERANCE) -end diff --git a/test/analysis/gecko.jl b/test/analysis/gecko.jl deleted file mode 100644 index eab3a7083..000000000 --- a/test/analysis/gecko.jl +++ /dev/null @@ -1,141 +0,0 @@ -@testset "GECKO" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - get_reaction_isozymes = - rid -> - haskey(ecoli_core_reaction_kcats, rid) ? - collect( - Isozyme( - Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), - ecoli_core_reaction_kcats[rid][i]..., - ) for (i, grr) in enumerate(reaction_gene_association(model, rid)) - ) : Isozyme[] - - get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) - - total_gene_product_mass = 100.0 - - bounded_model = - model |> with_changed_bounds( - ["EX_glc__D_e", "GLCpts"]; - lower = [-1000.0, -1.0], - upper = [nothing, 12.0], - ) - - gm = - bounded_model |> with_gecko( - reaction_isozymes = get_reaction_isozymes, - gene_product_bounds = g -> g == "b2779" ? (0.01, 0.06) : (0.0, 1.0), - gene_product_molar_mass = get_gene_product_mass, - gene_product_mass_group_bound = _ -> total_gene_product_mass, - ) - - opt_model = flux_balance_analysis( - gm, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - rxn_fluxes = flux_dict(gm, opt_model) - prot_concens = gene_product_dict(gm, opt_model) - - @test isapprox( - rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], - 0.812827846796761, - atol = TEST_TOLERANCE, - ) - - prot_mass = sum(ecoli_core_gene_product_masses[gid] * c for (gid, c) in prot_concens) - mass_groups = gene_product_mass_group_dict(gm, opt_model) - - @test isapprox(prot_mass, total_gene_product_mass, atol = TEST_TOLERANCE) - @test isapprox(prot_mass, mass_groups["uncategorized"], atol = TEST_TOLERANCE) - - # test enzyme objective - growth_lb = rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"] * 0.9 - opt_model = flux_balance_analysis( - gm, - Tulip.Optimizer; - modifications = [ - change_objective(genes(gm); weights = [], sense = COBREXA.MIN_SENSE), - change_constraint("BIOMASS_Ecoli_core_w_GAM", lb = growth_lb), - change_optimizer_attribute("IPM_IterationsLimit", 1000), - ], - ) - mass_groups_min = gene_product_mass_group_dict(gm, opt_model) - @test mass_groups_min["uncategorized"] < mass_groups["uncategorized"] -end - -@testset "GECKO small model" begin - #= - Implement the small model found in the supplment of the - original GECKO paper. This model is nice to troubleshoot with, - because the stoich matrix is small. - =# - m = StandardModel("gecko") - m1 = Metabolite("m1") - m2 = Metabolite("m2") - m3 = Metabolite("m3") - m4 = Metabolite("m4") - - @add_reactions! m begin - "r1", nothing → m1, 0, 100 - "r2", nothing → m2, 0, 100 - "r3", m1 + m2 → m3, 0, 100 - "r4", m3 → m4, 0, 100 - "r5", m2 ↔ m4, -100, 100 - "r6", m4 → nothing, 0, 100 - end - - gs = [Gene("g$i") for i = 1:5] - - m.reactions["r2"].grr = [["g5"]] - m.reactions["r3"].grr = [["g1"]] - m.reactions["r4"].grr = [["g1"], ["g2"]] - m.reactions["r5"].grr = [["g3", "g4"]] - m.reactions["r6"].objective_coefficient = 1.0 - - add_genes!(m, gs) - add_metabolites!(m, [m1, m2, m3, m4]) - - reaction_isozymes = Dict( - "r3" => [Isozyme(Dict("g1" => 1), 1.0, 1.0)], - "r4" => - [Isozyme(Dict("g1" => 1), 2.0, 2.0), Isozyme(Dict("g2" => 1), 3.0, 3.0)], - "r5" => [Isozyme(Dict("g3" => 1, "g4" => 2), 70.0, 70.0)], - ) - gene_product_bounds = Dict( - "g1" => (0.0, 10.0), - "g2" => (0.0, 10.0), - "g3" => (0.0, 10.0), - "g4" => (0.0, 10.0), - ) - - gene_product_molar_mass = Dict("g1" => 1.0, "g2" => 2.0, "g3" => 3.0, "g4" => 4.0) - - gene_product_mass_group_bound = Dict("uncategorized" => 0.5) - - gm = make_gecko_model( - m; - reaction_isozymes, - gene_product_bounds, - gene_product_molar_mass, - gene_product_mass_group_bound, - ) - - opt_model = flux_balance_analysis( - gm, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - rxn_fluxes = flux_dict(gm, opt_model) - gene_products = gene_product_dict(gm, opt_model) - mass_groups = gene_product_mass_group_dict(gm, opt_model) - - @test isapprox(rxn_fluxes["r6"], 3.181818181753438, atol = TEST_TOLERANCE) - @test isapprox(gene_products["g4"], 0.09090909090607537, atol = TEST_TOLERANCE) - @test isapprox(mass_groups["uncategorized"], 0.5, atol = TEST_TOLERANCE) - @test length(genes(gm)) == 4 - @test length(genes(gm.inner)) == 5 -end diff --git a/test/analysis/knockouts.jl b/test/analysis/knockouts.jl deleted file mode 100644 index 6d59ecc8c..000000000 --- a/test/analysis/knockouts.jl +++ /dev/null @@ -1,132 +0,0 @@ -@testset "single_knockout" begin - m = StandardModel() - add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite("B")) - - add_gene!(m, Gene("g1")) - add_gene!(m, Gene("g2")) - add_reaction!( - m, - Reaction("v1", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"]]), - ) - add_reaction!( - m, - Reaction("v2", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1", "g2"]]), - ) - add_reaction!( - m, - Reaction("v3", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"], ["g2"]]), - ) - add_reaction!( - m, - Reaction( - "v4", - metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1", "g2"], ["g2"]], - ), - ) - - opt_model = make_optimization_model(m, Tulip.Optimizer) - knockout("g1")(m, opt_model) - - # Knockout should remove v1 - @test normalized_rhs(opt_model[:lbs][1]) == 0 - @test normalized_rhs(opt_model[:ubs][1]) == 0 - - # Knockout should remove [g1, g2] (AND) and thus remove reaction - @test normalized_rhs(opt_model[:lbs][2]) == 0 - @test normalized_rhs(opt_model[:ubs][2]) == 0 - - # Knockout should remove [g1], but keep reaction (OR) - @test normalized_rhs(opt_model[:lbs][3]) == 1000 - @test normalized_rhs(opt_model[:ubs][3]) == 1000 - - # Knockout should remove [g1, g2] (AND), but keep reaction (OR) - @test normalized_rhs(opt_model[:lbs][4]) == 1000 - @test normalized_rhs(opt_model[:ubs][4]) == 1000 -end - -@testset "multiple_knockouts" begin - m = StandardModel() - add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite("B")) - add_gene!(m, Gene("g1")) - add_gene!(m, Gene("g2")) - add_gene!(m, Gene("g3")) - add_reaction!( - m, - Reaction("v1", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"], ["g3"]]), - ) - add_reaction!( - m, - Reaction( - "v2", - metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1", "g2"], ["g3"]], - ), - ) - add_reaction!( - m, - Reaction( - "v3", - metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1"], ["g2"], ["g3"]], - ), - ) - - opt_model = make_optimization_model(m, Tulip.Optimizer) - knockout(["g1", "g3"])(m, opt_model) - - # Reaction 1 should be knocked out, because both - # gene1 and gene 3 are knocked out - @test normalized_rhs(opt_model[:lbs][1]) == 0 - @test normalized_rhs(opt_model[:ubs][1]) == 0 - - # Reaction 2 should be knocked out, because both - # [g1, g2] is an AND relationship - @test normalized_rhs(opt_model[:lbs][1]) == 0 - @test normalized_rhs(opt_model[:ubs][1]) == 0 - - # Reaction 3 should stay, because gene2 is still - # available (the arrays have an OR relationship) - @test normalized_rhs(opt_model[:lbs][3]) == 1000 - @test normalized_rhs(opt_model[:ubs][3]) == 1000 -end - -@testset "Knockouts on realistic models" begin - for model in [ - load_model(StandardModel, model_paths["e_coli_core.json"]), #test on standardModel - load_model(model_paths["e_coli_core.json"]), #then on JSONModel with the same contents - ] - - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lb = -12, ub = -12), - change_sense(MAX_SENSE), - change_optimizer_attribute("IPM_IterationsLimit", 110), - knockout(["b0978", "b0734"]), # knockouts out cytbd - ], - ) - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 0.2725811189335953, - atol = TEST_TOLERANCE, - ) - - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lb = -12, ub = -12), - change_sense(MAX_SENSE), - change_optimizer_attribute("IPM_IterationsLimit", 110), - knockout("b2779"), # knockouts out enolase - ], - ) - @test isapprox(sol["BIOMASS_Ecoli_core_w_GAM"], 0.0, atol = TEST_TOLERANCE) - end -end diff --git a/test/analysis/loopless.jl b/test/analysis/loopless.jl deleted file mode 100644 index e54cab549..000000000 --- a/test/analysis/loopless.jl +++ /dev/null @@ -1,16 +0,0 @@ -@testset "Loopless FBA" begin - - model = load_model(model_paths["e_coli_core.json"]) - - sol = flux_balance_analysis_dict( - model, - GLPK.Optimizer; - modifications = [add_loopless_constraints()], - ) - - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 0.8739215069684292, - atol = TEST_TOLERANCE, - ) -end diff --git a/test/analysis/max_min_driving_force.jl b/test/analysis/max_min_driving_force.jl deleted file mode 100644 index 5e6c7e065..000000000 --- a/test/analysis/max_min_driving_force.jl +++ /dev/null @@ -1,59 +0,0 @@ -@testset "Max-min driving force analysis" begin - - model = load_model(model_paths["e_coli_core.json"]) - - flux_solution = flux_balance_analysis_dict( - model, - GLPK.Optimizer; - modifications = [add_loopless_constraints()], - ) - - sol = max_min_driving_force( - model, - reaction_standard_gibbs_free_energies, - Tulip.Optimizer; - flux_solution = flux_solution, - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - concentration_ratios = Dict( - ("atp_c", "adp_c") => 10.0, - ("nadh_c", "nad_c") => 0.13, - ("nadph_c", "nadp_c") => 1.3, - ), - concentration_lb = 1e-6, - concentration_ub = 100e-3, - ignore_reaction_ids = ["H2Ot"], - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - @test isapprox(sol.mmdf, 1.7661155558545698, atol = TEST_TOLERANCE) - - sols = max_min_driving_force_variability( - model, - reaction_standard_gibbs_free_energies, - Tulip.Optimizer; - bounds = gamma_bounds(0.9), - flux_solution = flux_solution, - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - concentration_ratios = Dict{Tuple{String,String},Float64}( - ("atp_c", "adp_c") => 10.0, - ("nadh_c", "nad_c") => 0.13, - ("nadph_c", "nadp_c") => 1.3, - ), - constant_concentrations = Dict{String,Float64}( - # "pi_c" => 10e-3 - ), - concentration_lb = 1e-6, - concentration_ub = 100e-3, - ignore_reaction_ids = ["H2Ot"], - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - pyk_idx = first(indexin(["PYK"], reactions(model))) - @test isapprox( - sols[pyk_idx, 1].dg_reactions["PYK"], - -1.5895040002691128; - atol = TEST_TOLERANCE, - ) -end diff --git a/test/analysis/minimize_metabolic_adjustment.jl b/test/analysis/minimize_metabolic_adjustment.jl deleted file mode 100644 index dbb115992..000000000 --- a/test/analysis/minimize_metabolic_adjustment.jl +++ /dev/null @@ -1,14 +0,0 @@ -@testset "MOMA" begin - model = test_toyModel() - - sol = [looks_like_biomass_reaction(rid) ? 0.5 : 0.0 for rid in reactions(model)] - - moma = minimize_metabolic_adjustment_analysis_dict( - model, - sol, - Clarabel.Optimizer; - modifications = [silence], - ) - - @test isapprox(moma["biomass1"], 0.07692307692307691, atol = QP_TEST_TOLERANCE) -end diff --git a/test/analysis/moment.jl b/test/analysis/moment.jl deleted file mode 100644 index d8a0264b2..000000000 --- a/test/analysis/moment.jl +++ /dev/null @@ -1,21 +0,0 @@ -@testset "Moment algorithm" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - ksas = Dict(rid => 1000.0 for rid in reactions(model)) - protein_mass_fraction = 0.56 - - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - add_moment_constraints(ksas, protein_mass_fraction;), - change_constraint("EX_glc__D_e", lb = -1000), - ], - ) - - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 0.6623459899423948, - atol = TEST_TOLERANCE, - ) -end diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl deleted file mode 100644 index a0775e51f..000000000 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ /dev/null @@ -1,17 +0,0 @@ -@testset "Parsimonious flux balance analysis with StandardModel" begin - model = test_toyModel() - - d = parsimonious_flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_constraint("EX_m1(e)", lb = -10.0), - change_optimizer_attribute("IPM_IterationsLimit", 500), - ], - qp_modifications = [change_optimizer(Clarabel.Optimizer), silence], - ) - - # The used optimizer doesn't really converge to the same answer everytime - # here, we therefore tolerate a wide range of results. - @test isapprox(d["biomass1"], 10.0, atol = QP_TEST_TOLERANCE) -end diff --git a/test/analysis/sampling/affine_hit_and_run.jl b/test/analysis/sampling/affine_hit_and_run.jl deleted file mode 100644 index 03841eb7f..000000000 --- a/test/analysis/sampling/affine_hit_and_run.jl +++ /dev/null @@ -1,29 +0,0 @@ -@testset "Sampling Tests" begin - - model = load_model(model_paths["e_coli_core.json"]) - - cm = CoreCoupling(model, zeros(1, n_reactions(model)), [17.0], [19.0]) - - pfk, tala = indexin(["PFK", "TALA"], reactions(cm)) - cm.C[:, [pfk, tala]] .= 1.0 - - warmup = warmup_from_variability(cm, Tulip.Optimizer; workers = W) - - samples = affine_hit_and_run( - cm, - warmup, - sample_iters = 10 * (1:3), - workers = W, - chains = length(W), - ) - - @test size(samples, 1) == size(warmup, 1) - @test size(samples, 2) == size(warmup, 2) * 3 * length(W) - - lbs, ubs = bounds(model) - @test all(samples .>= lbs) - @test all(samples .<= ubs) - @test all(cm.C * samples .>= cm.cl) - @test all(cm.C * samples .<= cm.cu) - @test all(stoichiometry(model) * samples .< TEST_TOLERANCE) -end diff --git a/test/analysis/sampling/warmup_variability.jl b/test/analysis/sampling/warmup_variability.jl deleted file mode 100644 index c4287ecca..000000000 --- a/test/analysis/sampling/warmup_variability.jl +++ /dev/null @@ -1,16 +0,0 @@ -@testset "Warm up point generation" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - rid = "EX_glc__D_e" - pts = warmup_from_variability( - model |> with_changed_bound(rid; lower = -2, upper = 2), - Tulip.Optimizer, - 100; - workers = W, - ) - - idx = first(indexin([rid], reactions(model))) - @test size(pts) == (95, 100) - @test all(pts[idx, :] .>= -2) - @test all(pts[idx, :] .<= 2) -end diff --git a/test/analysis/screening.jl b/test/analysis/screening.jl deleted file mode 100644 index d710f4fe8..000000000 --- a/test/analysis/screening.jl +++ /dev/null @@ -1,61 +0,0 @@ - -@testset "Screening functions" begin - m = test_toyModel() - - # nothing to analyze - @test_throws DomainError screen(m; analysis = identity) - - # array dimensionalities must match - @test_throws DomainError screen( - m; - analysis = identity, - variants = [[], []], - args = [() ()], - ) - - # sizes must match - @test_throws DomainError screen( - m; - analysis = identity, - variants = [[], []], - args = [()], - ) - - # argument handling - @test screen(m, analysis = identity, variants = [[]]) == [m] - @test screen(m, analysis = identity, args = [()]) == [m] - @test screen(m, analysis = (a, b) -> b, args = [(1,), (2,)]) == [1, 2] - - # test modifying some reactions - quad_rxn(i) = (m::CoreModel) -> begin - mm = copy(m) - mm.S = copy(m.S) - mm.S[:, i] .^= 2 - return mm - end - - @test screen_variants( - m, - [[quad_rxn(i)] for i = 1:3], - m -> flux_balance_analysis_vec(m, Tulip.Optimizer); - workers = W, - ) == [ - [250.0, -250.0, -1000.0, 250.0, 1000.0, 250.0, 250.0], - [500.0, 500.0, 1000.0, 500.0, -1000.0, 500.0, 500.0], - [500.0, 500.0, 1000.0, -500.0, 1000.0, 500.0, 500.0], - ] - - # test solver modifications - @test screen( - m; - analysis = (m, sense) -> flux_balance_analysis_vec( - m, - Tulip.Optimizer; - modifications = [change_sense(sense)], - ), - args = [(MIN_SENSE,), (MAX_SENSE,)], - ) == [ - [-500.0, -500.0, -1000.0, 500.0, 1000.0, -500.0, -500.0], - [500.0, 500.0, 1000.0, -500.0, -1000.0, 500.0, 500.0], - ] -end diff --git a/test/analysis/smoment.jl b/test/analysis/smoment.jl deleted file mode 100644 index cba241217..000000000 --- a/test/analysis/smoment.jl +++ /dev/null @@ -1,41 +0,0 @@ -@testset "SMOMENT" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) - - get_reaction_isozyme = - rid -> - haskey(ecoli_core_reaction_kcats, rid) ? - argmax( - smoment_isozyme_speed(get_gene_product_mass), - Isozyme( - Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), - ecoli_core_reaction_kcats[rid][i]..., - ) for (i, grr) in enumerate(reaction_gene_association(model, rid)) - ) : nothing - - smoment_model = - model |> - with_changed_bounds( - ["EX_glc__D_e", "GLCpts"], - lower = [-1000.0, -1.0], - upper = [nothing, 12.0], - ) |> - with_smoment( - reaction_isozyme = get_reaction_isozyme, - gene_product_molar_mass = get_gene_product_mass, - total_enzyme_capacity = 100.0, - ) - - rxn_fluxes = flux_balance_analysis_dict( - smoment_model, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - @test isapprox( - rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], - 0.8907273630431708, - atol = TEST_TOLERANCE, - ) -end diff --git a/test/aqua.jl b/test/aqua.jl index 1688b83e3..4c083591b 100644 --- a/test/aqua.jl +++ b/test/aqua.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + @testset "Automated QUality Assurance" begin # We can't do Aqua.test_all here (yet) because the ambiguity tests fail in # deps. Instead let's pick the other sensible tests. diff --git a/test/base/logging/log.jl b/test/base/logging/log.jl deleted file mode 100644 index a41342d6a..000000000 --- a/test/base/logging/log.jl +++ /dev/null @@ -1,13 +0,0 @@ -COBREXA.@_make_logging_tag TEST "testing stuff" - -log_TEST() -@testset "Logging on" begin - @test _TEST_log_enabled - @test_logs (:warn, "qweasdzxc") @_TEST_log @warn "qweasdzxc" -end - -log_TEST(false) -@testset "Logging off" begin - @test !_TEST_log_enabled - @test_logs @_TEST_log @warn "all okay!" -end diff --git a/test/base/types.jl b/test/base/types.jl deleted file mode 100644 index 09f3b16bc..000000000 --- a/test/base/types.jl +++ /dev/null @@ -1,4 +0,0 @@ -@testset "CoreModel type" begin - cp = test_LP() - @test cp isa CoreModel -end diff --git a/test/base/types/CoreModel.jl b/test/base/types/CoreModel.jl deleted file mode 100644 index 61e9ba96a..000000000 --- a/test/base/types/CoreModel.jl +++ /dev/null @@ -1,19 +0,0 @@ -@testset "CoreModel generic interface" begin - model = load_model(CoreModel, model_paths["e_coli_core.mat"]) - - @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) - @test reaction_stoichiometry(model, 44) == Dict("ac_e" => -1) -end - -@testset "Conversion from and to StandardModel" begin - cm = load_model(CoreModel, model_paths["e_coli_core.mat"]) - - sm = convert(StandardModel, cm) - cm2 = convert(CoreModel, sm) - - @test Set(reactions(cm)) == Set(reactions(sm)) - @test Set(reactions(cm)) == Set(reactions(cm2)) - - @test reaction_gene_association(sm, reactions(sm)[1]) == - reaction_gene_association(cm, reactions(sm)[1]) -end diff --git a/test/base/types/CoreModelCoupled.jl b/test/base/types/CoreModelCoupled.jl deleted file mode 100644 index d64ba1ae9..000000000 --- a/test/base/types/CoreModelCoupled.jl +++ /dev/null @@ -1,31 +0,0 @@ -@testset "CoreModelCoupled generic interface" begin - model = load_model(CoreModelCoupled, model_paths["e_coli_core.mat"]) - - @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) - @test reaction_stoichiometry(model, 44) == Dict("ac_e" => -1) - -end - -@testset "Conversion from and to StandardModel" begin - cm = load_model(CoreModelCoupled, model_paths["e_coli_core.mat"]) - - sm = convert(StandardModel, cm) - cm2 = convert(CoreModelCoupled, sm) - - @test Set(reactions(cm)) == Set(reactions(sm)) - @test Set(reactions(cm)) == Set(reactions(cm2)) - - @test reaction_gene_association(sm, reactions(sm)[1]) == - reaction_gene_association(cm, reactions(sm)[1]) - - @test reaction_gene_association_vec(cm)[1:3] == [ - [["b3916"], ["b1723"]], - [ - ["b0902", "b0903", "b2579"], - ["b0902", "b0903"], - ["b0902", "b3114"], - ["b3951", "b3952"], - ], - [["b4025"]], - ] -end diff --git a/test/base/types/FluxSummary.jl b/test/base/types/FluxSummary.jl deleted file mode 100644 index 1f9601f5e..000000000 --- a/test/base/types/FluxSummary.jl +++ /dev/null @@ -1,24 +0,0 @@ -@testset "Flux summary" begin - model = load_model(model_paths["e_coli_core.json"]) - - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 200)], - ) - - fr = flux_summary(sol; keep_unbounded = true, large_flux_bound = 25) - - @test isapprox( - fr.biomass_fluxes["BIOMASS_Ecoli_core_w_GAM"], - 0.8739215022690006; - atol = TEST_TOLERANCE, - ) - @test isapprox(fr.export_fluxes["EX_co2_e"], 22.80983339307183; atol = TEST_TOLERANCE) - @test isapprox(fr.import_fluxes["EX_o2_e"], -21.799492758430517; atol = TEST_TOLERANCE) - @test isapprox( - fr.unbounded_fluxes["EX_h2o_e"], - 29.175827202663395; - atol = TEST_TOLERANCE, - ) -end diff --git a/test/base/types/FluxVariabilitySummary.jl b/test/base/types/FluxVariabilitySummary.jl deleted file mode 100644 index 1ed04b8c8..000000000 --- a/test/base/types/FluxVariabilitySummary.jl +++ /dev/null @@ -1,22 +0,0 @@ -@testset "Flux variability summary" begin - model = load_model(model_paths["e_coli_core.json"]) - - sol = flux_variability_analysis_dict( - model, - Tulip.Optimizer; - bounds = objective_bounds(0.90), - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 2000)], - ) - - fr = flux_variability_summary(sol) - @test isapprox( - fr.biomass_fluxes["BIOMASS_Ecoli_core_w_GAM"][1], - 0.7865293520891825; - atol = TEST_TOLERANCE, - ) - @test isapprox( - fr.exchange_fluxes["EX_for_e"][2], - 11.322324494491848; - atol = TEST_TOLERANCE, - ) -end diff --git a/test/base/types/Gene.jl b/test/base/types/Gene.jl deleted file mode 100644 index 92c9e2fa0..000000000 --- a/test/base/types/Gene.jl +++ /dev/null @@ -1,32 +0,0 @@ -@testset "Gene: construction, printing, utils" begin - g = Gene() - - # test defaults - @test isempty(g.notes) - @test isempty(g.annotations) - - # Now assign - g.id = "gene1" - g.notes = Dict("notes" => ["blah", "blah"]) - g.annotations = Dict("sboterm" => ["sbo"], "ncbigene" => ["ads", "asds"]) - - # Test pretty printing - @test all(contains.(sprint(show, MIME("text/plain"), g), ["gene1", "blah", "asds"])) - - # Test duplicate annotation finder - g2 = Gene("gene2") - g2.annotations = Dict("sboterm" => ["sbo2"], "ncbigene" => ["fff", "ggg"]) - g3 = Gene("g3") - g3.annotations = Dict("sboterm" => ["sbo3"], "ncbigene" => ["ads"]) - g4 = Gene("g4") - g4.annotations = Dict("sboterm" => ["sbo4"], "ncbigene" => ["ads22", "asd22s"]) - gdict = OrderedDict(g.id => g for g in [g, g2, g3, g4]) # this is how genes are stored in StandardModel - - idx = annotation_index(gdict) - @test length(idx["ncbigene"]["ads"]) > 1 - @test "gene1" in idx["ncbigene"]["ads"] - - ambiguous = ambiguously_identified_items(idx) - @test "g3" in ambiguous - @test !("g4" in ambiguous) -end diff --git a/test/base/types/JSONModel.jl b/test/base/types/JSONModel.jl deleted file mode 100644 index 3c63a6895..000000000 --- a/test/base/types/JSONModel.jl +++ /dev/null @@ -1,16 +0,0 @@ -@testset "Conversion from and to SBML model" begin - json_model = model_paths["iJO1366.json"] - - jm = load_json_model(json_model) - sm = convert(StandardModel, jm) - jm2 = convert(JSONModel, sm) - - @test Set(reactions(jm)) == Set(reactions(sm)) - @test Set(reactions(jm)) == Set(reactions(jm2)) -end - -@testset "JSONModel generic interface" begin - model = load_model(model_paths["e_coli_core.json"]) - - @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) -end diff --git a/test/base/types/MATModel.jl b/test/base/types/MATModel.jl deleted file mode 100644 index 10103d8d1..000000000 --- a/test/base/types/MATModel.jl +++ /dev/null @@ -1,18 +0,0 @@ - -@testset "Conversion from and to MATLAB model" begin - filename = model_paths["iJO1366.mat"] - - mm = load_mat_model(filename) - sm = convert(StandardModel, mm) - mm2 = convert(MATModel, sm) - - @test Set(reactions(mm)) == Set(reactions(sm)) - @test Set(reactions(mm)) == Set(reactions(mm2)) -end - -@testset "MATModel generic interface" begin - model = load_model(model_paths["e_coli_core.mat"]) - - @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) - @test reaction_stoichiometry(model, 44) == Dict("ac_e" => -1) -end diff --git a/test/base/types/Metabolite.jl b/test/base/types/Metabolite.jl deleted file mode 100644 index 189504517..000000000 --- a/test/base/types/Metabolite.jl +++ /dev/null @@ -1,31 +0,0 @@ -@testset "Metabolite" begin - m1 = Metabolite() - m1.id = "met1" - m1.formula = "C6H12O6N" - m1.charge = 1 - m1.compartment = "c" - m1.notes = Dict("notes" => ["blah", "blah"]) - m1.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["ads", "asds"]) - - @test all( - contains.( - sprint(show, MIME("text/plain"), m1), - ["met1", "C6H12O6N", "blah", "asds"], - ), - ) - - m2 = Metabolite("met2") - - m2.formula = "C6H12O6N" - - m3 = Metabolite("met3") - m3.formula = "X" - m3.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["ad2s", "asds"]) - - m4 = Metabolite("met4") - m4.formula = "X" - m4.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["adxxx2s", "asdxxxs"]) - - md = OrderedDict(m.id => m for m in [m1, m2, m3]) - @test issetequal(["met1", "met3"], ambiguously_identified_items(annotation_index(md))) -end diff --git a/test/base/types/Reaction.jl b/test/base/types/Reaction.jl deleted file mode 100644 index 3fa5a1c07..000000000 --- a/test/base/types/Reaction.jl +++ /dev/null @@ -1,106 +0,0 @@ -@testset "Reaction" begin - m1 = Metabolite("m1") - m1.formula = "C2H3" - m2 = Metabolite("m2") - m2.formula = "H3C2" - m3 = Metabolite("m3") - m4 = Metabolite("m4") - m5 = Metabolite("m5") - m6 = Metabolite("m6") - m7 = Metabolite("m7") - m8 = Metabolite("m8") - m9 = Metabolite("m9") - m10 = Metabolite("m10") - m11 = Metabolite("m11") - m12 = Metabolite("m12") - - g1 = Gene("g1") - g2 = Gene("g2") - g3 = Gene("g3") - - r1 = Reaction() - r1.id = "r1" - r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) - r1.lb = -100.0 - r1.ub = 100.0 - r1.grr = [["g1", "g2"], ["g3"]] - r1.subsystem = "glycolysis" - r1.notes = Dict("notes" => ["blah", "blah"]) - r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - r1.objective_coefficient = 1.0 - - @test all( - contains.( - sprint(show, MIME("text/plain"), r1), - ["r1", "100.0", "g1 and g2", "glycolysis", "blah", "biocyc"], - ), - ) - - rlongfor = Reaction( - "rlongfor", - Dict( - m1.id => -1.0, - m2.id => -1.0, - m3.id => -1.0, - m4.id => -1.0, - m5.id => -1.0, - m6.id => -1.0, - m7.id => 1.0, - m8.id => 1.0, - m9.id => 1.0, - m10.id => 1.0, - m11.id => 1.0, - m12.id => 1.0, - ), - :forward, - ) - @test contains(sprint(show, MIME("text/plain"), rlongfor), "...") - - rlongrev = Reaction( - "rlongrev", - Dict( - m1.id => -1.0, - m2.id => -1.0, - m3.id => -1.0, - m4.id => -1.0, - m5.id => -1.0, - m6.id => -1.0, - m7.id => 1.0, - m8.id => 1.0, - m9.id => 1.0, - m10.id => 1.0, - m11.id => 1.0, - m12.id => 1.0, - ), - :reverse, - ) - @test occursin("...", sprint(show, MIME("text/plain"), rlongrev)) - - r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :reverse) - @test r2.lb == -1000.0 && r2.ub == 0.0 - - r3 = Reaction("r3", Dict(m3.id => -1.0, m4.id => 1.0), :forward) - @test r3.lb == 0.0 && r3.ub == 1000.0 - - r4 = Reaction("r4", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) - r4.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - @test r4.lb == -1000.0 && r4.ub == 1000.0 - - rd = OrderedDict(r.id => r for r in [r1, r2, r3, r4]) - @test issetequal(["r1", "r4"], ambiguously_identified_items(annotation_index(rd))) - - id = check_duplicate_reaction(r4, rd) - @test id == "r3" - - r5 = Reaction("r5", Dict(m3.id => -11.0, m4.id => 1.0), :bidirectional) - id = check_duplicate_reaction(r5, rd) - @test id == "r3" - - r5 = Reaction("r5", Dict(m3.id => -11.0, m4.id => 1.0), :bidirectional) - id = check_duplicate_reaction(r5, rd; only_metabolites = false) - @test isnothing(id) - - r5 = Reaction("r5", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) - id = check_duplicate_reaction(r5, rd; only_metabolites = false) - @test id == "r3" -end diff --git a/test/base/types/SBMLModel.jl b/test/base/types/SBMLModel.jl deleted file mode 100644 index 554ff60e7..000000000 --- a/test/base/types/SBMLModel.jl +++ /dev/null @@ -1,36 +0,0 @@ - -@testset "Conversion from and to SBML model" begin - sbmlm = load_sbml_model(model_paths["ecoli_core_model.xml"]) - sm = convert(StandardModel, sbmlm) - sbmlm2 = convert(SBMLModel, sm) - - @test Set(reactions(sbmlm)) == Set(reactions(sbmlm2)) - @test Set(reactions(sbmlm)) == Set(reactions(sm)) - @test Set(metabolites(sbmlm)) == Set(metabolites(sbmlm2)) - sp(x) = x.species - @test all([ - issetequal( - sp.(sbmlm.sbml.reactions[i].reactants), - sp.(sbmlm2.sbml.reactions[i].reactants), - ) && issetequal( - sp.(sbmlm.sbml.reactions[i].products), - sp.(sbmlm2.sbml.reactions[i].products), - ) for i in reactions(sbmlm2) - ]) - st(x) = isnothing(x.stoichiometry) ? 1.0 : x.stoichiometry - @test all([ - issetequal( - st.(sbmlm.sbml.reactions[i].reactants), - st.(sbmlm2.sbml.reactions[i].reactants), - ) && issetequal( - st.(sbmlm.sbml.reactions[i].products), - st.(sbmlm2.sbml.reactions[i].products), - ) for i in reactions(sbmlm2) - ]) -end - -@testset "SBMLModel generic interface" begin - model = load_model(model_paths["e_coli_core.xml"]) - - @test reaction_stoichiometry(model, "R_EX_ac_e") == Dict("M_ac_e" => -1) -end diff --git a/test/base/types/StandardModel.jl b/test/base/types/StandardModel.jl deleted file mode 100644 index ef836eec7..000000000 --- a/test/base/types/StandardModel.jl +++ /dev/null @@ -1,141 +0,0 @@ -@testset "StandardModel generic interface" begin - # create a small model - m1 = Metabolite("m1") - m1.formula = "C2H3" - m1.compartment = "cytosol" - m2 = Metabolite("m2") - m2.formula = "H3C2" - m3 = Metabolite("m3") - m3.charge = -1 - m4 = Metabolite("m4") - m4.notes = Dict("confidence" => ["iffy"]) - m4.annotations = Dict("sbo" => ["blah"]) - - g1 = Gene("g1") - g2 = Gene("g2") - g2.notes = Dict("confidence" => ["iffy"]) - g2.annotations = Dict("sbo" => ["blah"]) - g3 = Gene("g3") - - r1 = Reaction() - r1.id = "r1" - r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) - r1.lb = -100.0 - r1.ub = 100.0 - r1.grr = [["g1", "g2"], ["g3"]] - r1.subsystem = "glycolysis" - r1.notes = Dict("notes" => ["blah", "blah"]) - r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - r1.objective_coefficient = 1.0 - - r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :reverse) - r3 = Reaction("r3", Dict(m3.id => -1.0, m4.id => 1.0), :forward) - r4 = Reaction("r4", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) - r4.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - - mets = [m1, m2, m3, m4] - gs = [g1, g2, g3] - rxns = [r1, r2, r3, r4] - - model = StandardModel() - model.id = "model" - model.reactions = OrderedDict(r.id => r for r in rxns) - model.metabolites = OrderedDict(m.id => m for m in mets) - model.genes = OrderedDict(g.id => g for g in gs) - - @test contains(sprint(show, MIME("text/plain"), model), "StandardModel") - - @test "r1" in reactions(model) - @test "m4" in metabolites(model) - @test "g2" in genes(model) - @test n_reactions(model) == 4 - @test n_metabolites(model) == 4 - @test n_genes(model) == 3 - - S_test = spzeros(4, 4) - S_test[1, 1] = -1.0 - S_test[2, 1] = 1.0 - S_test[1, 2] = -2.0 - S_test[4, 2] = 1.0 - S_test[3, 3] = -1.0 - S_test[4, 3] = 1.0 - S_test[3, 4] = -1.0 - S_test[4, 4] = 1.0 - @test S_test == stoichiometry(model) - - lb_test = spzeros(4) - lb_test[1] = -100.0 - lb_test[2] = -1000.0 - lb_test[3] = 0.0 - lb_test[4] = -1000.0 - ub_test = spzeros(4) - ub_test[1] = 100.0 - ub_test[2] = 0.0 - ub_test[3] = 1000.0 - ub_test[4] = 1000.0 - lbs, ubs = bounds(model) - @test lb_test == lbs - @test ub_test == ubs - - @test balance(model) == spzeros(n_metabolites(model)) - - obj_test = spzeros(4) - obj_test[1] = 1.0 - @test objective(model) == obj_test - - @test [["g1", "g2"], ["g3"]] == reaction_gene_association(model, "r1") - @test isnothing(reaction_gene_association(model, "r2")) - - @test metabolite_formula(model, "m2")["C"] == 2 - @test isnothing(metabolite_formula(model, "m3")) - - @test metabolite_charge(model, "m3") == -1 - @test isnothing(metabolite_charge(model, "m2")) - - @test metabolite_compartment(model, "m1") == "cytosol" - @test isnothing(metabolite_compartment(model, "m2")) - - @test reaction_subsystem(model, "r1") == "glycolysis" - @test isnothing(reaction_subsystem(model, "r2")) - - @test metabolite_notes(model, "m4")["confidence"] == ["iffy"] - @test metabolite_annotations(model, "m4")["sbo"] == ["blah"] - @test isempty(metabolite_notes(model, "m3")) - @test isempty(metabolite_annotations(model, "m3")) - - @test gene_notes(model, "g2")["confidence"] == ["iffy"] - @test gene_annotations(model, "g2")["sbo"] == ["blah"] - @test isempty(gene_notes(model, "g1")) - @test isempty(gene_annotations(model, "g1")) - - @test reaction_notes(model, "r1")["notes"] == ["blah", "blah"] - @test reaction_annotations(model, "r1")["biocyc"] == ["ads", "asds"] - @test isempty(reaction_notes(model, "r2")) - @test isempty(reaction_annotations(model, "r2")) - - @test reaction_stoichiometry(model, "r1") == Dict("m1" => -1.0, "m2" => 1.0) - - # To do: test convert - same_model = convert(StandardModel, model) - @test same_model == model - - jsonmodel = convert(JSONModel, model) - stdmodel = convert(StandardModel, jsonmodel) - @test issetequal(reactions(jsonmodel), reactions(stdmodel)) - @test issetequal(genes(jsonmodel), genes(stdmodel)) - @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) - jlbs, jubs = bounds(jsonmodel) - slbs, subs = bounds(stdmodel) - @test issetequal(jlbs, slbs) - @test issetequal(jubs, subs) - jS = stoichiometry(jsonmodel) - sS = stoichiometry(stdmodel) - j_r1_index = findfirst(x -> x == "r1", reactions(jsonmodel)) - s_r1_index = findfirst(x -> x == "r1", reactions(stdmodel)) - j_m1_index = findfirst(x -> x == "m1", metabolites(jsonmodel)) - j_m2_index = findfirst(x -> x == "m2", metabolites(jsonmodel)) - s_m1_index = findfirst(x -> x == "m1", metabolites(stdmodel)) - s_m2_index = findfirst(x -> x == "m2", metabolites(stdmodel)) - @test jS[j_m1_index, j_r1_index] == sS[s_m1_index, s_r1_index] - @test jS[j_m2_index, j_r1_index] == sS[s_m2_index, s_r1_index] -end diff --git a/test/base/types/abstract/MetabolicModel.jl b/test/base/types/abstract/MetabolicModel.jl deleted file mode 100644 index 59f2ea088..000000000 --- a/test/base/types/abstract/MetabolicModel.jl +++ /dev/null @@ -1,12 +0,0 @@ - -struct FakeModel <: MetabolicModel - dummy::Int -end - -@testset "Base abstract model methods require proper minimal implementation" begin - @test_throws MethodError reactions(123) - x = FakeModel(123) - for m in [reactions, metabolites, stoichiometry, bounds, objective] - @test_throws MethodError m(x) - end -end diff --git a/test/base/utils/CoreModel.jl b/test/base/utils/CoreModel.jl deleted file mode 100644 index 91b4b9c66..000000000 --- a/test/base/utils/CoreModel.jl +++ /dev/null @@ -1,16 +0,0 @@ -@testset "CoreModel utilities" begin - cp = test_LP() - @test n_reactions(cp) == 3 - @test n_metabolites(cp) == 4 - @test n_coupling_constraints(cp) == 0 - - cp2 = test_LP() - @test isequal(cp, cp2) - cp2.S[1] = 1 - @test !isequal(cp, cp2) - @test isequal(cp, copy(cp)) - - cp = test_coupledLP() - @test n_coupling_constraints(cp) == 2000 - @test isequal(cp, copy(cp)) -end diff --git a/test/base/utils/Serialized.jl b/test/base/utils/Serialized.jl deleted file mode 100644 index ef015383c..000000000 --- a/test/base/utils/Serialized.jl +++ /dev/null @@ -1,25 +0,0 @@ - -@testset "Serialized models" begin - m = test_simpleLP() - - sm = serialize_model(m, tmpfile("toy1.serialized")) - sm2 = serialize_model(sm, tmpfile("toy2.serialized")) - - @test typeof(sm) == Serialized{CoreModel} # expected type - @test typeof(sm2) == Serialized{CoreModel} # no multi-layer serialization - - precache!(sm) - - @test isequal(m, sm.m) # the data is kept okay - @test sm2.m == nothing # nothing is cached here - @test isequal(m, COBREXA.Serialization.deserialize(tmpfile("toy2.serialized"))) # it was written as-is - @test issetequal( - reactions(convert(StandardModel, sm)), - reactions(convert(StandardModel, sm2)), - ) - sm.m = nothing - @test issetequal( - metabolites(convert(CoreModelCoupled, sm)), - metabolites(convert(CoreModelCoupled, sm2)), - ) -end diff --git a/test/base/utils/StandardModel.jl b/test/base/utils/StandardModel.jl deleted file mode 100644 index 6d80bc7a3..000000000 --- a/test/base/utils/StandardModel.jl +++ /dev/null @@ -1,24 +0,0 @@ -@testset "StandardModel utilities" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - # FBA - fluxes = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], - ) - - # bounds setting - cbm = make_optimization_model(model, Tulip.Optimizer) - ubs = cbm[:ubs] - lbs = cbm[:lbs] - glucose_index = first(indexin(["EX_glc__D_e"], reactions(model))) - o2_index = first(indexin(["EX_o2_e"], reactions(model))) - atpm_index = first(indexin(["ATPM"], reactions(model))) - set_optmodel_bound!(glucose_index, cbm; ub = -1.0, lb = -1.0) - @test normalized_rhs(ubs[glucose_index]) == -1.0 - @test normalized_rhs(lbs[glucose_index]) == 1.0 - set_optmodel_bound!(o2_index, cbm; ub = 1.0, lb = 1.0) - @test normalized_rhs(ubs[o2_index]) == 1.0 - @test normalized_rhs(lbs[o2_index]) == -1.0 -end diff --git a/test/base/utils/fluxes.jl b/test/base/utils/fluxes.jl deleted file mode 100644 index 5dbc3129b..000000000 --- a/test/base/utils/fluxes.jl +++ /dev/null @@ -1,23 +0,0 @@ -@testset "Flux utilities" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - fluxes = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], - ) - - consuming, producing = metabolite_fluxes(model, fluxes) - @test isapprox(consuming["atp_c"]["PFK"], -7.47738; atol = TEST_TOLERANCE) - @test isapprox(producing["atp_c"]["PYK"], 1.75818; atol = TEST_TOLERANCE) - - # remove the biomass production from the fluxes, so that there's some atom - # disbalance that can be measured - delete!(fluxes, "BIOMASS_Ecoli_core_w_GAM") - - # atom tracker - atom_fluxes_out = atom_fluxes(model, fluxes) - @test isapprox(atom_fluxes_out["C"], 37.190166489763214; atol = TEST_TOLERANCE) - @test isapprox(atom_fluxes_out["O"], 41.663071522672226; atol = TEST_TOLERANCE) - @test isapprox(atom_fluxes_out["N"], 4.765319167566247; atol = TEST_TOLERANCE) -end diff --git a/test/base/utils/looks_like.jl b/test/base/utils/looks_like.jl deleted file mode 100644 index 6b88dccb5..000000000 --- a/test/base/utils/looks_like.jl +++ /dev/null @@ -1,86 +0,0 @@ -@testset "Looks like in CoreModel, detailed test" begin - cp = test_LP() - @test isempty(filter(looks_like_exchange_reaction, reactions(cp))) - - cp = test_simpleLP() - @test isempty(filter(looks_like_exchange_reaction, reactions(cp))) - - cp = CoreModel( - [-1.0 -1 -2; 0 -1 0; 0 0 0], - zeros(3), - ones(3), - ones(3), - ones(3), - ["EX_m1"; "r2"; "r3"], - ["m1"; "m2"; "m3"], - ) - @test filter(looks_like_exchange_reaction, reactions(cp)) == ["EX_m1"] - - cp = CoreModel( - [-1.0 0 0; 0 0 -1; 0 -1 0], - zeros(3), - ones(3), - ones(3), - ones(3), - ["EX_m1"; "Exch_m3"; "Ex_m2"], - ["m1"; "m2"; "m3_e"], - ) - @test filter(looks_like_exchange_reaction, reactions(cp)) == - ["EX_m1", "Exch_m3", "Ex_m2"] - @test filter( - x -> looks_like_exchange_reaction(x; exchange_prefixes = ["Exch_"]), - reactions(cp), - ) == ["Exch_m3"] - - # this is originally the "toyModel1.mat" - cp = test_toyModel() - - @test filter(looks_like_exchange_reaction, reactions(cp)) == - ["EX_m1(e)", "EX_m3(e)", "EX_biomass(e)"] - @test filter( - x -> looks_like_exchange_reaction(x; exclude_biomass = true), - reactions(cp), - ) == ["EX_m1(e)", "EX_m3(e)"] - @test filter(looks_like_extracellular_metabolite, metabolites(cp)) == ["m1[e]", "m3[e]"] - @test filter(looks_like_biomass_reaction, reactions(cp)) == - ["EX_biomass(e)", "biomass1"] - @test filter( - x -> looks_like_biomass_reaction(x; exclude_exchanges = true), - reactions(cp), - ) == ["biomass1"] -end - -@testset "Looks like functions, basic" begin - model = load_model(model_paths["e_coli_core.json"]) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - - model = load_model(model_paths["e_coli_core.xml"]) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - - model = load_model(model_paths["e_coli_core.mat"]) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - - model = convert(StandardModel, model) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - - model = convert(CoreModelCoupled, model) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 -end - -@testset "Ontology usage in is_xxx_reaction" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - # macro generated, so only test positive and negative case - @test !is_biomass_reaction(model, "PFL") - @test is_biomass_reaction(model, "BIOMASS_Ecoli_core_w_GAM") -end diff --git a/test/base/utils/reaction.jl b/test/base/utils/reaction.jl deleted file mode 100644 index 8d68f307e..000000000 --- a/test/base/utils/reaction.jl +++ /dev/null @@ -1,47 +0,0 @@ -@testset "Reaction utilities" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - # FBA - fluxes = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], - ) - - # test if reaction is balanced - @test reaction_mass_balanced(model, "PFL") - @test !reaction_mass_balanced(model, "BIOMASS_Ecoli_core_w_GAM") - @test reaction_mass_balanced(model, model.reactions["PFL"]) - @test !reaction_mass_balanced(model, model.reactions["BIOMASS_Ecoli_core_w_GAM"]) - @test reaction_mass_balanced(model, Dict("h_c" => -1.0, "h_e" => 1.0)) - @test !reaction_mass_balanced(model, Dict("h_c" => -1.0, "h2o_c" => 1.0)) - - # test if a reaction is a boundary reaction - @test !is_boundary(model.reactions["PFL"]) - @test is_boundary(model.reactions["EX_glc__D_e"]) - @test !is_boundary(model, "PFL") - @test is_boundary(model, "EX_glc__D_e") - @test !is_boundary(model, model.reactions["PFL"]) - @test is_boundary(model, model.reactions["EX_glc__D_e"]) - - # single-reaction atom balance - @test reaction_atom_balance(model, "FBA")["C"] == 0.0 - @test isapprox( - reaction_atom_balance(model, "BIOMASS_Ecoli_core_w_GAM")["C"], - -42.5555; - atol = TEST_TOLERANCE, - ) - @test reaction_atom_balance(model, model.reactions["FBA"])["C"] == 0.0 - @test isapprox( - reaction_atom_balance(model, model.reactions["BIOMASS_Ecoli_core_w_GAM"])["C"], - -42.5555; - atol = TEST_TOLERANCE, - ) - @test reaction_atom_balance(model, Dict("h_c" => -1.0, "h2o_c" => 1.0))["H"] == 1.0 - - # test if reaction equation can be built back into a sensible reaction string - req = Dict("coa_c" => -1, "for_c" => 1, "accoa_c" => 1, "pyr_c" => -1) - rstr_out = stoichiometry_string(req) - @test occursin("coa_c", split(rstr_out, " = ")[1]) - @test occursin("for", split(rstr_out, " = ")[2]) -end diff --git a/test/coverage/coverage-summary.jl b/test/coverage/coverage-summary.jl deleted file mode 100644 index 0801ca4b1..000000000 --- a/test/coverage/coverage-summary.jl +++ /dev/null @@ -1,12 +0,0 @@ - -using Coverage - -cd(joinpath(@__DIR__, "..", "..")) do - processed = process_folder() - covered_lines, total_lines = get_summary(processed) - percentage = covered_lines / total_lines * 100 - println("($(percentage)%) covered") -end - -# submit the report to codecov.io -Codecov.submit_local(Codecov.process_folder(), pwd(), slug = "LCSB-BioCore/COBREXA.jl") diff --git a/test/data_downloaded.jl b/test/data_downloaded.jl index 66951f0cd..c7bc21985 100644 --- a/test/data_downloaded.jl +++ b/test/data_downloaded.jl @@ -1,79 +1,62 @@ -function check_data_file_hash(path, expected_checksum) - actual_checksum = bytes2hex(sha256(open(path))) - if actual_checksum != expected_checksum - @error "The downloaded data file `$path' seems to be different from the expected one. Tests will likely fail." actual_checksum expected_checksum - end -end - -function download_data_file(url, path, hash) - if isfile(path) - check_data_file_hash(path, hash) - @info "using cached `$path'" - return path - end - - Downloads.download(url, path) - check_data_file_hash(path, hash) - return path -end - +# TODO this should be downloaded by documentation scripts isdir("downloaded") || mkdir("downloaded") df(s) = joinpath("downloaded", s) +const dl = A.download_data_file model_paths = Dict{String,String}( - "iJO1366.json" => download_data_file( + "iJO1366.json" => dl( "http://bigg.ucsd.edu/static/models/iJO1366.json", df("iJO1366.json"), "9376a93f62ad430719f23e612154dd94c67e0d7c9545ed9d17a4d0c347672313", ), - "iJO1366.mat" => download_data_file( + "iJO1366.mat" => dl( "http://bigg.ucsd.edu/static/models/iJO1366.mat", df("iJO1366.mat"), "b5cfe21b6369a00e45d600b783f89521f5cc953e25ee52c5f1d0a3f83743be30", ), - "iJO1366.xml" => download_data_file( + "iJO1366.xml" => dl( "http://bigg.ucsd.edu/static/models/iJO1366.xml", df("iJO1366.xml"), "d6d9ec61ef6f155db5bb2f49549119dc13b96f6098b403ef82ea4240b27232eb", ), - "ecoli_core_model.xml" => download_data_file( + "ecoli_core_model.xml" => dl( "http://systemsbiology.ucsd.edu/sites/systemsbiology.ucsd.edu/files/Attachments/Images/downloads/Ecoli_core/ecoli_core_model.xml", df("ecoli_core_model.xml"), "78692f8509fb36534f4f9b6ade23b23552044f3ecd8b48d84d484636922ae907", ), - "e_coli_core.json" => download_data_file( + "e_coli_core.json" => dl( "http://bigg.ucsd.edu/static/models/e_coli_core.json", df("e_coli_core.json"), "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", ), - "e_coli_core.xml" => download_data_file( + "e_coli_core.xml" => dl( "http://bigg.ucsd.edu/static/models/e_coli_core.xml", df("e_coli_core.xml"), "b4db506aeed0e434c1f5f1fdd35feda0dfe5d82badcfda0e9d1342335ab31116", ), - "e_coli_core.mat" => download_data_file( + "e_coli_core.mat" => dl( "http://bigg.ucsd.edu/static/models/e_coli_core.mat", df("e_coli_core.mat"), "478e6fa047ede1b248975d7565208ac9363a44dd64aad1900b63127394f4175b", ), - "iJR904.mat" => download_data_file( + "iJR904.mat" => dl( "http://bigg.ucsd.edu/static/models/iJR904.mat", df("iJR904.mat"), "d17be86293d4caafc32b829da4e2d0d76eb45e1bb837e0138327043a83e20c6e", ), - "Recon3D.json" => download_data_file( + "Recon3D.json" => dl( "http://bigg.ucsd.edu/static/models/Recon3D.json", df("Recon3D.json"), "aba925f17547a42f9fdb4c1f685d89364cbf4979bbe7862e9f793af7169b26d5", ), - "yeast-GEM.mat" => download_data_file( + "yeast-GEM.mat" => dl( "https://github.com/SysBioChalmers/yeast-GEM/raw/v8.6.2/model/yeast-GEM.mat", df("yeast-GEM.mat"), "c2587e258501737e0141cd47e0f854a60a47faee2d4c6ad582a00e437676b181", ), - "yeast-GEM.xml" => download_data_file( + "yeast-GEM.xml" => dl( "https://github.com/SysBioChalmers/yeast-GEM/raw/v8.6.2/model/yeast-GEM.xml", df("yeast-GEM.xml"), "c728b09d849b744ec7640cbf15776d40fb2d9cbd0b76a840a8661b626c1bd4be", diff --git a/test/data_static.jl b/test/data_static.jl deleted file mode 100644 index a80d46d16..000000000 --- a/test/data_static.jl +++ /dev/null @@ -1,552 +0,0 @@ -test_LP() = CoreModel( - zeros(4, 3), - zeros(4), - ones(3), - ones(3), - ones(3), - ["r$x" for x = 1:3], - ["m$x" for x = 1:4], -) - -test_simpleLP() = CoreModel( - [ - 1.0 1.0 - -1.0 1.0 - ], - [3.0, 1.0], - [-0.25, 1.0], - -ones(2), - 2.0 * ones(2), - ["r$x" for x = 1:2], - ["m$x" for x = 1:2], -) - -test_simpleLP2() = CoreModel( - zeros(2, 2), - [0.0, 0.0], - [-0.25, 1.0], - -ones(2), - 2.0 * ones(2), - ["r$x" for x = 1:2], - ["m$x" for x = 1:2], -) - -test_sparseLP() = CoreModel( - sprand(4000, 3000, 0.5), - sprand(4000, 0.5), - sprand(3000, 0.5), - sprand(3000, 0.5), - sprand(3000, 0.5), - ["r$x" for x = 1:3000], - ["m$x" for x = 1:4000], -) - -test_coupledLP() = CoreModelCoupled( - CoreModel( - sprand(4000, 3000, 0.5), - sprand(4000, 0.5), - sprand(3000, 0.5), - sprand(3000, 0.5), - sprand(3000, 0.5), - ["r$x" for x = 1:3000], - ["m$x" for x = 1:4000], - ), - sprand(2000, 3000, 0.5), - sprand(2000, 0.5), - sprand(2000, 0.5), -) - -test_toyModel() = CoreModel( - [ - -1.0 1.0 0.0 0.0 0.0 0.0 0.0 - -2.0 0.0 1.0 0.0 0.0 0.0 0.0 - 1.0 0.0 0.0 0.0 0.0 0.0 -1.0 - 0.0 -1.0 0.0 -1.0 0.0 0.0 0.0 - 0.0 0.0 -1.0 0.0 -1.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 -1.0 1.0 - ], - zeros(6), - [0, 0, 0, 0, 0, 0, 1.0], - fill(-1000.0, 7), - fill(1000.0, 7), - ["r1", "m1t", "m3t", "EX_m1(e)", "EX_m3(e)", "EX_biomass(e)", "biomass1"], - ["m1[c]", "m3[c]", "m2[c]", "m1[e]", "m3[e]", "biomass[c]"], -) - -const reaction_standard_gibbs_free_energies = Dict{String,Float64}( - #= - ΔᵣG⁰ data from Equilibrator using the E. coli core model's reactions - To generate this data manually, go to https://equilibrator.weizmann.ac.il/ and - enter each reaction into the search bar, fill in the ΔG⁰ below from there. To generate - automatically, use the eQuilibrator.jl package. - =# - "ACALD" => -21.268198981756314, - "PTAr" => 8.651025243027988, - "ALCD2x" => 17.47260052762408, - "PDH" => -34.246780702043225, - "PYK" => -24.48733600711958, - "CO2t" => 0.0, - "MALt2_2" => -6.839209921974724, - "CS" => -39.330940148207475, - "PGM" => -4.470553692565886, - "TKT1" => -1.4976299544215408, - "ACONTa" => 8.46962176350985, - "GLNS" => -15.771242654033706, - "ICL" => 9.53738025507404, - "FBA" => 23.376920310319235, - "SUCCt3" => -43.97082007726285, - "FORt2" => -3.4211767267069124, - "G6PDH2r" => -7.3971867270165035, - "AKGDH" => -28.235442362320782, - "TKT2" => -10.318436817276165, - "FRD7" => 73.61589524884772, - "SUCOAS" => -1.1586968559555615, - "FBP" => -11.606887851480572, - "ICDHyr" => 5.398885342794983, - "AKGt2r" => 10.085299238039797, - "GLUSy" => -47.21690395283849, - "TPI" => 5.621932460512994, - "FORt" => 13.509690780237293, - "ACONTb" => -1.622946931741609, - "GLNabc" => -30.194559792842753, - "RPE" => -3.3881719029747615, - "ACKr" => 14.027197131450492, - "THD2" => -33.84686533309243, - "PFL" => -19.814421615735, - "RPI" => 4.477649590945703, - "D_LACt2" => -3.4223903975852004, - "TALA" => -0.949571985515206, - "PPCK" => 10.659841402564751, - "ACt2r" => -3.4196348363397995, - "NH4t" => -13.606633927039097, - "PGL" => -25.94931748161696, - "NADTRHD" => -0.014869680795754903, - "PGK" => 19.57192102020454, - "LDH_D" => 20.04059765689044, - "ME1" => 12.084968268076864, - "PIt2r" => 10.415108493818785, - "ATPS4r" => -37.570267233299816, - "PYRt2" => -3.422891289768689, - "GLCpts" => -45.42430981510088, - "GLUDy" => 32.834943812395665, - "CYTBD" => -59.700410815493775, - "FUMt2_2" => -6.845105500839001, - "FRUpts2" => -42.67529760694199, - "GAPD" => 0.5307809794271634, - "H2Ot" => -1.5987211554602254e-14, - "PPC" => -40.81304419704113, - "NADH16" => -80.37770501380615, - "PFK" => -18.546314942995934, - "MDH" => 25.912462872631522, - "PGI" => 2.6307087407442395, - "O2t" => 0.0, - "ME2" => 12.099837948872533, - "GND" => 10.312275879236381, - "SUCCt2_2" => -6.82178244356977, - "GLUN" => -14.381960140443113, - "ETOHt2r" => -16.930867506944217, - "ADK1" => 0.3893321583896068, - "ACALDt" => -3.197442310920451e-14, - "SUCDi" => -73.61589524884772, - "ENO" => -3.8108376097261782, - "MALS" => -39.22150045995042, - "GLUt2r" => -3.499043558772904, - "PPS" => -6.0551989457468665, - "FUM" => -3.424133018702122, -) - -const ecoli_core_gene_product_masses = Dict{String,Float64}( - #= - Data downloaded from Uniprot for E. coli K12, - gene mass in kDa. To obtain these data yourself, go to - Uniprot: https://www.uniprot.org/ - and search using these terms: - =# - "b4301" => 23.214, - "b1602" => 48.723, - "b4154" => 65.972, - "b3236" => 32.337, - "b1621" => 56.627, - "b1779" => 35.532, - "b3951" => 85.96, - "b1676" => 50.729, - "b3114" => 85.936, - "b1241" => 96.127, - "b2276" => 52.044, - "b1761" => 48.581, - "b3925" => 35.852, - "b3493" => 53.389, - "b3733" => 31.577, - "b2926" => 41.118, - "b0979" => 42.424, - "b4015" => 47.522, - "b2296" => 43.29, - "b4232" => 36.834, - "b3732" => 50.325, - "b2282" => 36.219, - "b2283" => 100.299, - "b0451" => 44.515, - "b2463" => 82.417, - "b0734" => 42.453, - "b3738" => 30.303, - "b3386" => 24.554, - "b3603" => 59.168, - "b2416" => 63.562, - "b0729" => 29.777, - "b0767" => 36.308, - "b3734" => 55.222, - "b4122" => 60.105, - "b2987" => 53.809, - "b2579" => 14.284, - "b0809" => 26.731, - "b1524" => 33.516, - "b3612" => 56.194, - "b3735" => 19.332, - "b3731" => 15.068, - "b1817" => 35.048, - "b1603" => 54.623, - "b1773" => 30.81, - "b4090" => 16.073, - "b0114" => 99.668, - "b3962" => 51.56, - "b2464" => 35.659, - "b2976" => 80.489, - "b1818" => 27.636, - "b2285" => 18.59, - "b1702" => 87.435, - "b1849" => 42.434, - "b1812" => 50.97, - "b0902" => 28.204, - "b3403" => 59.643, - "b1612" => 60.299, - "b1854" => 51.357, - "b0811" => 27.19, - "b0721" => 14.299, - "b2914" => 22.86, - "b1297" => 53.177, - "b0723" => 64.422, - "b3919" => 26.972, - "b3115" => 43.384, - "b4077" => 47.159, - "b3528" => 45.436, - "b0351" => 33.442, - "b2029" => 51.481, - "b1819" => 30.955, - "b0728" => 41.393, - "b2935" => 72.212, - "b2415" => 9.119, - "b0727" => 44.011, - "b0116" => 50.688, - "b0485" => 32.903, - "b3736" => 17.264, - "b0008" => 35.219, - "b3212" => 163.297, - "b3870" => 51.904, - "b4014" => 60.274, - "b2280" => 19.875, - "b2133" => 64.612, - "b2278" => 66.438, - "b0118" => 93.498, - "b2288" => 16.457, - "b3739" => 13.632, - "b3916" => 34.842, - "b3952" => 32.43, - "b2925" => 39.147, - "b2465" => 73.043, - "b2297" => 77.172, - "b2417" => 18.251, - "b4395" => 24.065, - "b3956" => 99.063, - "b0722" => 12.868, - "b2779" => 45.655, - "b0115" => 66.096, - "b0733" => 58.205, - "b1478" => 35.38, - "b2492" => 30.565, - "b0724" => 26.77, - "b0755" => 28.556, - "b1136" => 45.757, - "b2286" => 68.236, - "b0978" => 57.92, - "b1852" => 55.704, - "b2281" => 20.538, - "b2587" => 47.052, - "b2458" => 36.067, - "b0904" => 30.991, - "b1101" => 50.677, - "b0875" => 23.703, - "b3213" => 52.015, - "b2975" => 58.92, - "b0720" => 48.015, - "b0903" => 85.357, - "b1723" => 32.456, - "b2097" => 38.109, - "b3737" => 8.256, - "b0810" => 24.364, - "b4025" => 61.53, - "b1380" => 36.535, - "b0356" => 39.359, - "b2277" => 56.525, - "b1276" => 97.677, - "b4152" => 15.015, - "b1479" => 63.197, - "b4153" => 27.123, - "b4151" => 13.107, - "b2287" => 25.056, - "b0474" => 23.586, - "b2284" => 49.292, - "b1611" => 50.489, - "b0726" => 105.062, - "b2279" => 10.845, -) - -const ecoli_core_protein_stoichiometry = Dict{String,Vector{Vector{Float64}}}( - #= - Data made up, each isozyme is assumed to be composed of - only one subunit each. - =# - "ACALD" => [[1.0], [1.0]], - "PTAr" => [[1.0], [1.0]], - "ALCD2x" => [[1.0], [1.0], [1.0]], - "PDH" => [[1.0, 1.0, 1.0]], - "PYK" => [[1.0], [1.0]], - "CO2t" => [[1.0]], - "MALt2_2" => [[1.0]], - "CS" => [[1.0]], - "PGM" => [[1.0], [1.0], [1.0]], - "TKT1" => [[1.0], [1.0]], - "ACONTa" => [[1.0], [1.0]], - "GLNS" => [[1.0], [1.0]], - "ICL" => [[1.0]], - "FBA" => [[1.0], [1.0], [1.0]], - "FORt2" => [[1.0], [1.0]], - "G6PDH2r" => [[1.0]], - "AKGDH" => [[1.0, 1.0, 1.0]], - "TKT2" => [[1.0], [1.0]], - "FRD7" => [[1.0, 1.0, 1.0, 1.0]], - "SUCOAS" => [[1.0, 1.0]], - "FBP" => [[1.0], [1.0]], - "ICDHyr" => [[1.0]], - "AKGt2r" => [[1.0]], - "GLUSy" => [[1.0, 1.0]], - "TPI" => [[1.0]], - "FORt" => [[1.0], [1.0]], - "ACONTb" => [[1.0], [1.0]], - "GLNabc" => [[1.0, 1.0, 1.0]], - "RPE" => [[1.0], [1.0]], - "ACKr" => [[1.0], [1.0], [1.0]], - "THD2" => [[1.0, 1.0]], - "PFL" => [[1.0, 1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]], - "RPI" => [[1.0], [1.0]], - "D_LACt2" => [[1.0], [1.0]], - "TALA" => [[1.0], [1.0]], - "PPCK" => [[1.0]], - "ACt2r" => [[1.0]], - "NH4t" => [[1.0], [1.0]], - "PGL" => [[1.0]], - "NADTRHD" => [[1.0], [1.0, 1.0]], - "PGK" => [[1.0]], - "LDH_D" => [[1.0], [1.0]], - "ME1" => [[1.0]], - "PIt2r" => [[1.0], [1.0]], - "ATPS4r" => [ - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], - ], - "GLCpts" => [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], - "GLUDy" => [[1.0]], - "CYTBD" => [[1.0, 1.0], [1.0, 1.0]], - "FUMt2_2" => [[1.0]], - "FRUpts2" => [[1.0, 1.0, 1.0, 1.0, 1.0]], - "GAPD" => [[1.0]], - "H2Ot" => [[1.0], [1.0]], - "PPC" => [[1.0]], - "NADH16" => [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]], - "PFK" => [[1.0], [1.0]], - "MDH" => [[1.0]], - "PGI" => [[1.0]], - "O2t" => [[1.0]], - "ME2" => [[1.0]], - "GND" => [[1.0]], - "SUCCt2_2" => [[1.0]], - "GLUN" => [[1.0], [1.0], [1.0]], - "ETOHt2r" => [[1.0]], - "ADK1" => [[1.0]], - "ACALDt" => [[1.0]], - "SUCDi" => [[1.0, 1.0, 1.0, 1.0]], - "ENO" => [[1.0]], - "MALS" => [[1.0], [1.0]], - "GLUt2r" => [[1.0]], - "PPS" => [[1.0]], - "FUM" => [[1.0], [1.0], [1.0]], -) - -const ecoli_core_reaction_kcats = Dict{String,Vector{Tuple{Float64,Float64}}}( - #= - Data taken from Heckmann, David, et al. "Machine learning applied to enzyme - turnover numbers reveals protein structural correlates and improves metabolic - models." Nature communications 9.1 (2018): 1-10. Assume forward and reverse - kcats are the same, and each isozyme has the same kcat. - =# - "ACALD" => - [(568.1130792316333, 568.1130792316333), (568.856126503717, 568.856126503717)], - "PTAr" => [ - (1171.9703624351055, 1171.9703624351055), - (1173.7231032615289, 1173.7231032615289), - ], - "ALCD2x" => [ - (75.9547881894345, 75.9547881894345), - (75.96334310351442, 75.96334310351442), - (76.1472359297987, 76.1472359297987), - ], - "PDH" => [(529.7610874857239, 529.7610874857239)], - "PYK" => [ - (422.0226052080562, 422.0226052080562), - (422.1332899347833, 422.1332899347833), - ], - "MALt2_2" => [(234.03664660088714, 234.03664660088714)], - "CS" => [(113.29607453875758, 113.29607453875758)], - "PGM" => [ - (681.4234715886669, 681.4234715886669), - (681.6540601244343, 681.6540601244343), - (680.5234799168278, 680.5234799168278), - ], - "TKT1" => [ - (311.16139580671637, 311.16139580671637), - (311.20967965149947, 311.20967965149947), - ], - "ACONTa" => [ - (191.02308213992006, 191.02308213992006), - (191.03458045697235, 191.03458045697235), - ], - "GLNS" => [ - (89.83860937287024, 89.83860937287024), - (89.82177852142014, 89.82177852142014), - ], - "ICL" => [(17.45922330097792, 17.45922330097792)], - "FBA" => [ - (373.425646787578, 373.425646787578), - (372.74936053215833, 372.74936053215833), - (372.88627228768166, 372.88627228768166), - ], - "FORt2" => [ - (233.93045260179326, 233.93045260179326), - (233.84804009142908, 233.84804009142908), - ], - "G6PDH2r" => [(589.3761070080022, 589.3761070080022)], - "AKGDH" => [(264.48071159327156, 264.48071159327156)], - "TKT2" => [ - (467.4226876901618, 467.4226876901618), - (468.1440593542596, 468.1440593542596), - ], - "FRD7" => [(90.20637824912605, 90.20637824912605)], - "SUCOAS" => [(18.494387648707622, 18.494387648707622)], - "FBP" => [ - (568.5346256470805, 568.5346256470805), - (567.6367759041788, 567.6367759041788), - ], - "ICDHyr" => [(39.62446791678959, 39.62446791678959)], - "AKGt2r" => [(234.99097804446805, 234.99097804446805)], - "GLUSy" => [(33.262997317319055, 33.262997317319055)], - "TPI" => [(698.301904211076, 698.301904211076)], - "FORt" => [ - (234.38391855848187, 234.38391855848187), - (234.34725576182922, 234.34725576182922), - ], - "ACONTb" => [ - (159.74612206327865, 159.74612206327865), - (159.81975755249232, 159.81975755249232), - ], - "GLNabc" => [(233.80358131677775, 233.80358131677775)], - "RPE" => [ - (1772.4850826683305, 1772.4850826683305), - (1768.8536177485582, 1768.8536177485582), - ], - "ACKr" => [ - (554.611547307207, 554.611547307207), - (555.112707891257, 555.112707891257), - (555.2464368932744, 555.2464368932744), - ], - "THD2" => [(24.739139801185537, 24.739139801185537)], - "PFL" => [ - (96.56316095411077, 96.56316095411077), - (96.65024313036014, 96.65024313036014), - (96.60761818004025, 96.60761818004025), - (96.49541118899961, 96.49541118899961), - ], - "RPI" => [ - (51.771578021074234, 51.771578021074234), - (51.81603467243345, 51.81603467243345), - ], - "D_LACt2" => [ - (233.51709131524734, 233.51709131524734), - (233.83187606098016, 233.83187606098016), - ], - "TALA" => [ - (109.05210545422884, 109.05210545422884), - (109.04246437049026, 109.04246437049026), - ], - "PPCK" => [(218.4287805666016, 218.4287805666016)], - "PGL" => [(2120.4297518987964, 2120.4297518987964)], - "NADTRHD" => [ - (186.99387360624777, 186.99387360624777), - (187.16629305266423, 187.16629305266423), - ], - "PGK" => [(57.641966636896335, 57.641966636896335)], - "LDH_D" => [ - (31.11118891764946, 31.11118891764946), - (31.12493425054357, 31.12493425054357), - ], - "ME1" => [(487.0161203971232, 487.0161203971232)], - "PIt2r" => [ - (233.8651331835765, 233.8651331835765), - (234.27374798581067, 234.27374798581067), - ], - "ATPS4r" => [ - (7120.878030435999, 7120.878030435999), - (7116.751386037507, 7116.751386037507), - ], - "GLCpts" => [ - (233.9009878400008, 233.9009878400008), - (233.66656882114864, 233.66656882114864), - (233.66893882934883, 233.66893882934883), - ], - "GLUDy" => [(105.32811069172409, 105.32811069172409)], - "CYTBD" => [ - (153.18512795009505, 153.18512795009505), - (153.2429537682265, 153.2429537682265), - ], - "FUMt2_2" => [(234.37495609395967, 234.37495609395967)], - "FRUpts2" => [(234.1933863380989, 234.1933863380989)], - "GAPD" => [(128.76795529111456, 128.76795529111456)], - "PPC" => [(165.52424516841342, 165.52424516841342)], - "NADH16" => [(971.7487306963936, 971.7487306963936)], - "PFK" => [ - (1000.4626204522712, 1000.4626204522712), - (1000.5875517343595, 1000.5875517343595), - ], - "MDH" => [(25.931655783969283, 25.931655783969283)], - "PGI" => [(468.11833198138834, 468.11833198138834)], - "ME2" => [(443.0973626307168, 443.0973626307168)], - "GND" => [(240.1252264230952, 240.1252264230952)], - "SUCCt2_2" => [(234.18109388303225, 234.18109388303225)], - "GLUN" => [ - (44.76358496525738, 44.76358496525738), - (44.84850207360875, 44.84850207360875), - (44.76185250415503, 44.76185250415503), - ], - "ADK1" => [(111.64869652600649, 111.64869652600649)], - "SUCDi" => [(680.3193833053011, 680.3193833053011)], - "ENO" => [(209.35855069219886, 209.35855069219886)], - "MALS" => [ - (252.7540503869977, 252.7540503869977), - (252.2359738678874, 252.2359738678874), - ], - "GLUt2r" => [(234.22890837451837, 234.22890837451837)], - "PPS" => [(706.1455885214322, 706.1455885214322)], - "FUM" => [ - (1576.8372583425075, 1576.8372583425075), - (1576.233088455828, 1576.233088455828), - (1575.9638204848736, 1575.9638204848736), - ], -) diff --git a/test/doctests.jl b/test/doctests.jl deleted file mode 100644 index 50da9f2d5..000000000 --- a/test/doctests.jl +++ /dev/null @@ -1,14 +0,0 @@ -using Test, Documenter, COBREXA - -ex = quote - using COBREXA, Tulip - include(joinpath(dirname(pathof(COBREXA)), "..", "test", "data_static.jl")) - model = test_LP() -end - -# set module-level metadata -DocMeta.setdocmeta!(COBREXA, :DocTestSetup, ex; recursive = true) - -@testset "Documentation tests" begin - doctest(COBREXA; manual = true) -end diff --git a/test/io/h5.jl b/test/io/h5.jl deleted file mode 100644 index 82f29852f..000000000 --- a/test/io/h5.jl +++ /dev/null @@ -1,31 +0,0 @@ - -@testset "HDF5 model SBML model" begin - model = load_model(CoreModel, model_paths["e_coli_core.xml"]) - fn = "ecoli_test.h5.noextension" - h5m = save_model(model, fn, extension = ".h5") - @test h5m isa HDF5Model - @test h5m.filename == fn - @test h5m.h5 == nothing #the file should not be open by default - - h5 = load_model(fn, extension = ".h5") - precache!(h5) - @test !isnothing(h5.h5) - - # briefly test that the loading is okay - @test n_reactions(model) == n_reactions(h5) - @test n_metabolites(model) == n_metabolites(h5) - @test issetequal(reactions(model), reactions(h5)) - @test issetequal(metabolites(model), metabolites(h5)) - @test issorted(metabolites(h5)) - @test issorted(reactions(h5)) - @test size(stoichiometry(model)) == size(stoichiometry(h5)) - @test isapprox(sum(stoichiometry(model)), sum(stoichiometry(h5))) - rxnp = sortperm(reactions(model)) - @test bounds(model)[1][rxnp] == bounds(h5)[1] - @test bounds(model)[2][rxnp] == bounds(h5)[2] - @test objective(model)[rxnp] == objective(h5) - @test all(iszero, balance(h5)) - - close(h5) - @test isnothing(h5.h5) -end diff --git a/test/io/io.jl b/test/io/io.jl deleted file mode 100644 index 4eb037f0b..000000000 --- a/test/io/io.jl +++ /dev/null @@ -1,26 +0,0 @@ -@testset "Opening models from BIGG" begin - - sbmlmodel = load_model(model_paths["iJO1366.xml"]) - @test sbmlmodel isa SBMLModel - @test n_reactions(sbmlmodel) == 2583 - - matlabmodel = load_model(model_paths["iJO1366.mat"]) - @test matlabmodel isa MATModel - @test n_reactions(matlabmodel) == 2583 - - jsonmodel = load_model(model_paths["iJO1366.json"]) - @test jsonmodel isa JSONModel - @test n_reactions(jsonmodel) == 2583 - - @test Set(lowercase.(reactions(sbmlmodel))) == - Set("r_" .* lowercase.(reactions(matlabmodel))) - @test Set(lowercase.(reactions(sbmlmodel))) == - Set("r_" .* lowercase.(reactions(jsonmodel))) - - # specifically test parsing of gene-reaction associations in Recon - reconmodel = load_model(StandardModel, model_paths["Recon3D.json"]) - @test n_reactions(reconmodel) == 10600 - recon_grrs = [r.grr for (i, r) in reconmodel.reactions if !isnothing(r.grr)] - @test length(recon_grrs) == 5938 - @test sum(length.(recon_grrs)) == 13903 -end diff --git a/test/io/json.jl b/test/io/json.jl deleted file mode 100644 index 1a7922266..000000000 --- a/test/io/json.jl +++ /dev/null @@ -1,24 +0,0 @@ -@testset "Test conversion from JSONModel to StandardModel" begin - - jsonmodel = load_model(model_paths["e_coli_core.json"]) - stdmodel = convert(StandardModel, jsonmodel) - - # test if same reaction ids - @test issetequal(reactions(jsonmodel), reactions(stdmodel)) - @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) - @test issetequal(genes(jsonmodel), genes(stdmodel)) - # not the best tests since it is possible that error could cancel each other out: - @test sum(stoichiometry(jsonmodel)) == sum(stoichiometry(stdmodel)) - jlbs, jubs = bounds(jsonmodel) - slbs, subs = bounds(jsonmodel) - @test sum(jlbs) == sum(slbs) - @test sum(jubs) == sum(subs) -end - -@testset "Save JSON model" begin - model = load_model(CoreModel, model_paths["e_coli_core.json"]) - testpath = tmpfile("modeltest.json") - save_model(model, testpath) - wrote = convert(CoreModel, load_json_model(testpath)) - @test isequal(model, wrote) -end diff --git a/test/io/mat.jl b/test/io/mat.jl deleted file mode 100644 index e1bc8b5e5..000000000 --- a/test/io/mat.jl +++ /dev/null @@ -1,22 +0,0 @@ - -@testset "Import MAT model" begin - cp = load_model(CoreModel, model_paths["iJR904.mat"]) - @test cp isa CoreModel - @test size(cp.S) == (761, 1075) -end - -@testset "Save MAT model" begin - loaded = load_model(CoreModel, model_paths["iJR904.mat"]) - testpath = tmpfile("iJR904-clone.mat") - save_model(loaded, testpath) - wrote = load_model(CoreModel, testpath) - @test wrote isa CoreModel - @test isequal(wrote, loaded) -end - -@testset "Import yeast-GEM (mat)" begin - m = load_model(StandardModel, model_paths["yeast-GEM.mat"]) - @test n_metabolites(m) == 2744 - @test n_reactions(m) == 4063 - @test n_genes(m) == 1160 -end diff --git a/test/io/sbml.jl b/test/io/sbml.jl deleted file mode 100644 index 5ab28d222..000000000 --- a/test/io/sbml.jl +++ /dev/null @@ -1,33 +0,0 @@ - -@testset "SBML import and conversion" begin - sbmlm = load_sbml_model(model_paths["ecoli_core_model.xml"]) - m = convert(CoreModel, sbmlm) - - @test size(stoichiometry(sbmlm)) == (92, 95) - @test size(stoichiometry(m)) == (n_metabolites(sbmlm), n_reactions(sbmlm)) - @test length(m.S.nzval) == 380 - @test length.(bounds(sbmlm)) == (95, 95) - @test length.(bounds(m)) == (95, 95) - @test all([length(m.xl), length(m.xu), length(m.c)] .== 95) - - @test metabolites(m)[1:3] == ["M_13dpg_c", "M_2pg_c", "M_3pg_c"] - @test reactions(m)[1:3] == ["R_ACALD", "R_ACALDt", "R_ACKr"] - - cm = convert(CoreModelCoupled, sbmlm) - @test n_coupling_constraints(cm) == 0 -end - -@testset "Save SBML model" begin - model = load_model(CoreModel, model_paths["e_coli_core.xml"]) - testpath = tmpfile("modeltest.xml") - save_model(convert(SBMLModel, model), testpath) - wrote = convert(CoreModel, load_sbml_model(testpath)) - @test isequal(model, wrote) -end - -@testset "Import yeast-GEM (sbml)" begin - m = load_model(StandardModel, model_paths["yeast-GEM.xml"]) - @test n_metabolites(m) == 2744 - @test n_reactions(m) == 4063 - @test n_genes(m) == 1160 -end diff --git a/test/reconstruction/CoreModel.jl b/test/reconstruction/CoreModel.jl deleted file mode 100644 index 1a72a826e..000000000 --- a/test/reconstruction/CoreModel.jl +++ /dev/null @@ -1,255 +0,0 @@ -@testset "Change bounds" begin - cp = test_LP() - - change_bound!(cp, 1, lower = -10, upper = 10) - @test cp.xl[1] == -10 - @test cp.xu[1] == 10 - change_bounds!(cp, [1, 2]; lower = [-11, -12.2], upper = [11, 23.0]) - @test cp.xl[2] == -12.2 - @test cp.xu[1] == 11 - - change_bound!(cp, "r1", lower = -101, upper = 101) - @test cp.xl[1] == -101 - @test cp.xu[1] == 101 - change_bounds!(cp, ["r1", "r2"]; lower = [-113, -12.23], upper = [114, 233.0]) - @test cp.xl[2] == -12.23 - @test cp.xu[1] == 114 - change_bounds!(cp, ["r1", "r2"]; lower = [-114, nothing], upper = [nothing, 2333.0]) - @test cp.xl[1] == -114 - @test cp.xl[2] == -12.23 - @test cp.xu[1] == 114 - @test cp.xu[2] == 2333 - - new_model = change_bound(cp, 1, lower = -10, upper = 10) - @test new_model.xl[1] == -10 - @test new_model.xu[1] == 10 - new_model = change_bounds(cp, [1, 2]; lower = [-11, -12.2], upper = [11, 23.0]) - @test new_model.xl[2] == -12.2 - @test new_model.xu[1] == 11 - - new_model = change_bound(cp, "r1", lower = -101, upper = 101) - @test new_model.xl[1] == -101 - @test new_model.xu[1] == 101 - new_model = change_bounds(cp, ["r1", "r2"]; lower = [-113, -12.23], upper = [113, 1000]) - @test new_model.xl[2] == -12.23 - @test new_model.xu[1] == 113 - new_model = - change_bounds(cp, ["r1", "r2"]; lower = [nothing, -10], upper = [110, nothing]) - @test new_model.xl[1] == -114 - @test new_model.xl[2] == -10 - @test new_model.xu[1] == 110 - @test new_model.xu[2] == 2333 -end - -@testset "Verify consistency" begin - cp = test_LP() - (new_reactions, new_mets) = verify_consistency( - cp, - reshape(cp.S[:, end], :, 1), - [1.0, 2.0, 3.0, 4.0], - [2.0], - [-1.0], - [1.0], - ["r4"], - ["m1", "m2", "m3", "m6"], - [1], - [4], - ) - @test new_reactions == [1] - @test new_mets == [4] - - (new_reactions, new_mets) = verify_consistency( - cp, - reshape(cp.S[:, end], :, 1), - [1.0, 2.0, 3.0, 4.0], - [2.0], - [-1.0], - [1.0], - ["r1"], - ["m1", "m2", "m3", "m6"], - [], - [4], - ) - @test new_reactions == [] - @test new_mets == [4] -end - -@testset "Add reactions (checking existence and consistency)" begin - cp = test_LP() - (new_cp, new_reactions, new_mets) = add_reactions( - cp, - cp.S[:, end], - [1.0, 2.0, 3.0, 4.0], - 2.0, - -1.0, - 1.0, - check_consistency = true, - ) - @test n_reactions(cp) + 1 == n_reactions(new_cp) - - (new_cp, new_reactions, new_mets) = add_reactions( - cp, - cp.S[:, end], - [1.0, 2.0, 3.0, 4.0], - 2.0, - -1.0, - 1.0, - "r1", - ["m1", "m2", "m3", "m6"], - check_consistency = true, - ) - @test n_reactions(cp) == n_reactions(new_cp) - @test n_metabolites(cp) + 1 == n_metabolites(new_cp) -end - -@testset "Add reactions" begin - cp = test_LP() - cp = add_reactions(cp, 2.0 * ones(4), 3 .* ones(4), 2.0, -1.0, 1.0) - @test size(cp.S) == (8, 4) - cp = add_reactions(cp, 2.0 * ones(4, 1), 3 .* ones(4), 2 .* ones(1), -ones(1), ones(1)) - @test size(cp.S) == (12, 5) - cp = add_reactions( - cp, - 2.0 * ones(4, 10), - 3 .* ones(4), - 2 .* ones(10), - -ones(10), - ones(10), - ) - @test size(cp.S) == (16, 15) - - cp = test_sparseLP() - @test size(cp.S) == (4000, 3000) - cp = add_reactions(cp, 2.0 * sprand(4000, 0.5), 3 .* sprand(4000, 0.5), 2.0, -1.0, 1.0) - @test size(cp.S) == (8000, 3001) - cp = add_reactions( - cp, - 2.0 * sprand(4000, 1, 0.5), - 3 .* sprand(4000, 0.5), - 2 .* sprand(1, 0.5), - -sprand(1, 0.5), - sprand(1, 0.5), - ) - @test size(cp.S) == (12000, 3002) - cp = add_reactions( - cp, - 2.0 * sprand(4000, 1000, 0.5), - 3 .* sprand(4000, 0.5), - 2 .* sprand(1000, 0.5), - -sprand(1000, 0.5), - sprand(1000, 0.5), - ) - @test size(cp.S) == (16000, 4002) - - cp = test_sparseLP() - @test size(cp.S) == (4000, 3000) - cp = add_reactions(cp, 2.0 * ones(4000), 3 .* ones(4000), 2.0, -1.0, 1.0) - @test size(cp.S) == (8000, 3001) - cp = add_reactions( - cp, - 2.0 * ones(4000, 1), - 3 .* ones(4000), - 2 .* ones(1), - -ones(1), - ones(1), - ) - @test size(cp.S) == (12000, 3002) - cp = add_reactions( - cp, - 2.0 * ones(4000, 1000), - 3 .* ones(4000), - 2 .* ones(1000), - -ones(1000), - ones(1000), - ) - @test size(cp.S) == (16000, 4002) - - # proper subset of existing metabolites - cp = test_LP() - new_cp = add_reactions(cp, [-1.0], zeros(1), 1.0, 0.0, 1.0, "r4", ["m1"]) - @test n_reactions(cp) + 1 == n_reactions(new_cp) - - @test_throws DimensionMismatch add_reactions( - cp, - 2.0 * ones(4000, 1), - 3 .* ones(4000), - 2 .* ones(2), - -ones(1), - ones(1), - ) -end - -@testset "Remove reactions" begin - cp = test_LP() - cp = remove_reaction(cp, 2) - @test size(cp.S) == (4, 2) - cp = remove_reactions(cp, [2, 1]) - @test size(cp.S) == (4, 0) - - cp = test_LP() - cp = remove_reaction(cp, "r1") - @test size(cp.S) == (4, 2) - cp = remove_reactions(cp, ["r2"]) - @test size(cp.S) == (4, 1) - - lp = CoreModel( - [1.0 1 1 0; 1 1 1 0; 1 1 1 0; 0 0 0 1], - collect(1.0:4), - collect(1.0:4), - collect(1.0:4), - collect(1.0:4), - ["r1"; "r2"; "r3"; "r4"], - ["m1"; "m2"; "m3"; "m4"], - ) - - modLp = remove_reactions(lp, [4; 1]) - @test stoichiometry(modLp) == stoichiometry(lp)[:, 2:3] - @test balance(modLp) == balance(lp) - @test objective(modLp) == objective(lp)[2:3] - @test bounds(modLp)[1] == bounds(lp)[1][2:3] - @test bounds(modLp)[2] == bounds(lp)[2][2:3] - @test reactions(modLp) == reactions(lp)[2:3] - @test metabolites(modLp) == metabolites(lp) -end - -@testset "Remove metabolites" begin - model = load_model(CoreModel, model_paths["e_coli_core.json"]) - - m1 = remove_metabolites(model, ["glc__D_e", "for_c"]) - m2 = remove_metabolite(model, "glc__D_e") - m3 = remove_metabolites(model, Int.(indexin(["glc__D_e", "for_c"], metabolites(model)))) - m4 = remove_metabolite(model, first(indexin(["glc__D_e"], metabolites(model)))) - - @test size(stoichiometry(m1)) == (70, 90) - @test size(stoichiometry(m2)) == (71, 93) - @test size(stoichiometry(m3)) == (70, 90) - @test size(stoichiometry(m4)) == (71, 93) - @test all((!in(metabolites(m1))).(["glc__D_e", "for_c"])) - @test !(["glc__D_e"] in metabolites(m2)) - @test all((!in(metabolites(m3))).(["glc__D_e", "for_c"])) - @test !(["glc__D_e"] in metabolites(m4)) -end - -@testset "Core in place modifications" begin - toymodel = test_toyModel() - - rxn1 = Reaction("nr1"; metabolites = Dict("m1[c]" => -1, "m3[c]" => 1)) - rxn2 = Reaction("nr2"; metabolites = Dict("m1[c]" => -1, "m2[c]" => 1)) - rxn3 = Reaction("nr3"; metabolites = Dict("m2[c]" => -1, "m3[c]" => 1)) - rxn3.lb = 10 - - add_reaction!(toymodel, rxn1) - @test toymodel.S[1, 8] == -1 - @test toymodel.S[2, 8] == 1 - @test all(toymodel.S[3:end, 8] .== 0) - @test size(toymodel.S) == (6, 8) - @test toymodel.rxns[end] == "nr1" - - add_reactions!(toymodel, [rxn2, rxn3]) - @test size(toymodel.S) == (6, 10) - @test toymodel.xl[end] == 10 - - change_objective!(toymodel, "nr1") - @test objective(toymodel)[8] == 1.0 - @test objective(toymodel)[7] == 0.0 -end diff --git a/test/reconstruction/CoreModelCoupled.jl b/test/reconstruction/CoreModelCoupled.jl deleted file mode 100644 index 2305e25fd..000000000 --- a/test/reconstruction/CoreModelCoupled.jl +++ /dev/null @@ -1,183 +0,0 @@ -@testset "Coupling constraints" begin - cp = convert(CoreModelCoupled, test_LP()) - @test size(cp.lm.S) == (4, 3) - @test size(stoichiometry(convert(CoreModel, cp))) == (4, 3) - new_cp = add_coupling_constraints(cp, stoichiometry(cp)[end, :], -1.0, 1.0) - @test n_coupling_constraints(cp) + 1 == n_coupling_constraints(new_cp) - - new_cp = - add_coupling_constraints(cp, stoichiometry(cp)[1:2, :], [-1.0; -1.0], [1.0; 1.0]) - @test n_coupling_constraints(cp) + 2 == n_coupling_constraints(new_cp) - - n_c = n_coupling_constraints(cp) - add_coupling_constraints!(cp, stoichiometry(cp)[end, :], -1.0, 1.0) - @test n_c + 1 == n_coupling_constraints(cp) - add_coupling_constraints!(cp, stoichiometry(cp)[1:2, :], [-1.0; -1.0], [1.0; 1.0]) - @test n_c + 3 == n_coupling_constraints(cp) - - n_c = n_coupling_constraints(cp) - remove_coupling_constraints!(cp, 1) - @test n_c - 1 == n_coupling_constraints(cp) - remove_coupling_constraints!(cp, [1, 2]) - @test n_c - 3 == n_coupling_constraints(cp) - @test n_coupling_constraints(cp) == 0 - - cp = test_coupledLP() - n_c = n_coupling_constraints(cp) - new_cp = remove_coupling_constraints(cp, 1) - @test size(coupling(cp)) == (n_c, n_reactions(cp)) - @test n_c - 1 == n_coupling_constraints(new_cp) - @test n_coupling_constraints(cp) == n_c - new_cp = remove_coupling_constraints(cp, [1, 2]) - @test n_c - 2 == n_coupling_constraints(new_cp) - new_cp = remove_coupling_constraints(cp, Vector(1:n_coupling_constraints(cp))) - @test n_coupling_constraints(new_cp) == 0 - @test n_coupling_constraints(cp) == n_c - - cp = test_coupledLP() - change_coupling_bounds!(cp, [3, 1], cl = [-10.0, -20], cu = [10.0, 20]) - cl, cu = coupling_bounds(cp) - @test cl[[1, 3]] == [-20, -10] - @test cu[[1, 3]] == [20, 10] - change_coupling_bounds!(cp, [1000, 1001], cl = [-50.0, -60.0]) - cl, cu = coupling_bounds(cp) - @test cl[[1000, 1001]] == [-50.0, -60.0] -end - -@testset "Add reactions" begin - cp = convert(CoreModelCoupled, test_LP()) - cp = add_coupling_constraints(cp, stoichiometry(cp)[end, :], -1.0, 1.0) - - new_cp = add_reactions(cp, 2.0 * ones(4), 3 .* ones(4), 2.0, -1.0, 1.0) - @test new_cp isa CoreModelCoupled - @test cp.C == new_cp.C[:, 1:end-1] - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - new_cp = add_reactions( - cp, - 2.0 * ones(4), - 3 .* ones(4), - 2.0, - -1.0, - 1.0, - "r4", - ["m$i" for i = 1:4], - ) - @test cp.C == new_cp.C[:, 1:end-1] - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - new_cp = add_reactions( - cp, - 2.0 * ones(4, 10), - 3 .* ones(4), - 2 .* ones(10), - -ones(10), - ones(10), - ) - @test cp.C == new_cp.C[:, 1:end-10] - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - new_cp = add_reactions( - cp, - 2.0 * ones(4, 10), - 3 .* ones(4), - 2 .* ones(10), - -ones(10), - ones(10), - ["r$i" for i = 1:10], - ["m$i" for i = 1:4], - ) - @test cp.C == new_cp.C[:, 1:end-7] # 3 reactions were already present - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - new_cp = - add_reactions(cp, 2.0 * sprand(4000, 0.5), 3 .* sprand(4000, 0.5), 2.0, -1.0, 1.0) - @test cp.C == new_cp.C[:, 1:end-1] - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - cm = CoreModel( - 2.0 * ones(4, 10), - 3 .* ones(4), - 2 .* ones(10), - -ones(10), - ones(10), - ["r$i" for i = 1:10], - ["m$i" for i = 1:4], - ) - new_cp = add_reactions(cp, cm) - @test cp.C == new_cp.C[:, 1:end-7] # 3 reactions were already present - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu -end - -@testset "Remove reactions" begin - cp = convert(CoreModelCoupled, test_LP()) - cp = add_coupling_constraints(cp, 1.0 .* collect(1:n_reactions(cp)), -1.0, 1.0) - - new_cp = remove_reactions(cp, [3, 2]) - @test new_cp isa CoreModelCoupled - @test new_cp.C[:] == cp.C[:, 1] - @test new_cp.cl == cp.cl - @test new_cp.cu == cp.cu - - new_cp = remove_reaction(cp, 2) - @test new_cp.C == cp.C[:, [1, 3]] - @test new_cp.cl == cp.cl - @test new_cp.cu == cp.cu - - new_cp = remove_reaction(cp, "r1") - @test new_cp.C == cp.C[:, 2:3] - @test new_cp.cl == cp.cl - @test new_cp.cu == cp.cu - - new_cp = remove_reactions(cp, ["r1", "r3"]) - @test new_cp.C[:] == cp.C[:, 2] - @test new_cp.cl == cp.cl - @test new_cp.cu == cp.cu - - new_cp = remove_reactions(cp, [1, 4]) - @test new_cp.C == cp.C[:, 2:3] - - new_cp = remove_reactions(cp, [1, 1, 2]) - @test new_cp.C[:] == cp.C[:, 3] -end - -@testset "Change bounds" begin - cp = convert(CoreModelCoupled, test_LP()) - @test cp isa CoreModelCoupled - - change_bound!(cp, 1, lower = -10, upper = 10) - @test cp.lm.xl[1] == -10 - @test cp.lm.xu[1] == 10 - change_bounds!(cp, [1, 2]; lower = [-11, -12.2], upper = [11, 23.0]) - @test cp.lm.xl[2] == -12.2 - @test cp.lm.xu[1] == 11 - - change_bound!(cp, "r1", lower = -101, upper = 101) - @test cp.lm.xl[1] == -101 - @test cp.lm.xu[1] == 101 - change_bounds!(cp, ["r1", "r2"]; lower = [-113, -12.23], upper = [113, 233.0]) - @test cp.lm.xl[2] == -12.23 - @test cp.lm.xu[1] == 113 - - new_model = change_bound(cp, 1, lower = -10, upper = 10) - @test new_model.lm.xl[1] == -10 - @test new_model.lm.xu[1] == 10 - new_model = change_bounds(cp, [1, 2]; lower = [-11, -12.2], upper = [11, 23.0]) - @test new_model.lm.xl[2] == -12.2 - @test new_model.lm.xu[1] == 11 - - new_model = change_bound(cp, "r1", lower = -101, upper = 101) - @test new_model.lm.xl[1] == -101 - @test new_model.lm.xu[1] == 101 - new_model = - change_bounds(cp, ["r1", "r2"]; lower = [-113, -12.23], upper = [113, 233.0]) - @test new_model.lm.xl[2] == -12.23 - @test new_model.lm.xu[1] == 113 - -end diff --git a/test/reconstruction/Reaction.jl b/test/reconstruction/Reaction.jl deleted file mode 100644 index 7d193fd21..000000000 --- a/test/reconstruction/Reaction.jl +++ /dev/null @@ -1,36 +0,0 @@ -@testset "Construction overloading" begin - model = load_model(StandardModel, model_paths["iJO1366.json"]) - - rxn_original = model.reactions["NADH16pp"] - nadh = model.metabolites["nadh_c"] - h_c = model.metabolites["h_c"] - q8 = model.metabolites["q8_c"] - q8h2 = model.metabolites["q8h2_c"] - nad = model.metabolites["nad_c"] - h_p = model.metabolites["h_p"] - - rxn = nadh + 4.0 * h_c + 1.0 * q8 → 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test rxn.lb == 0.0 && rxn.ub > 0.0 - - rxn = 1.0 * nadh + 4.0 * h_c + q8 ← 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test rxn.lb < 0.0 && rxn.ub == 0.0 - - rxn = 1.0 * nadh + 4.0 * h_c + 1.0 * q8 ↔ q8h2 + nad + 3.0 * h_p - @test rxn.lb < 0.0 && rxn.ub > 0.0 - - rxn = 1.0 * nadh → nothing - @test length(rxn.metabolites) == 1 - - rxn = nothing → nadh - @test length(rxn.metabolites) == 1 - - rxn = nothing → 1.0nadh - @test length(rxn.metabolites) == 1 - - rxn = 1.0 * nadh + 4.0 * h_c + 1.0 * q8 → 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test prod(values(rxn.metabolites)) == -12 - @test ("q8h2_c" in [x for x in keys(rxn.metabolites)]) - - rxn = nadh + 4.0 * h_c + 1.0 * q8 → 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test rxn.lb == 0.0 && rxn.ub > 0.0 -end diff --git a/test/reconstruction/SerializedModel.jl b/test/reconstruction/SerializedModel.jl deleted file mode 100644 index d3ce0a17c..000000000 --- a/test/reconstruction/SerializedModel.jl +++ /dev/null @@ -1,15 +0,0 @@ - -@testset "Serialized modifications" begin - m = test_LP() - - sm = serialize_model(m, tmpfile("recon.serialized")) - m2 = unwrap_serialized(sm) - - @test typeof(m2) == typeof(m) - - sm = serialize_model(m, tmpfile("recon.serialized")) - m2 = remove_reaction(sm, reactions(m)[3]) - - @test typeof(m2) == typeof(m) - @test !(reactions(m)[3] in reactions(m2)) -end diff --git a/test/reconstruction/StandardModel.jl b/test/reconstruction/StandardModel.jl deleted file mode 100644 index 24c602381..000000000 --- a/test/reconstruction/StandardModel.jl +++ /dev/null @@ -1,116 +0,0 @@ -@testset "Model manipulation" begin - m1 = Metabolite("m1") - m2 = Metabolite("m2") - m3 = Metabolite("m3") - m4 = Metabolite("m4") - mets = [m1, m2, m3, m4] - m5 = Metabolite("m5") - m6 = Metabolite("m6") - m7 = Metabolite("m7") - - g1 = Gene("g1") - g2 = Gene("g2") - g3 = Gene("g3") - g4 = Gene("g4") - genes = [g1, g2, g3, g4] - g5 = Gene("g5") - g6 = Gene("g6") - g7 = Gene("g7") - - r1 = Reaction("r1", Dict(m1.id => -1.0, m2.id => 1.0), :forward) - r2 = Reaction("r2", Dict(m2.id => -2.0, m3.id => 1.0), :bidirectional) - r2.grr = [["g2"], ["g1", "g3"]] - r3 = Reaction("r3", Dict(m1.id => -1.0, m4.id => 2.0), :reverse) - r4 = Reaction("r4", Dict(m1.id => -5.0, m4.id => 2.0), :reverse) - r5 = Reaction("r5", Dict(m1.id => -11.0, m4.id => 2.0, m3.id => 2.0), :reverse) - - rxns = [r1, r2] - - model = StandardModel() - model.id = "model" - model.reactions = OrderedDict(r.id => r for r in rxns) - model.metabolites = OrderedDict(m.id => m for m in mets) - model.genes = OrderedDict(g.id => g for g in genes) - - # change bound tests - in place - change_bound!(model, "r2"; lower = -10, upper = 10) - @test model.reactions["r2"].lb == -10 - @test model.reactions["r2"].ub == 10 - change_bound!(model, "r2"; lower = -100) - @test model.reactions["r2"].lb == -100 - @test model.reactions["r2"].ub == 10 - change_bound!(model, "r2"; upper = 111) - @test model.reactions["r2"].lb == -100 - @test model.reactions["r2"].ub == 111 - - change_bounds!(model, ["r1", "r2"]; lower = [-110, -220], upper = [110.0, 220.0]) - @test model.reactions["r1"].lb == -110 - @test model.reactions["r1"].ub == 110 - @test model.reactions["r2"].lb == -220 - @test model.reactions["r2"].ub == 220 - - # change bound - new model - new_model = change_bound(model, "r2"; lower = -10, upper = 10) - @test new_model.reactions["r2"].lb == -10 - @test new_model.reactions["r2"].ub == 10 - - new_model = change_bound(model, "r2"; lower = -10) - @test new_model.reactions["r2"].lb == -10 - @test new_model.reactions["r2"].ub == 220 - - new_model = change_bounds(model, ["r1", "r2"]; lower = [-10, -20], upper = [10.0, 20.0]) - @test new_model.reactions["r1"].lb == -10 - @test new_model.reactions["r1"].ub == 10 - @test new_model.reactions["r2"].lb == -20 - @test new_model.reactions["r2"].ub == 20 - - new_model = - change_bounds(model, ["r1", "r2"]; lower = [-10, nothing], upper = [nothing, 20.0]) - @test new_model.reactions["r1"].lb == -10 - @test new_model.reactions["r1"].ub == 110 - @test new_model.reactions["r2"].lb == -220 - @test new_model.reactions["r2"].ub == 20 - - ### reactions - add_reactions!(model, [r3, r4]) - @test length(model.reactions) == 4 - - add_reaction!(model, r5) - @test length(model.reactions) == 5 - - remove_reactions!(model, ["r5", "r4"]) - @test length(model.reactions) == 3 - - remove_reaction!(model, "r1") - @test length(model.reactions) == 2 - - ### metabolites - add_metabolites!(model, [m5, m6]) - @test length(model.metabolites) == 6 - - add_metabolite!(model, m7) - @test length(model.metabolites) == 7 - - remove_metabolites!(model, ["m5", "m4"]) - @test length(model.metabolites) == 5 - - remove_metabolite!(model, "m1") - @test length(model.metabolites) == 4 - - ### genes - add_genes!(model, [g5, g6]) - @test length(model.genes) == 6 - - add_gene!(model, g7) - @test length(model.genes) == 7 - - remove_genes!(model, ["g5", "g4"]) - @test length(model.genes) == 5 - - remove_gene!(model, "g1") - @test length(model.genes) == 4 - - ### objective - change_objective!(model, "r2") - @test model.reactions["r2"].objective_coefficient == 1.0 -end diff --git a/test/reconstruction/add_reactions.jl b/test/reconstruction/add_reactions.jl deleted file mode 100644 index 823432f37..000000000 --- a/test/reconstruction/add_reactions.jl +++ /dev/null @@ -1,25 +0,0 @@ -@testset "@add_reactions! helper" begin - mod = StandardModel() - A = Metabolite("A") - B = Metabolite("B") - C = Metabolite("C") - add_metabolites!(mod, [A, B, C]) - - @add_reactions! mod begin - "v1", nothing ↔ A - "v2", nothing ↔ B, -500 - "v3", nothing ↔ C, -500, 500 - end - - rxn = mod.reactions["v1"] - @test rxn.lb == -1000.0 - @test rxn.ub == 1000.0 - - rxn = mod.reactions["v2"] - @test rxn.lb == -500 - @test rxn.ub == 1000.0 - - rxn = mod.reactions["v3"] - @test rxn.lb == -500 - @test rxn.ub == 500 -end diff --git a/test/reconstruction/community.jl b/test/reconstruction/community.jl deleted file mode 100644 index fc532fb10..000000000 --- a/test/reconstruction/community.jl +++ /dev/null @@ -1,263 +0,0 @@ -@testset "CoreModel: Detailed community stoichiometrix matrix check" begin - m1 = test_toyModel() - m2 = test_toyModel() - ex_rxn_mets = Dict("EX_m1(e)" => "m1[e]", "EX_m3(e)" => "m3[e]") - - c1 = join_with_exchanges(CoreModel, [m1, m2], ex_rxn_mets) - - # test of stoichs are the same - @test all(c1.S[1:6, 1:7] .== c1.S[7:12, 8:14]) - # test if each models exchange reactions have been added to the environmental exchange properly - @test sum(c1.S[:, 4]) == 0 - @test sum(c1.S[:, 5]) == 0 - @test sum(c1.S[:, 11]) == 0 - @test sum(c1.S[:, 12]) == 0 - @test sum(c1.S[:, 15]) == -1 - @test sum(c1.S[:, 16]) == -1 - # test if exchange metablites with environment are added properly - @test c1.S[13, 4] == c1.S[13, 11] == 1 - @test c1.S[14, 5] == c1.S[14, 12] == 1 - # test if environmental exchanges have been added properly - @test c1.S[13, 15] == c1.S[14, 16] == -1 - # test of bounds set properly - lb, ub = bounds(c1) - @test all(lb[1:14] .== -ub[1:14] .== -1000) - @test all(lb[15:16] .== -ub[15:16] .== 0.0) - - add_community_objective!( - c1, - Dict("species_1_biomass[c]" => 1.0, "species_2_biomass[c]" => 1.0), - ) - @test c1.S[6, end] == -1.0 - @test c1.S[12, end] == -1.0 - - c2 = join_with_exchanges( - CoreModel, - [m1, m2], - ex_rxn_mets; - biomass_ids = ["biomass1", "biomass1"], - ) - # test if same base stoich matrix - @test all(c2.S[1:14, 1:16] .== c1.S[:, 1:16]) - # test if biomass reaction and metabolites are added correctly - @test all(c2.S[:, end] .== 0) - @test c2.S[15, 7] == 1 - @test c2.S[16, 14] == 1 - - update_community_objective!( - c2, - "community_biomass", - Dict("species_1_biomass1" => 0.1, "species_2_biomass1" => 0.9), - ) - @test c2.S[15, end] == -0.1 - @test c2.S[16, end] == -0.9 -end - -@testset "CoreModel: Small model join" begin - m1 = load_model(model_paths["e_coli_core.json"]) - m2 = load_model(CoreModel, model_paths["e_coli_core.json"]) - - exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for - ex_rxn in reactions(m2) if looks_like_exchange_reaction(ex_rxn) - ) - - biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] - - community = join_with_exchanges( - CoreModel, - [m1, m2], - exchange_rxn_mets; - biomass_ids = biomass_ids, - ) - - env_ex_inds = indexin(keys(exchange_rxn_mets), reactions(community)) - m2_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m2)) - community.xl[env_ex_inds] .= m2.xl[m2_ex_inds] - community.xu[env_ex_inds] .= m2.xu[m2_ex_inds] - - biomass_ids = Dict( - "species_1_BIOMASS_Ecoli_core_w_GAM" => 1.0, - "species_2_BIOMASS_Ecoli_core_w_GAM" => 1.0, - ) - - update_community_objective!(community, "community_biomass", biomass_ids) - - d = flux_balance_analysis_dict(community, Tulip.Optimizer) - @test size(stoichiometry(community)) == (166, 211) - @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) -end - -@testset "CoreModel: Heterogenous model join" begin - m1 = load_model(CoreModel, model_paths["e_coli_core.json"]) - m2 = load_model(CoreModel, model_paths["iJO1366.mat"]) - - exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for - ex_rxn in reactions(m2) if looks_like_exchange_reaction(ex_rxn) - ) - - biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ec_iJO1366_core_53p95M"] - - community = join_with_exchanges( - CoreModel, - [m1, m2], - exchange_rxn_mets; - biomass_ids = biomass_ids, - ) - - env_ex_inds = indexin(keys(exchange_rxn_mets), reactions(community)) - m2_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m2)) - m1_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m1)) - - for (env_ex, m2_ex, m1_ex) in zip(env_ex_inds, m2_ex_inds, m1_ex_inds) - m2lb = isnothing(m2_ex) ? 0.0 : m2.xl[m2_ex] - m2ub = isnothing(m2_ex) ? 0.0 : m2.xu[m2_ex] - - m1lb = isnothing(m1_ex) ? 0.0 : m1.xl[m1_ex] - m1ub = isnothing(m1_ex) ? 0.0 : m1.xu[m1_ex] - - community.xl[env_ex] = m1lb + m2lb - community.xu[env_ex] = m1ub + m2ub - end - - biomass_ids = Dict( - "species_1_BIOMASS_Ecoli_core_w_GAM" => 1.0, - "species_2_BIOMASS_Ec_iJO1366_core_53p95M" => 1.0, - ) - - update_community_objective!(community, "community_biomass", biomass_ids) - - d = flux_balance_analysis_dict( - community, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - @test size(stoichiometry(community)) == (2203, 3003) - @test isapprox(d["community_biomass"], 0.8739215069675402, atol = TEST_TOLERANCE) -end - -@testset "CoreModel: Community model modifications" begin - m1 = load_model(CoreModel, model_paths["e_coli_core.json"]) - - exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m1, ex_rxn))) for - ex_rxn in reactions(m1) if looks_like_exchange_reaction(ex_rxn) - ) - - biomass_ids = ["BIOMASS_Ecoli_core_w_GAM"] - - community = - join_with_exchanges(CoreModel, [m1], exchange_rxn_mets; biomass_ids = biomass_ids) - - env_ex_inds = indexin(keys(exchange_rxn_mets), reactions(community)) - m1_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m1)) - community.xl[env_ex_inds] .= m1.xl[m1_ex_inds] - community.xu[env_ex_inds] .= m1.xu[m1_ex_inds] - - m2 = load_model(CoreModel, model_paths["e_coli_core.json"]) - - community = add_model_with_exchanges( - community, - m2, - exchange_rxn_mets; - model_name = "species_2", - biomass_id = "BIOMASS_Ecoli_core_w_GAM", - ) - - biomass_ids = Dict( - "species_1_BIOMASS_Ecoli_core_w_GAM" => 1.0, - "species_2_BIOMASS_Ecoli_core_w_GAM" => 1.0, - ) - - update_community_objective!(community, "community_biomass", biomass_ids) - - d = flux_balance_analysis_dict(community, Tulip.Optimizer) - - @test size(stoichiometry(community)) == (166, 211) - @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) -end - -@testset "StandardModel: Detailed community stoichiometrix matrix check" begin - m1 = test_toyModel() - m2 = test_toyModel() - ex_rxn_mets = Dict("EX_m1(e)" => "m1[e]", "EX_m3(e)" => "m3[e]") - - c1 = join_with_exchanges(StandardModel, [m1, m2], ex_rxn_mets) - @test size(stoichiometry(c1)) == (14, 16) - - # test if each models exchange reactions have been added to the environmental exchange properly - @test c1.reactions["EX_m1(e)"].metabolites["m1[e]"] == -1 - @test c1.reactions["EX_m3(e)"].metabolites["m3[e]"] == -1 - - # test if exchange metabolites with environment are added properly - @test "m1[e]" in metabolites(c1) - @test "m3[e]" in metabolites(c1) - - # test if environmental exchanges have been added properly - @test c1.reactions["species_1_EX_m1(e)"].metabolites["m1[e]"] == -1 - @test c1.reactions["species_1_EX_m1(e)"].metabolites["species_1_m1[e]"] == 1 - @test c1.reactions["species_2_EX_m3(e)"].metabolites["m3[e]"] == -1 - @test c1.reactions["species_2_EX_m3(e)"].metabolites["species_2_m3[e]"] == 1 - - # test of bounds set properly - lb, ub = bounds(c1) # this only works because the insertion order is preserved (they get added last) - @test all(lb[1:14] .== -ub[1:14] .== -1000) - @test all(lb[15:16] .== -ub[15:16] .== 0.0) - - add_community_objective!( - c1, - Dict("species_1_biomass[c]" => 1.0, "species_2_biomass[c]" => 1.0), - ) - @test c1.reactions["community_biomass"].metabolites["species_2_biomass[c]"] == -1 - @test c1.reactions["community_biomass"].metabolites["species_1_biomass[c]"] == -1 - - c2 = join_with_exchanges( - StandardModel, - [m1, m2], - ex_rxn_mets; - biomass_ids = ["biomass1", "biomass1"], - ) - @test size(stoichiometry(c2)) == (16, 17) - - # test if biomass reaction and metabolites are added correctly - @test isempty(c2.reactions["community_biomass"].metabolites) -end - -@testset "StandardModel: coarse community models checks" begin - m1 = load_model(StandardModel, model_paths["e_coli_core.json"]) - - exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m1, ex_rxn))) for - ex_rxn in reactions(m1) if looks_like_exchange_reaction(ex_rxn) - ) - - biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] - - c = join_with_exchanges( - StandardModel, - [m1, m1], - exchange_rxn_mets; - biomass_ids = biomass_ids, - ) - - for rid in keys(exchange_rxn_mets) - c.reactions[rid].lb = m1.reactions[rid].lb - c.reactions[rid].ub = m1.reactions[rid].ub - end - - @test c.reactions["species_1_BIOMASS_Ecoli_core_w_GAM"].metabolites["species_1_BIOMASS_Ecoli_core_w_GAM"] == - 1.0 - - biomass_ids = Dict( - "species_1_BIOMASS_Ecoli_core_w_GAM" => 1.0, - "species_2_BIOMASS_Ecoli_core_w_GAM" => 1.0, - ) - - update_community_objective!(c, "community_biomass", biomass_ids) - - d = flux_balance_analysis_dict(c, Tulip.Optimizer) - @test size(stoichiometry(c)) == (166, 211) - @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) -end diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl deleted file mode 100644 index 884b31063..000000000 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ /dev/null @@ -1,47 +0,0 @@ -@testset "Gap fill with minimum reactions" begin - #= - Implement the small model that should be gapfilled. - =# - model = StandardModel("partial model") - - (m1, m2, m3, m4, m5, m6, m7, m8) = Metabolite.("m$i" for i = 1:8) - - @add_reactions! model begin - "r1", nothing → m1, 0, 1 - "r2", m1 ↔ m2, -10, 100 - "r3", m1 → m3, 0, 100 - "r4", m2 ↔ m4, 0, 100 - # "r5", m3 → m4, 0, 100 - "r6", m4 → nothing, 0, 100 - # "r7", m2 → m7 + m6, 0, 100 - "r8", m7 → m8, 0, 100 - "r9", m8 → nothing, 0, 100 - # "r10", m6 → nothing, 0, 100 - "r11", m2 + m3 + m7 → nothing, 0, 100 - "r12", m3 → m5, -10, 10 - end - - model.reactions["r11"].objective_coefficient = 1.0 - - add_metabolites!(model, [m1, m2, m3, m4, m5, m7, m8]) - - r5 = Reaction("r5", Dict("m3" => -1, "m4" => 1), :forward) - r7 = Reaction("r7", Dict("m2" => -1, "m7" => 1, "m6" => 1), :forward) - r10 = Reaction("r10", Dict("m6" => -1), :forward) - rA = Reaction("rA", Dict("m1" => -1, "m2" => 1, "m3" => 1), :forward) - rB = Reaction("rB", Dict("m2" => -1, "m9" => 1), :forward) - rC = Reaction("rC", Dict("m9" => -1, "m10" => 1), :bidirectional) - rD = Reaction("rC", Dict("m10" => -1), :reverse) - - universal_reactions = [r5, r7, r10, rA, rB, rC, rD] - - rxns = - gapfill_minimum_reactions( - model, - universal_reactions, - GLPK.Optimizer; - objective_bounds = (0.1, 1000.0), - ) |> gapfilled_rids(universal_reactions) - - @test issetequal(["r7", "r10"], rxns) -end diff --git a/test/reconstruction/knockouts.jl b/test/reconstruction/knockouts.jl deleted file mode 100644 index e7d5d4388..000000000 --- a/test/reconstruction/knockouts.jl +++ /dev/null @@ -1,66 +0,0 @@ -""" -The gene-reaction rules (grr) are written as -[[gene1 and gene2...] or [gene3 and...] ...] -so for a reaction to be available it is sufficient that one group -is available, but inside a group all of the genes need to be available -""" - -@testset "knockout_single_gene" begin - m = StandardModel() - add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite("B")) - add_gene!(m, Gene("g1")) - add_gene!(m, Gene("g2")) - add_reaction!( - m, - Reaction("v1", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"]]), - ) - add_reaction!( - m, - Reaction("v2", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1", "g2"]]), - ) - add_reaction!( - m, - Reaction("v3", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"], ["g2"]]), - ) - add_reaction!( - m, - Reaction( - "v4", - metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1", "g2"], ["g2"]], - ), - ) - - remove_gene!(m, "g1", knockout_reactions = true) - - @test length(m.reactions) == 2 - @test !haskey(m.reactions, "v1") - @test !haskey(m.reactions, "v2") -end - -@testset "knockout_multiple_genes" begin - m = StandardModel() - add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite("B")) - add_gene!(m, Gene("g1")) - add_gene!(m, Gene("g2")) - add_gene!(m, Gene("g3")) - add_reaction!( - m, - Reaction( - "v1", - metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1"], ["g2"], ["g3"]], - ), - ) - add_reaction!( - m, - Reaction("v2", metabolites = Dict("A" => 1.0, "B" => -1.0), grr = [["g1"], ["g3"]]), - ) - - remove_genes!(m, ["g1", "g3"], knockout_reactions = true) - - @test haskey(m.reactions, "v1") - @test !haskey(m.reactions, "v2") -end diff --git a/test/runtests.jl b/test/runtests.jl index e19df54f1..232973799 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,73 +1,66 @@ -using COBREXA, Test +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +using COBREXA, Test using Aqua -using Distributed -using Downloads -using JSON -using JuMP -using LinearAlgebra -using MAT -using OrderedCollections + using Clarabel -using SHA -using SparseArrays -using Statistics -using Tulip +using Distributed +import AbstractFBCModels as A using GLPK # for MILPs -# tolerance for comparing analysis results (should be a bit bigger than the -# error tolerance in computations) -TEST_TOLERANCE = 10 * COBREXA._constants.tolerance -QP_TEST_TOLERANCE = 1e-2 # for Clarabel +# testing constants +const TEST_TOLERANCE = 1e-3 +const QP_TEST_TOLERANCE = 1e-2 # for Clarabel +# helper functions for running tests en masse print_timing(fn, t) = @info "$(fn) done in $(round(t; digits = 2))s" -# helper functions for running tests en masse function run_test_file(path...) fn = joinpath(path...) t = @elapsed include(fn) print_timing(fn, t) end -function run_test_dir(dir, comment = "Directory $dir/") - @testset "$comment" begin - run_test_file.(joinpath.(dir, filter(fn -> endswith(fn, ".jl"), readdir(dir)))) +function run_doc_examples() + for dir in filter(endswith(".jl"), readdir("../docs/src/examples", join = true)) + @testset "docs/$(basename(dir))" begin + run_test_file(dir) + end end end # set up the workers for Distributed, so that the tests that require more # workers do not unnecessarily load the stuff multiple times W = addprocs(2) -t = @elapsed @everywhere using COBREXA, Tulip, JuMP -print_timing("import of packages", t) t = @elapsed @everywhere begin - model = Model(Tulip.Optimizer) - @variable(model, 0 <= x <= 1) - @objective(model, Max, x) - optimize!(model) + using COBREXA + import Tulip, JuMP end -print_timing("JuMP+Tulip code warmup", t) - -# make sure there's a directory for temporary data -tmpdir = "tmpfiles" -isdir(tmpdir) || mkdir(tmpdir) -tmpfile(x...) = joinpath(tmpdir, x...) # load the test models run_test_file("data_static.jl") run_test_file("data_downloaded.jl") -# import base files +# TODO data_static and data_downloaded need to be interned into the demos. +# Instead let's make a single "doc running directory" that runs all the +# documentation, which doesn't get erased to improve the test caching. + @testset "COBREXA test suite" begin - run_test_dir(joinpath("base", "types", "abstract"), "Abstract types") - run_test_dir(joinpath("base", "types"), "Base model types") - run_test_dir(joinpath("base", "logging"), "Logging") - run_test_dir("base", "Base functionality") - run_test_dir(joinpath("base", "utils"), "Utilities") - run_test_dir("io", "I/O functions") - run_test_dir("reconstruction") - run_test_dir("analysis") - run_test_dir(joinpath("analysis", "sampling"), "Sampling") + run_doc_examples() run_test_file("aqua.jl") end