├── .python-version ├── .pylintrc ├── screenshot.png ├── cnapy ├── data │ ├── check.png │ ├── clear.png │ ├── cross.png │ ├── d-font.png │ ├── heat.png │ ├── onoff.png │ ├── qmark.png │ ├── redo.png │ ├── save.png │ ├── undo.png │ ├── zoom-in.png │ ├── zoom-out.png │ ├── default-color.png │ ├── 200px-Gnome-document-save.svg.png │ ├── 240px-Gnome-document-save-as.svg.png │ ├── check.svg │ ├── d-font.svg │ ├── cross.svg │ ├── qmark.svg │ ├── clear.svg │ ├── save.svg │ ├── zoom-out.svg │ ├── zoom-in.svg │ ├── onoff.svg │ ├── undo.svg │ ├── heat.svg │ ├── redo.svg │ └── default-color.svg ├── tests │ └── test.py ├── __init__.py ├── gui_elements │ ├── __init__.py │ ├── in_out_flux_dialog.py │ ├── rename_map_dialog.py │ ├── about_dialog.py │ ├── box_position_dialog.py │ ├── download_dialog.py │ ├── solver_buttons.py │ ├── annotation_widget.py │ ├── model_info.py │ ├── efmtool_dialog.py │ ├── reaction_table_widget.py │ ├── clipboard_calculator.py │ ├── flux_optimization_dialog.py │ ├── config_cobrapy_dialog.py │ ├── yield_optimization_dialog.py │ ├── configuration_gurobi.py │ ├── yield_space_dialog.py │ ├── efm_dialog.py │ ├── configuration_cplex.py │ ├── plot_space_dialog.py │ └── gene_list.py ├── resources.qrc ├── __main__.py ├── core_gui.py ├── flux_vector_container.py └── utils_for_cnapy_api.py ├── docs ├── _config.yml ├── assets │ └── css │ │ └── style.scss └── index.md ├── environment.yml ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── python-publish.yml │ └── ci-test.yml ├── CONTRIBUTING.md ├── cnapy.py ├── setup.py ├── pyproject.toml ├── cnapy.spec ├── .gitignore ├── installers ├── install_cnapy_here.sh └── install_cnapy_here.bat ├── testscript.py └── README.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | extension-pkg-whitelist=PyQt5 -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/screenshot.png -------------------------------------------------------------------------------- /cnapy/data/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/check.png -------------------------------------------------------------------------------- /cnapy/data/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/clear.png -------------------------------------------------------------------------------- /cnapy/data/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/cross.png -------------------------------------------------------------------------------- /cnapy/data/d-font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/d-font.png -------------------------------------------------------------------------------- /cnapy/data/heat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/heat.png -------------------------------------------------------------------------------- /cnapy/data/onoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/onoff.png -------------------------------------------------------------------------------- /cnapy/data/qmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/qmark.png -------------------------------------------------------------------------------- /cnapy/data/redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/redo.png -------------------------------------------------------------------------------- /cnapy/data/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/save.png -------------------------------------------------------------------------------- /cnapy/data/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/undo.png -------------------------------------------------------------------------------- /cnapy/data/zoom-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/zoom-in.png -------------------------------------------------------------------------------- /cnapy/data/zoom-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/zoom-out.png -------------------------------------------------------------------------------- /cnapy/data/default-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/default-color.png -------------------------------------------------------------------------------- /cnapy/data/200px-Gnome-document-save.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/200px-Gnome-document-save.svg.png -------------------------------------------------------------------------------- /cnapy/data/240px-Gnome-document-save-as.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnapy-org/CNApy/HEAD/cnapy/data/240px-Gnome-document-save-as.svg.png -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | logo: https://raw.githubusercontent.com/cnapy-org/CNApy/master/cnapy/data/cnapylogo_no_text.svg 3 | show_downloads: false 4 | -------------------------------------------------------------------------------- /docs/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | 6 | .wrapper { 7 | width:1060px; 8 | margin:0 auto; 9 | } 10 | section { 11 | width:700px; 12 | float:right; 13 | padding-bottom:50px; 14 | } -------------------------------------------------------------------------------- /cnapy/tests/test.py: -------------------------------------------------------------------------------- 1 | ''' Tests ''' 2 | import cobra 3 | 4 | import cnapy.core 5 | 6 | 7 | def test_efm_computation(): 8 | model = cobra.Model() 9 | scen_values = {} 10 | cnapy.core.efm_computation(model, scen_values, True) 11 | -------------------------------------------------------------------------------- /cnapy/data/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: cnapy 2 | channels: 3 | - cnapy 4 | - conda-forge 5 | - defaults 6 | - Gurobi 7 | - IBMDecisionOptimization 8 | dependencies: 9 | - pytest=7.2 10 | - pylint=2.15 11 | - autopep8=2.0 12 | - pydantic=1.10 13 | - matplotlib-base=3.6 14 | - pip=23 15 | - python=3.10 16 | - qtpy=2.3 17 | - pyqtwebengine=5.15 18 | - appdirs=1.4 19 | - cobra>=0.29 20 | - qtconsole=5.4 21 | - requests=2.28 22 | - psutil=5.9 23 | - efmtool_link>=0.0.6 24 | - optlang_enumerator>=0.0.11 25 | - straindesign>=1.11 26 | - nest-asyncio 27 | - gurobi 28 | - cplex 29 | - numpy=1.23 30 | - scipy=1.12 # Starting from 1.13, we get StrainDesign errors :-/ 31 | - openpyxl -------------------------------------------------------------------------------- /cnapy/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2022 CNApy organization 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # -*- coding: utf-8 -*- 17 | -------------------------------------------------------------------------------- /cnapy/gui_elements/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2022 CNApy organization 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # -*- coding: utf-8 -*- 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Please complete the following information:** 27 | - OS: [e.g. linux] 28 | - Python version [e.g. 3.7] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for considering contributing to CNApy! This file contains instructions that will help you to make a contribution. 4 | 5 | ## How to make a contribution 6 | 7 | * (optional) Open an [issue](https://github.com/cnapy-org/CNApy/issues/new) describing what you want to do. 8 | * Fork the [cnapy-org/cnapy](https://github.com/cnapy-org/CNApy/) repo and create a branch for your changes. 9 | * Submit a pull request to master with your changes. 10 | * Respond to feedback on your pull request. 11 | * If everything is fine your pull request is merged. 12 | 13 | ## License 14 | 15 | Any contribution intentionally submitted for inclusion in the work by you, shall be licensed under the terms of the [Apache 2.0 license](https://github.com/cnapy-org/CNApy/blob/master/LICENSE) without any additional terms or conditions. 16 | -------------------------------------------------------------------------------- /cnapy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2022 CNApy organization 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | from cnapy.__main__ import main_cnapy 17 | from sys import argv 18 | 19 | main_cnapy( 20 | project_path=None if len(argv) < 2 else argv[1], 21 | scenario_path=None if len(argv) < 3 else argv[2], 22 | ) 23 | -------------------------------------------------------------------------------- /cnapy/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /data/qmark.png 5 | /data/check.png 6 | /data/cross.png 7 | /data/onoff.png 8 | /data/heat.png 9 | /data/default-color.png 10 | /data/d-font.png 11 | /data/save.png 12 | /data/200px-Gnome-document-save.svg.png 13 | /data/240px-Gnome-document-save-as.svg.png 14 | /data/clear.png 15 | /data/undo.png 16 | /data/redo.png 17 | /data/zoom-in.png 18 | /data/zoom-out.png 19 | 20 | 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2022 CNApy organization 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # -*- coding: utf-8 -*- 17 | from setuptools import setup 18 | 19 | setup( 20 | name='cnapy', 21 | description='An integrated environment for metabolic network analysis.', 22 | long_description=open('README.md', encoding="utf8").read(), 23 | long_description_content_type="text/asciidoc", 24 | packages=['cnapy', 'cnapy.gui_elements'], 25 | package_dir={'cnapy': 'cnapy'}, 26 | package_data={'cnapy': [ 27 | 'data/*.svg', 'data/escher_cnapy.html', 'data/escher.min.js']} 28 | ) 29 | -------------------------------------------------------------------------------- /cnapy/data/d-font.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "cnapy" 3 | version = "1.2.7" 4 | authors = [ 5 | { name="Sven Thiele" }, 6 | { name="Axel von Kamp" }, 7 | { name="Pavlos Stephanos Bekiaris" }, 8 | { name="Philipp Schneider" } 9 | ] 10 | description = "An integrated environment for metabolic network analysis." 11 | readme = "README.md" 12 | requires-python = "==3.10.*" 13 | classifiers = [ 14 | "Programming Language :: Python :: 3", 15 | "License :: OSI Approved :: Apache Software License", 16 | "Operating System :: OS Independent", 17 | ] 18 | dependencies = [ 19 | "appdirs>=1.4", 20 | "matplotlib>=3.6", 21 | "requests>=2.28", 22 | "cobra>=0.30", 23 | "efmtool_link>=0.0.8", 24 | "optlang_enumerator>=0.0.12", 25 | "straindesign>=1.12", 26 | "qtpy>=2.3", 27 | "pyqtwebengine>=5.15", 28 | "qtconsole==5.4", 29 | "gurobipy>=12.0", 30 | "cplex==22.1.2.0", # We use this specific version as it's currently the last one with support for ARM CPUs 31 | "numpy==1.23", 32 | "scipy==1.12", 33 | "openpyxl", 34 | "jpype1==1.5.0", 35 | "setuptools", 36 | "cobrak==0.0.2", 37 | ] 38 | 39 | [project.scripts] 40 | cnapy = "cnapy.__main__:main_cnapy" 41 | [project.urls] 42 | Homepage = "https://github.com/cnapy-org/CNApy" 43 | Issues = "https://github.com/cnapy-org/CNApy/issues" 44 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | workflow_dispatch: 15 | branches: 16 | - "pypi" 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | deploy: 22 | 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Set up Python 28 | uses: actions/setup-python@v3 29 | with: 30 | python-version: '3.x' 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install build 35 | - name: Build package 36 | run: python -m build 37 | - name: Publish package 38 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 39 | with: 40 | user: __token__ 41 | password: ${{ secrets.PYPI_API_TOKEN }} 42 | -------------------------------------------------------------------------------- /cnapy/data/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /cnapy.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | from PyInstaller.utils.hooks import collect_data_files, collect_all, collect_submodules 3 | 4 | datas, binaries, hiddenimports = collect_all('debugpy') # cf. https://github.com/pyinstaller/pyinstaller/issues/5363 5 | hiddenimports += collect_submodules('xmlrpc') 6 | datas += collect_data_files('efmtool_link') 7 | datas += collect_data_files('straindesign') 8 | datas += collect_data_files('gurobipy') 9 | datas += collect_data_files('cnapy') 10 | 11 | 12 | a = Analysis( 13 | ['cnapy.py'], 14 | pathex=[], 15 | binaries=binaries, 16 | datas=datas, 17 | hiddenimports=hiddenimports, 18 | hookspath=[], 19 | hooksconfig={}, 20 | runtime_hooks=[], 21 | excludes=[], 22 | noarchive=False, 23 | optimize=0, 24 | ) 25 | pyz = PYZ(a.pure) 26 | 27 | exe = EXE( 28 | pyz, 29 | a.scripts, 30 | [], 31 | exclude_binaries=True, 32 | name='cnapy', 33 | debug=False, 34 | bootloader_ignore_signals=False, 35 | strip=False, 36 | upx=True, 37 | console=True, 38 | disable_windowed_traceback=False, 39 | argv_emulation=False, 40 | target_arch=None, 41 | codesign_identity=None, 42 | entitlements_file=None, 43 | ) 44 | coll = COLLECT( 45 | exe, 46 | a.binaries, 47 | a.datas, 48 | strip=False, 49 | upx=True, 50 | upx_exclude=[], 51 | name='cnapy', 52 | ) 53 | -------------------------------------------------------------------------------- /cnapy/data/qmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /cnapy/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2022 CNApy organization 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | 16 | import os 17 | import site 18 | from jpype._jvmfinder import getDefaultJVMPath, JVMNotFoundException, JVMNotSupportedException 19 | 20 | try: 21 | getDefaultJVMPath() 22 | except (JVMNotFoundException, JVMNotSupportedException): 23 | for path in site.getsitepackages(): 24 | # in one of these conda puts the JRE 25 | os.environ['JAVA_HOME'] = os.path.join(path, 'Library') 26 | try: 27 | getDefaultJVMPath() 28 | break 29 | except (JVMNotFoundException, JVMNotSupportedException): 30 | pass 31 | 32 | from cnapy.application import Application 33 | 34 | def main_cnapy( 35 | project_path: None | str = None, 36 | scenario_path: None | str = None, 37 | ): 38 | Application( 39 | project_path=project_path, 40 | scenario_path=scenario_path, 41 | ) 42 | 43 | if __name__ == "__main__": 44 | main_cnapy() 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual studio code 2 | .vscode 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *.cover 44 | .hypothesis/ 45 | .pytest_cache/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | db.sqlite3 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # celery beat schedule file 73 | celerybeat-schedule 74 | 75 | # SageMath parsed files 76 | *.sage.py 77 | 78 | # Environments 79 | .env 80 | .venv 81 | env/ 82 | venv/ 83 | ENV/ 84 | env.bak/ 85 | venv.bak/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | .spyproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # mkdocs documentation 95 | /site 96 | 97 | # mypy 98 | .mypy_cache/ 99 | cnapy-config.txt 100 | 101 | # uv files 102 | uv.lock 103 | -------------------------------------------------------------------------------- /cnapy/data/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 6 | 7 | -------------------------------------------------------------------------------- /cnapy/gui_elements/in_out_flux_dialog.py: -------------------------------------------------------------------------------- 1 | """The in out flux dialog""" 2 | from qtpy.QtWidgets import (QDialog, QHBoxLayout, QLabel, 3 | QPushButton, QVBoxLayout, QComboBox) 4 | from cnapy.appdata import AppData 5 | 6 | 7 | class InOutFluxDialog(QDialog): 8 | """A dialog to plot in out fluxes on a metabolite""" 9 | 10 | def __init__(self, appdata: AppData): 11 | QDialog.__init__(self) 12 | self.setWindowTitle("Compute in/out fluxes") 13 | 14 | self.appdata = appdata 15 | 16 | self.layout = QVBoxLayout() 17 | 18 | t1 = QLabel("Choose metabolite") 19 | self.layout.addWidget(t1) 20 | self.metabolite_chooser = QComboBox() 21 | self.layout.addWidget(self.metabolite_chooser) 22 | l = QHBoxLayout() 23 | self.button = QPushButton("Plot") 24 | self.cancel = QPushButton("Close") 25 | l.addWidget(self.button) 26 | l.addWidget(self.cancel) 27 | self.layout.addItem(l) 28 | self.setLayout(self.layout) 29 | 30 | # Connecting the signal 31 | self.cancel.clicked.connect(self.reject) 32 | self.button.clicked.connect(self.compute) 33 | self.update() 34 | 35 | def update(self): 36 | sorted_metabolite_ids = sorted([x.id for x in self.appdata.project.cobra_py_model.metabolites], reverse=True) 37 | for metabolite_id in sorted_metabolite_ids: 38 | self.metabolite_chooser.insertItem(0, metabolite_id) 39 | 40 | def compute(self): 41 | metabolite = self.metabolite_chooser.currentText() 42 | self.appdata.window.centralWidget().in_out_fluxes(metabolite) 43 | -------------------------------------------------------------------------------- /cnapy/core_gui.py: -------------------------------------------------------------------------------- 1 | import gurobipy 2 | import io 3 | import traceback 4 | import cobra 5 | from qtpy.QtWidgets import QMessageBox 6 | 7 | 8 | def except_likely_community_model_error() -> None: 9 | """Shows a message in the case that using a (size-limited) community edition solver version probably caused an error.""" 10 | community_error_text = "Solver error. One possible reason: You set CPLEX or Gurobi as solver although you only use their\n"+\ 11 | "Community edition which only work for small models. To solve this, either follow the instructions under\n"+\ 12 | "'Config->Configure IBM CPLEX full version' or 'Config->Configure Gurobi full version', or use a different solver such as GLPK." 13 | msgBox = QMessageBox() 14 | msgBox.setWindowTitle("Error") 15 | msgBox.setText(community_error_text) 16 | msgBox.setIcon(QMessageBox.Warning) 17 | msgBox.exec() 18 | 19 | 20 | def get_last_exception_string() -> str: 21 | output = io.StringIO() 22 | traceback.print_exc(file=output) 23 | return output.getvalue() 24 | 25 | 26 | def has_community_error_substring(string: str) -> bool: 27 | return ("Model too large for size-limited license" in string) or ("1016: Community Edition" in string) 28 | 29 | 30 | def model_optimization_with_exceptions(model: cobra.Model) -> None: 31 | try: 32 | return model.optimize() 33 | except Exception: 34 | exstr = get_last_exception_string() 35 | # Check for substrings of Gurobi and CPLEX community edition errors 36 | if has_community_error_substring(exstr): 37 | except_likely_community_model_error() 38 | -------------------------------------------------------------------------------- /.github/workflows/ci-test.yml: -------------------------------------------------------------------------------- 1 | name: CI Test 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build-linux: 7 | runs-on: "ubuntu-latest" 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Python 11 | uses: actions/setup-python@v4 12 | with: 13 | python-version: '3.10' 14 | - name: Install dependencies 15 | run: | 16 | python -m pip install --upgrade pip 17 | pip install pytest>=7.2 18 | - name: Install CNApy 19 | run: | 20 | pip install . 21 | - name: Test CNApy 22 | run: | 23 | pytest -v ./cnapy/tests/test.py 24 | 25 | build-windows: 26 | runs-on: "windows-latest" 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Set up Python 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: '3.10' 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install pytest>=7.2 37 | - name: Install CNApy 38 | run: | 39 | pip install . 40 | - name: Test CNApy 41 | run: | 42 | pytest -v ./cnapy/tests/test.py 43 | 44 | build-macos: 45 | runs-on: "macos-latest" 46 | steps: 47 | - uses: actions/checkout@v4 48 | - name: Set up Python 49 | uses: actions/setup-python@v4 50 | with: 51 | python-version: '3.10' 52 | - name: Install dependencies 53 | run: | 54 | python -m pip install --upgrade pip 55 | pip install pytest>=7.2 56 | - name: Install CNApy 57 | run: | 58 | pip install . 59 | - name: Test CNApy 60 | run: | 61 | pytest -v ./cnapy/tests/test.py 62 | -------------------------------------------------------------------------------- /cnapy/gui_elements/rename_map_dialog.py: -------------------------------------------------------------------------------- 1 | """The rename map dialog""" 2 | from qtpy.QtWidgets import (QDialog, QHBoxLayout, QLabel, QLineEdit, 3 | QPushButton, QVBoxLayout) 4 | from cnapy.appdata import AppData 5 | 6 | 7 | class RenameMapDialog(QDialog): 8 | """A dialog to rename maps""" 9 | 10 | def __init__(self, appdata: AppData, central_widget): 11 | QDialog.__init__(self) 12 | self.setWindowTitle("Rename map") 13 | 14 | self.appdata = appdata 15 | self.central_widget = central_widget 16 | self.layout = QVBoxLayout() 17 | h1 = QHBoxLayout() 18 | label = QLabel("Enter new map name") 19 | self.layout.addWidget(label) 20 | self.idx = self.central_widget.map_tabs.currentIndex() 21 | self.old_name = self.central_widget.map_tabs.tabText(self.idx) 22 | 23 | self.name_field = QLineEdit(self.old_name) 24 | h1.addWidget(self.name_field) 25 | self.layout.addItem(h1) 26 | 27 | l2 = QHBoxLayout() 28 | self.button = QPushButton("Rename") 29 | self.cancel = QPushButton("Cancel") 30 | l2.addWidget(self.button) 31 | l2.addWidget(self.cancel) 32 | self.layout.addItem(l2) 33 | self.setLayout(self.layout) 34 | 35 | # Connecting the signal 36 | self.cancel.clicked.connect(self.reject) 37 | self.button.clicked.connect(self.apply) 38 | 39 | def apply(self): 40 | new_name = self.name_field.text() 41 | if not new_name in self.appdata.project.maps.keys(): 42 | self.appdata.project.maps[new_name] = self.appdata.project.maps.pop( 43 | self.old_name) 44 | self.central_widget.map_tabs.setTabText(self.idx, new_name) 45 | m = self.central_widget.map_tabs.widget(self.idx) 46 | m.name = new_name 47 | self.central_widget.parent.unsaved_changes() 48 | self.accept() 49 | -------------------------------------------------------------------------------- /cnapy/gui_elements/about_dialog.py: -------------------------------------------------------------------------------- 1 | """The cnapy about dialog""" 2 | from qtpy.QtCore import Qt 3 | from qtpy.QtWidgets import QDialog, QLabel, QPushButton, QVBoxLayout 4 | from cnapy.appdata import AppData 5 | 6 | 7 | class AboutDialog(QDialog): 8 | """An about dialog""" 9 | 10 | def __init__(self, appdata: AppData): 11 | QDialog.__init__(self) 12 | 13 | self.setWindowTitle("About CNApy") 14 | 15 | self.text1 = QLabel( 16 | "Version: {version}\ 17 | \n\nCNApy is an integrated environment for metabolic modeling.\ 18 | \nFor more information visit us at:".format(version=appdata.version)) 19 | self.text1.setAlignment(Qt.AlignCenter) 20 | 21 | self.url1 = QLabel( 22 | " https://github.com/cnapy-org/CNApy ") 23 | self.url1.setOpenExternalLinks(True) 24 | self.url1.setAlignment(Qt.AlignCenter) 25 | 26 | self.text2 = QLabel( 27 | "
If you use CNApy in your scientific work, please consider to cite CNApy's publication:
" 28 | "Thiele et al. (2022). CNApy: a CellNetAnalyzer GUI in Python for analyzing and designing metabolic networks. " 29 | "Bioinformatics 38, 1467-1469:" 30 | ) 31 | self.text2.setAlignment(Qt.AlignCenter) 32 | 33 | self.url2 = QLabel( 34 | " https://doi.org/10.1093/bioinformatics/btab828 ") 35 | self.url2.setOpenExternalLinks(True) 36 | self.url2.setAlignment(Qt.AlignCenter) 37 | 38 | 39 | self.button = QPushButton("Close") 40 | 41 | self.layout = QVBoxLayout() 42 | self.layout.addWidget(self.text1) 43 | self.layout.addWidget(self.url1) 44 | self.layout.addWidget(self.text2) 45 | self.layout.addWidget(self.url2) 46 | self.layout.addWidget(self.button) 47 | self.setLayout(self.layout) 48 | 49 | # Connecting the signal 50 | self.button.clicked.connect(self.reject) 51 | -------------------------------------------------------------------------------- /cnapy/data/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /cnapy/data/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /cnapy/data/zoom-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /installers/install_cnapy_here.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Adapted from https://raw.githubusercontent.com/mamba-org/micromamba-releases/main/install.sh 3 | 4 | set -eu 5 | 6 | # CNApy version 7 | CNAPY_VERSION="1.2.7" 8 | 9 | # Folders 10 | BIN_FOLDER="${BIN_FOLDER:-./cnapy-${CNAPY_VERSION}}" 11 | CONDA_FORGE_YES="${CONDA_FORGE_YES:-yes}" 12 | 13 | # Computing artifact location 14 | case "$(uname)" in 15 | Linux) 16 | PLATFORM="linux" ;; 17 | Darwin) 18 | PLATFORM="osx" ;; 19 | *NT*) 20 | PLATFORM="win" ;; 21 | esac 22 | 23 | ARCH="$(uname -m)" 24 | case "$ARCH" in 25 | aarch64|ppc64le|arm64) 26 | ;; # pass 27 | *) 28 | ARCH="64" ;; 29 | esac 30 | 31 | case "$PLATFORM-$ARCH" in 32 | linux-aarch64|linux-ppc64le|linux-64|osx-arm64|osx-64|win-64) 33 | ;; # pass 34 | *) 35 | echo "Failed to detect your operating system. This installer only supports linux-aarch64|linux-ppc64le|linux-64|osx-arm64|osx-64|win-64" >&2 36 | exit 1 37 | ;; 38 | esac 39 | 40 | RELEASE_URL="https://github.com/mamba-org/micromamba-releases/releases/latest/download/micromamba-${PLATFORM}-${ARCH}" 41 | 42 | # Downloading artifact 43 | mkdir -p "${BIN_FOLDER}" 44 | if hash curl >/dev/null 2>&1; then 45 | curl "${RELEASE_URL}" -o "${BIN_FOLDER}/micromamba" -fsSL --compressed ${CURL_OPTS:-} 46 | elif hash wget >/dev/null 2>&1; then 47 | wget ${WGET_OPTS:-} -qO "${BIN_FOLDER}/micromamba" "${RELEASE_URL}" 48 | else 49 | echo "Neither curl nor wget was found. Please install one of them on your system." >&2 50 | exit 1 51 | fi 52 | chmod +x "${BIN_FOLDER}/micromamba" 53 | 54 | ./cnapy-${CNAPY_VERSION}/micromamba create -y -p ./cnapy-${CNAPY_VERSION}/cnapy-environment python=3.10 pip openjdk -r ./cnapy-${CNAPY_VERSION}/ -c conda-forge 55 | ./cnapy-${CNAPY_VERSION}/micromamba run -p ./cnapy-${CNAPY_VERSION}/cnapy-environment -r ./cnapy-${CNAPY_VERSION}/ pip install --no-cache-dir uv 56 | ./cnapy-${CNAPY_VERSION}/micromamba run -p ./cnapy-${CNAPY_VERSION}/cnapy-environment -r ./cnapy-${CNAPY_VERSION}/ uv --no-cache pip install --no-cache-dir cnapy 57 | 58 | cat << 'EOF' > ./cnapy-${CNAPY_VERSION}/run_cnapy.sh 59 | #!/bin/bash 60 | 61 | # Add CPLEX variable here, e.g. 62 | # export PYTHONPATH=/path_to_cplex/cplex/python/3.10/x86-64_linux 63 | 64 | export LD_LIBRARY_PATH="./cnapy-environment/lib/" # For Linux 65 | export DYLD_LIBRARY_PATH="./cnapy-environment/lib/" # For MacOS 66 | ./micromamba run -p ./cnapy-environment cnapy 67 | EOF 68 | 69 | # Make the shell script executable 70 | chmod +x ./cnapy-${CNAPY_VERSION}/run_cnapy.sh 71 | 72 | echo CNApy was succesfully installed! 73 | echo You can now run CNApy by executing run_cnapy.sh in the newly created cnapy-${CNAPY_VERSION} subfolder. 74 | echo To deinstall CNApy later, simply delete the cnapy-${CNAPY_VERSION} subfolder. 75 | -------------------------------------------------------------------------------- /testscript.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import json 3 | import os 4 | from random import randint 5 | from tempfile import TemporaryDirectory 6 | from zipfile import ZipFile 7 | 8 | import cobra 9 | from qtpy.QtGui import QColor 10 | from cnapy.application import Application 11 | 12 | 13 | def run(cna: Application): 14 | print("Hello!") 15 | open_project(cna, str(os.path.join( 16 | cna.appdata.work_directory, 'ECC2comp.cna'))) 17 | disco(cna) 18 | print("I like all colors.") 19 | 20 | 21 | def disco(cna: Application): 22 | view = cna.centralWidget().map_tabs.widget(0) 23 | for key in cna.appdata.project.maps["Core Metabolism"]["boxes"]: 24 | r = randint(1, 255) 25 | g = randint(1, 255) 26 | b = randint(1, 255) 27 | color = QColor(r, g, b) 28 | view.reaction_boxes[key].set_color(color) 29 | view = cna.centralWidget().reaction_list 30 | root = view.reaction_list.invisibleRootItem() 31 | child_count = root.childCount() 32 | for i in range(child_count): 33 | r = randint(1, 255) 34 | g = randint(1, 255) 35 | b = randint(1, 255) 36 | color = QColor(r, g, b) 37 | item = root.child(i) 38 | item.setBackground(2, QColor(r, g, b)) 39 | 40 | cna.centralWidget().tabs.setCurrentIndex(0) 41 | 42 | 43 | def open_project(cna, filename): 44 | 45 | temp_dir = TemporaryDirectory() 46 | 47 | with ZipFile(filename, 'r') as zip_ref: 48 | zip_ref.extractall(temp_dir.name) 49 | 50 | with open(temp_dir.name+"/box_positions.json", 'r') as fp: 51 | maps = json.load(fp) 52 | 53 | count = 1 54 | for _name, m in maps.items(): 55 | m["background"] = temp_dir.name + \ 56 | "/map" + str(count) + ".svg" 57 | count += 1 58 | # load meta_data 59 | with open(temp_dir.name+"/meta.json", 'r') as fp: 60 | meta_data = json.load(fp) 61 | 62 | cobra_py_model = cobra.io.read_sbml_model( 63 | temp_dir.name + "/model.sbml") 64 | 65 | cna.appdata.temp_dir = temp_dir 66 | cna.appdata.project.maps = maps 67 | cna.appdata.project.meta_data = meta_data 68 | cna.appdata.project.cobra_py_model = cobra_py_model 69 | cna.set_current_filename(filename) 70 | cna.recreate_maps() 71 | cna.centralWidget().mode_navigator.clear() 72 | cna.appdata.project.scen_values.clear() 73 | cna.appdata.scenario_past.clear() 74 | cna.appdata.scenario_future.clear() 75 | for r in cna.appdata.project.cobra_py_model.reactions: 76 | if 'cnapy-default' in r.annotation.keys(): 77 | cna.centralWidget().update_reaction_value( 78 | r.id, r.annotation['cnapy-default']) 79 | cna.nounsaved_changes() 80 | 81 | # if project contains maps move splitter and fit mapview 82 | if len(cna.appdata.project.maps) > 0: 83 | (_, r) = cna.centralWidget().splitter2.getRange(1) 84 | cna.centralWidget().splitter2.moveSplitter(r*0.8, 1) 85 | cna.centralWidget().fit_mapview() 86 | 87 | cna.centralWidget().update() 88 | -------------------------------------------------------------------------------- /cnapy/gui_elements/box_position_dialog.py: -------------------------------------------------------------------------------- 1 | """The cnapy clipboard calculator dialog""" 2 | from tkinter.tix import X_REGION 3 | from qtpy.QtWidgets import (QButtonGroup, QComboBox, QDialog, QHBoxLayout, QLabel, 4 | QLineEdit, QMessageBox, QPushButton, QRadioButton, 5 | QVBoxLayout) 6 | 7 | from cnapy.appdata import AppData 8 | # from cnapy.gui_elements.map_view import ReactionBox 9 | 10 | 11 | class BoxPositionDialog(QDialog): 12 | """A dialog to perform exact box positioning.""" 13 | 14 | def __init__(self, reaction_box, map): 15 | QDialog.__init__(self) 16 | self.setWindowTitle("Set reaction box position") 17 | 18 | self.reaction_box = reaction_box 19 | self.map = map 20 | 21 | self.layout = QVBoxLayout() 22 | x_label = QLabel("X coordinate (horizontal):") 23 | hor_x = QHBoxLayout() 24 | self.x_pos = QLineEdit() 25 | self.x_pos.setText(str(round(self.reaction_box.x()))) 26 | hor_x.addWidget(x_label) 27 | hor_x.addWidget(self.x_pos) 28 | 29 | hor_y = QHBoxLayout() 30 | y_label = QLabel("Y coordinate (vertical):") 31 | self.y_pos = QLineEdit() 32 | self.y_pos.setText(str(round(self.reaction_box.y()))) 33 | hor_y.addWidget(y_label) 34 | hor_y.addWidget(self.y_pos) 35 | 36 | self.layout.addItem(hor_x) 37 | self.layout.addItem(hor_y) 38 | 39 | hor_buttons = QHBoxLayout() 40 | self.button = QPushButton("Set position") 41 | self.close = QPushButton("Close") 42 | hor_buttons.addWidget(self.button) 43 | hor_buttons.addWidget(self.close) 44 | self.layout.addItem(hor_buttons) 45 | self.setLayout(self.layout) 46 | 47 | # Connecting the signal 48 | self.close.clicked.connect(self.accept) 49 | self.button.clicked.connect(self.set_position) 50 | 51 | def set_position(self): 52 | x_str = self.x_pos.text() 53 | y_str = self.y_pos.text() 54 | 55 | try: 56 | x_float = float(x_str) 57 | except ValueError: 58 | msgBox = QMessageBox() 59 | msgBox.setWindowTitle("X position error") 60 | msgBox.setText("The X value you typed in is no valid number, hence, the new box position could not be set.") 61 | msgBox.setIcon(QMessageBox.Warning) 62 | msgBox.exec() 63 | return 64 | 65 | try: 66 | y_float = float(y_str) 67 | except ValueError: 68 | msgBox = QMessageBox() 69 | msgBox.setWindowTitle("Y position error") 70 | msgBox.setText("The Y value you typed in is no valid number, hence, the new box position could not be set.") 71 | msgBox.setIcon(QMessageBox.Warning) 72 | msgBox.exec() 73 | return 74 | 75 | self.map.appdata.project.maps[self.map.name]["boxes"][self.reaction_box.id][0] = x_float 76 | self.map.appdata.project.maps[self.map.name]["boxes"][self.reaction_box.id][1] = y_float 77 | self.map.update_reaction(self.reaction_box.id, self.reaction_box.id) 78 | self.map.central_widget.parent.unsaved_changes() 79 | self.accept() 80 | -------------------------------------------------------------------------------- /cnapy/gui_elements/download_dialog.py: -------------------------------------------------------------------------------- 1 | """The CNApy download examples files dialog""" 2 | import os 3 | import urllib.request 4 | from zipfile import ZipFile 5 | 6 | from qtpy.QtWidgets import ( 7 | QLabel, QDialog, QHBoxLayout, QPushButton, 8 | QVBoxLayout, QMessageBox, 9 | ) 10 | 11 | from cnapy.appdata import AppData 12 | 13 | 14 | class DownloadDialog(QDialog): 15 | """A dialog to create a CNApy-projects directory and download example files""" 16 | 17 | def __init__(self, appdata: AppData): 18 | QDialog.__init__(self) 19 | self.setWindowTitle("Create folder with example projects?") 20 | 21 | self.appdata = appdata 22 | self.layout = QVBoxLayout() 23 | 24 | label_line = QVBoxLayout() 25 | label = QLabel( 26 | "Should CNApy download metabolic network example projects to your CNApy working directory?\n" 27 | "This requires an active internet connection.\n" 28 | "If a working directory error occurs, you can solve by setting a working directory under 'Config->Configure CNApy'.\n" 29 | "In this configuration dialog, you can also change CNApy's font size." 30 | ) 31 | label_line.addWidget(label) 32 | self.layout.addItem(label_line) 33 | 34 | button_line = QHBoxLayout() 35 | self.download_btn = QPushButton("Yes, download main example projects") 36 | self.download_all_btn = QPushButton("Yes, download all available projects") 37 | self.close = QPushButton("No, do not download") 38 | button_line.addWidget(self.download_btn) 39 | button_line.addWidget(self.download_all_btn) 40 | button_line.addWidget(self.close) 41 | self.layout.addItem(button_line) 42 | self.setLayout(self.layout) 43 | 44 | # Connecting the signal 45 | self.close.clicked.connect(self.accept) 46 | self.download_btn.clicked.connect(self.download) 47 | self.download_all_btn.clicked.connect(lambda: self.download(download_all=True)) 48 | 49 | def download(self, download_all=False): 50 | work_directory = self.appdata.work_directory 51 | if not os.path.exists(work_directory): 52 | print("Create uncreated work directory:", work_directory) 53 | os.mkdir(work_directory) 54 | 55 | if download_all: 56 | targets = ["all_cnapy_projects.zip"] 57 | else: 58 | targets = ["main_cnapy_projects.zip"] 59 | for t in targets: 60 | target = os.path.join(work_directory, t) 61 | if not os.path.exists(target): 62 | url = 'https://github.com/cnapy-org/CNApy-projects/releases/latest/download/' + t 63 | print("Downloading", url, "to", target, "...") 64 | urllib.request.urlretrieve(url, target) 65 | print("Done!") 66 | 67 | zip_path = os.path.join(work_directory, t) 68 | print("Extracting", zip_path, "...") 69 | with ZipFile(zip_path, 'r') as zip_file: 70 | zip_file.extractall(path=work_directory) 71 | print("Done!") 72 | os.remove(zip_path) 73 | 74 | self.accept() 75 | 76 | msgBox = QMessageBox() 77 | msgBox.setWindowTitle("Projects download complete") 78 | msgBox.setText( 79 | "Projects were downloaded successfully in the working directory." 80 | ) 81 | msgBox.setIcon(QMessageBox.Information) 82 | msgBox.exec() 83 | -------------------------------------------------------------------------------- /cnapy/data/onoff.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 48 | 52 | 56 | 67 | 68 | 70 | 71 | 73 | image/svg+xml 74 | 76 | 77 | 78 | 79 | 80 | 85 | 90 | 95 | 100 | 108 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /cnapy/flux_vector_container.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy 3 | from qtpy.QtWidgets import QMessageBox 4 | 5 | 6 | class FluxVectorContainer: 7 | def __init__(self, matORfname, reac_id=None, irreversible=None, unbounded=None): 8 | if type(matORfname) is str: 9 | try: 10 | l = numpy.load(matORfname, allow_pickle=True) # allow_pickle to read back sparse matrices saved as fv_mat 11 | self.fv_mat = l['fv_mat'] 12 | except Exception: 13 | QMessageBox.critical( 14 | None, 15 | 'Could not open file', 16 | "File could not be opened as it does not seem to be a valid EFM file. " 17 | "Maybe the file got the .npz ending for other reasons than being a scenario file or the file is corrupted." 18 | ) 19 | return 20 | if self.fv_mat.dtype == numpy.object: # in this case assume fv_mat is scipy.sparse 21 | self.fv_mat = self.fv_mat.tolist() # not sure why this works... 22 | self.reac_id = l['reac_id'].tolist() 23 | self.irreversible = l['irreversible'] 24 | self.unbounded = l['unbounded'] 25 | else: 26 | if reac_id is None: 27 | raise TypeError('reac_id must be provided') 28 | self.fv_mat = matORfname # each flux vector is a row in fv_mat 29 | self.reac_id = reac_id # corresponds to the columns of fv_mat 30 | if irreversible is None: 31 | self.irreversible = numpy.array(0) 32 | else: 33 | self.irreversible = irreversible 34 | if unbounded is None: 35 | self.unbounded = numpy.array(0) 36 | else: 37 | self.unbounded = unbounded 38 | 39 | def __len__(self): 40 | return self.fv_mat.shape[0] 41 | 42 | def is_integer_vector_rounded(self, idx, decimals=0): 43 | # TODO: does not yet work when fv_mat is list of lists sparse matrix 44 | # return all([val.is_integer() for val in numpy.round(self.fv_mat[idx, :], decimals)]) 45 | return all(round(val, decimals).is_integer() for val in self.fv_mat[idx, :]) 46 | 47 | def __getitem__(self, idx): 48 | return{self.reac_id[i]: float(self.fv_mat[idx, i]) for i in range(len(self.reac_id)) if self.fv_mat[idx, i] != 0} 49 | 50 | def save(self, fname): 51 | numpy.savez_compressed(fname, fv_mat=self.fv_mat, reac_id=self.reac_id, irreversible=self.irreversible, 52 | unbounded=self.unbounded) 53 | 54 | def clear(self): 55 | self.fv_mat = numpy.zeros((0, 0)) 56 | self.reac_id = [] 57 | self.irreversible = numpy.array(0) 58 | self.unbounded = numpy.array(0) 59 | 60 | 61 | class FluxVectorMemmap(FluxVectorContainer): 62 | ''' 63 | This class can be used to open an efmtool binary-doubles file directly as a memory map 64 | ''' 65 | 66 | def __init__(self, fname, reac_id, containing_temp_dir=None): 67 | if containing_temp_dir is not None: 68 | # keep the temporary directory alive 69 | self._containing_temp_dir = containing_temp_dir 70 | self._memmap_fname = os.path.join(containing_temp_dir.name, fname) 71 | else: 72 | self._memmap_fname = fname 73 | self._containing_temp_dir = None 74 | with open(self._memmap_fname, 'rb') as fh: 75 | num_efm = numpy.fromfile(fh, dtype='>i8', count=1)[0] 76 | num_reac = numpy.fromfile(fh, dtype='>i4', count=1)[0] 77 | super().__init__(numpy.memmap(self._memmap_fname, mode='r+', dtype='>d', 78 | offset=13, shape=(num_efm, num_reac), order='C'), reac_id) 79 | 80 | def clear(self): 81 | # lose the reference to the memmap (does not have a close() method) 82 | del self.fv_mat 83 | super().clear() 84 | # if this was the last reference to the temporary directory it is now deleted 85 | self._containing_temp_dir = None 86 | 87 | def __del__(self): 88 | del self.fv_mat # lose the reference to the memmap so that the later implicit deletion of the temporary directory can proceed without problems 89 | -------------------------------------------------------------------------------- /cnapy/gui_elements/solver_buttons.py: -------------------------------------------------------------------------------- 1 | from importlib import find_loader as module_exists 2 | from qtpy.QtWidgets import (QButtonGroup, QRadioButton, QVBoxLayout) 3 | from straindesign import select_solver 4 | from straindesign.names import CPLEX, GUROBI, GLPK, SCIP 5 | from typing import Tuple 6 | 7 | 8 | def get_solver_buttons(appdata) -> Tuple[QVBoxLayout, QButtonGroup]: 9 | # find available solvers 10 | avail_solvers = [] 11 | if module_exists('cplex'): 12 | avail_solvers += [CPLEX] 13 | if module_exists('gurobipy'): 14 | avail_solvers += [GUROBI] 15 | if module_exists('swiglpk'): 16 | avail_solvers += [GLPK] 17 | if module_exists('pyscipopt'): 18 | avail_solvers += [SCIP] 19 | 20 | # Get solver button group 21 | solver_buttons_layout = QVBoxLayout() 22 | solver_buttons = {} 23 | solver_buttons["group"] = QButtonGroup() 24 | # CPLEX 25 | solver_buttons[CPLEX] = QRadioButton("IBM CPLEX") 26 | solver_buttons[CPLEX].setProperty('name', CPLEX) 27 | solver_buttons[CPLEX].setProperty('cobrak_name', "cplex_direct") 28 | if CPLEX not in avail_solvers: 29 | solver_buttons[CPLEX].setEnabled(False) 30 | solver_buttons[CPLEX].setToolTip('CPLEX is not set up with your python environment. '+\ 31 | 'Install CPLEX and follow the steps of the python setup \n'+\ 32 | r'(https://www.ibm.com/docs/en/icos/22.1.0?topic=cplex-setting-up-python-api)') 33 | solver_buttons_layout.addWidget(solver_buttons[CPLEX]) 34 | solver_buttons["group"].addButton(solver_buttons[CPLEX]) 35 | # Gurobi 36 | solver_buttons[GUROBI] = QRadioButton("Gurobi") 37 | solver_buttons[GUROBI].setProperty('name',GUROBI) 38 | solver_buttons[GUROBI].setProperty('cobrak_name', "gurobi_direct") 39 | if GUROBI not in avail_solvers: 40 | solver_buttons[GUROBI].setEnabled(False) 41 | solver_buttons[GUROBI].setToolTip('Gurobi is not set up with your python environment. '+\ 42 | 'Install Gurobi and follow the steps of the python setup (preferably option 3) \n'+\ 43 | r'(https://support.gurobi.com/hc/en-us/articles/360044290292-How-do-I-install-Gurobi-for-Python-)') 44 | solver_buttons_layout.addWidget(solver_buttons[GUROBI]) 45 | solver_buttons["group"].addButton(solver_buttons[GUROBI]) 46 | # GLPK 47 | solver_buttons[GLPK] = QRadioButton("GLPK") 48 | solver_buttons[GLPK].setProperty('name',GLPK) 49 | solver_buttons[GLPK].setProperty('cobrak_name', "glpk") 50 | if GLPK not in avail_solvers: 51 | solver_buttons[GLPK].setEnabled(False) 52 | solver_buttons[GLPK].setToolTip('GLPK is not set up with your python environment. '+\ 53 | 'GLPK should have been installed together with the COBRA toolbox. \n'\ 54 | 'Reinstall the COBRA toolbox for your Python environment.') 55 | solver_buttons_layout.addWidget(solver_buttons[GLPK]) 56 | solver_buttons["group"].addButton(solver_buttons[GLPK]) 57 | # SCIP 58 | solver_buttons[SCIP] = QRadioButton("SCIP") 59 | solver_buttons[SCIP].setProperty('name',SCIP) 60 | solver_buttons[SCIP].setProperty('cobrak_name', "scip") 61 | if SCIP not in avail_solvers: 62 | solver_buttons[SCIP].setEnabled(False) 63 | solver_buttons[SCIP].setToolTip('SCIP is not set up with your python environment. '+\ 64 | 'Install SCIP following the steps of the PySCIPOpt manual \n'+\ 65 | r'(https://github.com/scipopt/PySCIPOpt') 66 | solver_buttons_layout.addWidget(solver_buttons[SCIP]) 67 | solver_buttons["group"].addButton(solver_buttons[SCIP]) 68 | # optlang_enumerator 69 | solver_buttons['OPTLANG'] = QRadioButton() 70 | solver_buttons['OPTLANG'].setProperty('name','OPTLANG') 71 | solver_buttons['OPTLANG'].setToolTip('optlang_enumerator supports calculation of reaction MCS only.\n'+\ 72 | 'Reaction knock-ins and setting of intervention costs are possible.\n'+\ 73 | 'The solver can be changed via COBRApy settings.') 74 | solver_buttons_layout.addWidget(solver_buttons['OPTLANG']) 75 | solver_buttons["group"].addButton(solver_buttons['OPTLANG']) 76 | # check best available solver 77 | if avail_solvers: 78 | # Set cobrapy default solver if available 79 | solver = select_solver(None, appdata.project.cobra_py_model) 80 | solver_buttons[solver].setChecked(True) 81 | 82 | return solver_buttons_layout, solver_buttons 83 | -------------------------------------------------------------------------------- /cnapy/utils_for_cnapy_api.py: -------------------------------------------------------------------------------- 1 | """Functions which will be later added to CNAPy's API""" 2 | import requests 3 | from dataclasses import dataclass 4 | from qtpy.QtCore import Qt 5 | from qtpy.QtGui import QColor 6 | from qtpy.QtWidgets import QMessageBox, QTreeWidgetItem 7 | 8 | 9 | @dataclass() 10 | class IdentifiersOrgResult: 11 | connection_error: bool 12 | is_key_valid: bool 13 | is_key_value_pair_valid: bool 14 | 15 | 16 | def check_in_identifiers_org(widget: QTreeWidgetItem): 17 | widget.setCursor(Qt.BusyCursor) 18 | rows = widget.annotation_widget.annotation.rowCount() 19 | invalid_red = QColor(255, 0, 0) 20 | for i in range(0, rows): 21 | if widget.annotation_widget.annotation.item(i, 0) is not None: 22 | key = widget.annotation_widget.annotation.item(i, 0).text() 23 | else: 24 | key = "" 25 | if widget.annotation_widget.annotation.item(i, 1) is not None: 26 | values = widget.annotation_widget.annotation.item(i, 1).text() 27 | else: 28 | values = "" 29 | if (key == "") or (values == ""): 30 | continue 31 | 32 | if values.startswith("["): 33 | values = values.replace("', ", "'\b,").replace('", ', '"\b,').replace("[", "")\ 34 | .replace("]", "").replace("'", "").replace('"', "") 35 | values = values.split("\b,") 36 | else: 37 | values = [values] 38 | 39 | for value in values: 40 | identifiers_org_result = check_identifiers_org_entry(key, value) 41 | 42 | if identifiers_org_result.connection_error: 43 | msgBox = QMessageBox() 44 | msgBox.setWindowTitle("Connection error!") 45 | msgBox.setTextFormat(Qt.RichText) 46 | msgBox.setText("

identifiers.org could not be accessed. Either the internet connection isn't working or the server is currently down.

") 47 | msgBox.setIcon(QMessageBox.Warning) 48 | msgBox.exec() 49 | break 50 | 51 | if (not identifiers_org_result.is_key_value_pair_valid) and (":" in value): 52 | split_value = value.split(":") 53 | identifiers_org_result = check_identifiers_org_entry(split_value[0], split_value[1]) 54 | 55 | 56 | if not identifiers_org_result.is_key_valid: 57 | widget.annotation_widget.annotation.item(i, 0).setBackground(invalid_red) 58 | 59 | if not identifiers_org_result.is_key_value_pair_valid: 60 | widget.annotation_widget.annotation.item(i, 1).setBackground(invalid_red) 61 | 62 | if not identifiers_org_result.is_key_value_pair_valid: 63 | break 64 | widget.setCursor(Qt.ArrowCursor) 65 | 66 | 67 | def check_identifiers_org_entry(key: str, value: str) -> IdentifiersOrgResult: 68 | identifiers_org_result = IdentifiersOrgResult( 69 | False, 70 | False, 71 | False, 72 | ) 73 | 74 | # Check key 75 | url_key_check = f"https://resolver.api.identifiers.org/{key}" 76 | try: 77 | result_object = requests.get(url_key_check) 78 | except requests.exceptions.RequestException: 79 | print("HTTP error") 80 | identifiers_org_result.connection_error = True 81 | return identifiers_org_result 82 | result_json = result_object.json() 83 | 84 | if result_json["errorMessage"] is None: 85 | identifiers_org_result.is_key_valid = True 86 | else: 87 | return identifiers_org_result 88 | 89 | # Check key:value pair 90 | url_key_value_pair_check = f"https://resolver.api.identifiers.org/{key}:{value}" 91 | try: 92 | result_object = requests.get(url_key_value_pair_check) 93 | except requests.exceptions.RequestException: 94 | print("HTTP error") 95 | identifiers_org_result.connection_error = True 96 | return identifiers_org_result 97 | result_json = result_object.json() 98 | 99 | if result_json["errorMessage"] is None: 100 | identifiers_org_result.is_key_value_pair_valid = True 101 | else: 102 | return identifiers_org_result 103 | 104 | return identifiers_org_result 105 | -------------------------------------------------------------------------------- /cnapy/gui_elements/annotation_widget.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import webbrowser 3 | 4 | from qtpy.QtCore import Qt, Signal, Slot 5 | from qtpy.QtGui import QIcon 6 | from qtpy.QtWidgets import (QHBoxLayout, QHeaderView, QLabel, QPushButton, QSizePolicy, QTableWidget, 7 | QTableWidgetItem, QVBoxLayout, QMessageBox) 8 | 9 | from cnapy.utils_for_cnapy_api import check_in_identifiers_org 10 | 11 | 12 | class AnnotationWidget(QVBoxLayout): 13 | def __init__(self, parent): 14 | super().__init__() 15 | self.parent = parent 16 | 17 | lh = QHBoxLayout() 18 | label = QLabel("Annotations:") 19 | lh.addWidget(label) 20 | 21 | check_button = QPushButton("identifiers.org check") 22 | check_button.setIcon(QIcon.fromTheme("list-add")) 23 | policy = QSizePolicy() 24 | policy.ShrinkFlag = True 25 | check_button.setSizePolicy(policy) 26 | check_button.clicked.connect(self.check_in_identifiers_org) 27 | lh.addWidget(check_button) 28 | self.addItem(lh) 29 | 30 | lh2 = QHBoxLayout() 31 | self.annotation = QTableWidget(0, 2) 32 | self.annotation.setHorizontalHeaderLabels( 33 | ["key", "value"]) 34 | self.annotation.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) 35 | lh2.addWidget(self.annotation) 36 | 37 | lh3 = QVBoxLayout() 38 | self.add_anno = QPushButton("+") 39 | self.add_anno.clicked.connect(self.add_anno_row) 40 | lh3.addWidget(self.add_anno) 41 | self.delete_anno = QPushButton("-") 42 | self.delete_anno.clicked.connect(self.delete_anno_row) 43 | lh3.addWidget(self.delete_anno) 44 | self.open_annotation = QPushButton("Open chosen\nin browser") 45 | self.open_annotation.clicked.connect(self.open_in_browser) 46 | lh3.addWidget(self.open_annotation) 47 | lh2.addItem(lh3) 48 | self.addItem(lh2) 49 | 50 | self.annotation.itemChanged.connect(parent.throttler.throttle) 51 | 52 | def add_anno_row(self): 53 | i = self.annotation.rowCount() 54 | self.annotation.insertRow(i) 55 | 56 | def apply_annotation(self, model_element): 57 | model_element.annotation = {} 58 | rows = self.annotation.rowCount() 59 | for i in range(0, rows): 60 | annotation_item = self.annotation.item(i, 0) 61 | if annotation_item is None: 62 | continue 63 | key = annotation_item.text() 64 | if self.annotation.item(i, 1) is None: 65 | value = "" 66 | else: 67 | value = self.annotation.item(i, 1).text() 68 | if value.startswith("["): 69 | try: 70 | value = ast.literal_eval(value) 71 | except: # if parsing as list does not work keep the raw text 72 | pass 73 | 74 | model_element.annotation[key] = value 75 | 76 | def check_in_identifiers_org(self): 77 | check_in_identifiers_org(self.parent) 78 | 79 | deleteAnnotation = Signal(str) 80 | def delete_anno_row(self): 81 | row_to_delete = self.annotation.currentRow() 82 | try: 83 | identifier_type = self.annotation.item(row_to_delete, 0).text() 84 | self.deleteAnnotation.emit(identifier_type) 85 | self.annotation.removeRow(row_to_delete) 86 | except AttributeError: 87 | pass 88 | 89 | def open_in_browser(self): 90 | current_row = self.annotation.currentRow() 91 | if current_row >= 0: 92 | try: 93 | identifier_type = self.annotation.item(current_row, 0).text() 94 | identifier_value = self.annotation.item(current_row, 1).text() 95 | except AttributeError: 96 | pass 97 | if identifier_value.startswith("["): 98 | identifier_value = ast.literal_eval(identifier_value)[0] 99 | url = f"https://identifiers.org/{identifier_type}:{identifier_value}" 100 | webbrowser.open_new_tab(url) 101 | else: 102 | QMessageBox.information(self.parent, 'Select annotation', 103 | 'Select one of the annotations from the list by clicking on a row.') 104 | 105 | def update_annotations(self, annotation): 106 | self.annotation.itemChanged.disconnect( 107 | self.parent.throttler.throttle 108 | ) 109 | self.annotation.setRowCount(len(annotation)) 110 | self.annotation.clearContents() 111 | i = 0 112 | for key, anno in annotation.items(): 113 | keyl = QTableWidgetItem(key) 114 | iteml = QTableWidgetItem(str(anno)) 115 | self.annotation.setItem(i, 0, keyl) 116 | self.annotation.setItem(i, 1, iteml) 117 | i += 1 118 | 119 | self.annotation.itemChanged.connect( 120 | self.parent.throttler.throttle) 121 | -------------------------------------------------------------------------------- /installers/install_cnapy_here.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | :: Set the PowerShell script file name 5 | set "psFile=install_cnapy.ps1" 6 | 7 | :: Write the PowerShell script to a file 8 | echo # Adapted from https://raw.githubusercontent.com/mamba-org/micromamba-releases/main/install.ps1 > "%psFile%" 9 | echo. >> "%psFile%" 10 | echo $CNAPY_VERSION = "1.2.7" ^# Replace with the actual version if needed >> "%psFile%" 11 | echo $RELEASE_URL="https://github.com/mamba-org/micromamba-releases/releases/latest/download/micromamba-win-64" >> "%psFile%" 12 | echo. >> "%psFile%" 13 | echo Write-Output "Downloading micromamba from $RELEASE_URL" >> "%psFile%" 14 | echo curl.exe -L -o micromamba.exe $RELEASE_URL >> "%psFile%" 15 | echo. >> "%psFile%" 16 | echo ^# Get the directory where the script is located >> "%psFile%" 17 | echo $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path >> "%psFile%" 18 | echo. >> "%psFile%" 19 | echo $InstallDir = Join-Path -Path $ScriptDir -ChildPath "cnapy-$CNAPY_VERSION" >> "%psFile%" 20 | echo New-Item -ItemType Directory -Force -Path $InstallDir ^| out-null >> "%psFile%" 21 | echo. >> "%psFile%" 22 | echo $MAMBA_INSTALL_PATH = Join-Path -Path $InstallDir -ChildPath "micromamba.exe" >> "%psFile%" 23 | echo. >> "%psFile%" 24 | echo Write-Output "`nInstalling micromamba to $InstallDir`n" >> "%psFile%" 25 | echo Move-Item -Force micromamba.exe $MAMBA_INSTALL_PATH ^| out-null >> "%psFile%" 26 | echo. >> "%psFile%" 27 | echo ^# Use ^& to execute the micromamba commands stored in the variable >> "%psFile%" 28 | echo ^& $MAMBA_INSTALL_PATH create -y -p "./cnapy-$CNAPY_VERSION/cnapy-environment" python=3.10 pip openjdk -r "./cnapy-$CNAPY_VERSION/" -c conda-forge >> "%psFile%" 29 | echo Copy-Item -Path "cnapy-1.2.7/condabin/mamba.bat" -Destination "cnapy-1.2.7/condabin/micromamba.bat" >> "%psFile%" 30 | echo ^& $MAMBA_INSTALL_PATH run -p "./cnapy-$CNAPY_VERSION/cnapy-environment" -r "./cnapy-$CNAPY_VERSION/" pip install --no-cache-dir uv >> "%psFile%" 31 | echo ^& $MAMBA_INSTALL_PATH run -p "./cnapy-$CNAPY_VERSION/cnapy-environment" -r "./cnapy-$CNAPY_VERSION/" uv --no-cache pip install --no-cache-dir cnapy >> "%psFile%" 32 | echo. >> "%psFile%" 33 | echo ^# Create a new PowerShell file called "cnapy_runner_helper.ps1" >> "%psFile%" 34 | echo $PsFilePath = Join-Path -Path $InstallDir -ChildPath "cnapy_runner_helper.ps1" >> "%psFile%" 35 | echo $PsFileContent = "$MAMBA_INSTALL_PATH run -p `"$InstallDir\cnapy-environment`" -r `"$InstallDir\`" cnapy" >> "%psFile%" 36 | echo Set-Content -Path $PsFilePath -Value $PsFileContent >> "%psFile%" 37 | echo. >> "%psFile%" 38 | echo ^# Create a new batch file called "RUN_CNApy.bat" >> "%psFile%" 39 | echo $BatchFilePath = Join-Path -Path $InstallDir -ChildPath "RUN_CNApy.bat" >> "%psFile%" 40 | echo $BatchFileContent = "powershell -NoProfile -ExecutionPolicy Bypass -File `"$InstallDir\cnapy_runner_helper.ps1`"" >> "%psFile%" 41 | echo Set-Content -Path $BatchFilePath -Value $BatchFileContent >> "%psFile%" 42 | echo. >> "%psFile%" 43 | echo ^# Create desktop icon using PowerShell >> "%psFile%" 44 | echo $ShortcutPath = [System.IO.Path]::Combine($Env:USERPROFILE, "Desktop", "CNApy-$CNAPY_VERSION.lnk") >> "%psFile%" 45 | echo $WScriptShell = New-Object -ComObject WScript.Shell >> "%psFile%" 46 | echo $Shortcut = $WScriptShell.CreateShortcut($ShortcutPath) >> "%psFile%" 47 | echo $Shortcut.TargetPath = $BatchFilePath >> "%psFile%" 48 | echo ^# $Shortcut.IconLocation = Join-Path -Path $ScriptDir -ChildPath "icon\CNApy_Icon.ico" >> "%psFile%" 49 | echo $Shortcut.WorkingDirectory = $ScriptDir >> "%psFile%" 50 | echo $Shortcut.Save() >> "%psFile%" 51 | echo. >> "%psFile%" 52 | echo Write-Output "`nDesktop shortcut created successfully`n" >> "%psFile%" 53 | 54 | :: Ensure the PowerShell script file exists before running it 55 | if exist "%psFile%" ( 56 | :: Run the PowerShell script 57 | powershell -NoProfile -ExecutionPolicy Bypass -File "%psFile%" 58 | if %errorlevel% neq 0 ( 59 | echo An error occurred while running the PowerShell script. CNApy was not installed correctly. 60 | echo If PowerShell was not found, install it on your device. 61 | del "%psFile%" 62 | pause 63 | exit /b 1 64 | ) 65 | 66 | :: Delete the PowerShell script file 67 | del "%psFile%" 68 | 69 | :: Congratulate the user 70 | echo Congratulations! CNApy was successfully installed! 71 | echo To run CNApy, double-click on the newly created CNApy-1.2.7 desktop icon or, 72 | echo alternatively, double-click on the RUN_CNApy.bat file in the newly created cnapy-1.2.7 subfolder. 73 | echo To deinstall CNApy later, simply delete the newly created cnapy-1.2.7 subfolder. 74 | pause 75 | ) else ( 76 | echo PowerShell script file not found: %psFile% 77 | echo Maybe your disk is full or you need to install CNApy in a folder where you allowed to write new files. 78 | echo This is because, often, folders such as the default Programs folder are restricted, so that other folders might work. 79 | echo Alternatively, you might need to run this installer with administrator priviledges! 80 | pause 81 | ) 82 | 83 | endlocal 84 | -------------------------------------------------------------------------------- /cnapy/gui_elements/model_info.py: -------------------------------------------------------------------------------- 1 | """The model info view""" 2 | 3 | from qtpy.QtCore import Signal, Slot, QSignalBlocker 4 | from qtpy.QtWidgets import (QLabel, QTextEdit, QVBoxLayout, QWidget, QComboBox, QGroupBox) 5 | 6 | from straindesign.parse_constr import linexpr2dict 7 | from cnapy.appdata import AppData 8 | from cnapy.gui_elements.scenario_tab import OptimizationDirection 9 | from cnapy.utils import QComplReceivLineEdit 10 | 11 | class ModelInfo(QWidget): 12 | """A widget that shows infos about the model""" 13 | 14 | def __init__(self, appdata: AppData): 15 | QWidget.__init__(self) 16 | self.appdata = appdata 17 | 18 | layout = QVBoxLayout() 19 | group = QGroupBox("Objective function (as defined in the model)") 20 | self.objective_group_layout: QVBoxLayout = QVBoxLayout() 21 | self.global_objective = QComplReceivLineEdit(self, []) 22 | self.objective_group_layout.addWidget(self.global_objective) 23 | label = QLabel("Optimization direction") 24 | self.objective_group_layout.addWidget(label) 25 | self.opt_direction = QComboBox() 26 | self.opt_direction.insertItems(0, ["minimize", "maximize"]) 27 | self.objective_group_layout.addWidget(self.opt_direction) 28 | group.setLayout(self.objective_group_layout) 29 | layout.addWidget(group) 30 | 31 | label = QLabel("Description") 32 | layout.addWidget(label) 33 | self.description = QTextEdit() 34 | self.description.setPlaceholderText("Enter a project description") 35 | layout.addWidget(self.description) 36 | 37 | self.setLayout(layout) 38 | 39 | self.current_global_objective = {} 40 | self.global_objective.textCorrect.connect(self.change_global_objective) 41 | self.opt_direction.currentIndexChanged.connect(self.global_optimization_direction_changed) 42 | self.description.textChanged.connect(self.description_changed) 43 | 44 | self.update() 45 | 46 | def update(self): 47 | self.global_objective.set_wordlist(self.appdata.project.cobra_py_model.reactions.list_attr("id")) 48 | with QSignalBlocker(self.global_objective): 49 | self.global_objective.setText(self.format_objective_expression()) 50 | self.current_global_objective = {} 51 | for r in self.appdata.project.cobra_py_model.reactions: 52 | if r.objective_coefficient != 0: 53 | self.current_global_objective[r.id] = r.objective_coefficient 54 | with QSignalBlocker(self.opt_direction): 55 | self.opt_direction.setCurrentIndex(OptimizationDirection[self.appdata.project.cobra_py_model.objective_direction].value) 56 | 57 | if "description" in self.appdata.project.meta_data: 58 | description = self.appdata.project.meta_data["description"] 59 | else: 60 | description = "" 61 | 62 | with QSignalBlocker(self.description): 63 | self.description.setText(description) 64 | 65 | @Slot(bool) 66 | def change_global_objective(self, yes: bool): 67 | if yes: 68 | new_objective = linexpr2dict(self.global_objective.text(), 69 | self.appdata.project.cobra_py_model.reactions.list_attr("id")) 70 | if new_objective != self.current_global_objective: 71 | for reac_id in self.current_global_objective.keys(): 72 | self.appdata.project.cobra_py_model.reactions.get_by_id(reac_id).objective_coefficient = 0 73 | for reac_id, coeff in new_objective.items(): 74 | self.appdata.project.cobra_py_model.reactions.get_by_id(reac_id).objective_coefficient = coeff 75 | self.current_global_objective = new_objective 76 | self.globalObjectiveChanged.emit() 77 | 78 | def format_objective_expression(self) -> str: 79 | first = True 80 | res = "" 81 | model = self.appdata.project.cobra_py_model 82 | for r in model.reactions: 83 | if r.objective_coefficient != 0: 84 | if first: 85 | res += str(r.objective_coefficient) + " " + str(r.id) 86 | first = False 87 | else: 88 | if r.objective_coefficient > 0: 89 | res += " +" + \ 90 | str(r.objective_coefficient) + " " + str(r.id) 91 | else: 92 | res += " "+str(r.objective_coefficient) + \ 93 | " " + str(r.id) 94 | 95 | return res 96 | 97 | @Slot(int) 98 | def global_optimization_direction_changed(self, index: int): 99 | self.appdata.project.cobra_py_model.objective_direction = OptimizationDirection(index).name 100 | self.globalObjectiveChanged.emit() 101 | 102 | def description_changed(self): 103 | self.appdata.project.meta_data["description"] = self.description.toPlainText() 104 | self.appdata.window.unsaved_changes() 105 | 106 | globalObjectiveChanged = Signal() 107 | -------------------------------------------------------------------------------- /cnapy/gui_elements/efmtool_dialog.py: -------------------------------------------------------------------------------- 1 | """The cnapy elementary flux modes calculator dialog""" 2 | from qtpy.QtCore import Qt, QThread, Signal, Slot 3 | from qtpy.QtWidgets import (QCheckBox, QDialog, QHBoxLayout, QMessageBox, 4 | QPushButton, QVBoxLayout, QTextEdit) 5 | 6 | import cnapy.core 7 | from cnapy.appdata import AppData 8 | 9 | 10 | class EFMtoolDialog(QDialog): 11 | """A dialog to set up EFM calculation""" 12 | 13 | def __init__(self, appdata: AppData, central_widget): 14 | QDialog.__init__(self) 15 | self.setWindowTitle("Elementary Flux Mode Computation") 16 | 17 | self.appdata = appdata 18 | self.central_widget = central_widget 19 | 20 | self.layout = QVBoxLayout() 21 | 22 | l1 = QHBoxLayout() 23 | self.constraints = QCheckBox("consider 0 in current scenario as off") 24 | self.constraints.setCheckState(Qt.Checked) 25 | l1.addWidget(self.constraints) 26 | self.layout.addItem(l1) 27 | 28 | self.text_field = QTextEdit("*** EFMtool output ***") 29 | self.text_field.setReadOnly(True) 30 | self.layout.addWidget(self.text_field) 31 | 32 | lx = QHBoxLayout() 33 | self.button = QPushButton("Compute") 34 | self.cancel = QPushButton("Close") 35 | lx.addWidget(self.button) 36 | lx.addWidget(self.cancel) 37 | self.layout.addItem(lx) 38 | 39 | self.setLayout(self.layout) 40 | 41 | # Connecting the signal 42 | self.cancel.clicked.connect(self.reject) 43 | self.button.clicked.connect(self.compute) 44 | 45 | def compute(self): 46 | self.setCursor(Qt.BusyCursor) 47 | self.efm_computation = EFMComputationThread(self.appdata.project.cobra_py_model, self.appdata.project.scen_values, 48 | self.constraints.checkState() == Qt.Checked) 49 | self.button.setText("Abort computation") 50 | self.button.clicked.disconnect(self.compute) 51 | self.button.clicked.connect(self.efm_computation.activate_abort) 52 | self.rejected.connect(self.efm_computation.activate_abort) # for the X button of the window frame 53 | self.cancel.hide() 54 | self.efm_computation.send_progress_text.connect(self.receive_progress_text) 55 | self.efm_computation.finished_computation.connect(self.conclude_computation) 56 | self.efm_computation.start() 57 | 58 | def conclude_computation(self): 59 | self.setCursor(Qt.ArrowCursor) 60 | if self.efm_computation.abort: 61 | self.accept() 62 | else: 63 | if self.efm_computation.ems is None: 64 | # in this case the progress window should still be left open and the cancel button reappear 65 | self.button.hide() 66 | self.cancel.show() 67 | QMessageBox.information(self, 'No modes', 68 | 'An error occured and modes have not been calculated.') 69 | else: 70 | self.accept() 71 | if len(self.efm_computation.ems) == 0: 72 | QMessageBox.information(self, 'No modes', 73 | 'No elementary modes exist.') 74 | else: 75 | self.appdata.project.modes = self.efm_computation.ems 76 | self.central_widget.mode_navigator.current = 0 77 | self.central_widget.mode_navigator.scenario = self.efm_computation.scenario 78 | self.central_widget.mode_navigator.set_to_efm() 79 | self.central_widget.update_mode() 80 | 81 | @Slot(str) 82 | def receive_progress_text(self, text): 83 | self.text_field.append(text) 84 | # self.central_widget.console._append_plain_text(text) # causes some kind of deadlock?!? 85 | 86 | class EFMComputationThread(QThread): 87 | def __init__(self, model, scen_values, constraints): 88 | super().__init__() 89 | self.model = model 90 | self.scen_values = scen_values 91 | self.constraints = constraints 92 | self.abort = False 93 | self.ems = None 94 | self.scenario = None 95 | 96 | def do_abort(self): 97 | return self.abort 98 | 99 | def activate_abort(self): 100 | self.abort = True 101 | 102 | def run(self): 103 | (self.ems, self.scenario) = cnapy.core.efm_computation(self.model, self.scen_values, self.constraints, 104 | print_progress_function=self.print_progress_function, abort_callback=self.do_abort) 105 | self.finished_computation.emit() 106 | 107 | def print_progress_function(self, text): 108 | print(text) 109 | self.send_progress_text.emit(text) 110 | 111 | # the output from efmtool needs to be passed as a signal because all Qt widgets must 112 | # run on the main thread and their methods cannot be safely called from other threads 113 | send_progress_text = Signal(str) 114 | finished_computation = Signal() 115 | -------------------------------------------------------------------------------- /cnapy/gui_elements/reaction_table_widget.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from qtpy.QtCore import Qt, Signal, Slot 3 | from qtpy.QtWidgets import QApplication, QTableWidget, QTableWidgetItem, QAbstractItemView, QPlainTextEdit, QFrame 4 | from qtpy.QtGui import QMouseEvent, QTextCursor 5 | 6 | 7 | class ModelElementType(Enum): 8 | METABOLITE = 1 9 | GENE = 2 10 | 11 | 12 | class ReactionString(QPlainTextEdit): 13 | def __init__(self, reaction, metabolite_list): 14 | super().__init__() 15 | reaction_string = reaction.build_reaction_string() + " " # extra space to be able to click outside the equation without triggering a jump to the metabolite 16 | self.setPlainText(reaction_string) 17 | self.text_width = self.fontMetrics().horizontalAdvance(reaction_string) 18 | self.setReadOnly(True) 19 | self.setFrameStyle(QFrame.NoFrame) 20 | self.model = reaction.model 21 | self.metabolite_list = metabolite_list 22 | 23 | jumpToMetabolite = Signal(str) 24 | 25 | def mouseReleaseEvent(self, event: QMouseEvent): 26 | if event.button() == Qt.LeftButton: 27 | text_cursor: QTextCursor = self.textCursor() 28 | if not text_cursor.hasSelection(): 29 | start: int = text_cursor.position() 30 | text: str = self.toPlainText() 31 | if start >= len(text): 32 | return 33 | while start > 0: 34 | start -= 1 35 | if text[start].isspace(): 36 | break 37 | text = text[start:].split(maxsplit=1)[0] 38 | if self.model.metabolites.has_id(text): 39 | self.jumpToMetabolite.emit(text) 40 | self.metabolite_list.set_current_item(text) 41 | 42 | class ReactionTableWidget(QTableWidget): 43 | def __init__(self, appdata, element_type: ModelElementType) -> None: 44 | super().__init__() 45 | 46 | self.appdata = appdata 47 | self.element_type = element_type 48 | self.setColumnCount(2) 49 | self.setHorizontalHeaderLabels(["Id", "Reaction"]) 50 | self.horizontalHeader().setStretchLastSection(True) 51 | self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) 52 | self.horizontalHeader().sectionResized.connect(self.section_resized) 53 | 54 | def update_state(self, id_text, metabolite_list): 55 | QApplication.setOverrideCursor(Qt.BusyCursor) 56 | QApplication.processEvents() # to put the change above into effect 57 | self.clearContents() 58 | self.setRowCount(0) # also resets manually changed row heights 59 | 60 | if self.element_type is ModelElementType.METABOLITE: 61 | model_elements = self.appdata.project.cobra_py_model.metabolites 62 | elif self.element_type is ModelElementType.GENE: 63 | model_elements = self.appdata.project.cobra_py_model.genes 64 | 65 | if model_elements.has_id(id_text): 66 | metabolite_or_gene = model_elements.get_by_id( 67 | id_text 68 | ) 69 | self.setSortingEnabled(False) 70 | self.setRowCount(len(metabolite_or_gene.reactions)) 71 | for i, reaction in enumerate(metabolite_or_gene.reactions): 72 | item = QTableWidgetItem(reaction.id) 73 | item.setToolTip(reaction.name) 74 | self.setItem(i, 0, item) 75 | reaction_string_widget = ReactionString(reaction, metabolite_list) 76 | reaction_string_widget.jumpToMetabolite.connect(self.emit_jump_to_metabolite) 77 | self.setCellWidget(i, 1, reaction_string_widget) 78 | self.setSortingEnabled(True) 79 | self.section_resized(1, self.horizontalHeader().sectionSize(1), self.horizontalHeader().sectionSize(1)) 80 | QApplication.restoreOverrideCursor() 81 | 82 | @Slot(int, int, int) 83 | def section_resized(self, index: int, old_size: int, new_size: int): 84 | if index == 1: 85 | for row in range(self.rowCount()): 86 | reaction_string_widget: ReactionString = self.cellWidget(row, index) 87 | font_metrics= reaction_string_widget.fontMetrics() 88 | base_height = font_metrics.lineSpacing() 89 | margins = reaction_string_widget.contentsMargins() 90 | height_margin = 12 91 | if reaction_string_widget.text_width + margins.left() + margins.right() > new_size: 92 | reaction_string_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 93 | self.setRowHeight(row, base_height*2 + font_metrics.leading() + height_margin) # font_metrics.leading(): space between two lines 94 | else: 95 | reaction_string_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 96 | self.setRowHeight(row, base_height + height_margin) 97 | 98 | jumpToMetabolite = Signal(str) 99 | def emit_jump_to_metabolite(self, metabolite): 100 | self.jumpToMetabolite.emit(metabolite) 101 | -------------------------------------------------------------------------------- /cnapy/gui_elements/clipboard_calculator.py: -------------------------------------------------------------------------------- 1 | """The cnapy clipboard calculator dialog""" 2 | from qtpy.QtWidgets import (QButtonGroup, QComboBox, QDialog, QHBoxLayout, 3 | QLineEdit, QMessageBox, QPushButton, QRadioButton, 4 | QVBoxLayout) 5 | 6 | from cnapy.appdata import AppData 7 | 8 | 9 | class ClipboardCalculator(QDialog): 10 | """A dialog to perform arithmetics with the clipboard""" 11 | 12 | def __init__(self, appdata: AppData): 13 | QDialog.__init__(self) 14 | self.setWindowTitle("Clipboard calculator") 15 | 16 | self.appdata = appdata 17 | self.layout = QVBoxLayout() 18 | l1 = QHBoxLayout() 19 | self.left = QVBoxLayout() 20 | self.l1 = QRadioButton("Current values") 21 | self.l2 = QRadioButton("Clipboard values") 22 | h1 = QHBoxLayout() 23 | self.l3 = QRadioButton() 24 | self.left_value = QLineEdit("0") 25 | h1.addWidget(self.l3) 26 | h1.addWidget(self.left_value) 27 | self.lqb = QButtonGroup() 28 | self.lqb.addButton(self.l1) 29 | self.l1.setChecked(True) 30 | self.lqb.addButton(self.l2) 31 | self.lqb.addButton(self.l3) 32 | 33 | self.left.addWidget(self.l1) 34 | self.left.addWidget(self.l2) 35 | self.left.addItem(h1) 36 | op = QVBoxLayout() 37 | self.op = QComboBox() 38 | self.op.insertItem(1, "+") 39 | self.op.insertItem(2, "-") 40 | self.op.insertItem(3, "*") 41 | self.op.insertItem(4, "/") 42 | op.addWidget(self.op) 43 | self.right = QVBoxLayout() 44 | self.r1 = QRadioButton("Current values") 45 | self.r2 = QRadioButton("Clipboard values") 46 | h2 = QHBoxLayout() 47 | self.r3 = QRadioButton() 48 | self.right_value = QLineEdit("0") 49 | h2.addWidget(self.r3) 50 | h2.addWidget(self.right_value) 51 | 52 | self.rqb = QButtonGroup() 53 | self.rqb.addButton(self.r1) 54 | self.r1.setChecked(True) 55 | self.rqb.addButton(self.r2) 56 | self.rqb.addButton(self.r3) 57 | 58 | self.right.addWidget(self.r1) 59 | self.right.addWidget(self.r2) 60 | self.right.addItem(h2) 61 | l1.addItem(self.left) 62 | l1.addItem(op) 63 | l1.addItem(self.right) 64 | self.layout.addItem(l1) 65 | 66 | l2 = QHBoxLayout() 67 | self.button = QPushButton("Compute") 68 | self.close = QPushButton("Close") 69 | l2.addWidget(self.button) 70 | l2.addWidget(self.close) 71 | self.layout.addItem(l2) 72 | self.setLayout(self.layout) 73 | 74 | # Connecting the signal 75 | self.close.clicked.connect(self.accept) 76 | self.button.clicked.connect(self.compute) 77 | 78 | def compute(self): 79 | l_comp = {} 80 | r_comp = {} 81 | if self.l1.isChecked(): 82 | l_comp = self.appdata.project.comp_values 83 | 84 | for (key, value) in self.appdata.project.scen_values.items(): 85 | l_comp[key] = value 86 | elif self.l2.isChecked(): 87 | try: 88 | l_comp = self.appdata.clipboard_comp_values 89 | except AttributeError: 90 | QMessageBox.warning( 91 | None, 92 | "No clipboard created yet", 93 | "Clipboard arithmetics do not work as no clipboard was created yet. Store values to a clipboard first to solve this problem." 94 | ) 95 | return 96 | 97 | if self.r1.isChecked(): 98 | r_comp = self.appdata.project.comp_values 99 | 100 | for (key, value) in self.appdata.project.scen_values.items(): 101 | r_comp[key] = value 102 | elif self.r2.isChecked(): 103 | r_comp = self.appdata.clipboard_comp_values 104 | 105 | for key in self.appdata.project.comp_values: 106 | if self.l3.isChecked(): 107 | lv_comp = (float(self.left_value.text()), 108 | float(self.left_value.text())) 109 | else: 110 | lv_comp = l_comp[key] 111 | if self.r3.isChecked(): 112 | rv_comp = (float(self.right_value.text()), 113 | float(self.right_value.text())) 114 | else: 115 | rv_comp = r_comp[key] 116 | 117 | res = self.combine(lv_comp, rv_comp) 118 | 119 | if key in self.appdata.project.scen_values.keys(): 120 | self.appdata.project.scen_values[key] = res 121 | self.appdata.project.comp_values[key] = res 122 | 123 | self.appdata.project.comp_values_type = 0 124 | self.appdata.window.centralWidget().update() 125 | 126 | def combine(self, lv, rv): 127 | (llb, lub) = lv 128 | (rlb, rub) = rv 129 | if self.op.currentText() == "+": 130 | return (llb+rlb, lub+rub) 131 | if self.op.currentText() == "-": 132 | return (llb-rlb, lub-rub) 133 | if self.op.currentText() == "*": 134 | return (llb*rlb, lub*rub) 135 | if self.op.currentText() == "/": 136 | return (llb/rlb, lub/rub) 137 | -------------------------------------------------------------------------------- /cnapy/data/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /cnapy/gui_elements/flux_optimization_dialog.py: -------------------------------------------------------------------------------- 1 | """The cnapy flux optimization dialog""" 2 | from random import randint 3 | from numpy import isinf 4 | import re 5 | from qtpy.QtCore import Qt, Slot 6 | from qtpy.QtWidgets import (QDialog, QHBoxLayout, QLabel, QComboBox, 7 | QMessageBox, QPushButton, QVBoxLayout) 8 | 9 | from cnapy.appdata import AppData 10 | from cnapy.gui_elements.central_widget import CentralWidget 11 | from cnapy.utils import QComplReceivLineEdit 12 | from straindesign import fba, linexpr2dict, linexprdict2str, avail_solvers 13 | from straindesign.names import * 14 | 15 | class FluxOptimizationDialog(QDialog): 16 | """A dialog to perform flux optimization""" 17 | 18 | def __init__(self, appdata: AppData, central_widget: CentralWidget): 19 | QDialog.__init__(self) 20 | self.setWindowTitle("Flux optimization") 21 | 22 | self.appdata = appdata 23 | self.central_widget = central_widget 24 | 25 | numr = len(self.appdata.project.cobra_py_model.reactions) 26 | self.reac_ids = self.appdata.project.reaction_ids.id_list 27 | if numr > 1: 28 | r1 = self.appdata.project.cobra_py_model.reactions[randint(0,numr-1)].id 29 | else: 30 | r1 = 'r_product' 31 | 32 | self.layout = QVBoxLayout() 33 | l = QLabel("Maximize (or minimize) a linear flux expression with reaction identifiers and \n"+ \ 34 | "(optionally) coefficients. Keep in mind that exchange reactions are often defined \n"+\ 35 | "in the direction of export. Consider changing coefficient signs.") 36 | self.layout.addWidget(l) 37 | editor_layout = QHBoxLayout() 38 | self.sense_combo = QComboBox() 39 | self.sense_combo.insertItems(0,['maximize', 'minimize']) 40 | self.sense_combo.setMinimumWidth(120) 41 | editor_layout.addWidget(self.sense_combo) 42 | open_bracket = QLabel(' ') # QLabel('(') 43 | font = open_bracket.font() 44 | font.setPointSize(30) 45 | open_bracket.setFont(font) 46 | editor_layout.addWidget(open_bracket) 47 | flux_expr_layout = QVBoxLayout() 48 | self.expr = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, check=True) 49 | self.expr.setPlaceholderText('flux expression (e.g. 1.0 '+r1+')') 50 | flux_expr_layout.addWidget(self.expr) 51 | editor_layout.addItem(flux_expr_layout) 52 | # close_bracket = QLabel(')') 53 | # font = close_bracket.font() 54 | # font.setPointSize(30) 55 | # close_bracket.setFont(font) 56 | # editor_layout.addWidget(close_bracket) 57 | self.layout.addItem(editor_layout) 58 | 59 | l3 = QHBoxLayout() 60 | self.button = QPushButton("Compute") 61 | self.cancel = QPushButton("Close") 62 | l3.addWidget(self.button) 63 | l3.addWidget(self.cancel) 64 | self.layout.addItem(l3) 65 | self.setLayout(self.layout) 66 | 67 | # Connecting the signal 68 | self.expr.textCorrect.connect(self.validate_dialog) 69 | self.cancel.clicked.connect(self.reject) 70 | self.button.clicked.connect(self.compute) 71 | 72 | self.validate_dialog() 73 | 74 | @Slot(bool) 75 | def validate_dialog(self,b=True): 76 | if self.expr.is_valid: 77 | self.button.setEnabled(True) 78 | else: 79 | self.button.setEnabled(False) 80 | 81 | def compute(self): 82 | self.setCursor(Qt.BusyCursor) 83 | if self.sense_combo.currentText() == 'maximize': 84 | sense = 'Maximum' 85 | else: 86 | sense = 'Minimum' 87 | with self.appdata.project.cobra_py_model as model: 88 | self.appdata.project.load_scenario_into_model(model) 89 | solver = re.search('('+'|'.join(avail_solvers)+')',model.solver.interface.__name__) 90 | if solver is not None: 91 | solver = solver[0] 92 | sol = fba(model, 93 | obj=self.expr.text(), 94 | obj_sense=self.sense_combo.currentText()) 95 | if sol.status == UNBOUNDED and isinf(sol.objective_value): 96 | self.set_boxes(sol) 97 | QMessageBox.warning(self, sense+' unbounded. ', 98 | 'Flux expression "'+linexprdict2str(linexpr2dict(self.expr.text(),self.reac_ids))+\ 99 | '" is unbounded. \nParts of the shown example flux distribution can be scaled indefinitely.',) 100 | elif sol.status == OPTIMAL: 101 | self.set_boxes(sol) 102 | QMessageBox.information(self, 'Solution', 103 | 'Optimum ('+linexprdict2str(linexpr2dict(self.expr.text(),self.reac_ids))+\ 104 | '): '+str(round(sol.objective_value,9)) + \ 105 | '\nShowing optimal example flux distribution.') 106 | else: 107 | QMessageBox.warning(self, 'Problem infeasible.', 108 | 'The scenario seems to be infeasible.',) 109 | return 110 | self.setCursor(Qt.ArrowCursor) 111 | self.accept() 112 | 113 | def set_boxes(self,sol): 114 | # write results into comp_values 115 | idx = 0 116 | for r in self.reac_ids: 117 | self.appdata.project.comp_values[r] = ( 118 | float(sol.fluxes[r]), float(sol.fluxes[r])) 119 | idx = idx+1 120 | self.appdata.project.comp_values_type = 0 121 | self.central_widget.update() 122 | -------------------------------------------------------------------------------- /cnapy/gui_elements/config_cobrapy_dialog.py: -------------------------------------------------------------------------------- 1 | """The COBRApy configuration dialog""" 2 | import configparser 3 | import os 4 | import appdirs 5 | 6 | import cobra 7 | from cobra.util.solver import interface_to_str, solvers 8 | from multiprocessing import cpu_count 9 | 10 | from qtpy.QtCore import Signal 11 | from qtpy.QtGui import QDoubleValidator, QIntValidator 12 | from qtpy.QtWidgets import (QMessageBox, QComboBox, QDialog, 13 | QHBoxLayout, QLabel, QLineEdit, QPushButton, 14 | QVBoxLayout) 15 | from cnapy.appdata import AppData 16 | 17 | 18 | class ConfigCobrapyDialog(QDialog): 19 | """A dialog to set values in cobrapy-config.txt""" 20 | 21 | def __init__(self, appdata: AppData): 22 | QDialog.__init__(self) 23 | self.setWindowTitle("Configure COBRApy") 24 | 25 | self.appdata = appdata 26 | self.layout = QVBoxLayout() 27 | 28 | # allow MILP solvers only? 29 | # SCIPY currently not even usable for FBA 30 | avail_solvers = list(set(solvers.keys()) - {'scipy'}) 31 | 32 | h2 = QHBoxLayout() 33 | label = QLabel("Default solver:\n(set when loading a model)") 34 | h2.addWidget(label) 35 | self.default_solver = QComboBox() 36 | self.default_solver.addItems(avail_solvers) 37 | self.default_solver.setCurrentIndex(avail_solvers.index( 38 | interface_to_str(cobra.Configuration().solver))) 39 | h2.addWidget(self.default_solver) 40 | self.layout.addItem(h2) 41 | 42 | h9 = QHBoxLayout() 43 | label = QLabel("Solver for current model:") 44 | h9.addWidget(label) 45 | self.current_solver = QComboBox() 46 | self.current_solver.addItems(avail_solvers) 47 | self.current_solver.setCurrentIndex(avail_solvers.index( 48 | interface_to_str(appdata.project.cobra_py_model.problem))) 49 | h9.addWidget(self.current_solver) 50 | self.layout.addItem(h9) 51 | 52 | h7 = QHBoxLayout() 53 | label = QLabel( 54 | "Number of processes for multiprocessing (e.g. FVA):") 55 | h7.addWidget(label) 56 | self.num_processes = QLineEdit() 57 | self.num_processes.setFixedWidth(100) 58 | self.num_processes.setText(str(cobra.Configuration().processes)) 59 | validator = QIntValidator(1, cpu_count(), self) 60 | self.num_processes.setValidator(validator) 61 | h7.addWidget(self.num_processes) 62 | self.layout.addItem(h7) 63 | 64 | h8 = QHBoxLayout() 65 | label = QLabel( 66 | "Default tolerance:\n(set when loading a model)") 67 | h8.addWidget(label) 68 | self.default_tolerance = QLineEdit() 69 | self.default_tolerance.setFixedWidth(100) 70 | self.default_tolerance.setText(str(cobra.Configuration().tolerance)) 71 | validator = QDoubleValidator(self) 72 | validator.setBottom(1e-9) # probably a reasonable consensus value 73 | self.default_tolerance.setValidator(validator) 74 | h8.addWidget(self.default_tolerance) 75 | self.layout.addItem(h8) 76 | 77 | h10 = QHBoxLayout() 78 | label = QLabel( 79 | "Tolerance for current model:") 80 | h10.addWidget(label) 81 | self.current_tolerance = QLineEdit() 82 | self.current_tolerance.setFixedWidth(100) 83 | self.current_tolerance.setText( 84 | str(self.appdata.project.cobra_py_model.tolerance)) 85 | validator = QDoubleValidator(self) 86 | validator.setBottom(0) 87 | self.current_tolerance.setValidator(validator) 88 | h10.addWidget(self.current_tolerance) 89 | self.layout.addItem(h10) 90 | 91 | l2 = QHBoxLayout() 92 | self.button = QPushButton("Apply Changes") 93 | self.cancel = QPushButton("Close") 94 | l2.addWidget(self.button) 95 | l2.addWidget(self.cancel) 96 | self.layout.addItem(l2) 97 | self.setLayout(self.layout) 98 | 99 | self.cancel.clicked.connect(self.reject) 100 | self.button.clicked.connect(self.apply) 101 | 102 | def apply(self): 103 | cobra.Configuration().solver = self.default_solver.currentText() 104 | cobra.Configuration().processes = int(self.num_processes.text()) 105 | try: 106 | val = float(self.default_tolerance.text()) 107 | if 1e-9 <= val <= 0.1: 108 | cobra.Configuration().tolerance = val 109 | else: 110 | raise ValueError 111 | except: 112 | QMessageBox.critical(self, "Cannot set default tolerance", 113 | "Choose a value between 0.1 and 1e-9 as default tolerance.") 114 | return 115 | try: 116 | self.appdata.project.cobra_py_model.solver = self.current_solver.currentText() 117 | self.appdata.project.cobra_py_model.tolerance = float( 118 | self.current_tolerance.text()) 119 | self.optlang_solver_set.emit() 120 | except Exception as e: 121 | QMessageBox.critical( 122 | self, "Cannot set current solver/tolerance", str(e)) 123 | return 124 | 125 | parser = configparser.ConfigParser() 126 | parser.add_section('cobrapy-config') 127 | parser.set('cobrapy-config', 'solver', 128 | interface_to_str(cobra.Configuration().solver)) 129 | parser.set('cobrapy-config', 'processes', 130 | str(cobra.Configuration().processes)) 131 | parser.set('cobrapy-config', 'tolerance', 132 | str(cobra.Configuration().tolerance)) 133 | 134 | try: 135 | fp = open(self.appdata.cobrapy_conf_path, "w") 136 | except FileNotFoundError: 137 | os.makedirs(appdirs.user_config_dir( 138 | "cnapy", roaming=True, appauthor=False)) 139 | fp = open(self.appdata.cobrapy_conf_path, "w") 140 | 141 | parser.write(fp) 142 | fp.close() 143 | 144 | self.accept() 145 | 146 | optlang_solver_set = Signal() 147 | -------------------------------------------------------------------------------- /cnapy/data/heat.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 37 | 39 | 41 | 45 | 49 | 50 | 52 | 56 | 60 | 64 | 65 | 73 | 83 | 84 | 86 | 87 | 89 | image/svg+xml 90 | 92 | 93 | 94 | 95 | 96 | 99 | 102 | 106 | 111 | 112 | 116 | 118 | 120 | 122 | 126 | 130 | 134 | 138 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /cnapy/data/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /cnapy/gui_elements/yield_optimization_dialog.py: -------------------------------------------------------------------------------- 1 | """The cnapy yield optimization dialog""" 2 | from random import randint 3 | from numpy import isnan, isinf 4 | import re 5 | from qtpy.QtCore import Qt, Signal, Slot 6 | from qtpy.QtWidgets import (QDialog, QHBoxLayout, QLabel, QComboBox, 7 | QMessageBox, QPushButton, QVBoxLayout, QFrame) 8 | 9 | from cnapy.appdata import AppData 10 | from cnapy.gui_elements.central_widget import CentralWidget 11 | from cnapy.utils import QComplReceivLineEdit, QHSeperationLine 12 | from straindesign import yopt, linexpr2dict, linexprdict2str, avail_solvers 13 | from straindesign.names import * 14 | 15 | class YieldOptimizationDialog(QDialog): 16 | """A dialog to perform yield optimization""" 17 | 18 | def __init__(self, appdata: AppData, central_widget: CentralWidget): 19 | QDialog.__init__(self) 20 | self.setWindowTitle("Yield optimization") 21 | 22 | self.appdata = appdata 23 | self.central_widget = central_widget 24 | 25 | numr = len(self.appdata.project.cobra_py_model.reactions) 26 | self.reac_ids = self.appdata.project.reaction_ids.id_list 27 | if numr > 2: 28 | r1 = self.appdata.project.cobra_py_model.reactions[randint(0,numr-1)].id 29 | r2 = self.appdata.project.cobra_py_model.reactions[randint(0,numr-1)].id 30 | else: 31 | r1 = 'r_product' 32 | r2 = 'r_substrate' 33 | 34 | self.layout = QVBoxLayout() 35 | l = QLabel("Maximize (or minimize) a yield function. \n"+ \ 36 | "Numerator and denominator are specified as linear expressions \n"+ \ 37 | "with reaction identifiers and (optionally) coefficients.\n"+\ 38 | "Keep in mind that exchange reactions are often defined in the direction of export.\n"+ 39 | "Consider changing signs.") 40 | self.layout.addWidget(l) 41 | editor_layout = QHBoxLayout() 42 | self.sense_combo = QComboBox() 43 | self.sense_combo.insertItems(0,['maximize', 'minimize']) 44 | self.sense_combo.setMinimumWidth(120) 45 | editor_layout.addWidget(self.sense_combo) 46 | open_bracket = QLabel(' ') # QLabel('(') 47 | font = open_bracket.font() 48 | font.setPointSize(30) 49 | open_bracket.setFont(font) 50 | editor_layout.addWidget(open_bracket) 51 | num_den_layout = QVBoxLayout() 52 | self.numerator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, self.appdata.is_in_dark_mode, check=True) 53 | self.numerator.setPlaceholderText('numerator (e.g. 1.0 '+r1+')') 54 | self.denominator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, self.appdata.is_in_dark_mode, check=True) 55 | self.denominator.setPlaceholderText('denominator (e.g. 1.0 '+r2+')') 56 | num_den_layout.addWidget(self.numerator) 57 | sep = QHSeperationLine() 58 | sep.setFrameShadow(QFrame.Plain) 59 | sep.setLineWidth(2) 60 | num_den_layout.addWidget(sep) 61 | num_den_layout.addWidget(self.denominator) 62 | editor_layout.addItem(num_den_layout) 63 | self.layout.addItem(editor_layout) 64 | 65 | l3 = QHBoxLayout() 66 | self.button = QPushButton("Compute") 67 | self.cancel = QPushButton("Close") 68 | l3.addWidget(self.button) 69 | l3.addWidget(self.cancel) 70 | self.layout.addItem(l3) 71 | self.setLayout(self.layout) 72 | 73 | # Connecting the signal 74 | self.numerator.textCorrect.connect(self.validate_dialog) 75 | self.denominator.textCorrect.connect(self.validate_dialog) 76 | self.cancel.clicked.connect(self.reject) 77 | self.button.clicked.connect(self.compute) 78 | 79 | self.validate_dialog() 80 | 81 | @Slot(bool) 82 | def validate_dialog(self,b=True): 83 | if self.numerator.is_valid and self.denominator.is_valid: 84 | self.button.setEnabled(True) 85 | else: 86 | self.button.setEnabled(False) 87 | 88 | def compute(self): 89 | self.setCursor(Qt.BusyCursor) 90 | if self.sense_combo.currentText() == 'maximize': 91 | sense = 'Maximum' 92 | else: 93 | sense = 'Minimum' 94 | with self.appdata.project.cobra_py_model as model: 95 | self.appdata.project.load_scenario_into_model(model) 96 | solver = re.search('('+'|'.join(avail_solvers)+')',model.solver.interface.__name__) 97 | if solver is not None: 98 | solver = solver[0] 99 | sol = yopt(model, 100 | obj_num=self.numerator.text(), 101 | obj_den=self.denominator.text(), 102 | obj_sense=self.sense_combo.currentText(), 103 | solver=solver) 104 | if sol.status == UNBOUNDED and isinf(sol.objective_value): 105 | self.set_boxes(sol) 106 | QMessageBox.warning(self, sense+' yield is unbounded. ', 107 | 'Yield unbounded. \n'+\ 108 | 'Parts of the shown example flux distribution can be scaled indefinitely. The numerator "'+\ 109 | linexprdict2str(linexpr2dict(self.numerator.text(),self.reac_ids))+'" is unbounded.',) 110 | elif sol.status == UNBOUNDED and isnan(sol.objective_value): 111 | self.set_boxes(sol) 112 | QMessageBox.warning(self, sense+' yield is undefined. ', 113 | 'Yield undefined. \n'+\ 114 | 'The denominator "'+\ 115 | linexprdict2str(linexpr2dict(self.denominator.text(),self.reac_ids))+\ 116 | '" can take the value 0, as shown in the example flux distibution.',) 117 | elif sol.status == OPTIMAL: 118 | self.set_boxes(sol) 119 | if sol.scalable: 120 | txt_scalable = '\nThe shown example flux distribution can be scaled indefinitely.' 121 | else: 122 | txt_scalable = '' 123 | QMessageBox.information(self, 'Solution', 124 | 'Maximum yield ('+linexprdict2str(linexpr2dict(self.numerator.text(),self.reac_ids))+\ 125 | ') / ('+linexprdict2str(linexpr2dict(self.denominator.text(),self.reac_ids))+\ 126 | '): '+str(round(sol.objective_value,9)) + \ 127 | '\nShowing yield-optimal example flux distribution.' + txt_scalable) 128 | else: 129 | QMessageBox.warning(self, 'Problem infeasible.', 130 | 'The scenario seems to be infeasible.',) 131 | return 132 | self.setCursor(Qt.ArrowCursor) 133 | self.accept() 134 | 135 | def set_boxes(self,sol): 136 | # write results into comp_values 137 | idx = 0 138 | for r in self.reac_ids: 139 | self.appdata.project.comp_values[r] = ( 140 | float(sol.fluxes[r]), float(sol.fluxes[r])) 141 | idx = idx+1 142 | self.appdata.project.comp_values_type = 0 143 | self.central_widget.update() -------------------------------------------------------------------------------- /cnapy/data/default-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 53 | 54 | 55 | palette 56 | paint 57 | color 58 | 59 | 60 | 61 | 62 | Benjamin Pavie 63 | 64 | 65 | 66 | 68 | 70 | 72 | 74 | 75 | 76 | 77 | 82 | 91 | 94 | 98 | 108 | 118 | 128 | 138 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /cnapy/gui_elements/configuration_gurobi.py: -------------------------------------------------------------------------------- 1 | """The Gurobi configuration dialog""" 2 | import os 3 | import subprocess 4 | import sys 5 | import platform 6 | 7 | from qtpy.QtWidgets import (QDialog, QFileDialog, 8 | QLabel, QMessageBox, QPushButton, 9 | QVBoxLayout) 10 | from cnapy.appdata import AppData 11 | 12 | 13 | class GurobiConfigurationDialog(QDialog): 14 | """A dialog to set values in cnapy-config.txt""" 15 | 16 | def __init__(self, appdata: AppData): 17 | QDialog.__init__(self) 18 | self.setWindowTitle("Configure Gurobi Full Version") 19 | self.appdata = appdata 20 | 21 | self.layout = QVBoxLayout() 22 | 23 | label = QLabel( 24 | "By default, right after CNApy's installation, you have only access to the Gurobi Community Edition\n" 25 | "which can only handle up to 1000 variables simultaneously.\n" 26 | "In order to use the full version of Gurobi, with no variable number limit, follow the next steps in the given order:\n" 27 | "1. (only if not already done and only necessary if you encounter problems with the following steps despite the installation tips in step 3)\n" 28 | "Restart CNApy with administrator privileges as follows:\n" 29 | " i) Close this session of CNApy\n" 30 | " ii) Find out your operating system by looking at the next line:\n" 31 | f" {platform.system()}\n" 32 | " iii) Depending on your operating system:\n" 33 | " >Only if you use Windows: If you used CNApy's bat installer: Right click on RUN_CNApy.bat or the CNApy desktop icon\n" 34 | " or the CNApy entry in the start menu's program list and select 'Run as adminstrator'.\n" 35 | " If you didn't use CNApy's .bat installer but Python or conda/mamba, start your Windows console or Powershell with administrator rights\n" 36 | " and startup CNApy." 37 | " >Only if you use Linux or MacOS (MacOS may be called Darwin): The most common way is by using the 'sudo' command. If you used the\n" 38 | " CNApy sh installer, you can start CNApy with administrator rights through 'sudo run_cnapy.sh'. If you didn't use CNApy's .bat installer\n" 39 | " but Python or conda/mamba, run your usual CNApy command with 'sudo' in front of it.\n" 40 | " NOTE: It may be possible that you're not allowed to get administrator rights on your computer. If this is the case, contact your system's administrator to resolve the problem.\n" 41 | "2. (if not already done) Obtain an Gurobi license and download Gurobi itself onto your computer.\n" 42 | " NOTE: CNApy only works with recent Gurobi versions (not older than version 20.1.0)!\n" 43 | "3. (if not already done) Install Gurobi by following the Gurobi installer's instructions. Remember where you installed Gurobi.\n" 44 | " If given and possible, try to install gurobi only for the 'local user' (or similar). By doing this, you might avoid the need for\n" 45 | " administrator rights.\n" 46 | "4. Select the folder in which you've installed Gurobi by pressing the following button:" 47 | ) 48 | self.layout.addWidget(label) 49 | 50 | self.gurobi_directory = QPushButton() 51 | self.gurobi_directory.setText( 52 | "NOT SET YET! PLEASE SET THE PATH TO THE GUROBI MAIN FOLDER (see steps 1 to 3 above)." 53 | ) 54 | self.layout.addWidget(self.gurobi_directory) 55 | 56 | label = QLabel( 57 | "5. (only if step 3 was successful) Run the Gurobi Python connection script by pressing the following button:" 58 | ) 59 | self.python_run_button = QPushButton("Run Python connection script") 60 | self.layout.addWidget(label) 61 | self.layout.addWidget(self.python_run_button) 62 | 63 | label = QLabel( 64 | "6. After you've finished all previous steps 1 to 5, restart your computer and Gurobi should be correctly configured for CNApy!" 65 | ) 66 | self.layout.addWidget(label) 67 | 68 | self.close = QPushButton("Close") 69 | self.layout.addWidget(self.close) 70 | self.setLayout(self.layout) 71 | 72 | # Connect the signals 73 | self.gurobi_directory.clicked.connect(self.choose_gurobi_directory) 74 | self.python_run_button.clicked.connect( 75 | self.run_python_connection_script) 76 | self.close.clicked.connect(self.accept) 77 | 78 | self.has_set_existing_gurobi_directory = False 79 | 80 | def folder_error(self): 81 | QMessageBox.warning( 82 | self, 83 | "Folder Error", 84 | "ERROR: The folder you chose in step 3 does not seem to exist! " 85 | "Please choose an existing folder in which you have installed Gurobi (see steps 1-3).\n" 86 | ) 87 | 88 | def choose_gurobi_directory(self): 89 | dialog = QFileDialog(self) # , directory=self.gurobi_directory.text()) 90 | directory: str = dialog.getExistingDirectory() 91 | if (not directory) or (len(directory) == 0) or (not os.path.exists(directory)): 92 | self.folder_error() 93 | else: 94 | directory = directory.replace("\\", "/") 95 | if not directory.endswith("/"): 96 | directory += "/" 97 | 98 | self.gurobi_directory.setText(directory) 99 | self.has_set_existing_gurobi_directory = True 100 | 101 | def run_python_connection_script(self): 102 | if not self.has_set_existing_gurobi_directory: 103 | self.folder_error() 104 | else: 105 | try: 106 | QMessageBox.information( 107 | self, 108 | "Running", 109 | "The script is going to run as you press 'OK'.\nPlease wait for an error or success message which appears\nafter the script running has finished." 110 | ) 111 | python_exe_path = sys.executable 112 | python_dir = os.path.dirname(python_exe_path) 113 | python_exe_name = os.path.split(python_exe_path)[-1] 114 | command = f'cd "{python_dir}" && {python_exe_name} "{self.gurobi_directory.text()}setup.py" install' 115 | has_run_error = subprocess.check_call( 116 | command, 117 | shell=True 118 | ) # The " are introduces in order to handle paths with blank spaces 119 | except subprocess.CalledProcessError: 120 | has_run_error = True 121 | if has_run_error: 122 | QMessageBox.warning( 123 | self, 124 | "Run Error", 125 | "ERROR: Gurobi's setup.py run failed! " 126 | "Please check that you use a recent Gurobi version. CNApy isn't compatible with older Gurobi versions.\n" 127 | "Additionally, please check that you have followed the previous steps 1 to 3.\n" 128 | "If this error keeps going even though you've checked the previous error,\n" 129 | "try to run CNApy with administrator rights." 130 | ) 131 | else: 132 | QMessageBox.information( 133 | self, 134 | "Success", 135 | "Success in running the Python connection script! Now, you can proceed with the next steps." 136 | ) 137 | self.get_and_set_environmental_variable() 138 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # CNApy - An integrated environment for metabolic modeling 2 | 3 | [![Latest stable release](https://flat.badgen.net/github/release/cnapy-org/cnapy/stable)](https://github.com/cnapy-org/CNApy/releases/latest) 4 | [![Last commit](https://flat.badgen.net/github/last-commit/cnapy-org/cnapy)](https://github.com/cnapy-org/CNApy/commits/master) 5 | [![Open issues](https://flat.badgen.net/github/open-issues/cnapy-org/cnapy)](https://github.com/cnapy-org/CNApy/issues) 6 | [![Gitter chat](https://flat.badgen.net/gitter/members/cnapy-org/community)](https://gitter.im/cnapy-org/community) 7 | 8 | ![CNApy screenshot](https://raw.githubusercontent.com/cnapy-org/CNApy/master/screenshot.png) 9 | 10 | ## Introduction 11 | 12 | **If you have questions or suggestions regarding CNApy, you can use either of the [CNApy GitHub issues](https://github.com/cnapy-org/CNApy/issues), the [CNApy GitHub discussions](https://github.com/cnapy-org/CNApy/discussions) or the [CNApy Gitter chat room](https://gitter.im/cnapy-org/community).** 13 | 14 | CNApy is a Python-based graphical user interface for a) many common methods of Constraint-Based Reconstruction and Analysis (COBRA) with stoichiometric metabolic models, b) the visualization of COBRA calculation results and c) the creation and editing of metabolic models. 15 | 16 | Supported COBRA methods include Flux Balance Analysis (FBA), Flux Variability Analysis (FVA), Minimal Cut Sets (MCS), Elementary Flux Modes (EFM) and many more advanced strain design algorithms through its integration of the [StrainDesign](https://github.com/klamt-lab/straindesign) package. 17 | 18 | All calculation results can be visualized in CNApy's interactive metabolic maps, which can be directly edited by the user. [Escher maps](https://escher.github.io/#/) are also natively supported and can be created and edited inside CNApy. 19 | 20 | Aside of performing calculations on metabolic models, CNApy can also be used to create and/or edit metabolic models, including all important aspects of the model's reactions, metabolites and genes. CNApy supports the widely used [SBML standard format](https://sbml.org/) for model loading and export. 21 | 22 | **For more details on CNApy's many more features, see section [Documentation and Tutorials](#documentation-and-tutorials).** 23 | 24 | **For information about how to install CNApy, see section [Installation Options](#installation-options).** 25 | 26 | **For information about how to contribute to CNApy as a developer, see section [Contribute to the CNApy development](#contribute-to-the-cnapy-development).** 27 | 28 | **If you want to cite CNApy, see section [How to cite CNApy](#how-to-cite-cnapy).** 29 | 30 | *Associated project note*: If you want to use the well-known MATLAB-based *CellNetAnalyzer* (CNA), *which is not compatible with CNApy*, you can download it from [CNA's website](https://www2.mpi-magdeburg.mpg.de/projects/cna/cna.html). 31 | 32 | ## Documentation and Tutorials 33 | 34 | * The [CNApy guide](https://cnapy-org.github.io/CNApy-guide/) contains information for all major functions of CNApy. 35 | * Our [CNApy YouTube channel](https://www.youtube.com/channel/UCRIXSdzs5WnBE3_uukuNMlg) provides some videos of working with CNApy. 36 | * We also provide directly usable [CNApy example projects](https://github.com/cnapy-org/CNApy-projects/releases/latest) which include some of the most common *E. coli* models. These projects can also be downloaded within CNApy at its first start-up or via CNApy's File menu. 37 | 38 | ## Installation Options 39 | 40 | There are three ways to install CNApy: 41 | 42 | 1. As the easiest installation way which only works under Windows, you can use the .exe installer attached to the assets at the bottom of [CNApy's latest release](https://github.com/cnapy-org/CNApy/releases/latest). 43 | 2. Under any operating system, you can install CNApy as a conda package as described in section [Install CNApy as conda package](#install-cnapy-as-conda-package). 44 | 3. If you want to develop CNApy, follow the instruction for the successful cloning of CNApy in section [Setup the CNApy development environment](#setup-the-cnapy-development-environment). 45 | 46 | ## Contribute to the CNApy development 47 | 48 | Everyone is welcome to contribute to CNApy's development. [See our contribution file for more detailed instructions](https://github.com/cnapy-org/CNApy/blob/master/CONTRIBUTING.md). 49 | 50 | ## Install CNApy as conda package 51 | 52 | 1. We use conda as package manager to install CNApy, so that, if not already done yet, you have to install either the full-fledged [Anaconda](https://www.anaconda.com/) or the smaller [miniconda](https://docs.conda.io/en/latest/miniconda.html) conda installern on your system. 53 | 54 | 2. Add the additional channels used by CNApy to conda: 55 | 56 | ```sh 57 | conda config --add channels IBMDecisionOptimization 58 | conda config --add channels Gurobi 59 | ``` 60 | 61 | 3. Create a conda environment with all dependencies 62 | 63 | ```sh 64 | conda create -n cnapy-1.2.7 -c conda-forge -c cnapy cnapy=1.2.7 65 | ``` 66 | 67 | 4. Activate the cnapy conda environment 68 | 69 | ```sh 70 | conda activate cnapy-1.2.7 71 | ``` 72 | 73 | 5. Run CNApy within you activated conda environment 74 | 75 | ```sh 76 | cnapy 77 | ``` 78 | 79 | Furthermore, you can also perform the following optional steps: 80 | 81 | 6. (optional and only recommended if you have already installed CNApy by using conda) If you already have a cnapy environment, e.g., cnapy-1.X.X, you can delete it with the command 82 | 83 | ```sh 84 | # Here, the Xs stand for the last CNApy version you've installed by using conda 85 | conda env remove -n cnapy-1.X.X 86 | ``` 87 | 88 | 7. (optional, but recommended if you also use other Python distributions or Anaconda environments) In order to solve potential package version problems, set a systems variable called "PYTHONNOUSERSITE" to the value "True". 89 | 90 | Under Linux systems, you can do this with the following command: 91 | 92 | ```sh 93 | export PYTHONNOUSERSITE=True 94 | ``` 95 | 96 | Under Windows systems, you can do this by searching for your system's "environmental variables" and adding 97 | the variable PYTHONNOUSERSITE with the value True using Window's environmental variables setting window. 98 | 99 | ## Setup the CNApy development environment 100 | 101 | We use conda as package manager to install all dependencies. You can use [miniconda](https://docs.conda.io/en/latest/miniconda.html). 102 | If you have conda installed you can: 103 | 104 | 1. Create a conda development environment with all dependencies 105 | 106 | ```sh 107 | conda env create -n cnapy-dev -f environment.yml 108 | ``` 109 | 110 | 2. Activate the development environment 111 | 112 | ```sh 113 | conda activate cnapy-dev 114 | ``` 115 | 116 | 3. Checkout the latest cnapy development version using git 117 | 118 | ```sh 119 | git clone https://github.com/cnapy-org/CNApy.git 120 | ``` 121 | 122 | 4. Change into the source directory and run CNApy 123 | 124 | ```sh 125 | cd CNApy 126 | python cnapy.py 127 | ``` 128 | 129 | Any contribution intentionally submitted for inclusion in the work by you, shall be licensed under the terms of the Apache 2.0 license without any additional terms or conditions. 130 | 131 | ## How to cite CNApy 132 | 133 | If you use CNApy in your scientific work, please consider to cite CNApy's publication: 134 | 135 | Thiele et al. (2022). CNApy: a CellNetAnalyzer GUI in Python for analyzing and designing metabolic networks. 136 | *Bioinformatics* 38, 1467-1469, [doi.org/10.1093/bioinformatics/btab828](https://doi.org/10.1093/bioinformatics/btab828). 137 | -------------------------------------------------------------------------------- /cnapy/gui_elements/yield_space_dialog.py: -------------------------------------------------------------------------------- 1 | """The yield space plot dialog""" 2 | 3 | import matplotlib.pyplot as plt 4 | from random import randint 5 | import re 6 | from qtpy.QtCore import Qt, Signal 7 | from qtpy.QtWidgets import (QDialog, QHBoxLayout, QLabel, QGroupBox, 8 | QPushButton, QVBoxLayout, QFrame) 9 | import numpy 10 | from cnapy.utils import QComplReceivLineEdit, QHSeperationLine 11 | from straindesign import linexpr2dict, linexprdict2str, yopt, avail_solvers 12 | from straindesign.names import * 13 | 14 | class YieldSpaceDialog(QDialog): 15 | """A dialog to create yield space plots""" 16 | 17 | def __init__(self, appdata): 18 | QDialog.__init__(self) 19 | self.setWindowTitle("Yield space plotting") 20 | 21 | self.appdata = appdata 22 | 23 | numr = len(self.appdata.project.cobra_py_model.reactions) 24 | self.reac_ids = self.appdata.project.reaction_ids.id_list 25 | if numr > 2: 26 | r1 = self.appdata.project.cobra_py_model.reactions[randint(0,numr-1)].id 27 | r2 = self.appdata.project.cobra_py_model.reactions[randint(0,numr-1)].id 28 | r3 = self.appdata.project.cobra_py_model.reactions[randint(0,numr-1)].id 29 | r4 = self.appdata.project.cobra_py_model.reactions[randint(0,numr-1)].id 30 | else: 31 | r1 = 'r_product_x' 32 | r2 = 'r_substrate_x' 33 | r3 = 'r_product_y' 34 | r4 = 'r_substrate_y' 35 | 36 | self.layout = QVBoxLayout() 37 | text = QLabel('Specify the yield terms that should be used for the horizontal and yical axis.\n'+ 38 | 'Keep in mind that exchange reactions are often defined in the direction of export.\n'+ 39 | 'Consider changing signs.') 40 | self.layout.addWidget(text) 41 | 42 | editor_layout = QHBoxLayout() 43 | # Define for horizontal axis 44 | x_groupbox = QGroupBox('x-axis') 45 | x_num_den_layout = QVBoxLayout() 46 | self.x_numerator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, self.appdata.is_in_dark_mode, check=True) 47 | self.x_numerator.setPlaceholderText('numerator (e.g. 1.0 '+r1+')') 48 | self.x_denominator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, self.appdata.is_in_dark_mode, check=True) 49 | self.x_denominator.setPlaceholderText('denominator (e.g. 1.0 '+r2+')') 50 | x_num_den_layout.addWidget(self.x_numerator) 51 | sep = QHSeperationLine() 52 | sep.setFrameShadow(QFrame.Plain) 53 | sep.setLineWidth(3) 54 | x_num_den_layout.addWidget(sep) 55 | x_num_den_layout.addWidget(self.x_denominator) 56 | x_groupbox.setLayout(x_num_den_layout) 57 | editor_layout.addWidget(x_groupbox) 58 | # Define for vertical axis 59 | y_groupbox = QGroupBox('y-axis') 60 | y_num_den_layout = QVBoxLayout() 61 | self.y_numerator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, self.appdata.is_in_dark_mode, check=True) 62 | self.y_numerator.setPlaceholderText('numerator (e.g. '+r3+')') 63 | self.y_denominator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, self.appdata.is_in_dark_mode, check=True) 64 | self.y_denominator.setPlaceholderText('denominator (e.g. '+r4+')') 65 | y_num_den_layout.addWidget(self.y_numerator) 66 | sep = QHSeperationLine() 67 | sep.setFrameShadow(QFrame.Plain) 68 | sep.setLineWidth(3) 69 | y_num_den_layout.addWidget(sep) 70 | y_num_den_layout.addWidget(self.y_denominator) 71 | y_groupbox.setLayout(y_num_den_layout) 72 | editor_layout.addWidget(y_groupbox) 73 | self.layout.addItem(editor_layout) 74 | # buttons 75 | button_layout = QHBoxLayout() 76 | self.button = QPushButton("Plot") 77 | self.cancel = QPushButton("Close") 78 | button_layout.addWidget(self.button) 79 | button_layout.addWidget(self.cancel) 80 | self.layout.addItem(button_layout) 81 | self.setLayout(self.layout) 82 | 83 | # Connecting the signal 84 | self.cancel.clicked.connect(self.reject) 85 | self.button.clicked.connect(self.compute) 86 | 87 | def compute(self): 88 | self.setCursor(Qt.BusyCursor) 89 | with self.appdata.project.cobra_py_model as model: 90 | self.appdata.project.load_scenario_into_model(model) 91 | solver = re.search('('+'|'.join(avail_solvers)+')',model.solver.interface.__name__) 92 | if solver is not None: 93 | solver = solver[0] 94 | x_axis = '('+self.x_numerator.text()+') / ('+self.x_denominator.text()+')' 95 | y_axis = '('+self.y_numerator.text()+') / ('+self.y_denominator.text()+')' 96 | x_num = linexpr2dict(self.x_numerator.text(),self.reac_ids) 97 | x_den = linexpr2dict(self.x_denominator.text(),self.reac_ids) 98 | y_num = linexpr2dict(self.y_numerator.text(),self.reac_ids) 99 | y_den = linexpr2dict(self.y_denominator.text(),self.reac_ids) 100 | # get outmost points 101 | sol_hmin = yopt(model,obj_num=x_num,obj_den=x_den,solver=solver,obj_sense='minimize') 102 | sol_hmax = yopt(model,obj_num=x_num,obj_den=x_den,solver=solver,obj_sense='maximize') 103 | sol_vmin = yopt(model,obj_num=y_num,obj_den=y_den,solver=solver,obj_sense='minimize') 104 | sol_vmax = yopt(model,obj_num=y_num,obj_den=y_den,solver=solver,obj_sense='maximize') 105 | hmin = min((0,sol_hmin.objective_value)) 106 | hmax = max((0,sol_hmax.objective_value)) 107 | vmin = min((0,sol_vmin.objective_value)) 108 | vmax = max((0,sol_vmax.objective_value)) 109 | # abort if any of the yields are unbounded or undefined 110 | unbnd = [i+1 for i,v in enumerate([sol_hmin,sol_hmax,sol_vmin,sol_vmax]) if v.status == UNBOUNDED] 111 | if any(unbnd): 112 | raise Exception('One of the specified yields is unbounded or undefined. Yield space cannot be generated.') 113 | # compute points 114 | points = 50 115 | vals = numpy.zeros((points, 3)) 116 | vals[:, 0] = numpy.linspace(sol_hmin.objective_value, sol_hmax.objective_value, num=points) 117 | var = numpy.linspace(sol_hmin.objective_value, sol_hmax.objective_value, num=points) 118 | lb = numpy.full(points, numpy.nan) 119 | ub = numpy.full(points, numpy.nan) 120 | for i in range(points): 121 | constr = [{**x_num, **{k:-v*vals[i, 0] for k,v in x_den.items()}},'=',0] 122 | sol_vmin = yopt(model,constraints=constr,obj_num=y_num,obj_den=y_den,solver=solver,obj_sense='minimize') 123 | lb[i] = sol_vmin.objective_value 124 | sol_vmax = yopt(model,constraints=constr,obj_num=y_num,obj_den=y_den,solver=solver,obj_sense='maximize') 125 | ub[i] = sol_vmax.objective_value 126 | 127 | _fig, axes = plt.subplots() 128 | axes.set_xlabel(x_axis) 129 | axes.set_ylabel(y_axis) 130 | axes.set_xlim(hmin*1.05,hmax*1.05) 131 | axes.set_ylim(vmin*1.05,vmax*1.05) 132 | x = [v for v in var] + [v for v in reversed(var)] 133 | y = [v for v in lb] + [v for v in reversed(ub)] 134 | if lb[0] != ub[0]: 135 | x.extend([var[0], var[0]]) 136 | y.extend([lb[0], ub[0]]) 137 | 138 | plt.fill(x, y) 139 | plt.show() 140 | 141 | self.appdata.window.centralWidget().show_bottom_of_console() 142 | 143 | self.setCursor(Qt.ArrowCursor) 144 | -------------------------------------------------------------------------------- /cnapy/gui_elements/efm_dialog.py: -------------------------------------------------------------------------------- 1 | """The cnapy elementary flux modes calculator dialog""" 2 | import io 3 | 4 | from qtpy.QtCore import Qt 5 | from qtpy.QtGui import QIntValidator 6 | from qtpy.QtWidgets import (QCheckBox, QDialog, QGroupBox, QHBoxLayout, QLabel, 7 | QLineEdit, QMessageBox, QPushButton, QVBoxLayout) 8 | 9 | from cnapy.appdata import AppData 10 | from cnapy.flux_vector_container import FluxVectorContainer 11 | 12 | 13 | class EFMDialog(QDialog): 14 | """A dialog to set up EFM calculation""" 15 | 16 | def __init__(self, appdata: AppData, central_widget): 17 | QDialog.__init__(self) 18 | self.setWindowTitle("Elementary Flux Mode Computation") 19 | 20 | self.appdata = appdata 21 | self.central_widget = central_widget 22 | self.out = io.StringIO() 23 | self.err = io.StringIO() 24 | 25 | self.layout = QVBoxLayout() 26 | 27 | l1 = QHBoxLayout() 28 | self.constraints = QCheckBox("consider 0 in current scenario as off") 29 | self.constraints.setCheckState(Qt.Checked) 30 | l1.addWidget(self.constraints) 31 | self.layout.addItem(l1) 32 | 33 | l2 = QHBoxLayout() 34 | self.flux_bounds = QGroupBox( 35 | "use flux bounds to calculate elementary flux vectors") 36 | self.flux_bounds.setCheckable(True) 37 | self.flux_bounds.setChecked(False) 38 | 39 | vbox = QVBoxLayout() 40 | label = QLabel("Threshold for bounds to be unconstrained") 41 | vbox.addWidget(label) 42 | self.threshold = QLineEdit("100") 43 | validator = QIntValidator() 44 | validator.setBottom(0) 45 | self.threshold.setValidator(validator) 46 | vbox.addWidget(self.threshold) 47 | self.flux_bounds.setLayout(vbox) 48 | l2.addWidget(self.flux_bounds) 49 | self.layout.addItem(l2) 50 | 51 | l3 = QHBoxLayout() 52 | self.check_reversibility = QCheckBox( 53 | "check reversibility") 54 | self.check_reversibility.setCheckState(Qt.Checked) 55 | l3.addWidget(self.check_reversibility) 56 | self.layout.addItem(l3) 57 | 58 | l4 = QHBoxLayout() 59 | self.convex_basis = QCheckBox( 60 | "only convex basis") 61 | l4.addWidget(self.convex_basis) 62 | self.layout.addItem(l4) 63 | 64 | l5 = QHBoxLayout() 65 | self.isozymes = QCheckBox( 66 | "consider isozymes only once") 67 | l5.addWidget(self.isozymes) 68 | self.layout.addItem(l5) 69 | 70 | # TODO: choose solver 71 | 72 | l7 = QHBoxLayout() 73 | self.rational_numbers = QCheckBox( 74 | "use rational numbers") 75 | l7.addWidget(self.rational_numbers) 76 | self.layout.addItem(l7) 77 | 78 | lx = QHBoxLayout() 79 | self.button = QPushButton("Compute") 80 | self.cancel = QPushButton("Close") 81 | lx.addWidget(self.button) 82 | lx.addWidget(self.cancel) 83 | self.layout.addItem(lx) 84 | 85 | self.setLayout(self.layout) 86 | 87 | # Connecting the signal 88 | self.cancel.clicked.connect(self.reject) 89 | self.button.clicked.connect(self.compute) 90 | 91 | def compute(self): 92 | 93 | # create CobraModel for matlab 94 | self.appdata.create_cobra_model() 95 | 96 | # get some data 97 | reac_id = self.eng.get_reacID() 98 | 99 | # setting parameters 100 | a = self.eng.eval("constraints = {};", 101 | nargout=0, stdout=self.out, stderr=self.err) 102 | scenario = {} 103 | if self.constraints.checkState() == Qt.Checked or self.flux_bounds.isChecked(): 104 | onoff_str = "" 105 | for r in reac_id: 106 | if r in self.appdata.project.scen_values.keys(): 107 | (vl, vu) = self.appdata.project.scen_values[r] 108 | if vl == vu: 109 | if vl > 0: 110 | onoff_str = onoff_str+" NaN" # efmtool does not support 1 111 | elif vl == 0: 112 | scenario[r] = (0, 0) 113 | onoff_str = onoff_str+" 0" 114 | else: 115 | onoff_str = onoff_str+" NaN" 116 | print("WARN: negative value in scenario") 117 | else: 118 | onoff_str = onoff_str+" NaN" 119 | print("WARN: not fixed value in scenario") 120 | else: 121 | onoff_str = onoff_str+" NaN" 122 | 123 | onoff_str = "reaconoff = ["+onoff_str+"];" 124 | a = self.eng.eval(onoff_str, 125 | nargout=0, stdout=self.out, stderr=self.err) 126 | 127 | a = self.eng.eval("constraints.reaconoff = reaconoff;", 128 | nargout=0, stdout=self.out, stderr=self.err) 129 | 130 | if self.flux_bounds.isChecked(): 131 | threshold = float(self.threshold.text()) 132 | lb_str = "" 133 | ub_str = "" 134 | for r in reac_id: 135 | c_reaction = self.appdata.project.cobra_py_model.reactions.get_by_id( 136 | r) 137 | if r in self.appdata.project.scen_values: 138 | (vl, vu) = self.appdata.project.scen_values[r] 139 | else: 140 | vl = c_reaction.lower_bound 141 | vu = c_reaction.upper_bound 142 | if vl <= -threshold: 143 | vl = "NaN" 144 | if vu >= threshold: 145 | vu = "NaN" 146 | if vl == 0 and vu == 0: # already in reaconoff, can be skipped here 147 | vl = "NaN" 148 | vu = "NaN" 149 | lb_str = lb_str+" "+str(vl) 150 | ub_str = ub_str+" "+str(vu) 151 | 152 | lb_str = "lb = ["+lb_str+"];" 153 | a = self.eng.eval(lb_str, nargout=0, 154 | stdout=self.out, stderr=self.err) 155 | a = self.eng.eval("constraints.lb = lb;", nargout=0, 156 | stdout=self.out, stderr=self.err) 157 | 158 | ub_str = "ub = ["+ub_str+"];" 159 | a = self.eng.eval(ub_str, nargout=0, 160 | stdout=self.out, stderr=self.err) 161 | a = self.eng.eval("constraints.ub = ub;", nargout=0, 162 | stdout=self.out, stderr=self.err) 163 | 164 | # TODO set solver 4 = EFMTool 3 = MetaTool, 1 = cna Mex file, 0 = cna functions 165 | a = self.eng.eval("solver = 4;", nargout=0, 166 | stdout=self.out, stderr=self.err) 167 | 168 | if self.check_reversibility.checkState() == Qt.Checked: 169 | a = self.eng.eval("irrev_flag = 1;", nargout=0, 170 | stdout=self.out, stderr=self.err) 171 | else: 172 | a = self.eng.eval("irrev_flag = 0;", 173 | nargout=0, stdout=self.out, stderr=self.err) 174 | 175 | # convex basis computation is only possible with METATOOL solver=3 176 | if self.convex_basis.checkState() == Qt.Checked: 177 | a = self.eng.eval("conv_basis_flag = 1; solver = 3;", 178 | nargout=0, stdout=self.out, stderr=self.err) 179 | else: 180 | a = self.eng.eval("conv_basis_flag = 0;", 181 | nargout=0, stdout=self.out, stderr=self.err) 182 | 183 | if self.isozymes.checkState() == Qt.Checked: 184 | a = self.eng.eval("iso_flag = 1;", nargout=0, 185 | stdout=self.out, stderr=self.err) 186 | else: 187 | a = self.eng.eval("iso_flag = 0;", 188 | nargout=0, stdout=self.out, stderr=self.err) 189 | 190 | # default we have no macromolecules and display is et to ALL 191 | a = self.eng.eval("c_macro=[]; display= 'ALL';", 192 | nargout=0, stdout=self.out, stderr=self.err) 193 | 194 | if self.rational_numbers.checkState() == Qt.Checked: 195 | a = self.eng.eval("efmtool_options = {'arithmetic', 'fractional'};", 196 | nargout=0, stdout=self.out, stderr=self.err) 197 | else: 198 | a = self.eng.eval("efmtool_options = {};", 199 | nargout=0, stdout=self.out, stderr=self.err) 200 | 201 | 202 | def result2ui(self, ems, idx, reac_id, irreversible, unbounded, scenario): 203 | if len(ems) == 0: 204 | QMessageBox.information(self, 'No modes', 205 | 'Modes have not been calculated or do not exist.') 206 | else: 207 | self.appdata.project.modes = FluxVectorContainer( 208 | ems, [reac_id[int(i)-1] for i in idx[0]], irreversible, unbounded) 209 | self.central_widget.mode_navigator.current = 0 210 | self.central_widget.mode_navigator.scenario = scenario 211 | self.central_widget.mode_navigator.set_to_efm() 212 | self.central_widget.update_mode() 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CNApy: An integrated environment for metabolic modeling 2 | 3 | [![Latest stable release](https://flat.badgen.net/github/release/cnapy-org/cnapy/stable)](https://github.com/cnapy-org/CNApy/releases/latest) 4 | [![Last commit](https://flat.badgen.net/github/last-commit/cnapy-org/cnapy)](https://github.com/cnapy-org/CNApy/commits/master) 5 | [![Open issues](https://flat.badgen.net/github/open-issues/cnapy-org/cnapy)](https://github.com/cnapy-org/CNApy/issues) 6 | [![Gitter chat](https://flat.badgen.net/gitter/members/cnapy-org/community)](https://gitter.im/cnapy-org/community) 7 | 8 | ![CNApy screenshot](screenshot.png) 9 | 10 | ## Introduction 11 | 12 | CNApy [[Paper]](https://doi.org/10.1093/bioinformatics/btab828) is a Python-based graphical user interface for a) many common methods of Constraint-Based Reconstruction and Analysis (COBRA) with stoichiometric metabolic models, b) the visualization of COBRA calculation results as *interactive and editable* metabolic maps (including Escher maps [[GitHub]](https://escher.github.io/#/)[[Paper]]()) and c) the creation and editing of metabolic models, including its reactions, metabolites and genes. For model loading and export, CNApy supports the widely used SBML standard format [[Site]](https://sbml.org/)[[Paper]](https://www.embopress.org/doi/abs/10.15252/msb.20199110). 13 | 14 | Supported COBRA methods (partly provided by cobrapy [[GitHub]](https://github.com/opencobra/cobrapy)[[Paper]](https://doi.org/10.1186/1752-0509-7-74)) include: 15 | 16 | - Flux Balance Analysis (FBA) [[Review]](https://doi.org/10.1038/nbt.1614) 17 | - Flux Variability Analysis (FVA) [[Paper]](https://doi.org/10.1016/j.ymben.2003.09.002) 18 | - Yield optimization (based on linear-fractional programming) [[Paper]](https://doi.org/10.1016/j.ymben.2018.02.001) 19 | - Phase plane analyses (can include flux and/or yield optimizations) 20 | - Making measured *in vivo* flux scenarios stoichiometrically feasible, optionally also by altering a biomass reaction [[Paper]](https://academic.oup.com/bioinformatics/article/39/10/btad600/7284109) 21 | - Elementary Flux Modes (EFM) [[Review]](https://analyticalsciencejournals.onlinelibrary.wiley.com/doi/full/10.1002/biot.201200269) 22 | - Thermodynamic methods based on OptMDFpathway [[Paper]](https://doi.org/10.1371/journal.pcbi.1006492) 23 | - Many advanced strain design algorithms such as OptKnock [[Paper]](https://doi.org/10.1002/bit.10803), RobustKnock [[Paper]](https://doi.org/10.1093/bioinformatics/btp704), OptCouple [[Paper]](https://doi.org/10.1016/j.mec.2019.e00087) and advanced Minimal Cut Sets [[Paper]](https://doi.org/10.1371/journal.pcbi.1008110) through its StrainDesign [[GitHub]](https://github.com/klamt-lab/straindesign)[[Paper]](https://doi.org/10.1093/bioinformatics/btac632) integration 24 | 25 | **→ For information about how to install CNApy, see section [Installation Options](#installation-options)** 26 | 27 | **→ For more details on CNApy's many features, see section [Documentation and Tutorials](#documentation-and-tutorials)** 28 | 29 | **→ If you have questions, suggestions or bug reports regarding CNApy, you can use either of the [CNApy GitHub issues](https://github.com/cnapy-org/CNApy/issues), the [CNApy GitHub discussions](https://github.com/cnapy-org/CNApy/discussions) or the [CNApy Gitter chat room](https://gitter.im/cnapy-org/community)** 30 | 31 | **→ If you want to cite CNApy, see section [How to cite CNApy](#how-to-cite-cnapy)** 32 | 33 | **→ For information about how to contribute to CNApy as a developer, see section [Contribute to the CNApy development](#contribute-to-the-cnapy-development)** 34 | 35 | *Associated project note*: If you want to use the well-known MATLAB-based *CellNetAnalyzer* (CNA), *which is not compatible with CNApy*, you can download it from [CNA's website](https://www2.mpi-magdeburg.mpg.de/projects/cna/cna.html). 36 | 37 | ## Installation Options 38 | 39 | There are 4 alternative ways to install CNApy: 40 | 41 | 1. The easiest way for any user to install CNApy is by downloading its installer, which is provided for Windows, Linux and MacOS, see [Using CNApy installer](#using-cnapy-installer) for more. 42 | 2. If you already have installed Python 3.10 (no other version) on your system, you can install CNApy simply through ```pip install cnapy``` in your console. Afterwards, you can start CNApy's GUI by running either ```cnapy``` or, if this doesn't work, ```python -m cnapy``` where "python" must call your Python 3.10 installation. 43 | 3. If you already use conda or mamba (for mamba, just change the "conda" command to "mamba"), you can create a CNApy environment named ```cnapy-1.2.7``` as follows: 1) Run ```conda create --name cnapy-1.2.7 python=3.10 pip openjdk -c conda-forge```, 2) run ```conda activate cnapy-1.2.7```, 3) run ```pip install cnapy```. Then, you can start CNApy in the cnapy-1.2.7 conda environment by running either ```cnapy``` or, if this doesn't work, ```python -m cnapy```. Note that the [cnapy conda package](https://anaconda.org/cnapy/cnapy) is currently *not* being updated due to licensing uncertainties. 44 | 4. If you want to develop CNApy, follow the instruction for the cloning and setup of the CNApy repository using git and conda or mamba in section [Setup the CNApy development environment](#setup-the-cnapy-development-environment). 45 | 46 | ## Documentation and Tutorials 47 | 48 | - The [CNApy guide](https://cnapy-org.github.io/CNApy-guide/) contains information for all major functions of CNApy. 49 | - Our [CNApy YouTube channel](https://www.youtube.com/channel/UCRIXSdzs5WnBE3_uukuNMlg) provides some videos of working with CNApy. 50 | - We also provide directly usable [CNApy example projects](https://github.com/cnapy-org/CNApy-projects/releases/latest) which include some of the most common *E. coli* models. These projects can also be downloaded within CNApy at its first start-up or via CNApy's File menu. 51 | 52 | 53 | ## Using CNApy installer 54 | 55 | This installer lets you create a local installation of CNApy under Windows, Linux or MacOS by following these instructions: 56 | 57 | *If you use Windows:* 58 | 59 | - Download the Windows installer [from here](https://github.com/cnapy-org/CNApy/releases/download/v1.2.7/install_cnapy_here.bat) 60 | - Put this file into a folder where you want CNApy to be installed. 61 | - Double click on the file and let the CNApy installation run 62 | - Afterwards, you can run CNApy by either double-clicking on the newly created CNApy desktop icon, or by double-clicking "RUN_CNApy.bat" in the newly created cnapy-1.2.7 subfolder. 63 | 64 | *If you use Linux or MacOS*: 65 | 66 | - Download the Linux & MacOS installer [from here](https://github.com/cnapy-org/CNApy/releases/download/v1.2.7/install_cnapy_here.sh). 67 | - Put this file into a folder where you want CNApy to be installed. 68 | - Make the script executable by opening your console in the folder and run ```chmod u+x ./install_cnapy_here.sh```. Alternatively, if supported on your system, right-click on the file, go the file's settings and mark it as executable. 69 | - Now, either run ```./install_cnapy_here.sh``` in your console or, if supported on your system, double-click on install_cnapy_here.sh. 70 | - Finally, you can run CNApy by calling ```./run_cnapy.sh``` in your console (for this without another path beforehand, your console must point to the folder where run_cnapy.sh is located, e.g. if you are in the folder where install_cnapy_here.sh is located, through ```cd cnapy-1.2.7```). Alternatively, if supported by your system, double-click on "run_cnapy.sh" in the newly created cnapy-1.2.7 subfolder. 71 | 72 | Technical side note: CNApy's installer is utilizing [micromamba](https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html). 73 | 74 | ## Setup the CNApy development environment 75 | 76 | *Note:* The following instructions only have to be followed if you want to contribute to CNApy as a programmer. If this is not the case, follow other steps of the [Installation Options](#installation-options). 77 | 78 | Everyone is welcome to contribute to CNApy's development. [See our contribution file for general instructions](https://github.com/cnapy-org/CNApy/blob/master/CONTRIBUTING.md). Any contribution intentionally submitted for inclusion in the work by you, shall be licensed under the terms of the Apache 2.0 license without any additional terms or conditions. 79 | 80 | Programatically, we recommend to use uv [[GitHub]](https://github.com/astral-sh/uv) to install all dependencies and manage installed Python versions. Alternatively, one can also use conda/mamba for the same tasks, although you would have to install CNApy's dependencies manually. 81 | 82 | ### uv usage 83 | You can use uv for CNApy as follows: 84 | 85 | 1. Make sure that you have installed uv, e.g. through pip, pipx or another package manger (```apt```, ```brew```, ```nix``` ...) of your choice: 86 | 87 | ```sh 88 | # E.g., you can install uv through 89 | pip install uv # or 90 | pipx install uv 91 | ``` 92 | 93 | 2. Checkout the latest cnapy development version using git 94 | 95 | ```sh 96 | git clone https://github.com/cnapy-org/CNApy.git 97 | ``` 98 | 99 | 3. Change into the source directory and run CNApy 100 | 101 | ```sh 102 | cd CNApy 103 | uv run cnapy.py 104 | ``` 105 | 106 | uv will automatically install the correct Python version and CNApy dependencies (all done by reading CNApy's pyproject.toml file). If you get a Java/JDK/JVM/jpype error when running CNApy, consider installing OpenJDK [[Site]](https://openjdk.org/install/) on your system to fix this problem. 107 | 108 | ## How to cite CNApy 109 | 110 | If you use CNApy in your scientific work, please cite CNApy's publication: 111 | 112 | Thiele et al. (2022). CNApy: a CellNetAnalyzer GUI in Python for analyzing and designing metabolic networks. 113 | *Bioinformatics* 38, 1467-1469, [doi.org/10.1093/bioinformatics/btab828](https://doi.org/10.1093/bioinformatics/btab828). 114 | -------------------------------------------------------------------------------- /cnapy/gui_elements/configuration_cplex.py: -------------------------------------------------------------------------------- 1 | """The CPLEX configuration dialog""" 2 | import os 3 | import subprocess 4 | import sys 5 | import platform 6 | 7 | from qtpy.QtWidgets import (QDialog, QFileDialog, 8 | QLabel, QMessageBox, QPushButton, 9 | QVBoxLayout) 10 | from cnapy.appdata import AppData 11 | 12 | 13 | class CplexConfigurationDialog(QDialog): 14 | """A dialog to set values in cnapy-config.txt""" 15 | 16 | def __init__(self, appdata: AppData): 17 | QDialog.__init__(self) 18 | self.setWindowTitle("Configure IBM CPLEX Full Version") 19 | self.appdata = appdata 20 | 21 | self.layout = QVBoxLayout() 22 | 23 | label = QLabel( 24 | "By default, right after CNApy's installation, you have only access to the IBM CPLEX Community Edition\n" 25 | "which can only handle up to 1000 variables simultaneously.\n" 26 | "In order to use the full version of IBM CPLEX, with no variable number limit, follow the next steps in the given order:\n" 27 | "1. (only necessary if you encounter problems with the following steps despite the installation tips in step 3)\n" 28 | "Restart CNApy with administrator privileges as follows:\n" 29 | " i) Close this session of CNApy\n" 30 | " ii) Find out your operating system by looking at the next line:\n" 31 | f" {platform.system()}\n" 32 | " iii) Depending on your operating system:\n" 33 | " >Only if you use Windows: If you used CNApy's bat installer: Right click on RUN_CNApy.bat or the CNApy desktop icon\n" 34 | " or the CNApy entry in the start menu's program list and select 'Run as adminstrator'.\n" 35 | " If you didn't use CNApy's .bat installer but Python or conda/mamba, start your Windows console or Powershell with administrator rights\n" 36 | " and startup CNApy." 37 | " >Only if you use Linux or MacOS (MacOS may be called Darwin): The most common way is by using the 'sudo' command. If you used the\n" 38 | " CNApy sh installer, you can start CNApy with administrator rights through 'sudo run_cnapy.sh'. If you didn't use CNApy's .bat installer\n" 39 | " but Python or conda/mamba, run your usual CNApy command with 'sudo' in front of it.\n" 40 | " NOTE: It may be possible that you're not allowed to get administrator rights on your computer. If this is the case, contact your system's administrator to resolve the problem.\n" 41 | "2. (if not already done) Obtain an IBM CPLEX license and download IBM CPLEX itself onto your computer.\n" 42 | " NOTE: CNApy only works with recent IBM CPLEX versions (not older than version 20.1.0)!\n" 43 | "3. (if not already done) Install IBM CPLEX by following the IBM CPLEX installer's instructions. Remember where you installed IBM CPLEX.\n" 44 | " If given and possible, try to install CPLEX only for the 'local user' (or similar). By doing this, you might avoid the need for\n" 45 | " administrator rights.\n" 46 | "4. Select the folder in which you've installed IBM CPLEX by pressing the following button:" 47 | ) 48 | self.layout.addWidget(label) 49 | 50 | self.cplex_directory = QPushButton() 51 | self.cplex_directory.setText( 52 | "NOT SET YET! PLEASE SET THE PATH TO THE IBM CPLEX MAIN FOLDER (see steps 1 to 3 above)." 53 | ) 54 | self.layout.addWidget(self.cplex_directory) 55 | 56 | label = QLabel( 57 | "5. (only if step 3 was successful) Run the IBM CPLEX Python connection script by pressing the following button:" 58 | ) 59 | self.python_run_button = QPushButton("Run Python connection script") 60 | self.layout.addWidget(label) 61 | self.layout.addWidget(self.python_run_button) 62 | 63 | label = QLabel( 64 | "6. (only if step 4 was successful) Set an environmental variable called PYTHONPATH to the following folder..." 65 | ) 66 | self.layout.addWidget(label) 67 | 68 | self.environmental_variable_label = QLabel( 69 | "NOT SET YET! Please run the previous steps." 70 | ) 71 | self.layout.addWidget(self.environmental_variable_label) 72 | 73 | label = QLabel( 74 | "...In order the set this environmental variable, you have to look in the next line which operating system you use:\n" 75 | f"{platform.system()}\n" 76 | "Depending on what is said in the previous line, you have to do the following in order to set the environmental variable:\n" 77 | "> Only if you use Windows: Press Start (the Windows logo) in your task bar and open the settings (the wheel logo). In the settings menu, write 'variable' in the search bar.\n" 78 | "Select 'edit environmental variables for this account' (or similar) and, in the newly opened window, click the 'New' button. Write 'PYTHONPATH' as the\n" 79 | "variable's name and write, as a value, the path given above in this step 5. Then, click 'OK' and again 'OK'.\n" 80 | "> Only if you use Linux or MacOS (MacOS is also called Darwin): In your console, run 'export PYTHONPATH=PATH' (without the quotation marks) where PATH has to be the path\n" 81 | "given under this step 5 above. Alternatively, if this doesn't work, set the PYTHONPATH variable in the run_cnapy.sh in the quoted line and un-quote it." 82 | ) 83 | self.layout.addWidget(label) 84 | 85 | label = QLabel( 86 | "7. After you've finished all previous steps 1 to 5, restart your computer and IBM CPLEX should be correctly configured for CNApy!" 87 | ) 88 | self.layout.addWidget(label) 89 | 90 | self.close = QPushButton("Close") 91 | self.layout.addWidget(self.close) 92 | self.setLayout(self.layout) 93 | 94 | # Connect the signals 95 | self.cplex_directory.clicked.connect(self.choose_cplex_directory) 96 | self.python_run_button.clicked.connect( 97 | self.run_python_connection_script) 98 | self.close.clicked.connect(self.accept) 99 | 100 | self.has_set_existing_cplex_directory = False 101 | 102 | def folder_error(self): 103 | QMessageBox.warning( 104 | self, 105 | "Folder Error", 106 | "ERROR: The folder you chose in step 3 does not seem to exist! " 107 | "Please choose an existing folder in which you have installed IBM CPLEX (see steps 1-3).\n" 108 | ) 109 | 110 | def choose_cplex_directory(self): 111 | dialog = QFileDialog(self) # , directory=self.cplex_directory.text()) 112 | directory: str = dialog.getExistingDirectory() 113 | if (not directory) or (len(directory) == 0) or (not os.path.exists(directory)): 114 | self.folder_error() 115 | else: 116 | directory = directory.replace("\\", "/") 117 | if not directory.endswith("/"): 118 | directory += "/" 119 | 120 | self.cplex_directory.setText(directory) 121 | self.has_set_existing_cplex_directory = True 122 | 123 | def run_python_connection_script(self): 124 | if not self.has_set_existing_cplex_directory: 125 | self.folder_error() 126 | else: 127 | try: 128 | QMessageBox.information( 129 | self, 130 | "Running", 131 | "The script is going to run as you press 'OK'.\nPlease wait for an error or success message which appears\nafter the script running has finished." 132 | ) 133 | python_exe_path = sys.executable 134 | python_dir = os.path.dirname(python_exe_path) 135 | python_exe_name = os.path.split(python_exe_path)[-1] 136 | command = f'cd "{python_dir}" && {python_exe_name} "{self.cplex_directory.text()}python/setup.py" install' 137 | has_run_error = subprocess.check_call( 138 | command, 139 | shell=True 140 | ) # The " are introduces in order to handle paths with blank spaces 141 | except subprocess.CalledProcessError: 142 | has_run_error = True 143 | if has_run_error: 144 | QMessageBox.warning( 145 | self, 146 | "Run Error", 147 | "ERROR: IBM CPLEX's setup.py run failed! " 148 | "Please check that you use a recent IBM CPLEX version. CNApy isn't compatible with older IBM CPLEX versions.\n" 149 | "Additionally, please check that you have followed the previous steps 1 to 3.\n" 150 | "If this error keeps going even though you've checked the previous error,\n" 151 | "try to run CNApy with administrator rights." 152 | ) 153 | else: 154 | QMessageBox.information( 155 | self, 156 | "Success", 157 | "Success in running the Python connection script! Now, you can proceed with the next steps." 158 | ) 159 | self.get_and_set_environmental_variable() 160 | 161 | def get_and_set_environmental_variable(self): 162 | base_path = self.cplex_directory.text() + "cplex/python/3.10/" 163 | 164 | folders_list = [ 165 | base_path+folder for folder in os.listdir(base_path) if os.path.isdir(base_path+folder) 166 | ] 167 | 168 | if len(folders_list) > 1: 169 | folders_str = "You have to set the following multiple paths:\n" 170 | else: 171 | folders_str = "" 172 | folders_str += "\n".join(folders_list) 173 | 174 | if platform.system() == "Windows": 175 | folders_str = folders_str.replace("/", "\\") 176 | 177 | self.environmental_variable_label.setText(folders_str) 178 | -------------------------------------------------------------------------------- /cnapy/gui_elements/plot_space_dialog.py: -------------------------------------------------------------------------------- 1 | """The flux space plot dialog""" 2 | 3 | from random import randint 4 | from qtpy.QtCore import Qt 5 | from qtpy.QtWidgets import (QDialog, QHBoxLayout, QLabel, QMessageBox, QGroupBox, QComboBox, QLayout, 6 | QPushButton, QVBoxLayout, QFrame, QCheckBox,QLineEdit) 7 | from cnapy.utils import QComplReceivLineEdit, QHSeperationLine 8 | from straindesign import plot_flux_space 9 | from straindesign.names import * 10 | 11 | class PlotSpaceDialog(QDialog): 12 | """A dialog to create Flux space plots""" 13 | 14 | def __init__(self, appdata): 15 | QDialog.__init__(self) 16 | self.setWindowTitle("Plot phase plane/yield space") 17 | self.setMinimumWidth(500) 18 | 19 | self.appdata = appdata 20 | 21 | numr = len(self.appdata.project.cobra_py_model.reactions) 22 | self.r = ["" for _ in range(6)] 23 | if numr > 5: 24 | self.r = [self.appdata.project.cobra_py_model.reactions[randint(0,numr-1)].id for _ in self.r] 25 | else: 26 | self.r[0] = 'r_product_x' 27 | self.r[1] = 'r_substrate_x' 28 | self.r[2] = 'r_product_y' 29 | self.r[3] = 'r_substrate_y' 30 | self.r[4] = 'r_product_z' 31 | self.r[5] = 'r_substrate_z' 32 | 33 | self.layout = QVBoxLayout() 34 | self.layout.setAlignment(Qt.Alignment(Qt.AlignTop^Qt.AlignLeft)) 35 | self.layout.setSizeConstraint(QLayout.SetFixedSize) 36 | text = QLabel('Specify the yield terms that should be used for the different axes.\n'+ 37 | 'Keep in mind that exchange reactions are often defined in the direction of export.\n'+ 38 | 'Consider changing signs.') 39 | self.layout.addWidget(text) 40 | self.third_axis = QCheckBox('3D plot') 41 | self.third_axis.clicked.connect(self.box_3d_clicked) 42 | self.layout.addWidget(self.third_axis) 43 | points_layout = QHBoxLayout() 44 | points_layout.setAlignment(Qt.AlignLeft) 45 | numpoints_text = QLabel('Number of datapoints:') 46 | self.numpoints = QLineEdit('20') 47 | self.numpoints.setMaximumWidth(50) 48 | points_layout.addWidget(numpoints_text) 49 | points_layout.addWidget(self.numpoints) 50 | self.layout.addLayout(points_layout) 51 | 52 | editor_layout = QHBoxLayout() 53 | # Define for horizontal axis 54 | x_groupbox = QGroupBox('x-axis') 55 | x_num_den_layout = QVBoxLayout() 56 | self.x_combobox = QComboBox() 57 | self.x_combobox.insertItem(0,'rate') 58 | self.x_combobox.insertItem(1,'yield') 59 | self.x_combobox.currentTextChanged.connect(self.x_combo_changed) 60 | x_num_den_layout.addWidget(self.x_combobox) 61 | self.x_numerator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, check=True) 62 | self.x_numerator.setPlaceholderText('flux rate or expression (e.g. 1.0 '+self.r[0]+')') 63 | self.x_denominator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, check=True) 64 | self.x_denominator.setPlaceholderText('denominator (e.g. 1.0 '+self.r[1]+')') 65 | x_num_den_layout.addWidget(self.x_numerator) 66 | self.x_denominator.setHidden(True) 67 | self.x_sep = QHSeperationLine() 68 | self.x_sep.setFrameShadow(QFrame.Plain) 69 | self.x_sep.setLineWidth(2) 70 | self.x_sep.setHidden(True) 71 | x_num_den_layout.addWidget(self.x_sep) 72 | x_num_den_layout.addWidget(self.x_denominator) 73 | x_num_den_layout.setAlignment(Qt.AlignTop) 74 | x_groupbox.setLayout(x_num_den_layout) 75 | x_groupbox.setMinimumWidth(230) 76 | editor_layout.addWidget(x_groupbox) 77 | # Define for vertical axis 78 | y_groupbox = QGroupBox('y-axis') 79 | y_num_den_layout = QVBoxLayout() 80 | self.y_combobox = QComboBox() 81 | self.y_combobox.insertItem(0,'rate') 82 | self.y_combobox.insertItem(1,'yield') 83 | self.y_combobox.currentTextChanged.connect(self.y_combo_changed) 84 | y_num_den_layout.addWidget(self.y_combobox) 85 | self.y_numerator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, check=True) 86 | self.y_numerator.setPlaceholderText('flux rate or expression (e.g. '+self.r[2]+')') 87 | self.y_denominator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, check=True) 88 | self.y_denominator.setPlaceholderText('denominator (e.g. '+self.r[3]+')') 89 | y_num_den_layout.addWidget(self.y_numerator) 90 | self.y_denominator.setHidden(True) 91 | self.y_sep = QHSeperationLine() 92 | self.y_sep.setFrameShadow(QFrame.Plain) 93 | self.y_sep.setLineWidth(2) 94 | self.y_sep.setHidden(True) 95 | y_num_den_layout.addWidget(self.y_sep) 96 | y_num_den_layout.addWidget(self.y_denominator) 97 | y_groupbox.setLayout(y_num_den_layout) 98 | y_groupbox.setMinimumWidth(230) 99 | y_num_den_layout.setAlignment(Qt.AlignTop) 100 | editor_layout.addWidget(y_groupbox) 101 | # Define for longitudinal axis 102 | self.z_groupbox = QGroupBox('z-axis') 103 | z_num_den_layout = QVBoxLayout() 104 | self.z_combobox = QComboBox() 105 | self.z_combobox.insertItem(0,'rate') 106 | self.z_combobox.insertItem(1,'yield') 107 | self.z_combobox.currentTextChanged.connect(self.z_combo_changed) 108 | z_num_den_layout.addWidget(self.z_combobox) 109 | self.z_numerator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, check=True) 110 | self.z_numerator.setPlaceholderText('flux rate or expression (e.g. '+self.r[4]+')') 111 | self.z_denominator = QComplReceivLineEdit(self, self.appdata.project.reaction_ids, check=True) 112 | self.z_denominator.setPlaceholderText('denominator (e.g. '+self.r[5]+')') 113 | z_num_den_layout.addWidget(self.z_numerator) 114 | self.z_denominator.setHidden(True) 115 | self.z_sep = QHSeperationLine() 116 | self.z_sep.setFrameShadow(QFrame.Plain) 117 | self.z_sep.setLineWidth(2) 118 | self.z_sep.setHidden(True) 119 | z_num_den_layout.addWidget(self.z_sep) 120 | z_num_den_layout.addWidget(self.z_denominator) 121 | self.z_groupbox.setLayout(z_num_den_layout) 122 | self.z_groupbox.setMinimumWidth(230) 123 | z_num_den_layout.setAlignment(Qt.AlignTop) 124 | self.z_groupbox.setHidden(True) 125 | editor_layout.addWidget(self.z_groupbox) 126 | self.layout.addItem(editor_layout) 127 | # buttons 128 | button_layout = QHBoxLayout() 129 | self.button = QPushButton("Plot") 130 | self.cancel = QPushButton("Close") 131 | button_layout.addWidget(self.button) 132 | button_layout.addWidget(self.cancel) 133 | self.layout.addItem(button_layout) 134 | self.setLayout(self.layout) 135 | 136 | # Connecting the signal 137 | self.cancel.clicked.connect(self.reject) 138 | self.button.clicked.connect(self.compute) 139 | 140 | def compute(self): 141 | self.setCursor(Qt.BusyCursor) 142 | with self.appdata.project.cobra_py_model as model: 143 | self.appdata.project.load_scenario_into_model(model) 144 | if self.third_axis.isChecked(): 145 | axes = [[] for _ in range(3)] 146 | else: 147 | axes = [[] for _ in range(2)] 148 | if self.x_combobox.currentText() == 'yield': 149 | axes[0] = (self.x_numerator.text(),self.x_denominator.text()) 150 | else: 151 | axes[0] = (self.x_numerator.text()) 152 | if self.y_combobox.currentText() == 'yield': 153 | axes[1] = (self.y_numerator.text(),self.y_denominator.text()) 154 | else: 155 | axes[1] = (self.y_numerator.text()) 156 | if len(axes) == 3: 157 | if self.z_combobox.currentText() == 'yield': 158 | axes[2] = (self.z_numerator.text(),self.z_denominator.text()) 159 | else: 160 | axes[2] = (self.z_numerator.text()) 161 | try: 162 | plot_flux_space(model,axes,points=int(self.numpoints.text())) 163 | except Exception as e: 164 | QMessageBox.warning( 165 | self, 166 | "Error in plot calculation", 167 | "Plot space could not be calculated due to the following error:\n" 168 | f"{e}" 169 | ) 170 | self.appdata.window.centralWidget().show_bottom_of_console() 171 | self.setCursor(Qt.ArrowCursor) 172 | 173 | def box_3d_clicked(self): 174 | if self.third_axis.isChecked(): 175 | self.z_groupbox.setHidden(False) 176 | else: 177 | self.z_groupbox.setHidden(True) 178 | self.adjustSize() 179 | 180 | def x_combo_changed(self): 181 | if self.x_combobox.currentText() == 'yield': 182 | self.x_numerator.setPlaceholderText('numerator (e.g. 1.0 '+self.r[0]+')') 183 | self.x_sep.setHidden(False) 184 | self.x_denominator.setHidden(False) 185 | else: 186 | self.x_numerator.setPlaceholderText('flux rate or expression (e.g. 1.0 '+self.r[0]+')') 187 | self.x_sep.setHidden(True) 188 | self.x_denominator.setHidden(True) 189 | self.adjustSize() 190 | 191 | def y_combo_changed(self): 192 | if self.y_combobox.currentText() == 'yield': 193 | self.y_numerator.setPlaceholderText('numerator (e.g. 1.0 '+self.r[2]+')') 194 | self.y_sep.setHidden(False) 195 | self.y_denominator.setHidden(False) 196 | else: 197 | self.y_numerator.setPlaceholderText('flux rate or expression (e.g. 1.0 '+self.r[2]+')') 198 | self.y_sep.setHidden(True) 199 | self.y_denominator.setHidden(True) 200 | self.adjustSize() 201 | 202 | def z_combo_changed(self): 203 | if self.z_combobox.currentText() == 'yield': 204 | self.z_numerator.setPlaceholderText('numerator (e.g. 1.0 '+self.r[4]+')') 205 | self.z_sep.setHidden(False) 206 | self.z_denominator.setHidden(False) 207 | else: 208 | self.z_numerator.setPlaceholderText('flux rate or expression (e.g. 1.0 '+self.r[4]+')') 209 | self.z_sep.setHidden(True) 210 | self.z_denominator.setHidden(True) 211 | self.adjustSize() 212 | -------------------------------------------------------------------------------- /cnapy/gui_elements/gene_list.py: -------------------------------------------------------------------------------- 1 | """The gene list""" 2 | 3 | import cobra 4 | import cobra.manipulation 5 | from qtpy.QtCore import Qt, Signal, Slot 6 | from qtpy.QtWidgets import (QAction, QHBoxLayout, QLabel, 7 | QLineEdit, QMenu, QMessageBox, QPushButton, QSizePolicy, QSplitter, 8 | QTableWidgetItem, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget) 9 | 10 | from cnapy.appdata import AppData, ModelItemType 11 | from cnapy.utils import SignalThrottler, turn_red, turn_white, update_selected 12 | from cnapy.gui_elements.annotation_widget import AnnotationWidget 13 | from cnapy.gui_elements.reaction_table_widget import ModelElementType, ReactionTableWidget 14 | 15 | 16 | class GeneList(QWidget): 17 | """A list of genes""" 18 | 19 | def __init__(self, central_widget): 20 | QWidget.__init__(self) 21 | self.appdata: AppData = central_widget.appdata 22 | self.central_widget = central_widget 23 | self.last_selected = None 24 | 25 | self.gene_list = QTreeWidget() 26 | self.gene_list.setHeaderLabels(["Id", "Name"]) 27 | self.gene_list.setSortingEnabled(True) 28 | self.gene_list.sortByColumn(0, Qt.AscendingOrder) 29 | 30 | for m in self.appdata.project.cobra_py_model.genes: 31 | self.add_gene(m) 32 | self.gene_list.setContextMenuPolicy(Qt.CustomContextMenu) 33 | self.gene_list.customContextMenuRequested.connect( 34 | self.on_context_menu) 35 | 36 | # create context menu 37 | self.gene_mask = GenesMask(self, self.appdata) 38 | self.gene_mask.hide() 39 | 40 | self.layout = QVBoxLayout() 41 | self.layout.setContentsMargins(0, 0, 0, 0) 42 | 43 | self.splitter = QSplitter() 44 | self.splitter.setOrientation(Qt.Vertical) 45 | self.splitter.addWidget(self.gene_list) 46 | self.splitter.addWidget(self.gene_mask) 47 | self.layout.addWidget(self.splitter) 48 | self.setLayout(self.layout) 49 | 50 | self.gene_list.currentItemChanged.connect( 51 | self.gene_selected) 52 | self.gene_mask.geneChanged.connect( 53 | self.handle_changed_gene) 54 | self.gene_mask.jumpToReaction.connect( 55 | self.emit_jump_to_reaction) 56 | self.gene_mask.jumpToMetabolite.connect( 57 | self.emit_jump_to_metabolite 58 | ) 59 | 60 | def clear(self): 61 | self.gene_list.clear() 62 | self.gene_mask.hide() 63 | 64 | def add_gene(self, gene): 65 | item = QTreeWidgetItem(self.gene_list) 66 | item.setText(0, gene.id) 67 | item.setText(1, gene.name) 68 | item.setData(2, 0, gene) 69 | 70 | def on_context_menu(self, point): 71 | if len(self.appdata.project.cobra_py_model.genes) > 0: 72 | self.pop_menu.exec_(self.mapToGlobal(point)) 73 | 74 | def handle_changed_gene(self, gene: cobra.Gene): 75 | # Update gene item in list 76 | root = self.gene_list.invisibleRootItem() 77 | child_count = root.childCount() 78 | for i in range(child_count): 79 | item = root.child(i) 80 | if item.data(2, 0) == gene: 81 | old_id = item.text(0) 82 | item.setText(0, gene.id) 83 | item.setText(1, gene.name) 84 | break 85 | 86 | for reaction_x in self.appdata.project.cobra_py_model.reactions: 87 | reaction: cobra.Reaction = reaction_x 88 | gpr = reaction.gene_reaction_rule + " " 89 | if old_id in gpr: 90 | reaction.gene_reaction_rule = gpr.replace(old_id+" ", gene.id+" ").strip() 91 | 92 | self.last_selected = self.gene_mask.id.text() 93 | self.geneChanged.emit(old_id, gene) 94 | 95 | def update_selected(self, string, with_annotations): 96 | return update_selected( 97 | string=string, 98 | with_annotations=with_annotations, 99 | model_elements=self.appdata.project.cobra_py_model.genes, 100 | element_list=self.gene_list, 101 | ) 102 | 103 | def gene_selected(self, item, _column): 104 | if item is None: 105 | self.gene_mask.hide() 106 | else: 107 | self.gene_mask.show() 108 | gene: cobra.Gene = item.data(2, 0) 109 | 110 | self.gene_mask.gene = gene 111 | 112 | self.gene_mask.id.setText(gene.id) 113 | self.gene_mask.name.setText(gene.name) 114 | self.gene_mask.changed = False 115 | self.gene_mask.annotation_widget.update_annotations(gene.annotation) 116 | 117 | turn_white(self.gene_mask.name, self.appdata.is_in_dark_mode) 118 | self.gene_mask.is_valid = True 119 | self.gene_mask.reactions.update_state(self.gene_mask.id.text(), self.gene_mask.gene_list) 120 | self.central_widget.add_model_item_to_history(gene.id, gene.name, ModelItemType.Gene) 121 | 122 | def update(self): 123 | self.gene_list.clear() 124 | for m in self.appdata.project.cobra_py_model.genes: 125 | self.add_gene(m) 126 | 127 | if self.last_selected is None: 128 | self.gene_list.setCurrentItem(None) 129 | else: 130 | items = self.gene_list.findItems( 131 | self.last_selected, Qt.MatchExactly) 132 | 133 | for i in items: 134 | self.gene_list.setCurrentItem(i) 135 | self.gene_list.scrollToItem(i) 136 | break 137 | 138 | def set_current_item(self, key): 139 | self.last_selected = key 140 | self.update() 141 | 142 | def emit_jump_to_reaction(self, item: QTableWidgetItem): 143 | self.jumpToReaction.emit(item) 144 | 145 | def emit_jump_to_metabolite(self, metabolite): 146 | self.jumpToMetabolite.emit(metabolite) 147 | 148 | itemActivated = Signal(str) 149 | geneChanged = Signal(str, cobra.Gene) 150 | jumpToReaction = Signal(str) 151 | jumpToMetabolite = Signal(str) 152 | computeInOutFlux = Signal(str) 153 | 154 | 155 | class GenesMask(QWidget): 156 | """The input mask for a genes""" 157 | 158 | def __init__(self, gene_list, appdata): 159 | QWidget.__init__(self) 160 | self.gene_list = gene_list 161 | self.appdata = appdata 162 | self.gene = None 163 | self.is_valid = True 164 | self.changed = False 165 | self.setAcceptDrops(False) 166 | 167 | layout = QVBoxLayout() 168 | l = QHBoxLayout() 169 | label = QLabel("Id:") 170 | self.id = QLabel("") 171 | l.addWidget(label) 172 | l.addWidget(self.id) 173 | layout.addItem(l) 174 | 175 | self.delete_button = QPushButton("Delete gene") 176 | self.delete_button.setToolTip( 177 | "Delete this gene and remove it from associated reactions." 178 | ) 179 | policy = QSizePolicy() 180 | policy.ShrinkFlag = True 181 | self.delete_button.setSizePolicy(policy) 182 | l.addWidget(self.delete_button) 183 | 184 | l = QHBoxLayout() 185 | label = QLabel("Name:") 186 | self.name = QLineEdit() 187 | l.addWidget(label) 188 | l.addWidget(self.name) 189 | layout.addItem(l) 190 | 191 | self.throttler = SignalThrottler(500) 192 | self.throttler.triggered.connect(self.genes_data_changed) 193 | 194 | self.annotation_widget = AnnotationWidget(self) 195 | layout.addItem(self.annotation_widget) 196 | 197 | l = QVBoxLayout() 198 | label = QLabel("Reactions using this gene:") 199 | l.addWidget(label) 200 | l2 = QHBoxLayout() 201 | self.reactions = ReactionTableWidget (self.appdata, ModelElementType.GENE) 202 | l2.addWidget(self.reactions) 203 | l.addItem(l2) 204 | self.reactions.itemDoubleClicked.connect(self.emit_jump_to_reaction) 205 | self.reactions.jumpToMetabolite.connect(self.emit_jump_to_metabolite) 206 | layout.addItem(l) 207 | 208 | self.setLayout(layout) 209 | 210 | 211 | self.delete_button.clicked.connect(self.delete_gene) 212 | self.name.textEdited.connect(self.throttler.throttle) 213 | 214 | self.annotation_widget.deleteAnnotation.connect( 215 | self.delete_selected_annotation 216 | ) 217 | 218 | self.validate_mask() 219 | 220 | def add_anno_row(self): 221 | i = self.annotation.rowCount() 222 | self.annotation.insertRow(i) 223 | self.changed = True 224 | 225 | def apply(self): 226 | try: 227 | self.gene.name = self.name.text() 228 | self.annotation_widget.apply_annotation(self.gene) 229 | self.changed = False 230 | self.geneChanged.emit(self.gene) 231 | except ValueError: 232 | turn_red(self.name) 233 | QMessageBox.information( 234 | self, 'Invalid name', 'Could not apply name ' + 235 | self.name.text()+'.') 236 | 237 | def delete_gene(self): 238 | cobra.manipulation.remove_genes( 239 | model=self.appdata.project.cobra_py_model, 240 | gene_list=[self.gene], 241 | remove_reactions=False, 242 | ) 243 | self.appdata.window.unsaved_changes() 244 | self.hide() 245 | current_row_index = self.gene_list.gene_list.currentIndex().row() 246 | self.gene_list.gene_list.setCurrentItem(None) 247 | self.gene_list.last_selected = None 248 | self.gene_list.gene_list.takeTopLevelItem( 249 | current_row_index) 250 | self.appdata.window.setFocus() 251 | 252 | def delete_selected_annotation(self, identifier_key): 253 | try: 254 | del(self.gene.annotation[identifier_key]) 255 | self.appdata.window.unsaved_changes() 256 | except IndexError: 257 | pass 258 | 259 | def validate_name(self): 260 | try: 261 | cobra.Gene(id="test_id", name=self.name.text()) 262 | except ValueError: 263 | turn_red(self.name) 264 | return False 265 | else: 266 | turn_white(self.name, self.appdata.is_in_dark_mode) 267 | return True 268 | 269 | def validate_mask(self): 270 | valid_name = self.validate_name() 271 | if valid_name: 272 | self.is_valid = True 273 | else: 274 | self.is_valid = False 275 | 276 | def genes_data_changed(self): 277 | self.changed = True 278 | self.validate_mask() 279 | if self.is_valid: 280 | self.apply() 281 | self.reactions.update_state(self.id.text(), self.gene_list) 282 | 283 | @Slot(QTableWidgetItem) 284 | def emit_jump_to_reaction(self, item: QTableWidgetItem): 285 | self.jumpToReaction.emit(item.text()) 286 | 287 | def emit_jump_to_metabolite(self, metabolite): 288 | self.jumpToMetabolite.emit(metabolite) 289 | 290 | jumpToReaction = Signal(str) 291 | jumpToMetabolite = Signal(str) 292 | geneChanged = Signal(cobra.Gene) 293 | --------------------------------------------------------------------------------