├── .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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/cnapy/data/zoom-out.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/cnapy/data/zoom-in.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 | -------------------------------------------------------------------------------- /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 | 144 | -------------------------------------------------------------------------------- /cnapy/data/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 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 | [](https://github.com/cnapy-org/CNApy/releases/latest) 4 | [](https://github.com/cnapy-org/CNApy/commits/master) 5 | [](https://github.com/cnapy-org/CNApy/issues) 6 | [](https://gitter.im/cnapy-org/community) 7 | 8 |  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 | [](https://github.com/cnapy-org/CNApy/releases/latest) 4 | [](https://github.com/cnapy-org/CNApy/commits/master) 5 | [](https://github.com/cnapy-org/CNApy/issues) 6 | [](https://gitter.im/cnapy-org/community) 7 | 8 |  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]](