Skip to content

Commit

Permalink
Add support for scenario annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
Paulocracy committed Jan 31, 2024
1 parent c43c61f commit 1754fc7
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 19 deletions.
18 changes: 14 additions & 4 deletions cnapy/appdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,16 @@ def __init__(self):
self.description: str = ""
self.constraints: List[List(Dict, str, float)] = [] # [reaction_id: coefficient dictionary, type, rhs]
self.reactions = {} # reaction_id: (coefficient dictionary, lb, ub), can overwrite existing reactions
self.annotations = [] # List of dicts with: { "id": $reac_id, "key": $key_value, "value": $value_at_key }
self.file_name: str = ""
self.has_unsaved_changes = False
self.version: int = 2
self.version: int = 3

def save(self, filename: str):
json_dict = {'fluxes': self, 'pinned_reactions': list(self.pinned_reactions), 'description': self.description,
'objective_direction': self.objective_direction, 'objective_coefficients': self.objective_coefficients,
'use_scenario_objective': self.use_scenario_objective, 'reactions': self.reactions,
'constraints': self.constraints, 'version': self.version}
'constraints': self.constraints, "annotations": self.annotations, 'version': self.version}
with open(filename, 'w') as fp:
json.dump(json_dict, fp)
self.has_unsaved_changes = False
Expand Down Expand Up @@ -288,13 +289,15 @@ def load(self, filename: str, appdata: AppData, merge=False) -> Tuple[List[str],
self.constraints.append(constr)
else:
incompatible_constraints.append(constr)
if json_dict['version'] >= 3:
self.annotations = json_dict["annotations"]
for reac_id, val in json_dict['objective_coefficients'].items():
if reac_id in all_reaction_ids:
self.objective_coefficients[reac_id] = val
else:
unknown_ids.append(reac_id)
self.use_scenario_objective = json_dict['use_scenario_objective']
self.version = 2
self.version = 3
else:
flux_values = json_dict
elif filename.endswith('val'): # CellNetAnalyzer scenario
Expand All @@ -307,7 +310,7 @@ def load(self, filename: str, appdata: AppData, merge=False) -> Tuple[List[str],
reac_id, val = line.split()
val = float(val)
flux_values[reac_id] = (val, val)
except:
except Exception:
print("Could not parse line ", line)

reactions = []
Expand Down Expand Up @@ -445,6 +448,13 @@ def load_scenario_into_model(self, model: cobra.Model):
for (reaction, coeff) in zip(reactions, expression.values()):
constr.set_linear_coefficients({reaction.forward_variable: coeff, reaction.reverse_variable: -coeff})

reac_ids = [x.id for x in model.reactions]
for annotation in self.scen_values.annotations:
if annotation["key"] not in reac_ids:
continue
reaction: cobra.Reaction = model.reactions.get_by_id(annotation["key"])
reaction.annotation[annotation["key"]] = annotation["value"]

def collect_default_scenario_values(self) -> Tuple[List[str], List[Tuple[float, float]]]:
reactions = []
values = []
Expand Down
96 changes: 81 additions & 15 deletions cnapy/gui_elements/scenario_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class ScenarioReactionColumn(IntEnum):
LB = 2
UB = 3

class ScenarioAnnotationColumn(IntEnum):
Id = 0
Key = 1
Value = 2

class IDList(object):
"""
provides a list of identifiers (id_list) and a corresponding QStringListModel (ids_model)
Expand Down Expand Up @@ -61,7 +66,7 @@ def __init__(self, central_widget):
self.reaction_ids: IDList = IDList()

layout = QVBoxLayout()
group = QGroupBox("Scenario ojective")
group = QGroupBox("Scenario objective")
self.objective_group_layout = QVBoxLayout()
self.use_scenario_objective = QCheckBox("Use scenario objective (overrides the model objective)")
self.use_scenario_objective.setEnabled(True)
Expand All @@ -71,11 +76,13 @@ def __init__(self, central_widget):
self.scenario_objective.set_wordlist(self.reaction_ids.id_list, replace_completer_model=False)
self.scenario_objective.set_completer_model(self.reaction_ids.ids_model)
self.objective_group_layout.addWidget(self.scenario_objective)
label = QLabel("Optimization direction")
self.objective_group_layout.addWidget(label)
self.optimization_direction_layout = QHBoxLayout()
label = QLabel("Optimization direction:")
self.optimization_direction_layout.addWidget(label)
self.scenario_opt_direction = QComboBox()
self.scenario_opt_direction.insertItems(0, ["minimize", "maximize"])
self.objective_group_layout.addWidget(self.scenario_opt_direction)
self.optimization_direction_layout.addWidget(self.scenario_opt_direction)
self.objective_group_layout.addLayout(self.optimization_direction_layout)
group.setLayout(self.objective_group_layout)
layout.addWidget(group)

Expand Down Expand Up @@ -122,20 +129,44 @@ def __init__(self, central_widget):
self.constraints.horizontalHeader().setStretchLastSection(True)
lower_layout.addWidget(self.constraints)

scenario_annotations = QWidget()
annotations_layout = QVBoxLayout(scenario_annotations)
label = QLabel("Scenario annotations")
label.setToolTip("The IDs of the scenario annotations are the affected reaction ID.")
hbox = QHBoxLayout()
hbox.addWidget(label)
self.add_annotation = QPushButton("+")
self.add_annotation.clicked.connect(self.add_scenario_annotation)
hbox.addWidget(self.add_annotation)
self.delete_annotation = QPushButton("-")
self.delete_annotation.clicked.connect(self.delete_scenario_annotation)
hbox.addWidget(self.delete_annotation)
hbox.addStretch()
annotations_layout.addLayout(hbox)
self.annotations = QTableWidget(0, len(ScenarioAnnotationColumn))
self.annotations.setHorizontalHeaderLabels([ScenarioAnnotationColumn(i).name for i in range(len(ScenarioAnnotationColumn))])
self.annotations.setEditTriggers(QAbstractItemView.CurrentChanged | QAbstractItemView.SelectedClicked)
annotations_layout.addWidget(self.annotations)

bottom = QWidget()
bottom_layout = QVBoxLayout(bottom)
label = QLabel("Scenario description")
lower_layout.addWidget(label)
bottom_layout.addWidget(label)
self.description = QTextEdit()
self.description.setPlaceholderText("Enter a description for this scenario")
lower_layout.addWidget(self.description)
bottom_layout.addWidget(self.description)

splitter = QSplitter(Qt.Vertical)
splitter.addWidget(upper)
splitter.addWidget(lower)
splitter.addWidget(scenario_annotations)
splitter.addWidget(bottom)
layout.addWidget(splitter)
self.setLayout(layout)

self.reactions.currentCellChanged.connect(self.handle_current_cell_changed)
self.reactions.cellChanged.connect(self.cell_content_changed)
self.annotations.cellChanged.connect(self.cell_content_changed_annotations)
self.equation.editingFinished.connect(self.equation_edited)
self.scenario_objective.textCorrect.connect(self.change_scenario_objective_coefficients)
self.scenario_objective.editingFinished.connect(self.validate_objective)
Expand All @@ -147,7 +178,7 @@ def __init__(self, central_widget):
self.update()

def update(self):
self.update_reation_id_lists()
self.update_reaction_id_lists()
if self.recreate_scenario_items_needed:
self.recreate_scenario_items()
self.recreate_scenario_items_needed = False
Expand All @@ -167,10 +198,10 @@ def update(self):
item.setText(flux_text)
item.setBackground(QBrush(background_color))

def update_reation_id_lists(self):
def update_reaction_id_lists(self):
self.reaction_ids.set_ids(self.appdata.project.cobra_py_model.reactions.list_attr("id"),
self.appdata.project.scen_values.reactions.keys())

def recreate_scenario_items(self):
# assumes that the objective, reactions and constraints are all valid
with QSignalBlocker(self.scenario_objective):
Expand Down Expand Up @@ -218,7 +249,7 @@ def cell_content_changed(self, row: int, column: int):
self.reactions.item(row, ScenarioReactionColumn.Id).setText(reac_id)
QMessageBox.information(self, 'Reaction ID already in use',
'Choose a different reaction identifier.')
self.update_reation_id_lists()
self.update_reaction_id_lists()
self.check_constraints_and_objective()
self.scenario_changed()
if self.appdata.auto_fba:
Expand All @@ -240,11 +271,24 @@ def cell_content_changed(self, row: int, column: int):
self.reactions.item(row, ScenarioReactionColumn.LB).setBackground(lb_brush)
self.reactions.item(row, ScenarioReactionColumn.UB).setBackground(ub_brush)

@Slot(int, int)
def cell_content_changed_annotations(self, row: int, column: int):
reac_id: str = self.annotations.item(row, ScenarioAnnotationColumn.Id).text()
key: str = self.annotations.item(row, ScenarioAnnotationColumn.Key).text()
value: str = self.annotations.item(row, ScenarioAnnotationColumn.Value).text()
changed_annotation = {
"id": reac_id,
"key": key,
"value": value,
}
self.appdata.project.scen_values[row] = changed_annotation
self.scenario_changed()

def verify_bound(self, item: QTableWidgetItem):
try:
val = float(item.text())
brush = white_brush
except:
except Exception:
val = float('NaN')
brush = red_brush
return val, brush
Expand Down Expand Up @@ -279,7 +323,7 @@ def equation_edited(self):
with QSignalBlocker(self.reactions):
self.update_reaction_row(row, reac_id)
equation_valid = True
except:
except Exception:
turn_red(self.equation)
with QSignalBlocker(self.equation):
QMessageBox.information(self, 'Cannot parse equation',
Expand Down Expand Up @@ -337,7 +381,7 @@ def add_scenario_reaction(self):
while not self.verify_scenario_reaction_id(reac_id):
reac_id += "_"
self.appdata.project.scen_values.reactions[reac_id] = [{}, cobra.Configuration().lower_bound, cobra.Configuration().upper_bound]
self.update_reation_id_lists()
self.update_reaction_id_lists()
self.check_constraints_and_objective()
with QSignalBlocker(self.reactions):
self.new_reaction_row(row)
Expand All @@ -353,13 +397,35 @@ def delete_scenario_reaction(self):
reac_id: str = self.reactions.item(row, ScenarioReactionColumn.Id).data(Qt.UserRole)
self.reactions.removeRow(row)
del self.appdata.project.scen_values.reactions[reac_id]
self.update_reation_id_lists()
self.update_reaction_id_lists()
self.check_constraints_and_objective()
self.reactions.setCurrentCell(self.reactions.currentRow(), 0) # to make the cell appear selected in the GUI
self.scenario_changed()
if self.appdata.auto_fba:
self.central_widget.parent.fba()

def add_scenario_annotation(self):
row: int = self.annotations.rowCount()
self.annotations.setRowCount(row + 1)
with QSignalBlocker(self.annotations):
self.appdata.project.scen_values.annotations.append({})
self.new_annotation_row(row)
self.scenario_changed()

def delete_scenario_annotation(self):
row: int = self.annotations.currentRow()
if row >= 0:
del(self.appdata.project.scen_values.annotations[row])
self.annotations.removeRow(row)
self.annotations.setCurrentCell(self.annotations.currentRow(), 0) # to make the cell appear selected in the GUI
self.scenario_changed()

def new_annotation_row(self, row: int):
self.annotations.setItem(row, ScenarioAnnotationColumn.Id, QTableWidgetItem())
self.annotations.setItem(row, ScenarioAnnotationColumn.Key, QTableWidgetItem())
self.annotations.setItem(row, ScenarioAnnotationColumn.Value, QTableWidgetItem())
self.scenario_changed()

def new_reaction_row(self, row: int):
item = QTableWidgetItem()
self.reactions.setItem(row, ScenarioReactionColumn.Id, item)
Expand All @@ -386,7 +452,7 @@ def add_constraint(self):
self.appdata.project.scen_values.constraints.append(Scenario.empty_constraint)
self.constraints.setCurrentCell(row, 0)
self.scenario_changed()

def add_constraint_row(self):
row: int = self.constraints.rowCount()
self.constraints.setRowCount(row + 1)
Expand Down

0 comments on commit 1754fc7

Please sign in to comment.