cnapy 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cnapy/__init__.py +16 -0
- cnapy/__main__.py +37 -0
- cnapy/appdata.py +539 -0
- cnapy/application.py +241 -0
- cnapy/core.py +372 -0
- cnapy/core_gui.py +37 -0
- cnapy/data/blank.svg +259 -0
- cnapy/data/check.svg +4 -0
- cnapy/data/clear.svg +7 -0
- cnapy/data/cnapylogo.svg +707 -0
- cnapy/data/cnapylogo_no_text.svg +252 -0
- cnapy/data/cross.svg +6 -0
- cnapy/data/d-font.svg +21 -0
- cnapy/data/default-bg.svg +258 -0
- cnapy/data/default-color.svg +150 -0
- cnapy/data/escher.min.js +78 -0
- cnapy/data/escher_cnapy.html +305 -0
- cnapy/data/heat.svg +143 -0
- cnapy/data/onoff.svg +117 -0
- cnapy/data/qmark.svg +5 -0
- cnapy/data/redo.svg +44 -0
- cnapy/data/save.svg +16 -0
- cnapy/data/undo.svg +35 -0
- cnapy/data/zoom-in.svg +16 -0
- cnapy/data/zoom-out.svg +16 -0
- cnapy/flux_vector_container.py +88 -0
- cnapy/gui_elements/__init__.py +16 -0
- cnapy/gui_elements/about_dialog.py +50 -0
- cnapy/gui_elements/annotation_widget.py +120 -0
- cnapy/gui_elements/box_position_dialog.py +79 -0
- cnapy/gui_elements/central_widget.py +784 -0
- cnapy/gui_elements/clipboard_calculator.py +136 -0
- cnapy/gui_elements/config_cobrapy_dialog.py +146 -0
- cnapy/gui_elements/config_dialog.py +274 -0
- cnapy/gui_elements/configuration_cplex.py +179 -0
- cnapy/gui_elements/configuration_gurobi.py +139 -0
- cnapy/gui_elements/download_dialog.py +82 -0
- cnapy/gui_elements/efm_dialog.py +212 -0
- cnapy/gui_elements/efmtool_dialog.py +114 -0
- cnapy/gui_elements/escher_map_view.py +268 -0
- cnapy/gui_elements/flux_feasibility_dialog.py +571 -0
- cnapy/gui_elements/flux_optimization_dialog.py +121 -0
- cnapy/gui_elements/gene_list.py +292 -0
- cnapy/gui_elements/in_out_flux_dialog.py +42 -0
- cnapy/gui_elements/main_window.py +2445 -0
- cnapy/gui_elements/map_view.py +736 -0
- cnapy/gui_elements/mcs_dialog.py +454 -0
- cnapy/gui_elements/metabolite_list.py +479 -0
- cnapy/gui_elements/mode_navigator.py +459 -0
- cnapy/gui_elements/model_info.py +106 -0
- cnapy/gui_elements/plot_space_dialog.py +211 -0
- cnapy/gui_elements/reaction_table_widget.py +96 -0
- cnapy/gui_elements/reactions_list.py +1001 -0
- cnapy/gui_elements/rename_map_dialog.py +48 -0
- cnapy/gui_elements/scenario_tab.py +525 -0
- cnapy/gui_elements/solver_buttons.py +78 -0
- cnapy/gui_elements/strain_design_dialog.py +1845 -0
- cnapy/gui_elements/thermodynamics_dialog.py +614 -0
- cnapy/gui_elements/yield_optimization_dialog.py +143 -0
- cnapy/gui_elements/yield_space_dialog.py +143 -0
- cnapy/resources.py +21575 -0
- cnapy/sd_ci_optmdfpathway.py +322 -0
- cnapy/sd_class_interface.py +884 -0
- cnapy/utils.py +293 -0
- cnapy/utils_for_cnapy_api.py +104 -0
- cnapy-1.2.1.dist-info/LICENSE +201 -0
- cnapy-1.2.1.dist-info/METADATA +216 -0
- cnapy-1.2.1.dist-info/RECORD +70 -0
- cnapy-1.2.1.dist-info/WHEEL +5 -0
- cnapy-1.2.1.dist-info/top_level.txt +1 -0
cnapy/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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 -*-
|
cnapy/__main__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2022 CNApy organization
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import site
|
|
18
|
+
from jpype._jvmfinder import getDefaultJVMPath, JVMNotFoundException, JVMNotSupportedException
|
|
19
|
+
try:
|
|
20
|
+
getDefaultJVMPath()
|
|
21
|
+
except (JVMNotFoundException, JVMNotSupportedException):
|
|
22
|
+
for path in site.getsitepackages():
|
|
23
|
+
# in one of these conda puts the JRE
|
|
24
|
+
os.environ['JAVA_HOME'] = os.path.join(path, 'Library')
|
|
25
|
+
try:
|
|
26
|
+
getDefaultJVMPath()
|
|
27
|
+
break
|
|
28
|
+
except (JVMNotFoundException, JVMNotSupportedException):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
from cnapy.application import Application
|
|
32
|
+
|
|
33
|
+
def main_cnapy():
|
|
34
|
+
Application()
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main_cnapy()
|
cnapy/appdata.py
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""The application data"""
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import gurobipy
|
|
5
|
+
from configparser import ConfigParser
|
|
6
|
+
import pathlib
|
|
7
|
+
import pkg_resources
|
|
8
|
+
from tempfile import TemporaryDirectory
|
|
9
|
+
from typing import List, Set, Dict, Tuple
|
|
10
|
+
from ast import literal_eval as make_tuple
|
|
11
|
+
from math import isclose
|
|
12
|
+
import appdirs
|
|
13
|
+
from enum import IntEnum
|
|
14
|
+
|
|
15
|
+
import cobra
|
|
16
|
+
from optlang.symbolics import Zero
|
|
17
|
+
from optlang_enumerator.cobra_cnapy import CNApyModel
|
|
18
|
+
from qtpy.QtCore import Qt, Signal, QObject, QStringListModel
|
|
19
|
+
from qtpy.QtGui import QColor, QFont
|
|
20
|
+
from qtpy.QtWidgets import QMessageBox
|
|
21
|
+
|
|
22
|
+
# from straindesign.parse_constr import linexprdict2str # indirectly leads to a JVM restart exception?!?
|
|
23
|
+
|
|
24
|
+
class ModelItemType(IntEnum):
|
|
25
|
+
Metabolite = 0
|
|
26
|
+
Reaction = 1
|
|
27
|
+
Gene = 2
|
|
28
|
+
|
|
29
|
+
class AppData(QObject):
|
|
30
|
+
''' The application data '''
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
QObject.__init__(self)
|
|
34
|
+
self.version = "cnapy-1.2.1"
|
|
35
|
+
self.format_version = 2
|
|
36
|
+
self.unsaved = False
|
|
37
|
+
self.project = ProjectData()
|
|
38
|
+
self.modes_coloring = False
|
|
39
|
+
self.scen_color = QColor(255, 0, 127)
|
|
40
|
+
# more scencolors
|
|
41
|
+
self.scen_color_good = QColor(130, 190, 0)
|
|
42
|
+
self.scen_color_warn = QColor(255, 200, 0)
|
|
43
|
+
self.scen_color_bad = Qt.red
|
|
44
|
+
|
|
45
|
+
font = QFont()
|
|
46
|
+
font.setFamily(font.defaultFamily())
|
|
47
|
+
self.font_size = font.pointSize()
|
|
48
|
+
self.box_width = 80
|
|
49
|
+
self.box_height = 40
|
|
50
|
+
self.comp_color = QColor(0, 170, 255)
|
|
51
|
+
self.special_color_1 = QColor(255, 215, 0)
|
|
52
|
+
self.special_color_2 = QColor(150, 220, 0) # for bounds excluding 0
|
|
53
|
+
self.default_color = QColor(200, 200, 200)
|
|
54
|
+
self.abs_tol = 0.0001
|
|
55
|
+
self.rounding = 3
|
|
56
|
+
self.cna_path = ""
|
|
57
|
+
self.work_directory = str(os.path.join(
|
|
58
|
+
pathlib.Path.home(), "CNApy-projects"))
|
|
59
|
+
self.use_results_cache = False
|
|
60
|
+
self.results_cache_dir: pathlib.Path = pathlib.Path(".")
|
|
61
|
+
self.last_scen_directory = str(os.path.join(
|
|
62
|
+
pathlib.Path.home(), "CNApy-projects"))
|
|
63
|
+
self.temp_dir = TemporaryDirectory()
|
|
64
|
+
self.conf_path = os.path.join(appdirs.user_config_dir(
|
|
65
|
+
"cnapy", roaming=True, appauthor=False), "cnapy-config.txt")
|
|
66
|
+
self.cobrapy_conf_path = os.path.join(appdirs.user_config_dir(
|
|
67
|
+
"cnapy", roaming=True, appauthor=False), "cobrapy-config.txt")
|
|
68
|
+
self.scenario_past = []
|
|
69
|
+
self.scenario_future = []
|
|
70
|
+
self.recent_cna_files = []
|
|
71
|
+
self.auto_fba = False
|
|
72
|
+
|
|
73
|
+
def scen_values_set(self, reaction: str, values: Tuple[float, float]):
|
|
74
|
+
if self.project.scen_values.get(reaction, None) != values: # record only real changes
|
|
75
|
+
self.project.scen_values[reaction] = values
|
|
76
|
+
self.scenario_past.append(("set", reaction, values))
|
|
77
|
+
self.scenario_future.clear()
|
|
78
|
+
self.unsaved_scenario_changes()
|
|
79
|
+
|
|
80
|
+
def scen_values_set_multiple(self, reactions: List[str], values: List[Tuple[float, float]]):
|
|
81
|
+
for r, v in zip(reactions, values):
|
|
82
|
+
self.project.scen_values[r] = v
|
|
83
|
+
self.scenario_past.append(("set", reactions, values))
|
|
84
|
+
self.scenario_future.clear()
|
|
85
|
+
self.unsaved_scenario_changes()
|
|
86
|
+
|
|
87
|
+
def scen_values_pop(self, reaction: str):
|
|
88
|
+
self.project.scen_values.pop(reaction, None)
|
|
89
|
+
self.scenario_past.append(("pop", reaction, 0))
|
|
90
|
+
self.scenario_future.clear()
|
|
91
|
+
self.unsaved_scenario_changes()
|
|
92
|
+
|
|
93
|
+
def scen_values_clear(self):
|
|
94
|
+
self.project.scen_values.clear_flux_values()
|
|
95
|
+
self.scenario_past.append(("clear", "all", 0))
|
|
96
|
+
self.scenario_future.clear()
|
|
97
|
+
self.unsaved_scenario_changes()
|
|
98
|
+
|
|
99
|
+
def set_comp_value_as_scen_value(self, reaction: str):
|
|
100
|
+
val = self.project.comp_values.get(reaction, None)
|
|
101
|
+
if val:
|
|
102
|
+
self.scen_values_set(reaction, val)
|
|
103
|
+
self.unsaved_scenario_changes()
|
|
104
|
+
|
|
105
|
+
def recreate_scenario_from_history(self):
|
|
106
|
+
self.project.scen_values.clear_flux_values()
|
|
107
|
+
for (tag, reaction, values) in self.scenario_past:
|
|
108
|
+
if tag == "set":
|
|
109
|
+
if isinstance(reaction, list):
|
|
110
|
+
for r, v in zip(reaction, values):
|
|
111
|
+
self.project.scen_values[r] = v
|
|
112
|
+
else:
|
|
113
|
+
self.project.scen_values[reaction] = values
|
|
114
|
+
elif tag == "pop":
|
|
115
|
+
self.project.scen_values.pop(reaction, None)
|
|
116
|
+
elif tag == "clear":
|
|
117
|
+
self.project.scen_values.clear_flux_values()
|
|
118
|
+
self.unsaved_scenario_changes()
|
|
119
|
+
|
|
120
|
+
def format_flux_value(self, flux_value) -> str:
|
|
121
|
+
return str(round(float(flux_value), self.rounding)).rstrip("0").rstrip(".")
|
|
122
|
+
|
|
123
|
+
def flux_value_display(self, vl, vu): # -> str, color, bool
|
|
124
|
+
# We differentiate special cases like (vl==vu)
|
|
125
|
+
if isclose(vl, vu, abs_tol=self.abs_tol):
|
|
126
|
+
if self.modes_coloring:
|
|
127
|
+
if vl == 0:
|
|
128
|
+
background_color = Qt.red
|
|
129
|
+
else:
|
|
130
|
+
background_color = Qt.green
|
|
131
|
+
else:
|
|
132
|
+
background_color = self.comp_color
|
|
133
|
+
as_one = True
|
|
134
|
+
flux_text = self.format_flux_value(vl)
|
|
135
|
+
else:
|
|
136
|
+
if isclose(vl, 0.0, abs_tol=self.abs_tol):
|
|
137
|
+
background_color = self.special_color_1
|
|
138
|
+
elif isclose(vu, 0.0, abs_tol=self.abs_tol):
|
|
139
|
+
background_color = self.special_color_1
|
|
140
|
+
elif vl <= 0 and vu >= 0:
|
|
141
|
+
background_color = self.special_color_1
|
|
142
|
+
else:
|
|
143
|
+
background_color = self.special_color_2
|
|
144
|
+
as_one = False
|
|
145
|
+
flux_text = self.format_flux_value(vl) + ", " + self.format_flux_value(vu)
|
|
146
|
+
return flux_text, background_color, as_one
|
|
147
|
+
|
|
148
|
+
def save_cnapy_config(self):
|
|
149
|
+
try:
|
|
150
|
+
fp = open(self.conf_path, "w")
|
|
151
|
+
except FileNotFoundError:
|
|
152
|
+
os.makedirs(appdirs.user_config_dir("cnapy", roaming=True, appauthor=False))
|
|
153
|
+
fp = open(self.conf_path, "w")
|
|
154
|
+
parser = ConfigParser()
|
|
155
|
+
parser.add_section('cnapy-config')
|
|
156
|
+
parser.set('cnapy-config', 'version', self.version)
|
|
157
|
+
parser.set('cnapy-config', 'work_directory', self.work_directory)
|
|
158
|
+
parser.set('cnapy-config', 'scen_color', str(self.scen_color.rgb()))
|
|
159
|
+
parser.set('cnapy-config', 'comp_color', str(self.comp_color.rgb()))
|
|
160
|
+
parser.set('cnapy-config', 'spec1_color', str(self.special_color_1.rgb()))
|
|
161
|
+
parser.set('cnapy-config', 'spec2_color', str(self.special_color_2.rgb()))
|
|
162
|
+
parser.set('cnapy-config', 'default_color', str(self.default_color.rgb()))
|
|
163
|
+
parser.set('cnapy-config', 'font_size', str(self.font_size))
|
|
164
|
+
parser.set('cnapy-config', 'box_width', str(self.box_width))
|
|
165
|
+
parser.set('cnapy-config', 'rounding', str(self.rounding))
|
|
166
|
+
parser.set('cnapy-config', 'abs_tol', str(self.abs_tol))
|
|
167
|
+
parser.set('cnapy-config', 'use_results_cache', str(self.use_results_cache))
|
|
168
|
+
parser.set('cnapy-config', 'results_cache_directory', str(self.results_cache_dir))
|
|
169
|
+
parser.set('cnapy-config', 'recent_cna_files', str(self.recent_cna_files))
|
|
170
|
+
parser.write(fp)
|
|
171
|
+
fp.close()
|
|
172
|
+
|
|
173
|
+
def compute_color_onoff(self, value: Tuple[float, float]):
|
|
174
|
+
(vl, vh) = value
|
|
175
|
+
vl = round(vl, self.rounding)
|
|
176
|
+
vh = round(vh, self.rounding)
|
|
177
|
+
if vl < 0.0:
|
|
178
|
+
return QColor.fromRgb(0, 255, 0)
|
|
179
|
+
elif vh > 0.0:
|
|
180
|
+
return QColor.fromRgb(0, 255, 0)
|
|
181
|
+
else:
|
|
182
|
+
return QColor.fromRgb(255, 0, 0)
|
|
183
|
+
|
|
184
|
+
def compute_color_heat(self, value: Tuple[float, float], low, high):
|
|
185
|
+
(vl, vh) = value
|
|
186
|
+
vl = round(vl, self.rounding)
|
|
187
|
+
vh = round(vh, self.rounding)
|
|
188
|
+
mean = my_mean((vl, vh))
|
|
189
|
+
if mean > 0.0:
|
|
190
|
+
if high == 0.0:
|
|
191
|
+
h = 255
|
|
192
|
+
else:
|
|
193
|
+
h = round(mean * 255 / high)
|
|
194
|
+
return QColor.fromRgbF(255-h, 255, 255 - h)
|
|
195
|
+
else:
|
|
196
|
+
if low == 0.0:
|
|
197
|
+
h = 255
|
|
198
|
+
else:
|
|
199
|
+
h = round(mean * 255 / low)
|
|
200
|
+
return QColor.fromRgbF(255, 255 - h, 255 - h)
|
|
201
|
+
|
|
202
|
+
def low_and_high(self) -> Tuple[int, int]:
|
|
203
|
+
low = 0
|
|
204
|
+
high = 0
|
|
205
|
+
for value in self.project.scen_values.values():
|
|
206
|
+
mean = my_mean(value)
|
|
207
|
+
if mean < low:
|
|
208
|
+
low = mean
|
|
209
|
+
if mean > high:
|
|
210
|
+
high = mean
|
|
211
|
+
for value in self.project.comp_values.values():
|
|
212
|
+
mean = my_mean(value)
|
|
213
|
+
if mean < low:
|
|
214
|
+
low = mean
|
|
215
|
+
if mean > high:
|
|
216
|
+
high = mean
|
|
217
|
+
return (low, high)
|
|
218
|
+
|
|
219
|
+
def unsaved_scenario_changes(self):
|
|
220
|
+
self.project.scen_values.has_unsaved_changes = True
|
|
221
|
+
self.unsavedScenarioChanges.emit()
|
|
222
|
+
|
|
223
|
+
unsavedScenarioChanges = Signal()
|
|
224
|
+
|
|
225
|
+
class Scenario(Dict[str, Tuple[float, float]]):
|
|
226
|
+
empty_constraint = (None, "", "")
|
|
227
|
+
|
|
228
|
+
# cannot do this because of the import problem
|
|
229
|
+
# @staticmethod
|
|
230
|
+
# def format_constraint(constraint):
|
|
231
|
+
# return linexprdict2str(constraint[0])+" "+constraint[1]+" "+str(constraint[2])
|
|
232
|
+
|
|
233
|
+
def __init__(self):
|
|
234
|
+
super().__init__() # this dictionary contains the flux values
|
|
235
|
+
self.objective_coefficients: Dict[str, float] = {} # reaction ID, coefficient
|
|
236
|
+
self.objective_direction: str = "max"
|
|
237
|
+
self.use_scenario_objective: bool = False
|
|
238
|
+
self.pinned_reactions: Set[str] = set()
|
|
239
|
+
self.description: str = ""
|
|
240
|
+
self.constraints: List[List(Dict, str, float)] = [] # [reaction_id: coefficient dictionary, type, rhs]
|
|
241
|
+
self.reactions = {} # reaction_id: (coefficient dictionary, lb, ub), can overwrite existing reactions
|
|
242
|
+
self.annotations = [] # List of dicts with: { "id": $reac_id, "key": $key_value, "value": $value_at_key }
|
|
243
|
+
self.file_name: str = ""
|
|
244
|
+
self.has_unsaved_changes = False
|
|
245
|
+
self.version: int = 3
|
|
246
|
+
|
|
247
|
+
def save(self, filename: str):
|
|
248
|
+
json_dict = {'fluxes': self, 'pinned_reactions': list(self.pinned_reactions), 'description': self.description,
|
|
249
|
+
'objective_direction': self.objective_direction, 'objective_coefficients': self.objective_coefficients,
|
|
250
|
+
'use_scenario_objective': self.use_scenario_objective, 'reactions': self.reactions,
|
|
251
|
+
'constraints': self.constraints, "annotations": self.annotations, 'version': self.version}
|
|
252
|
+
with open(filename, 'w') as fp:
|
|
253
|
+
json.dump(json_dict, fp)
|
|
254
|
+
self.has_unsaved_changes = False
|
|
255
|
+
|
|
256
|
+
def load(self, filename: str, appdata: AppData, merge=False) -> Tuple[List[str], List, List]:
|
|
257
|
+
unknown_ids: List(str)= []
|
|
258
|
+
incompatible_constraints = []
|
|
259
|
+
skipped_scenario_reactions = []
|
|
260
|
+
if not merge:
|
|
261
|
+
self.clear()
|
|
262
|
+
with open(filename, 'r') as fp:
|
|
263
|
+
if filename.endswith('scen'): # CNApy scenario
|
|
264
|
+
self.file_name = filename
|
|
265
|
+
json_dict = json.load(fp)
|
|
266
|
+
if {'fluxes', 'pinned_reactions', 'description', 'objective_direction',
|
|
267
|
+
'objective_coefficients', 'use_scenario_objective', 'version'}.issubset(json_dict.keys()):
|
|
268
|
+
flux_values = json_dict['fluxes']
|
|
269
|
+
for reac_id in json_dict['pinned_reactions']:
|
|
270
|
+
if reac_id in appdata.project.cobra_py_model.reactions:
|
|
271
|
+
self.pinned_reactions.add(reac_id)
|
|
272
|
+
else:
|
|
273
|
+
unknown_ids.append(reac_id)
|
|
274
|
+
if not merge:
|
|
275
|
+
self.description = json_dict['description']
|
|
276
|
+
self.objective_direction = json_dict['objective_direction']
|
|
277
|
+
all_reaction_ids = set(appdata.project.cobra_py_model.reactions.list_attr("id"))
|
|
278
|
+
if json_dict['version'] > 1:
|
|
279
|
+
self.reactions = json_dict['reactions']
|
|
280
|
+
for reac_id in self.reactions:
|
|
281
|
+
if reac_id in all_reaction_ids:
|
|
282
|
+
skipped_scenario_reactions.append(reac_id)
|
|
283
|
+
for reac_id in skipped_scenario_reactions:
|
|
284
|
+
del self.reactions[reac_id]
|
|
285
|
+
self.constraints = []
|
|
286
|
+
all_reaction_ids.update(self.reactions)
|
|
287
|
+
for constr in json_dict['constraints']:
|
|
288
|
+
if constr[0] is None:
|
|
289
|
+
self.constraints.append(Scenario.empty_constraint)
|
|
290
|
+
elif set(constr[0].keys()).issubset(all_reaction_ids):
|
|
291
|
+
self.constraints.append(constr)
|
|
292
|
+
else:
|
|
293
|
+
incompatible_constraints.append(constr)
|
|
294
|
+
if json_dict['version'] >= 3:
|
|
295
|
+
self.annotations = json_dict["annotations"]
|
|
296
|
+
for reac_id, val in json_dict['objective_coefficients'].items():
|
|
297
|
+
if reac_id in all_reaction_ids:
|
|
298
|
+
self.objective_coefficients[reac_id] = val
|
|
299
|
+
else:
|
|
300
|
+
unknown_ids.append(reac_id)
|
|
301
|
+
self.use_scenario_objective = json_dict['use_scenario_objective']
|
|
302
|
+
self.version = 3
|
|
303
|
+
else:
|
|
304
|
+
flux_values = json_dict
|
|
305
|
+
elif filename.endswith('val'): # CellNetAnalyzer scenario
|
|
306
|
+
self.file_name = ""
|
|
307
|
+
flux_values = dict()
|
|
308
|
+
for line in fp:
|
|
309
|
+
line = line.strip()
|
|
310
|
+
if len(line) > 0 and not line.startswith("##"):
|
|
311
|
+
try:
|
|
312
|
+
reac_id, val = line.split()
|
|
313
|
+
val = float(val)
|
|
314
|
+
flux_values[reac_id] = (val, val)
|
|
315
|
+
except Exception:
|
|
316
|
+
print("Could not parse line ", line)
|
|
317
|
+
|
|
318
|
+
reactions = []
|
|
319
|
+
scen_values = []
|
|
320
|
+
for reac_id, val in flux_values.items():
|
|
321
|
+
found_reac_id = False
|
|
322
|
+
if reac_id in appdata.project.cobra_py_model.reactions:
|
|
323
|
+
found_reac_id = True
|
|
324
|
+
elif reac_id.startswith("R_"):
|
|
325
|
+
reac_id = reac_id[2:]
|
|
326
|
+
if reac_id in appdata.project.cobra_py_model.reactions:
|
|
327
|
+
found_reac_id = True
|
|
328
|
+
if found_reac_id:
|
|
329
|
+
reactions.append(reac_id)
|
|
330
|
+
scen_values.append(val)
|
|
331
|
+
else:
|
|
332
|
+
unknown_ids.append(reac_id)
|
|
333
|
+
appdata.scen_values_set_multiple(reactions, scen_values)
|
|
334
|
+
|
|
335
|
+
return unknown_ids, incompatible_constraints, skipped_scenario_reactions
|
|
336
|
+
|
|
337
|
+
def add_scenario_reactions_to_model(self, model: cobra.Model):
|
|
338
|
+
if len(self.reactions) > 0:
|
|
339
|
+
scenario_metabolites = set()
|
|
340
|
+
for metabolites,_,_ in self.reactions.values():
|
|
341
|
+
scenario_metabolites.update(metabolites.keys())
|
|
342
|
+
scenario_metabolites = scenario_metabolites.difference(model.metabolites.list_attr("id"))
|
|
343
|
+
model.add_metabolites([cobra.Metabolite(met_id) for met_id in scenario_metabolites])
|
|
344
|
+
for reac_id,(metabolites,lb,ub) in self.reactions.items():
|
|
345
|
+
if reac_id in model.reactions: # overwrite existing reaction
|
|
346
|
+
reaction = model.reactions.get_by_id(reac_id)
|
|
347
|
+
reaction.subtract_metabolites(reaction.metabolites, combine=True) # remove current metabolites
|
|
348
|
+
else:
|
|
349
|
+
reaction = cobra.Reaction(reac_id, lower_bound=lb, upper_bound=ub)
|
|
350
|
+
model.add_reactions([reaction])
|
|
351
|
+
reaction.add_metabolites(metabolites)
|
|
352
|
+
reaction.set_hash_value()
|
|
353
|
+
|
|
354
|
+
def clear_flux_values(self):
|
|
355
|
+
super().clear()
|
|
356
|
+
|
|
357
|
+
def clear(self):
|
|
358
|
+
super().clear()
|
|
359
|
+
self.__init__()
|
|
360
|
+
|
|
361
|
+
class IDList(object):
|
|
362
|
+
"""
|
|
363
|
+
provides a list of identifiers (id_list) and a corresponding QStringListModel (ids_model)
|
|
364
|
+
the identifiers can be set with the set_ids method
|
|
365
|
+
the implementation guarantees that id() of the properties id_list and ids_model
|
|
366
|
+
is constant so that they can be used like const references
|
|
367
|
+
"""
|
|
368
|
+
def __init__(self):
|
|
369
|
+
self._id_list: list = []
|
|
370
|
+
self._ids_model: QStringListModel = QStringListModel()
|
|
371
|
+
|
|
372
|
+
def set_ids(self, *id_lists: List[str]):
|
|
373
|
+
self._id_list.clear()
|
|
374
|
+
for id_list in id_lists:
|
|
375
|
+
self._id_list[len(self._id_list):] = id_list
|
|
376
|
+
self._ids_model.setStringList(self._id_list)
|
|
377
|
+
|
|
378
|
+
@property # getter only
|
|
379
|
+
def id_list(self) -> List[str]:
|
|
380
|
+
return self._id_list
|
|
381
|
+
|
|
382
|
+
@property # getter only
|
|
383
|
+
def ids_model(self) -> QStringListModel:
|
|
384
|
+
return self._ids_model
|
|
385
|
+
|
|
386
|
+
def replace_entry(self, old: str, new: str):
|
|
387
|
+
idx = self._id_list.index(old)
|
|
388
|
+
self._id_list[idx] = new
|
|
389
|
+
self._ids_model.setData(self._ids_model.index(idx), new)
|
|
390
|
+
|
|
391
|
+
def __len__(self) -> int:
|
|
392
|
+
return len(self._id_list)
|
|
393
|
+
|
|
394
|
+
class ProjectData:
|
|
395
|
+
''' The cnapy project data '''
|
|
396
|
+
|
|
397
|
+
def __init__(self):
|
|
398
|
+
self.name = "Unnamed project"
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
cobra.Model(id_or_model="empty", name="empty")
|
|
402
|
+
except gurobipy.GurobiError as error:
|
|
403
|
+
# See https://github.com/cnapy-org/CNApy/issues/490 and
|
|
404
|
+
# https://github.com/opencobra/cobrapy/issues/854
|
|
405
|
+
# When Gurobi (or another external solver) is found by cobrapy
|
|
406
|
+
# but not correctly configured, the model cannot be built :-|
|
|
407
|
+
# Hence, in this exception, we try the model instantiation
|
|
408
|
+
# with GLPK in the hope that it works with this solver :3
|
|
409
|
+
configuration = cobra.Configuration()
|
|
410
|
+
configuration.solver = "glpk"
|
|
411
|
+
|
|
412
|
+
msgBox = QMessageBox()
|
|
413
|
+
msgBox.setWindowTitle("Gurobi Error!")
|
|
414
|
+
msgBox.setText(
|
|
415
|
+
"Gurobi could not be set as solver due to the following error:\n" +\
|
|
416
|
+
error.message+"\n"+\
|
|
417
|
+
"If this error cannot be resolved, try using a different solver by changing " +\
|
|
418
|
+
"it under 'Config->Configure cobrapy').\n"+\
|
|
419
|
+
"Right now, GLPK is set as alternative solver instead of Gurobi."
|
|
420
|
+
)
|
|
421
|
+
msgBox.setIcon(QMessageBox.Warning)
|
|
422
|
+
msgBox.exec()
|
|
423
|
+
|
|
424
|
+
self.cobra_py_model = CNApyModel()
|
|
425
|
+
self.reaction_ids: IDList = IDList() # reaction IDs of the cobra_py_model and scenario reactions
|
|
426
|
+
|
|
427
|
+
default_map = CnaMap("Map")
|
|
428
|
+
self.maps = {"Map": default_map}
|
|
429
|
+
self.scen_values: Scenario = Scenario()
|
|
430
|
+
self.clipboard: Dict[str, Tuple[float, float]] = {}
|
|
431
|
+
self.solution: cobra.Solution = None
|
|
432
|
+
self.comp_values: Dict[str, Tuple[float, float]] = {}
|
|
433
|
+
self.comp_values_type = 0 # 0: simple flux vector, 1: bounds/FVA result
|
|
434
|
+
self.fva_values: Dict[str, Tuple[float, float]] = {} # store FVA results persistently
|
|
435
|
+
self.conc_values: Dict[str, float] = {} # Metabolite concentrations
|
|
436
|
+
self.df_values: Dict[str, float] = {} # Driving forces
|
|
437
|
+
self.modes = []
|
|
438
|
+
self.meta_data = {}
|
|
439
|
+
|
|
440
|
+
def load_scenario_into_model(self, model: cobra.Model):
|
|
441
|
+
for x in self.scen_values:
|
|
442
|
+
try:
|
|
443
|
+
y = model.reactions.get_by_id(x)
|
|
444
|
+
except KeyError:
|
|
445
|
+
print('reaction', x, 'not found!')
|
|
446
|
+
else:
|
|
447
|
+
y.bounds = self.scen_values[x]
|
|
448
|
+
y.set_hash_value()
|
|
449
|
+
|
|
450
|
+
self.scen_values.add_scenario_reactions_to_model(model)
|
|
451
|
+
|
|
452
|
+
if self.scen_values.use_scenario_objective:
|
|
453
|
+
model.objective = model.problem.Objective(
|
|
454
|
+
Zero, direction=self.scen_values.objective_direction)
|
|
455
|
+
for reac_id, coeff in self.scen_values.objective_coefficients.items():
|
|
456
|
+
try:
|
|
457
|
+
reaction: cobra.Reaction = model.reactions.get_by_id(reac_id)
|
|
458
|
+
except KeyError:
|
|
459
|
+
print('reaction', reac_id, 'not found!')
|
|
460
|
+
else:
|
|
461
|
+
model.objective.set_linear_coefficients(
|
|
462
|
+
{reaction.forward_variable: coeff, reaction.reverse_variable: -coeff})
|
|
463
|
+
|
|
464
|
+
for (expression, constraint_type, rhs) in self.scen_values.constraints:
|
|
465
|
+
if constraint_type == '=':
|
|
466
|
+
lb = rhs
|
|
467
|
+
ub = rhs
|
|
468
|
+
elif constraint_type == '<=':
|
|
469
|
+
lb = None
|
|
470
|
+
ub = rhs
|
|
471
|
+
elif constraint_type == '>=':
|
|
472
|
+
lb = rhs
|
|
473
|
+
ub = None
|
|
474
|
+
else:
|
|
475
|
+
print("Skipping constraint of unknown type", constraint_type)
|
|
476
|
+
continue
|
|
477
|
+
try:
|
|
478
|
+
reactions = model.reactions.get_by_any(list(expression))
|
|
479
|
+
except KeyError:
|
|
480
|
+
print("Skipping constraint containing a reaction that is not in the model:", expression)
|
|
481
|
+
continue
|
|
482
|
+
constr = model.problem.Constraint(Zero, lb=lb, ub=ub)
|
|
483
|
+
model.add_cons_vars(constr)
|
|
484
|
+
for (reaction, coeff) in zip(reactions, expression.values()):
|
|
485
|
+
constr.set_linear_coefficients({reaction.forward_variable: coeff, reaction.reverse_variable: -coeff})
|
|
486
|
+
|
|
487
|
+
reaction_ids = [reaction.id for reaction in model.reactions]
|
|
488
|
+
for annotation in self.scen_values.annotations:
|
|
489
|
+
if "reaction_id" not in annotation.keys():
|
|
490
|
+
continue
|
|
491
|
+
if annotation["reaction_id"] not in reaction_ids:
|
|
492
|
+
continue
|
|
493
|
+
reaction: cobra.Reaction = model.reactions.get_by_id(annotation["reaction_id"])
|
|
494
|
+
reaction.annotation[annotation["key"]] = annotation["value"]
|
|
495
|
+
|
|
496
|
+
def collect_default_scenario_values(self) -> Tuple[List[str], List[Tuple[float, float]]]:
|
|
497
|
+
reactions = []
|
|
498
|
+
values = []
|
|
499
|
+
for r in self.cobra_py_model.reactions:
|
|
500
|
+
if 'cnapy-default' in r.annotation.keys():
|
|
501
|
+
reactions.append(r.id)
|
|
502
|
+
values.append(parse_scenario(r.annotation['cnapy-default']))
|
|
503
|
+
return reactions, values
|
|
504
|
+
|
|
505
|
+
def update_reaction_id_lists(self):
|
|
506
|
+
self.reaction_ids.set_ids(self.cobra_py_model.reactions.list_attr("id"), self.scen_values.reactions.keys())
|
|
507
|
+
|
|
508
|
+
# currently unused
|
|
509
|
+
# def scenario_hash_value(self):
|
|
510
|
+
# return hashlib.md5(pickle.dumps(sorted(self.scen_values.items()))).digest()
|
|
511
|
+
|
|
512
|
+
def CnaMap(name):
|
|
513
|
+
background_svg = pkg_resources.resource_filename(
|
|
514
|
+
'cnapy', 'data/default-bg.svg')
|
|
515
|
+
return {"name": name,
|
|
516
|
+
"background": background_svg,
|
|
517
|
+
"bg-size": 1,
|
|
518
|
+
"box-size": 1,
|
|
519
|
+
"zoom": 0,
|
|
520
|
+
"pos": (0, 0),
|
|
521
|
+
"boxes": {},
|
|
522
|
+
"view": "cnapy", # either "cnapy" or "escher"
|
|
523
|
+
"escher_map_data": "" # JSON string
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
def parse_scenario(text: str) -> Tuple[float, float]:
|
|
527
|
+
"""parse a string that describes a valid scenario value"""
|
|
528
|
+
try:
|
|
529
|
+
x = float(text)
|
|
530
|
+
return (x, x)
|
|
531
|
+
except ValueError:
|
|
532
|
+
return(make_tuple(text))
|
|
533
|
+
|
|
534
|
+
def my_mean(value):
|
|
535
|
+
if isinstance(value, float):
|
|
536
|
+
return value
|
|
537
|
+
else:
|
|
538
|
+
(vl, vh) = value
|
|
539
|
+
return (vl+vh)/2
|