From 97c616323f5fc3184b5f2ddbea0c933aea1d05f3 Mon Sep 17 00:00:00 2001
From: Phil
Date: Fri, 24 Jul 2026 22:05:36 -0400
Subject: [PATCH 1/2] refactor: remove the efmtool/Java compression backend and
all JPype coupling
The Python `sparse_rref` compressor is the default, the tested path, and the one
every correctness gate runs. The legacy Java backend was strictly worse: it does not
normalize lumped-reaction scales, so a bound stated on a lumped reaction is read in
the lump's units -- the mechanism behind the 4484x biomass trap on iML1515. It also
needed a JVM plus jpype1, and its tests could only run on the Windows CI leg because
JPype's JNI bridge segfaults non-deterministically on Linux/macOS (jpype#934).
Removed:
- `straindesign/efmtool.jar` (2.2 MB) and `straindesign/efmtool_cmp_interface.py`
- eager JVM startup from `straindesign/__init__.py`, which ran on *every* import
- the `compression_backend` parameter, now single-valued, from `compress_model`,
`compress_model_coupled` and `compute_strain_designs`
- `compress_model_efmtool`, the backward-compat alias named after the backend
- `suppressed_reactions` from `compress_model_coupled`: it was documented as
"Java backend only ... ignored for the Python backend" and was in fact unused
- the `[java]` extra and the jar from `package-data` in `pyproject.toml`
- Java setup and jpype1 install steps from the CI matrix
- `docs/source/legacy_methods.rst`, developer's guide section 3.11 (196 lines), and
the JPype/JVM notes scattered through the guide, README, index and the
compression notebook
Tests: dropped the four Java parity tests, the `jpype_available` fixture, the
`java` marker and its platform skip, `--java`, and `TestEfmtoolBackendGpr`. Two were
kept by converting rather than deleting them: `test_basic_columns_rat_python` now
calls `compression.basic_columns_from_numpy` directly (the deleted function was a
thin shim over it), and the backend-parity FBA test becomes
`test_fba_optimum_recovered_through_map`, asserting that the compressed optimum
recovers the uncompressed one through the compression map. That is the part of the
old test that was actually about correctness rather than about having two backends,
and it still exercises the map factors.
Kept deliberately: the EFMTool citation in README and index.rst. The compression
approach follows Terzer & Stelling; only the bundled implementation is gone. Note
the jar carried its own Simplified BSD licence (and a vendored JMatIO) that the
repository never reproduced, so removing it also closes that gap.
Verified: no `efmtool`/`jpype`/`java` reference remains outside those two citations;
jpype is not imported on `import straindesign`; the wheel drops from 3.08 MB to
0.70 MB; e_coli_core gene-MCS is 455, set-identical to the canonical reference.
Co-Authored-By: Claude
---
.github/workflows/CI-test.yml | 16 -
README.md | 6 +-
docs/source/developers_guide.md | 210 +-------
docs/source/examples/JN_08_compression.ipynb | 4 +-
docs/source/index.rst | 3 +-
docs/source/legacy_methods.rst | 42 --
pyproject.toml | 5 -
straindesign/__init__.py | 9 -
straindesign/compression.py | 79 +--
straindesign/compute_strain_designs.py | 6 +-
straindesign/efmtool.jar | Bin 2356134 -> 0 bytes
straindesign/efmtool_cmp_interface.py | 529 -------------------
straindesign/networktools.py | 1 -
tests/conftest.py | 20 -
tests/test_04_preprocessing.py | 73 ---
tests/test_07_compression.py | 210 ++------
16 files changed, 63 insertions(+), 1150 deletions(-)
delete mode 100644 docs/source/legacy_methods.rst
delete mode 100644 straindesign/efmtool.jar
delete mode 100644 straindesign/efmtool_cmp_interface.py
diff --git a/.github/workflows/CI-test.yml b/.github/workflows/CI-test.yml
index f0480a47..6d6c0103 100644
--- a/.github/workflows/CI-test.yml
+++ b/.github/workflows/CI-test.yml
@@ -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: |
@@ -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: |
@@ -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: |
diff --git a/README.md b/README.md
index a2a0d1e8..b8ba4897 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ To get started, check out the [StrainDesign documentation](https://straindesign.
-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)[6]). 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)[6]), reimplemented in pure Python with exact rational arithmetic.
## Installation
@@ -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.
diff --git a/docs/source/developers_guide.md b/docs/source/developers_guide.md
index 662622de..33de71a1 100644
--- a/docs/source/developers_guide.md
+++ b/docs/source/developers_guide.md
@@ -29,7 +29,7 @@ which drifts with every edit. Grep for the symbol.
1. [**Orientation & the strain-design problem**](#ch1) — the MCS problem, SUPPRESS/PROTECT/bilevel semantics, interventions & cost, the binary `z` vector, invocation, and the master notation table.
2. [**The constraint-based foundation**](#ch2) — `Sv=0`, the flux polytope/cone, FBA & FVA as LPs, the internal standard form, and the convex geometry needed for duality.
-3. [**Network compression**](#ch3) — exact rational nullspace compression; parallel, coupled, conservation and blocked reductions; lump scaling; GPR propagation and simplification; compression maps; and the legacy efmtool backend.
+3. [**Network compression**](#ch3) — exact rational nullspace compression; parallel, coupled, conservation and blocked reductions; lump scaling; GPR propagation and simplification; compression maps;.
4. [**GPR integration**](#ch4) — GPR reduction and Boolean simplification, `extend_model_gpr`, reversible splitting, module remapping, and the two-compression-pass boundary.
5. [**FVA in preprocessing**](#ch5) — pre-compression sign classification, desired-region essentiality, final bound/module FVA, the single-classical-module fold, and size-1 MCS extraction.
6. [**Dualization (the mathematical core)**](#ch6) — LP duality, Farkas certificates, and the strong-duality encodings shared by the supported module types.
@@ -67,9 +67,7 @@ straindesign/
├── gurobi_interface.py # Gurobi backend (Gurobi_MILP_LP)
├── scip_interface.py # SCIP backend (SCIP_MILP_LP)
├── glpk_interface.py # GLPK backend (GLPK_MILP_LP)
-├── efmtool_cmp_interface.py # EFMtool JAR interface (legacy compression backend)
├── pool.py # SDPool: cross-platform multiprocessing pool
-└── efmtool.jar # Bundled EFMtool binary
```
Which chapter covers which module: compression → [Ch 3](#ch3); GPR / networktools → [Ch 4](#ch4), [Ch 12](#ch12); FVA / lptools /
@@ -1082,7 +1080,7 @@ nonzero flux in any steady state — a *contradicting* group. Then the master *a
and `contradicting_removed` is set, which is the flag that triggers a re-iteration of
the whole pass (→): removing a contradicting group changes the flux space and may make
previously-uncoupled reactions coupled. A consistent (nonempty) group removes only the slaves
-. This bound-intersection logic replaced a Java-era behaviour that could drop
+. This bound-intersection logic replaced an earlier behaviour that could drop
reactions incorrectly; getting the translate-and-intersect direction right (especially the `λ<0` flip
and the `±inf` handling) is exactly the subject of the closed issue #44 cautionary tale in [Ch 10](#ch10).
@@ -1107,9 +1105,7 @@ Two design points. First, this is a **row-rank reduction**, complementary to the
§3.4 — together they push `S` toward full rank (the §3.1 hypothesis). Second, the *ordering* matters:
conservation removal runs *before* the expensive coupled step in each cycle (`compress_model`,
–). Fewer metabolite rows means the nullspace RREF that drives coupling detection operates
-on a smaller matrix, so removing dependent rows first makes the costliest stage cheaper. (There is a
-legacy Java oracle, `_remove_conservation_relations_java` at, selectable via the
-`efmtool_rref` backend; the default `sparse_rref` path uses the pure-Python exact RREF above.)
+on a smaller matrix, so removing dependent rows first makes the costliest stage cheaper.
### 3.6 Blocked and zero-flux removal
@@ -1266,202 +1262,6 @@ re-injection, and gene translation — are owned by [Ch 9](#ch9); this section o
map that [Ch 9](#ch9) consumes.
-### 3.11 The legacy efmtool (Java) backend
-
-Everything in §3.2–§3.10 describes the **default** compression engine: the pure-Python, exact
-integer/rational `sparse_rref` backend. That engine is a *reimplementation*. The original backend —
-and the one every pre-1.15 release actually ran — was **efmtool**, Marco Terzer's Java tool for
-elementary-flux-mode enumeration and network compression (the compression stage of efmtool is exactly
-the coupled/zero/contradicting reduction that §3.4/§3.6 now do in Python). It is still shipped and
-still reachable, selected with `compression_backend='efmtool_rref'`, and this section documents how the
-bridge works and *why* it has been demoted to legacy. Reading it also explains the vocabulary the
-Python code inherited: the Python `CompressionMethod` enum (`compression.py`), the Python class
-name `StoichMatrixCompressor` (`compression.py`), and the `CoupledZero`/`CoupledCombine`/
-`CoupledContradicting` method names are all deliberate echoes of the efmtool Java API they replaced.
-
-#### 3.11.1 What efmtool is and how straindesign reaches it
-
-efmtool is a Java library (namespace `ch.javasoft.*`, packaged as `efmtool.jar` alongside the Python
-sources at `straindesign/efmtool.jar`). straindesign uses only its *compression* half — not its EFM
-enumeration — through the classes loaded in `efmtool_cmp_interface.py`–:
-`ch.javasoft.smx.impl.DefaultBigIntegerRationalMatrix` (an arbitrary-precision rational matrix),
-`ch.javasoft.smx.ops.Gauss` (rational Gaussian elimination), `ch.javasoft.metabolic.compress.
-StoichMatrixCompressor` and `CompressionMethod`, and `ch.javasoft.math.BigFraction` /
-`java.math.BigInteger`. The bridge is **JPype**: `_start_jvm` (`efmtool_cmp_interface.py`) starts an
-in-process JVM, adds `efmtool.jar` to the classpath, and imports the Java classes via
-`jpype.imports` so they become callable Python objects.
-
-The routing has three layers.
-
-1. **Import time.** `__init__.py`– calls `_start_jvm` *eagerly* at `import straindesign`.
- This is a no-op when jpype1 or a JVM is absent (neither is a package dependency), so a normal install
- never touches Java. When Java *is* present the JVM must be started here — before NumPy/OpenBLAS spins
- up worker threads — or JNI calls later crash with SIGBUS/SIGSEGV (`__init__.py`–; the code is
- littered with such mitigations, see §3.11.4).
-2. **Backend selection.** `compute_strain_designs` reads the kwarg
- `compression_backend = kwargs.get('compression_backend', 'sparse_rref')`
- (`compute_strain_designs.py`) and threads it into both `compress_model` calls
- . `compress_model` sets `use_java = (compression_backend == 'efmtool_rref')`
- (`compression.py`).
-3. **Dispatch inside the fixpoint.** Crucially, `efmtool_rref` does **not** replace the whole
- compression pipeline — only two of its three reducers. Inside the alternating fixpoint (§3.7,
- `compression.py`–):
- - **Parallel merge** (step 1, §3.8) is **always** the Python hash-based `compress_model_parallel` —
- efmtool has no equivalent and it is never routed to Java.
- - **Conservation removal** (step 2, §3.5) forks on `use_java` : Java goes through
- `_remove_conservation_relations_java`, Python through `remove_conservation_relations`.
- - **Coupled merge** (step 3, §3.4) forks inside `compress_model_coupled`: Java calls
- `compress_model_java` (`efmtool_cmp_interface.py`), Python calls `compress_cobra_model`.
-
- So `efmtool_rref` is really a **hybrid**: Python parallel-merge + Java conservation-removal + Java
- coupled-merge, iterated by the same Python fixpoint driver. The two backends differ only in the
- *nullspace/rank algorithm* used for steps 2 and 3.
-
-#### 3.11.2 Data marshalling: cobra model → Java → cobra model
-
-The coupled step, `compress_model_java` (`efmtool_cmp_interface.py`), is where the interesting
-marshalling lives. It mutates the cobra model in place and returns the same
-`{compressed_id: {orig_id: factor}}` reaction map that the Python backend produces, so the rest of the
-pipeline (module remapping, cost compression, decompression in [Ch 9](#ch9)) is backend-agnostic.
-
-**Into Java.**
-- `stoichmat_coeff_to_fraction(model)` first converts every stoichiometric coefficient to an
- exact `Fraction`/sympy-`Rational` — the same exactness discipline as §3.2.1, done *before* any Java
- call.
-- All gene rules are cleared, `r.gene_reaction_rule = ''`, matching the Python coupled path
- (§3.9); GPR is re-attached afterward (below).
-- A `DefaultBigIntegerRationalMatrix(num_met, num_active)` is allocated and filled column by
- column. Reactions whose upper bound is `≤ 0` are **flipped** to the forward direction
- (`model.reactions[mi] *= -1`,–) and their index recorded in `flipped`; efmtool's
- compressor assumes a canonical orientation. Each coefficient `v` is converted by
- `sympyRat2jBigIntegerPair` into a Java `BigInteger` numerator/denominator pair — using
- `BigInteger.valueOf` for values that fit in 63 bits and `BigInteger(str(...))` otherwise — and set as
- a `BigFraction(n, d)`. This path is **exact**: efmtool's `DefaultBigIntegerRational
- Matrix` is arbitrary-precision, so the Java core does *not* overflow.
-- A `StoichMatrixCompressor(subset_compression)` is built, where `subset_compression =
- [CoupledZero, CoupledCombine, CoupledContradicting]` : remove structurally
- zero-flux reactions, combine coupled groups, and drop contradicting groups — the Java analogues of
- §3.3's three removal kinds. `smc.compress(stoich_mat, reversible, …, reacNames, None)`
- returns a `comprec` whose `post` matrix is the reaction transformation (the Java counterpart of the
- Python `post` in §3.3, `v_original = post · v_compressed`).
-
-**Back to Python.** Here is the seam that matters for correctness:
-
-```python
-subset_matrix = jpypeArrayOfArrays2numpy_mat(comprec.post.getDoubleRows()) # :424 — DOUBLES
-```
-
-The *structure* of the compression (which original reaction maps into which compressed column, and the
-zero pattern) is read back as a **double-precision** numpy matrix via `getDoubleRows`. The
-per-reaction merge then:
-- flags a reaction zero-flux iff its `subset_matrix` row is all-zero;
-- for each compressed column `j`, gathers members from `subset_matrix[:,j].nonzero`, scales
- each member's stoichiometry by the **exact** factor `jBigFraction2sympyRat(comprec.post.
- getBigFractionValueAt(ai, j))` (–, exact `BigFraction → sympy.Rational`), and **rescales
- its bounds by `/= abs(subset_matrix[ai, j])`** (–, i.e. by a **double**);
-- merges member reactions into the group representative, concatenating ids with `*` and truncating past
- ~220 chars to `...` — the same naming convention as the parallel backend (§3.8);
-- records `subset_rxns`/`subset_stoich` per representative (negating the stoich for `flipped`
- reactions,–) and finally assembles `rational_map` from them.
-
-So the *factors* are exact rationals, but the *pattern detection and the bound rescaling* pass through
-double precision. The `suppressed_reactions` argument — reaction ids that must survive
-because a strain-design module references them — are excluded from the active set entirely and re-added
-as standalone identity entries, a workaround for efmtool's `CoupledContradicting` step,
-which will otherwise delete reactions it deems inconsistent (contrast the Python backend, which keeps
-them via the exact bounds-intersection of §3.4.4). Back in `compress_model_coupled` the Java branch
-then sweeps up any leftover `(0,0)` reactions (`compression.py`–) and — identically to the
-Python branch — re-attaches the **AND-combined GPR** from the pre-merge snapshot
-(`compression.py`–). GPR propagation is therefore the *same* for both backends on the
-coupled step.
-
-**The conservation path.** `_remove_conservation_relations_java` (`compression.py`) builds `S` as
-a LIL matrix, **densifies its transpose** (`stoich_mat.transpose.toarray`), and hands it
-to `basic_columns_rat_java` (`efmtool_cmp_interface.py`). That function wraps the dense array into a
-`DefaultBigIntegerRationalMatrix` via `numpy_mat2jpypeArrayOfArrays` — which builds a **`JDouble[rows,
-cols]`** — then runs `Gauss.getRationalInstance.rowEchelon(...)` and returns the
-pivot columns, i.e. the independent metabolite rows; the non-pivot metabolites are dependent
-(conservation relations) and removed (`compression.py`–). This is the exact-RREF
-independence oracle of §3.5, but computed in Java — and note it marshals the stoichiometry through a
-**dense double** array, both memory-heavy on genome-scale models and lossy for large coefficients.
-
-#### 3.11.3 Why it is legacy
-
-The pure-Python `sparse_rref` engine (§3.2) was written to replace efmtool for four concrete reasons,
-each a decisive advantage on a genome-scale correctness/performance workload:
-
-1. **No JVM / JPype dependency.** efmtool needs a JVM, the `efmtool.jar`, `jpype1`, and `sympy` all
- present and version-compatible (`_init_java`, `efmtool_cmp_interface.py`, raises `ImportError`
- for any missing piece). The Python backend needs only NumPy/SciPy, which straindesign already
- depends on. A default that requires a working Java toolchain is a default that fails on many
- installs.
-2. **Native-crash fragility.** The bridge is defensive to a degree that itself signals the risk:
- eager JVM startup ordered before OpenBLAS threads (§3.11.1); `gc.disable` wrapped around *every*
- JNI block (`efmtool_cmp_interface.py`–,–) because Python's garbage collector
- finalizing a JPype proxy mid-call causes Bus error / SIGSEGV; an `atexit` JVM-shutdown hook to dodge
- a JPype teardown race. None of this can occur in a pure-Python engine.
-3. **Big-integer safety at the interface.** efmtool's Java core is arbitrary-precision (`DefaultBig
- IntegerRationalMatrix`), so the *internal* arithmetic does not overflow. The hazard is at the
- **marshalling boundary**: the compression structure and bound rescaling are read back through
- `getDoubleRows` and `abs(subset_matrix[...])` in double precision (§3.11.2), and conservation
- removal pushes `S` through a dense `JDouble` array. On models whose exact subdeterminants are huge —
- the verified extreme is **yeast-GEM, needing ~263-bit coefficients** (§3.2.5) — a double cannot
- represent those magnitudes, so bound rescaling and pattern detection silently lose precision. The
- Python engine keeps *everything* in Python big integers / `Fraction` end to end and switches to a
- dict-of-`Fraction` store above int64 (§3.2.5), so it is exact even on yeast-GEM. This is the single
- most important reason the Python path is the default.
-4. **It is the default and the tested path.** The measured pipeline numbers (§3.1, and the iML1515
- timings in CONTEXT) are all on `sparse_rref`; that is the code that receives ongoing correctness
- work (e.g. the bounds-intersection fix of §3.4.4 / issue #44).
-
-**The trade-off, honestly stated.** efmtool is not bad code — it is a mature, well-tested Java library
-whose fraction-free rational Gauss elimination is fast compiled code, and for a decade it *was* the
-compression engine for this and related tools. If you have a JVM handy and a model whose coefficients
-stay comfortably inside double range, `efmtool_rref` will produce a correct compression at competitive
-speed. Its costs are the heavy dependency stack, the native-crash surface, and the double-precision
-marshalling seam. Given a pure-Python alternative that is exact to arbitrary precision, needs no JVM,
-and is the maintained default, the Java backend earns its "legacy" label: **there is essentially no
-production reason to select it.** The realistic remaining uses are (a) cross-validation — regression-
-testing the Python engine's output against the historical efmtool result on a model both can handle —
-and (b) a fallback if a bug were ever found in the Python RREF. For everyday strain design, leave
-`compression_backend` at its default.
-
-#### 3.11.4 Behavioral differences to be aware of
-
-The two backends are *intended* to produce the same lossless flux-space compression, but they are not
-byte-identical and a few divergences are worth knowing:
-
-- **GPR propagation is identical on the coupled step.** Both backends clear gene rules before merging
- and re-attach the AND-combined GPR from the saved AST snapshot in `compress_model_coupled`
- (`compression.py`–), and the parallel OR-combine is always the Python
- `compress_model_parallel` (§3.9). So GPR handling does *not* diverge between backends.
-- **Protected reactions are honored only by the Python backend.** `compress_model` passes gene-
- controlled reactions as `protected_reactions` (`no_coupled_compress_reacs`, `compression.py`–
- ) so they survive COMPRESS #1 un-merged and gene multiplicity is preserved for GPR
- integration (§3.4.2, [Ch 4](#ch4)). `compress_model_java` **ignores `protected_reactions`** — it reads only
- `suppressed_reactions`, which `compress_model` never populates on this path. On the Java backend those
- reactions can therefore be lumped in COMPRESS #1, a genuine semantic divergence in the gene-KO
- pipeline.
-- **Contradicting groups are handled differently.** efmtool's `CoupledContradicting` deletes groups it
- finds inconsistent (the reason `suppressed_reactions` exists as a shield). The Python backend instead
- computes the exact **bounds intersection** of the coupled group and removes only genuinely
- empty/zero groups (§3.4.4). This is precisely the logic whose Java-era version "could drop reactions
- incorrectly" — the cautionary tale of closed issue #44 ([Ch 10](#ch10)). The two backends can thus disagree on
- which reactions a contradicting group costs you.
-- **Direction bookkeeping differs.** The Java path physically flips `ub ≤ 0` reactions (`*= -1`) and
- negates their recorded stoich (`efmtool_cmp_interface.py`–,–); the Python
- coupled backend carries sign inside the exact `ratios` (§3.4.3). Same flux space, different maps —
- which is fine because decompression ([Ch 9](#ch9)) consumes whichever map its backend produced.
-- **Bound rescaling precision.** Java rescales merged-reaction bounds by a **double**
- (`efmtool_cmp_interface.py`–); the Python backend intersects bounds using exact rationals
- (§3.4.4). On well-scaled models this is invisible; on large-coefficient models it is another place the
- Java path can drift.
-
-The safe reading: `efmtool_rref` is preserved for provenance and cross-checking, exercises the same
-fixpoint and produces the same *kind* of map, but the exact-arithmetic Python backend is the one whose
-compression you should trust for correctness-sensitive strain design.
-
-
(ch4)=
## 4. GPR integration
@@ -5926,9 +5726,7 @@ pytest tests -v --log-cli-level=INFO --junit-xml=test-results.xml
```
**CI matrix** (`.github/workflows/CI-test.yml`): OS `ubuntu-latest` / `windows-latest`; Python
-`3.10`–`3.13`; both `pip` and `conda`. CPLEX is excluded for Python 3.13 (max supported: 3.12). A
-JPype/JVM-shutdown segfault on Ubuntu is tolerated via a JUnit-XML exit-code check rather than the raw
-process exit code.
+`3.10`–`3.13`; both `pip` and `conda`. CPLEX is excluded for Python 3.13 (max supported: 3.12).
**Correctness gates.** The canonical known-answer tests are the ones to keep green after any change to
the pipeline: gene-level MCS on `e_coli_core` = **455** solutions, and on `iML1515` = **393**. These
diff --git a/docs/source/examples/JN_08_compression.ipynb b/docs/source/examples/JN_08_compression.ipynb
index 7ff6762a..1800c978 100644
--- a/docs/source/examples/JN_08_compression.ipynb
+++ b/docs/source/examples/JN_08_compression.ipynb
@@ -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",
@@ -257,4 +257,4 @@
},
"nbformat": 4,
"nbformat_minor": 2
-}
\ No newline at end of file
+}
diff --git a/docs/source/index.rst b/docs/source/index.rst
index ce5e24c9..a0531587 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -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:`[6]` 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:`[6]`. It has no dependencies beyond NumPy/SciPy.
:html:``\ Installation:
================================================
@@ -136,7 +136,6 @@ How to cite:
examples/JN_08_compression.ipynb
9_cnapy_integration
api_reference
- legacy_methods
developers_guide
..
diff --git a/docs/source/legacy_methods.rst b/docs/source/legacy_methods.rst
deleted file mode 100644
index 7a2cfd60..00000000
--- a/docs/source/legacy_methods.rst
+++ /dev/null
@@ -1,42 +0,0 @@
-Legacy Methods
-==============
-
-This page documents optional legacy functionality that requires additional
-dependencies beyond the core StrainDesign installation.
-
-Java-based EFMTool compression (``compression_backend='efmtool_rref'``)
------------------------------------------------------------------------
-
-The default compression backend is ``compression_backend='sparse_rref'``, a pure Python
-implementation with no extra dependencies. A legacy Java-based backend is
-available for comparison or reproducibility purposes.
-
-To use it, install the optional Java dependency::
-
- pip install straindesign[java]
-
-or::
-
- pip install jpype1
-
-Then pass ``compression_backend='efmtool_rref'`` to :func:`~straindesign.compress_model`
-or to ``compute_strain_designs`` via the ``compression_backend`` keyword argument.
-
-JAVA_HOME path
---------------
-
-In some cases, using the ``efmtool_rref`` backend may fail with:
-
-``JVMNotFoundException: No JVM shared library file (libjli.dylib) found. Try setting up the JAVA_HOME environment variable.``
-
-In this case, make sure Java is installed correctly and the JAVA_HOME variable
-is set. See `JAVA_HOME environment variable `_
-for platform-specific instructions.
-
-If you're on OS X and get the error
-
-``OSError: [Errno 0] JVM DLL not found``
-
-check that your `Java and the JPype library is set up correctly `_.
-The easiest way to avoid this error is to use conda to install StrainDesign and
-Java together.
diff --git a/pyproject.toml b/pyproject.toml
index de5717b4..d4265de7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"
@@ -43,5 +40,3 @@ Source = "https://github.com/klamt-lab/straindesign/"
[tool.setuptools.packages.find]
include = ["straindesign*"]
-[tool.setuptools.package-data]
-straindesign = ["efmtool.jar"]
diff --git a/straindesign/__init__.py b/straindesign/__init__.py
index 34e0f363..3985839e 100644
--- a/straindesign/__init__.py
+++ b/straindesign/__init__.py
@@ -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 *
diff --git a/straindesign/compression.py b/straindesign/compression.py
index 262a3085..8d450c9a 100644
--- a/straindesign/compression.py
+++ b/straindesign/compression.py
@@ -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.
@@ -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
@@ -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.')
@@ -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,
@@ -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():
@@ -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).
@@ -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:
@@ -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.
@@ -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',
diff --git a/straindesign/compute_strain_designs.py b/straindesign/compute_strain_designs.py
index 7e53bc83..5cfd2588 100644
--- a/straindesign/compute_strain_designs.py
+++ b/straindesign/compute_strain_designs.py
@@ -386,7 +386,7 @@ def compute_strain_designs(model: Model, **kwargs: dict) -> SDSolutions:
"""
allowed_keys = {
MODULES, SETUP, SOLVER, MAX_COST, MAX_SOLUTIONS, 'M', 'compress', 'gene_kos', KOCOST, KICOST, GKOCOST, GKICOST, REGCOST,
- SOLUTION_APPROACH, 'advanced', 'use_scenario', T_LIMIT, SEED, MILP_THREADS, 'compression_backend', 'dump_preprocessed'
+ SOLUTION_APPROACH, 'advanced', 'use_scenario', T_LIMIT, SEED, MILP_THREADS, 'dump_preprocessed'
}
logging.info('Preparing strain design computation.')
if SETUP in kwargs:
@@ -564,7 +564,6 @@ def compute_strain_designs(model: Model, **kwargs: dict) -> SDSolutions:
# also exempt them from parallel merging so their names stay stable across the
# compression passes (keeps the coupled-exemption matching them by name)
no_par_compress_reacs.update(no_coupled_compress_reacs)
- compression_backend = kwargs.get('compression_backend', 'sparse_rref')
# --- Reversibility pre-tightening (BEFORE compress #1) ---
# Sign-only FVA (cheaper than full FVA): fix lb/ub to 0 for directions carrying no flux in the
# base polytope. Design-neutral (a base-infeasible direction stays infeasible under any module
@@ -586,7 +585,6 @@ def compute_strain_designs(model: Model, **kwargs: dict) -> SDSolutions:
logging.info('Compressing Network (' + str(len(cmp_model.reactions)) + ' reactions).')
t0 = time.time()
cmp_mapReac_1 = compress_model(cmp_model, no_par_compress_reacs,
- compression_backend=compression_backend,
propagate_gpr=True,
no_coupled_compress_reacs=no_coupled_compress_reacs)
sd_modules = compress_modules(sd_modules, cmp_mapReac_1)
@@ -671,7 +669,7 @@ def compute_strain_designs(model: Model, **kwargs: dict) -> SDSolutions:
t0 = time.time()
no_par_compress_reacs = _collect_no_par_compress_reacs(sd_modules)
cmp_mapReac_2 = compress_model(cmp_model, no_par_compress_reacs,
- compression_backend=compression_backend)
+)
sd_modules = compress_modules(sd_modules, cmp_mapReac_2)
cmp_ko_cost, cmp_ki_cost, cmp_mapReac_2 = compress_ki_ko_cost(
cmp_ko_cost, cmp_ki_cost, cmp_mapReac_2)
diff --git a/straindesign/efmtool.jar b/straindesign/efmtool.jar
deleted file mode 100644
index c18947955bfdcd53cc655a9a1abfee0cf4be47c1..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 2356134
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ûxmeMF
zcdb-3m#8QgdIP3XYDS(YHGCK773ChuV@k>Lc<4v2l8199mH|WTIDY+6+KV)bJq$I*
zt-7oVYlMD+NckeQP5YJ+2P2&OXI_vL2029=po1Xy{E9J`+g7boaL;|D%oj
zP-wPjWxplu;sI;|*`FKwf0c>-@a(0G}=g6af{I1PAp)KXfb<_VO@v1e7
zja`1!VeHKradHtUy_h@_g9=(75ZbMBpBIK%z_R`;ZQ5X2Wo-f}2*?WE{bvW+S-?*{
z6oDr_Js&&D9P_O#fAE`Xg%CX3P`qSdJ=#l}kF78V@^C6a29P%@+r~D_JN?1ip6nld
zg*Kd#nk%af17S{$RF1nKWZex2T?|BKV&JPe*E-l-FL-W44-7-@AK>#(Atzh8c!I>|
z!7v{PyaByp{Q!7I@#d$bkm^AQ{Lgo+jUy#O?ZYpDLr}C-*HFD@lY0Y^XH#oCosy|N
zs49o^J`NueG7ZPLyCu6!Z)tnoy>UdU9(xRlW7_Pw*(0DmP_kQ8=IHyW6z&)i!YxAR
zbj8aMie6=CnY?t`JQ0Zo`yjKEoP{C88GmNs-lzG}@vs79T*W!;5l!wn6TU{TQXF5R
z=%*dXQI~bmBw_;@qN{h%f9l+M_9Z>-TP45xuJ!-h?f&;+?f=;Ae#hi|2k@$Vzj6Op
zJL9eRy_i4%<+BLAiH4fJ1)>3}q{ccBRYaH$wXP?i;HThEWH*K}rUt1bx|K{rwrnMuA=6XufBmcg)jN%34B%wehS}$4V
zfpa6D!4Q$M^isE^GojyXO%O^J)I`KsH4o?N-n?`Es{M%59MyFYkj10Nr6pXm1DU-h
z+wZ(C&=u5Ku2VV-1{MP_yLE4*q53w20=8J~;Qh;_8QsQ&G=g%++H*M}@!9O~;ZfGB
zICcy%xC-bD0IpWJ#0;zvaeks@Ae>d(xxRSDq_dM$#+%*55sVnR_pQ5-c(|N&)kA+t
zV@hN^ECyo2x@hdOhqxov7)1%#?AHOS5Bg_~nN`{$z0rU4fH07B`G!7?h->
zM5FWN^MBxp^2JX{XYK72F}1w7+9!?ZedqB|`$XN$`FQp9gYe`paf^6O&4im5n4@Tn
zPq!DFvlqV(FKFBdkDL@eD@iNLbzl^pQPmJ_NaQ;FQyTJztAKhmB`=Sl89`c9|9RY>3`i8&
z`ze|!Yi)v2G-t&6s^chQ>Ltt3_UrxQ2KpDAJEjgyU!jG|(lRrT>ChBdr@2XZl+H+7
z)eT!_JRu`V0H-CU3u#+Z$Cy-SgiVlfGd38C*Ckfx9+R+4+xQcvx^M1Ub(*e68WYNt
z9HZFpyCUmXXqfy}k*HKyF!K@0I}D6h<^CLsknOrKT@p+pO@NR(nDX#3VRWAP-9k>&
z#D>~n(}0BC3?=4b(D&x_jrD{8@t{~}rea4D#VV{#s;#F!VJ)a2Mxs5izUdcxDp5w!
z1C8~n=LLqCR-zR`(o&N!a%)sZN1!xS#whN6t(|=m^T6>iAlTLj7@>EkKVcH7u5b&JRA2s>3DV9Y|&>7J&2`
z%*UbsD&MLkR{AkvGs-IL4NBO8hNx_I(fsQ!L2N?17(~AIDqUnk
z34)eeHELn`?M65}(Uf^S&kngQb#uzzk}l#7+(=GIpUP!527u5oJ;oPRWImSMp+s{n
zdYW3=j4`u@cq3wOAtEVSkBf85K}nEyZ)d0^^UcE-Yaq%FhWQ4sUMDzyWTVZffu+BSdA%ufp0FyP6L18Mcm
zJ;MLz7_YHS{9E~5{B!@~M9_b1SN>i6|8mstGh1i8byv7hfXPA<&xTt*gvE0T%7UBO#SC6wj6azGlrj
zOU>QKHs~a=NzA$EELM85TA#R1^0u}@?0x(mP=7k!{CeT<{JPWbx*wa;1E~?DvF}4j
zS>vCt;rAd&leidTi!j*>4T@5Bs3FaaifY?Ip@@gaMw;2z?|bb0sQs+K76D?ub0uC;
z=UPn;Y1u)fnA)jTp
zXi{d1k0h(#nCVv^clMGv%x!z>a_IWwI=6_c!|SPO(^gq1)_qKBy)r11uI1w5bYX6}
zGBb;Ivg?g#KP~w8k7ogK63#drBuXSjJP41>N2nW9mNlB)Jlz?ZT~i^L;Q+$*n|u%^
z&>Z*f$Z}I{Y^Qqb*t`QktSnnnw3MprEd4uLLk7|n%sVCc#aIPf0jFN*CgNAl-s^M~
zy)SV1K@+Fqy&^u9Je}H@S4lt22+O$;?kT8b0R|{$pofL$8gcA~VpuUh7uH3zsd;?3
z>p`V?nsTlv?0T4ohAX5^zXiK0;xwJDhS%D
zAO$A^cY`-go41~&1r29REyc#NaOPb==86*!=Q0M_B~^%Eg>7sLsxQLo|lk`5PWb1eZ5ol!Q(@?O*IfI4ptVX&bKzVkuc*}GVm
z(Dt)GPA$J`on-sh$#&;CiFtEMRu4K;;NOf3k_O{aYn>`&f@8c`_JwfXK)oIb6tngx
zL5`t4I#{aD7R{2h6fsCA#P4I`kYZao6d`v{Lf~Lhpj@Rcjri_!^>%p~rsMQ67UpFW
z@eYgZ@!_`VFo$A8Fn6j-QU~tbO;11Y*88SMiu?e7gr91qK_~2V(C@eZmKfA0Dax=^ISj84Q|
z{vK%!&2>QOIvWI5OPrp(k!EAt?Yr6$;F7{5R+1$!h9|3^u8Nj+kV3^cnA*U5=Q|5D
z*Ofgwgk=ZGAMw?n1GBc&sj;QO=E0blo{w{~B5@{IM4T`tZS~pj>wErz$Y2|9?L&kn
zbJt3r#U{ab**6i_pSq2w%LJaEzd8f*9KUWE_Ov|*gX4+jVV8mE<{6bo`qzwsWPlN?@sa>{h6BmlL>rHMdXwPFBz1A4tTgi7NC4CRrod{SaaB1PpNZ~
z*;wYp8S?GzL=PbWpN6uWW8#xB1amR(h_))W&6QWw)~0%a$Y0zemrBOXpX
zG>D~!zv?gY;E(J>X@v|qP#HOgOiU+=O1w>~Bq{PjI_1)KC!h71(oCqJ=Vc}5jiHG~
zyuI%{*gumVE??`=D=B*J+yt=P{idg9NKh}Ttl
z59mB7Xwwt()jY=ZEbDV=2hLBEBY)kXHJjfS%P8I0s(N;~q3XHrS++LDp+-Whnadzmy0A3`?o0V{@QU+cUWGERW}m6U}y6O4~y_^X}b3fRVr
z*1s@Ag9j{k{I@+{hvC13Njq)4bRScylufBJBVonq_>=oP93G_bXWc@qM3pc#{KSdQ
z`fV0$0{VuKb`xe@(w&<)(oT--x|7DsNY=e*m^TMTlrJ4iT6#j}T=8Dr#3y6-$&$U?
zeoab>)N&8yg-Z7jV58j$DchYQB+lkqD~(h7u};V{
z$D~lRKOY1=p4k#O7s=(Ewu0Cw5Z9FGmu)4DxR?CkZUi{{=5mWQ&$TW0pc6Cp|c8eXgCV4t96q4eq0Dm!FdD
zxDy!tRCJT0pM#>}@Ob1w2gqA*WzJ7b>iA6JWj#+jTxr<+OA6Zc^l9DerL@6a85G{U3mc06RddjZ{
z&WeP(GGO3%2<-XZpc2Q7&$a4+Kxwul27Yq@1a_F@hdajRjAGnC8n|V3H(E5DI`M_x#`rBw)O!1it%cts
z7aU2MVwO|xR_CcLFElo>PZ_|-&CZ99kncg+;gC+51K{5g5O3Ic^gm8)f-VfgI)b}#
ziahxZ?}ja)3l^N}Po6i97uYI`B~Mv!9plBz;g_ya-MHLeR
zKUpW0N+2Xfs54;U@r4uu@9w>26?T+^blsPRp;JrO#gV-HZX6
z82oEyQ^nlyCP2lM;PJkrXL@jakOX)F3*FK}`10>{3Z?ZdPuf(8Y1N84QRkm!t1{NP
z#i~KtR7Crhiaxeg!lqGI;L%hg@7i^1UivIU?=(L-e(|Tku*c^XP{DGN3^7b(bmt0Y
zC2K%t*Wb`Ix+U!jX{YjBE%ICyr_M4)`g+vygchof;{nFOjGKR_>`>va5-rk~R-@^b
zS$Y3-c0;theqzgJ*Mwpz6GpRWlr473R~+qm=F?BCLbEorA4crN$m$>TDLDHa0T|Vi
z=A}d!FT|W*&YtV)FN=!=eMI-#t~{+ZOeF>W{DP#Z!`n<|{&a
zP+@-uKC&a3H==veRk=MZOYu%Tt7eSqnuL0TC0l0KxYw;WV6nnvu=@TRuvA1ilpK4A
zV>@eNFI;?qv{Mnwu135qFAP#PP8i3wBs{E#yaPGaQpenl>KdMiqwq?TT3Kww;
zPdv63edIP9dYTDzO@tLVc(WM`vY}RMb*_SV|4BQ!pdDP*(h~O^{4&pCIqw<2sGa4_
z8qS<_(!Yi#3NETy&sY&d6p>pn(#`^YXji=^s!zA6viki!^l^r>6M%Qwir%AUH`qWfy5
zWmswIPFRypZqhWsHCxW5yK~)-)s|=^JnSL2kSFtNj$IP1MX)wRYzi4zp0ZS&Hk%d6KB00Onn;1ho+nr#n5Z6EXb7tsCaP%{FC*od|5`Dt
z7-bzzNn1XAlt4uw`_e0KsZmTkNI~c1oIbG-bZ9xOw&rn!$QsXls&eG4!V#TYFst%@
zO-uo;oc$;HPOtC`=aa`a7z?T&4JA~IK}%Sk*hYmZO~oEYvlV;=W;~VNY{E-`_ikhX
z`8fj%=x`P;do+*PYNJRs)8jRbwx@mjLLMX75hHjCg+vv7-H=@1qNy=B~c`o*oVNgh(=DmUM@m~qu0#bCx
z{t1c0mhrUXaX-dA9^q&e=E>cac8od4KDMcenz(x&Q7n1`6TNl<%&1%
zJX?Kx16!y}KHUJD+Q9Ab3_fX8v3n3zztT?HzhBH8cl
z8`JgAWq(S+mbMVohf;i3nOTuc}Y|@XvzcO3yv_1K(n*D&HH2EE+
z-fHX2@`m4IQQ38dQdX|5`IWrLc~*Ai7(#Uf^1Hh_^EQ-eH&Ur&>?(zKB#P%^&Dq^w
zNEv;X2)0w$ziW8bJ9XfSlWl9v43$1dtUZLHx^Gvi$&e}UXG}SMtu}#);?NQPAj0G$#r`=t);CGQ&h>UFKk>_pF2->!S@;&1|Smn?Qc%#_dv
zb8xjHxMGD{&6uEPO5qcJ?2Vnc%MFvhgotLltXy7fL0=xDD|rJDr=KU(5v~7}!EYh2
z_z6%~>n*GcDOq&!Yn4%TS)dN~yX$gGzI^{_nuc%K`HYwpg1=@lshBx<@IHdt`{dRz
zfe-ejQ)*XV#kVOR*+*mYrC>MBnz6*G{*lb5-VvOw+t(x|$H-?c>Ks^qi1XPNbH#)0
z!WUjGcb8b+(Q5E=?eog>#(d_qS-h1z4tXQ|{QG++^tCeg6Chn^;r%mQX91Ml+z2x)
z0-*@oKpD%qNswhIjz$1$@Yk>{5NU+pT*6EUlF`M!K4h{a;mAZO
zrta*9Pk35=*;FXe7b6SkKDFP$E382Nn6?=Lcn0vHGN6iDhkL%w-$HHoIWhTew8UU-
z+0SqfO`4gAf_)W{!DhfAANcIWyS{cK=}3#_pSJyAG(R|q7AJ!g$%H1FbQJt&(q;VN
zbhiQ5)9vxki>sJ?2{QF=973%R{7j##ShC+qpzw~lnt>j3H#$B&Bg4l526jmdE_w6E
zgEU6yWiK6z$F(97!&rG1fpVl>@wi*#HHy;cbIP~S3RFdw9a+-~b(}aJ(w&J=dO>a=
z?13xfO_DOF&u`|`aB-
zM%igx1*9}esnm2AbKob1uViey2Ff&$3olgh^x6iE*(A^NJi2C5hD7>bV+5x_@AbdX
zoqiAowCEb)41(?9Y1uSbWb&$$1LdW8OmrKff{SW|zmJLYUAfc5aM6oilI{jTphaN5
zPjn!-gn&8>)cc>t8zRcVGlhd>sUhsB{R3n6M5-D15r6zBC;p$9`hQ;{_Ai4(*w*>`
z<>