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.
Files changed (70) hide show
  1. cnapy/__init__.py +16 -0
  2. cnapy/__main__.py +37 -0
  3. cnapy/appdata.py +539 -0
  4. cnapy/application.py +241 -0
  5. cnapy/core.py +372 -0
  6. cnapy/core_gui.py +37 -0
  7. cnapy/data/blank.svg +259 -0
  8. cnapy/data/check.svg +4 -0
  9. cnapy/data/clear.svg +7 -0
  10. cnapy/data/cnapylogo.svg +707 -0
  11. cnapy/data/cnapylogo_no_text.svg +252 -0
  12. cnapy/data/cross.svg +6 -0
  13. cnapy/data/d-font.svg +21 -0
  14. cnapy/data/default-bg.svg +258 -0
  15. cnapy/data/default-color.svg +150 -0
  16. cnapy/data/escher.min.js +78 -0
  17. cnapy/data/escher_cnapy.html +305 -0
  18. cnapy/data/heat.svg +143 -0
  19. cnapy/data/onoff.svg +117 -0
  20. cnapy/data/qmark.svg +5 -0
  21. cnapy/data/redo.svg +44 -0
  22. cnapy/data/save.svg +16 -0
  23. cnapy/data/undo.svg +35 -0
  24. cnapy/data/zoom-in.svg +16 -0
  25. cnapy/data/zoom-out.svg +16 -0
  26. cnapy/flux_vector_container.py +88 -0
  27. cnapy/gui_elements/__init__.py +16 -0
  28. cnapy/gui_elements/about_dialog.py +50 -0
  29. cnapy/gui_elements/annotation_widget.py +120 -0
  30. cnapy/gui_elements/box_position_dialog.py +79 -0
  31. cnapy/gui_elements/central_widget.py +784 -0
  32. cnapy/gui_elements/clipboard_calculator.py +136 -0
  33. cnapy/gui_elements/config_cobrapy_dialog.py +146 -0
  34. cnapy/gui_elements/config_dialog.py +274 -0
  35. cnapy/gui_elements/configuration_cplex.py +179 -0
  36. cnapy/gui_elements/configuration_gurobi.py +139 -0
  37. cnapy/gui_elements/download_dialog.py +82 -0
  38. cnapy/gui_elements/efm_dialog.py +212 -0
  39. cnapy/gui_elements/efmtool_dialog.py +114 -0
  40. cnapy/gui_elements/escher_map_view.py +268 -0
  41. cnapy/gui_elements/flux_feasibility_dialog.py +571 -0
  42. cnapy/gui_elements/flux_optimization_dialog.py +121 -0
  43. cnapy/gui_elements/gene_list.py +292 -0
  44. cnapy/gui_elements/in_out_flux_dialog.py +42 -0
  45. cnapy/gui_elements/main_window.py +2445 -0
  46. cnapy/gui_elements/map_view.py +736 -0
  47. cnapy/gui_elements/mcs_dialog.py +454 -0
  48. cnapy/gui_elements/metabolite_list.py +479 -0
  49. cnapy/gui_elements/mode_navigator.py +459 -0
  50. cnapy/gui_elements/model_info.py +106 -0
  51. cnapy/gui_elements/plot_space_dialog.py +211 -0
  52. cnapy/gui_elements/reaction_table_widget.py +96 -0
  53. cnapy/gui_elements/reactions_list.py +1001 -0
  54. cnapy/gui_elements/rename_map_dialog.py +48 -0
  55. cnapy/gui_elements/scenario_tab.py +525 -0
  56. cnapy/gui_elements/solver_buttons.py +78 -0
  57. cnapy/gui_elements/strain_design_dialog.py +1845 -0
  58. cnapy/gui_elements/thermodynamics_dialog.py +614 -0
  59. cnapy/gui_elements/yield_optimization_dialog.py +143 -0
  60. cnapy/gui_elements/yield_space_dialog.py +143 -0
  61. cnapy/resources.py +21575 -0
  62. cnapy/sd_ci_optmdfpathway.py +322 -0
  63. cnapy/sd_class_interface.py +884 -0
  64. cnapy/utils.py +293 -0
  65. cnapy/utils_for_cnapy_api.py +104 -0
  66. cnapy-1.2.1.dist-info/LICENSE +201 -0
  67. cnapy-1.2.1.dist-info/METADATA +216 -0
  68. cnapy-1.2.1.dist-info/RECORD +70 -0
  69. cnapy-1.2.1.dist-info/WHEEL +5 -0
  70. 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