Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions .github/workflows/CI-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,6 @@ jobs:
echo 'export PATH="${{ env.pythonLocation }}/bin:$PATH"' >> ~/.bash_profile
- name: Check Python Version
run: python --version
- name: Java Setup
uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Set JAVA_HOME for mac OS
if: runner.os == 'macOS'
run: echo "JAVA_HOME=$(/usr/libexec/java_home)" >> "$GITHUB_ENV"
- name: Build for PyPI (if pip)
if: matrix.package-manager == 'pip'
run: |
Expand All @@ -97,10 +89,6 @@ jobs:
if: matrix.package-manager == 'pip'
run: |
pip install pyscipopt
- name: Install jpype1 (pip)
if: matrix.package-manager == 'pip'
run: |
pip install jpype1
- name: Install pytest (pip)
if: matrix.package-manager == 'pip'
run: |
Expand Down Expand Up @@ -139,10 +127,6 @@ jobs:
if: matrix.package-manager == 'conda'
run: |
micromamba install -c conda-forge pyscipopt scip
- name: Install jpype1 (conda)
if: matrix.package-manager == 'conda'
run: |
pip install jpype1
- name: Install pytest (conda)
if: matrix.package-manager == 'conda'
run: |
Expand Down
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ To get started, check out the [StrainDesign documentation](https://straindesign.
<img src="https://raw.githubusercontent.com/klamt-lab/straindesign/host_gifs/docs/plot.gif" width="340" alt="Flux-space animation">
</p>

Parts of the compression routine are done by efmtool's compression function ([csb.ethz.ch/tools/software/efmtool.html](https://csb.ethz.ch/tools/software/efmtool.html)<a href="#ref6"><sup>[6]</sup></a>). Therefore some source code from the [efmtool_link](https://github.com/cnapy-org/efmtool_link) package was adopted.
The compression routine follows the approach of efmtool's compression function ([csb.ethz.ch/tools/software/efmtool.html](https://csb.ethz.ch/tools/software/efmtool.html)<a href="#ref6"><sup>[6]</sup></a>), reimplemented in pure Python with exact rational arithmetic.

## Installation

Expand Down Expand Up @@ -51,10 +51,6 @@ pip install -e .

in the main folder. Through the installation with `-e`, updates from a `git pull` are at once available in your Python environment without the need for a reinstallation.

### Legacy Java backend (optional)

Java is not required for the default compression (`compression_backend='sparse_rref'`). A legacy Java-based EFMTool backend (`compression_backend='efmtool_rref'`) is optionally available via `pip install straindesign[java]`. For setup help see the [Legacy Methods](https://straindesign.readthedocs.io/en/latest/legacy_methods.html) documentation page.

## Install additional solvers

The cobra package is shipped with the GLPK solver. The more powerful commercial solvers IBM CPLEX and Gurobi may be used by cobra and the straindesign package. This makes sense in particular when using strain design algorithms like MCS, OptKnock etc. As another alternative solver, SCIP may be used. In the following, you will find installation instructions for the individual solvers.
Expand Down
210 changes: 4 additions & 206 deletions docs/source/developers_guide.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/source/examples/JN_08_compression.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"cell_type": "markdown",
"metadata": {},
"source": "# Standalone network compression\n\nAn effective network compression is essential to any strain design computation. Since it may also be of interest outside the context of strain design, this example may help you using the network compression routine independently. Likewise, StrainDesign also offers the integration of GPR rules into the metabolic networks as a separate function.\n\nThe network compression routine removes blocked reactions, removes conservation relations and then alternately lumps **coupled** reactions (compress_model_coupled, using a sparse integer RREF nullspace algorithm by default) and **parallel** reactions (compress_model_parallel). The compression returns a compressed network and a list of so-called \"compression maps\". Each map consists of a dictionary that contains complete information for reversing the compression steps successively and expand information obtained from the compressed model to the full model. Each entry of each map contains the id of a compressed reaction, associated with the original reaction names and their factor (provided as a rational number) with which they were lumped.\n\nThe default is compression_backend='sparse_rref' (pure Python, no extra dependencies). A legacy Java-based backend, compression_backend='efmtool_rref', is also available via pip install straindesign[java].\n\nFurthermore, the user can select reactions that should be exempt from the parallel compression. In the following, we provide the code snippet that can be used to call the compression."
"source": "# Standalone network compression\n\nAn effective network compression is essential to any strain design computation. Since it may also be of interest outside the context of strain design, this example may help you using the network compression routine independently. Likewise, StrainDesign also offers the integration of GPR rules into the metabolic networks as a separate function.\n\nThe network compression routine removes blocked reactions, removes conservation relations and then alternately lumps **coupled** reactions (compress_model_coupled, using a sparse integer RREF nullspace algorithm) and **parallel** reactions (compress_model_parallel). The compression returns a compressed network and a list of so-called \"compression maps\". Each map consists of a dictionary that contains complete information for reversing the compression steps successively and expand information obtained from the compressed model to the full model. Each entry of each map contains the id of a compressed reaction, associated with the original reaction names and their factor (provided as a rational number) with which they were lumped.\n\nFurthermore, the user can select reactions that should be exempt from the parallel compression. In the following, we provide the code snippet that can be used to call the compression."
},
{
"cell_type": "code",
Expand Down Expand Up @@ -257,4 +257,4 @@
},
"nbformat": 4,
"nbformat_minor": 2
}
}
3 changes: 1 addition & 2 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ The comprehensive StrainDesign package for MILP-based strain design computation
:width: 40%
:alt: Plot animation

The default compression uses a pure Python sparse RREF implementation. A legacy Java-based compression via EFMTool :html:`<a href="#ref6"><sup>[6]</sup></a>` is optionally available (see :doc:`legacy_methods`). Note that the Java backend (via JPype) is known to conflict with CPLEX's native library when both are loaded in the same Python session. If you use CPLEX, we recommend the default Python compression backend.
Network compression uses a pure Python sparse RREF implementation with exact rational arithmetic, following the approach of EFMTool :html:`<a href="#ref6"><sup>[6]</sup></a>`. It has no dependencies beyond NumPy/SciPy.

:html:`<a id="installation"></a>`\ Installation:
================================================
Expand Down Expand Up @@ -136,7 +136,6 @@ How to cite:
examples/JN_08_compression.ipynb
9_cnapy_integration
api_reference
legacy_methods
developers_guide

..
Expand Down
42 changes: 0 additions & 42 deletions docs/source/legacy_methods.rst

This file was deleted.

5 changes: 0 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ dependencies = [
"psutil",
]

[project.optional-dependencies]
java = ["jpype1"]

[project.urls]
Homepage = "https://github.com/klamt-lab/straindesign"
Documentation = "https://straindesign.readthedocs.io/en/latest/index.html"
Expand All @@ -43,5 +40,3 @@ Source = "https://github.com/klamt-lab/straindesign/"
[tool.setuptools.packages.find]
include = ["straindesign*"]

[tool.setuptools.package-data]
straindesign = ["efmtool.jar"]
11 changes: 1 addition & 10 deletions straindesign/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,6 @@ def __exit__(self, exit_type, exit_value, exit_traceback):
if module_exists("pyscipopt"):
avail_solvers.add(SCIP)

# Conditional eager JVM startup — required for stable JPype operation.
# The JVM must start before NumPy/OpenBLAS spawns worker threads, otherwise
# JNI calls crash with SIGBUS/SIGSEGV (jpype#808, jpype#934).
# No-op when jpype1 or Java is not installed (neither is a dependency).
# See developers_guide.md "efmtool_cmp_interface.py — JPype/JVM Initialization".
from .efmtool_cmp_interface import _start_jvm as _start_jvm
_start_jvm()
del _start_jvm

from .solver_interface import *
from .indicatorConstraints import *
from .pool import *
Expand All @@ -64,4 +55,4 @@ def __exit__(self, exit_type, exit_value, exit_traceback):
from .strainDesignProblem import *
from .strainDesignMILP import *
from .compute_strain_designs import *
from .compression import sparse_nullspace, sparse_nullspace as nullspace, RationalMatrix, ExactCOO
from .compression import sparse_nullspace, RationalMatrix, ExactCOO
79 changes: 14 additions & 65 deletions straindesign/compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -2092,7 +2092,7 @@ def simplify_model_gprs(model, budget=50000):
logging.info(' GPR rule simplification: %d rules, %d rewritten.' % (n, nchg))


def compress_model(model, no_par_compress_reacs=set(), compression_backend='sparse_rref', propagate_gpr=False,
def compress_model(model, no_par_compress_reacs=set(), propagate_gpr=False,
no_coupled_compress_reacs=set()):
"""Compress a metabolic model using multiple techniques.

Expand All @@ -2108,11 +2108,6 @@ def compress_model(model, no_par_compress_reacs=set(), compression_backend='spar
un-merged through COMPRESS#1 so that gene multiplicity is preserved exactly once
GPR rules are integrated (correct gene-regulatory semantics under compression).
To also exempt them from parallel merging, include them in no_par_compress_reacs.
compression_backend: Compression backend to use:
- 'sparse_rref' (default): Pure Python sparse integer RREF.
No external dependencies beyond NumPy/SciPy.
- 'efmtool_rref' (legacy): Java-based EFMTool via JPype.
Requires a JVM and the jpype1 package.
propagate_gpr: If True, propagate and simplify GPR rules through
compression (AND for coupled, OR for parallel merges).
Empty GPR rules are correctly handled: skipped in AND (always
Expand All @@ -2126,16 +2121,6 @@ def compress_model(model, no_par_compress_reacs=set(), compression_backend='spar
no_coupled_compress_reacs = set(no_coupled_compress_reacs)
with suppress_lp_context(model):
cmp_mapReac = []
use_java = (compression_backend == 'efmtool_rref')
if use_java:
# The Python compressor re-expresses each lump in one member's units (see
# StoichMatrixCompressor._restore_group_scale); the legacy Java backend does not, so a
# lump can come out at an extreme scale. The returned map carries the factor, so
# expanding a design stays exact -- but a bound stated on a lumped reaction is read in
# the lump's units, which is how 'biomass >= 0.001' can end up below feasibility tolerance.
LOG.warning(' Compression backend "efmtool_rref" does not normalize lumped-reaction '
'scales; bounds and constraints on lumped reactions are expressed in the '
'lump\'s units. Use "sparse_rref" if you constrain lumped reactions.')
LOG.info(' Removing blocked reactions.')
remove_blocked_reactions(model)
LOG.info(' Converting coefficients to rationals.')
Expand All @@ -2155,10 +2140,7 @@ def compress_model(model, no_par_compress_reacs=set(), compression_backend='spar
cmp_mapReac.append({"reac_map_exp": reac_map_exp, "parallel": True})

# 2. Conservation relation removal (reduces S rows for RREF)
if use_java:
_remove_conservation_relations_java(model)
else:
remove_conservation_relations(model)
remove_conservation_relations(model)

# 3. Exit if either parallel or coupled found nothing (after
# at least one full cycle). If one step found nothing,
Expand All @@ -2171,7 +2153,7 @@ def compress_model(model, no_par_compress_reacs=set(), compression_backend='spar
# 4. Coupled (expensive — nullspace/RREF)
numr_pre = len(model.reactions)
LOG.info(f' Compression {run}: Lumping coupled reactions.')
reac_map_exp = compress_model_coupled(model, compression_backend,
reac_map_exp = compress_model_coupled(model,
propagate_gpr=propagate_gpr,
protected_reactions=no_coupled_compress_reacs)
for new_reac, old_reac_val in reac_map_exp.items():
Expand All @@ -2198,38 +2180,20 @@ def compress_model(model, no_par_compress_reacs=set(), compression_backend='spar
return cmp_mapReac


def _remove_conservation_relations_java(model) -> None:
"""Remove conservation relations using Java efmtool."""
from . import efmtool_cmp_interface as efm
stoich_mat = create_stoichiometric_matrix(model, array_type='lil')
basic_mets = efm.basic_columns_rat_java(stoich_mat.transpose().toarray(), tolerance=0)
dependent = [model.metabolites[i] for i in set(range(len(model.metabolites))) - set(basic_mets)]
if dependent:
model.remove_metabolites(dependent)


def compress_model_coupled(model, compression_backend='sparse_rref', propagate_gpr=False,
suppressed_reactions=set(), protected_reactions=set()):
def compress_model_coupled(model, propagate_gpr=False, protected_reactions=set()):
"""Compress by lumping stoichiometrically coupled (dependent) reactions.

Identifies groups of reactions whose flux vectors are proportional in every
steady state (i.e. they share a common nullspace direction) and merges each
group into a single lumped reaction. Both the pure-Python and legacy Java
backends perform this operation; the compression_backend controls the nullspace algorithm.
group into a single lumped reaction, via the sparse integer RREF nullspace.

Args:
model: COBRA model to compress in-place
compression_backend: 'sparse_rref' (default, Python) or 'efmtool_rref' (Java legacy)
propagate_gpr: If True, AND-combine GPR rules of merged reactions
(with sympy simplification). Empty GPRs are skipped. Default False.
suppressed_reactions: Set of reaction IDs to exclude from compression
(Java backend only). Used to protect reactions referenced in strain
design constraints from being deleted by the Java compressor's
CoupledContradicting logic. Ignored for the Python backend (which
handles contradicting groups correctly via bounds intersection).
protected_reactions: Set of reaction IDs to exempt from coupled merging
(kept as their own reactions; the rest of their coupled group still
merges). Python (sparse_rref) backend only. Used to keep gene-controlled
merges). Used to keep gene-controlled
reactions intact through compression before GPR integration so that the
gene multiplicity is preserved (correct gene-regulatory semantics).

Expand All @@ -2239,27 +2203,18 @@ def compress_model_coupled(model, compression_backend='sparse_rref', propagate_g
# Compression is pure linear algebra; keep it off the optlang solver.
from straindesign.networktools import suppress_lp_context
with suppress_lp_context(model):
# Save GPR AST bodies before either backend clears them
# Save GPR AST bodies before compression clears them
if propagate_gpr:
saved_gpr_bodies = {r.id: r.gpr.body for r in model.reactions}

if compression_backend == 'efmtool_rref':
from .efmtool_cmp_interface import compress_model_java
reaction_map = compress_model_java(model, suppressed_reactions=suppressed_reactions)
# Clean up any remaining zero-flux reactions that the Java compressor created.
zero_flux = {r for r in model.reactions if r.lower_bound == 0 and r.upper_bound == 0}
for r in zero_flux:
reaction_map.pop(r.id, None)
if zero_flux:
model.remove_reactions(list(zero_flux), remove_orphans=True)
else:
# Clear gene rules to match Java behavior
for r in model.reactions:
r.gene_reaction_rule = ''
# Gene rules are cleared here and re-derived below from the saved ASTs, so a lumped
# reaction's rule is the AND-combination of its members rather than one member's.
for r in model.reactions:
r.gene_reaction_rule = ''

result = compress_cobra_model(model, methods=CompressionMethod.standard(), in_place=True,
protected_reactions=protected_reactions)
reaction_map = result.reaction_map
result = compress_cobra_model(model, methods=CompressionMethod.standard(), in_place=True,
protected_reactions=protected_reactions)
reaction_map = result.reaction_map

# Propagate GPR rules: AND-combine contributing reactions' GPR ASTs
if propagate_gpr:
Expand All @@ -2274,11 +2229,6 @@ def compress_model_coupled(model, compression_backend='sparse_rref', propagate_g
return reaction_map


# Backward-compatibility alias (old name referenced efmtool, but the function
# is backend-agnostic — the new name compress_model_coupled is preferred).
compress_model_efmtool = compress_model_coupled


def compress_model_parallel(model, protected_rxns=set(), propagate_gpr=False):
"""Compress by lumping parallel reactions.

Expand Down Expand Up @@ -2413,7 +2363,6 @@ def _parallel_key(i):
# High-level API
'compress_model',
'compress_model_coupled',
'compress_model_efmtool', # backward-compat alias
'compress_model_parallel',
# GPR propagation helpers
'_gpr_ast_to_expr',
Expand Down
Loading
Loading