diff --git a/cnapy/gui_elements/main_window.py b/cnapy/gui_elements/main_window.py index 84e49e9f..c17cab82 100644 --- a/cnapy/gui_elements/main_window.py +++ b/cnapy/gui_elements/main_window.py @@ -46,7 +46,12 @@ from cnapy.gui_elements.configuration_cplex import CplexConfigurationDialog from cnapy.gui_elements.configuration_cplex_new import CplexNewConfigurationDialog from cnapy.gui_elements.configuration_gurobi import GurobiConfigurationDialog -from cnapy.gui_elements.thermodynamics_dialog import ThermodynamicAnalysisTypes, ThermodynamicDialog +from cnapy.gui_elements.thermodynamics_dialog import ( + ThermodynamicAnalysisTypes, + ThermodynamicDialog, + COBRAK_AVAILABLE, + COBRAK_IMPORT_ERROR, +) import cnapy.utils as utils SBML_suffixes = "*.xml *.sbml *.xml.gz *.sbml.gz *.xml.zip *.sbml.zip" @@ -424,6 +429,18 @@ def __init__(self, appdata: AppData, project_path: str | None, scenario_path: st bottleneck_action.triggered.connect(self.perform_bottleneck_analysis) self.thermodynamic_menu.addAction(bottleneck_action) + # These three analyses require the optional COBRA-k package. When it is not + # installed, grey the entries out and explain via tooltip instead of letting + # the user open a dialog that cannot run. (perform_* still guard defensively.) + if not COBRAK_AVAILABLE: + for _cobrak_action in (optmdf_action, tfba_action, bottleneck_action): + _cobrak_action.setEnabled(False) + _cobrak_action.setToolTip( + "Requires the optional COBRA-k package " + "(install with: pip install cnapy[thermodynamics])" + ) + self.thermodynamic_menu.setToolTipsVisible(True) + self.thermodynamic_menu.addSeparator() dG0_menu = self.thermodynamic_menu.addMenu("Load dG'° values [in kJ/mol] (replacing all current values)...") @@ -2163,8 +2180,29 @@ def set_status_unknown(self): self.solver_status_symbol.setStyleSheet("color: black") self.solver_status_symbol.setText("?") + def _require_cobrak(self) -> bool: + """Warn (and return False) if the optional COBRA-k package is missing. + + The thermodynamic methods depend on cobrak, which is an optional + dependency. Guarding here lets CNApy run even when it is not installed. + """ + if COBRAK_AVAILABLE: + return True + QMessageBox.warning( + self, + "COBRA-k not installed", + "The thermodynamic analyses require the optional 'cobrak' (COBRA-k) " + "package, which is not installed.\n\n" + "Install it with 'pip install cnapy[thermodynamics]' or from " + "https://github.com/klamt-lab/COBRA-k\n\n" + f"Import error: {COBRAK_IMPORT_ERROR}", + ) + return False + @Slot() def perform_optmdfpathway(self): + if not self._require_cobrak(): + return # Has to be in self to keep computation thread self.optmdfpathway_dialog = ThermodynamicDialog( self.appdata, @@ -2175,6 +2213,8 @@ def perform_optmdfpathway(self): @Slot() def perform_thermodynamic_fba(self): + if not self._require_cobrak(): + return # Has to be in self to keep computation thread self.thermodynamic_fba_dialog = ThermodynamicDialog( self.appdata, @@ -2185,6 +2225,8 @@ def perform_thermodynamic_fba(self): @Slot() def perform_bottleneck_analysis(self): + if not self._require_cobrak(): + return # Has to be in self to keep computation thread self.bottleneck_dialog = ThermodynamicDialog( self.appdata, diff --git a/cnapy/gui_elements/thermodynamics_dialog.py b/cnapy/gui_elements/thermodynamics_dialog.py index 5c7ac496..f7d5cca4 100644 --- a/cnapy/gui_elements/thermodynamics_dialog.py +++ b/cnapy/gui_elements/thermodynamics_dialog.py @@ -18,11 +18,22 @@ from cnapy.gui_elements.central_widget import CentralWidget from cnapy.gui_elements.solver_buttons import get_solver_buttons from enum import Enum -from cobrak.constants import LNCONC_VAR_PREFIX, DF_VAR_PREFIX, MDF_VAR_ID, ALL_OK_KEY, OBJECTIVE_VAR_NAME, TERMINATION_CONDITION_KEY -from cobrak.dataclasses import ExtraLinearConstraint, Solver -from cobrak.lps import perform_lp_optimization, perform_lp_thermodynamic_bottleneck_analysis -from cobrak.io import load_annotated_cobrapy_model_as_cobrak_model -from cobrak.cobrapy_model_functionality import get_fullsplit_cobra_model +# cobrak (COBRA-k) powers the thermodynamic methods but is treated as an optional +# dependency so CNApy can be installed and started even when cobrak is unavailable +# in the environment. Import it lazily and gate the "Thermodynamic analyses" menu +# actions on COBRAK_AVAILABLE (see main_window.py). Enable the feature with +# `pip install cnapy[thermodynamics]`. +try: + from cobrak.constants import LNCONC_VAR_PREFIX, DF_VAR_PREFIX, MDF_VAR_ID, ALL_OK_KEY, OBJECTIVE_VAR_NAME, TERMINATION_CONDITION_KEY + from cobrak.dataclasses import ExtraLinearConstraint, Solver + from cobrak.lps import perform_lp_optimization, perform_lp_thermodynamic_bottleneck_analysis + from cobrak.io import load_annotated_cobrapy_model_as_cobrak_model + from cobrak.cobrapy_model_functionality import get_fullsplit_cobra_model + COBRAK_AVAILABLE = True + COBRAK_IMPORT_ERROR = "" +except ModuleNotFoundError as _cobrak_err: + COBRAK_AVAILABLE = False + COBRAK_IMPORT_ERROR = str(_cobrak_err) diff --git a/pyproject.toml b/pyproject.toml index 53404a4e..cfca441d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,9 +34,16 @@ dependencies = [ "openpyxl", "jpype1==1.5.0", "setuptools", - "cobrak==0.0.10", + "nest_asyncio", ] +[project.optional-dependencies] +# COBRA-k powers the thermodynamic analyses. It is kept as an optional extra rather +# than a hard dependency so that a plain `pip install cnapy` always succeeds and the +# GUI starts, even if cobrak cannot be installed in a given environment. Enable the +# feature with `pip install cnapy[thermodynamics]`. +thermodynamics = ["cobrak>=0.0.11"] + [project.scripts] cnapy = "cnapy.__main__:main_cnapy" [project.urls]