Skip to content

fix: read RAVEN 3 model files, and tolerate a non-double rev - #759

Merged
edkerk merged 3 commits into
developfrom
feat/raven2-read-raven3
Oct 7, 2026
Merged

edkerk merged 3 commits into
developfrom
feat/raven2-read-raven3

Conversation

@edkerk

@edkerk edkerk commented Oct 7, 2026

Copy link
Copy Markdown
Member

Lets RAVEN 2 read model files written by RAVEN 3 and raven-toolbox, so RAVEN 2 functionality stays usable against current models. Proposed for a 2.11.4 patch release.

Prompted by SysBioChalmers/Human-GEM#1143, where a user on RAVEN 2.11.3 could reach Human-GEM 2.1.0 through none of its formats.

What RAVEN 2 could not read

All in io/readYAMLmodel.m, measured against the released Human-GEM 2.1.0 YAML:

effect
- metaData: !!omap the exact-match case missed the tagged map, so the whole block was skipped — no id, name, version or date, silently
reaction-side notes: only rxnNotes was a case in the reactions section, so the read aborted: Unknown entry in yaml file: - notes: PXMP2 forms a non-selective pore...
ec-code inside annotation gathered as a MIRIAM instead of filling eccodes, which came back empty
smiles inside annotation same, for metSmiles
- genes: [] inline empty list not recognised
gecko_light only geckoLight was accepted

The first two are what blocked Human-GEM 2.1.0 outright.

Read tolerance only. What this version writes is unchanged — it still emits rxnNotes and an untagged metaData — so RAVEN 2 output stays valid for older RAVEN, and this stays a patch-sized change.

importModel: the model note

importModel looked for the note between xhtml"> and </body>. RAVEN's own exportModel writes that, but a writer serialising a notes dictionary emits one <p>key: value</p> per entry and no <body> at all, carrying the note under its own key. The labelled entry is now taken when there is one — so the other entries (version, and whatever else the dictionary held) stay out of the note — falling back to the <body> text otherwise.

mergeLinear: an integer rev

mergeLinear combines rev with a sparse logical:

pos = sum(reducedModel.S(twoNonZero(i),involvedRxns).' > 0 | reducedModel.rev(involvedRxns));

MATLAB rejects that for an integer class — Sparse integer array arithmetic operations are not supported — and a model built outside RAVEN can easily carry an integer rev (cobrapy's save_matlab_model writes np.array(reversibility) * 1, which is int64 on Linux/macOS). Coerced once on entry rather than failing several steps into ftINIT.

metComps

Documented as an integer column rather than a double. A reader that turns the 1-based index into a position needs an integer, and MATLAB's own save narrows an integer-valued double to an integer type on disk in any case; both classes index comps identically.

Verification

MATLAB R2024b on Vera, against the released Human-GEM files.

Human-GEM 2.1.0 YAML, previously a hard abort:

loaded: 32 fields, 12748 rxns, 8374 mets
  id "HumanGEM"   name "Generic genome-scale metabolic model of Homo sapiens"
  version "2.1.0"   date "2026-09-30"
  annotation 1   rev 1   eccodes 1   rxnNotes 1   subSystems 1   metComps 1

Model note through SBML:

annotation subfields: taxonomy, note
note: Genome-scale metabolic models are valuable tools to study metabolism...

mergeLinear on the released Human-GEM 2.1.0 .mat, whose rev is int64:

rev class before: int64
mergeLinear: OK (8751 rxns out)

The .mat itself is fixed separately in CompMetEng/raven-toolbox#168; this change means RAVEN 2 survives the files already published.

edkerk added 3 commits October 7, 2026 16:13
RAVEN 3 and raven-toolbox write YAML this version cannot read. The
metaData map is tagged (`- metaData: !!omap`), which the exact-match case
missed, so the whole block was skipped and the model came back with no
id, name, version or date. A reaction's free text moved from `rxnNotes`
to `notes`, which aborted the read outright with "Unknown entry in yaml
file". EC codes and SMILES moved inside the per-entry `annotation` block,
where they were gathered as MIRIAMs instead of filling eccodes and
metSmiles. An empty gene list is written inline, and the GECKO flag is
spelled `gecko_light`.

All of these are accepted on read now; what this version writes is
unchanged, so its own output stays valid for older RAVEN.

importModel also only found a model note inside a <body>, which RAVEN's
exportModel writes but a writer serialising a notes dictionary does not;
such a writer emits one "<p>key: value</p>" per entry, so the labelled
entry is taken when there is one and the <body> text otherwise.

mergeLinear combines rev with a sparse logical, which MATLAB refuses for
an integer class ("Sparse integer array arithmetic operations are not
supported"). A model built outside RAVEN can carry an integer rev, so it
is coerced once on entry rather than failing several steps into ftINIT.

metComps is documented as an integer column: a reader that turns the
1-based index into a position needs an integer, and MATLAB stores an
integer-valued double narrowed to an integer type in any case.
updateDocumentation regenerates the whole doc/ tree, but the committed
files were generated on Windows and every other one differs only in the
path separator of its index link and title (external\kegg vs
external/kegg). Only the three functions this branch changes are updated,
so the diff stays about the change rather than about the platform it was
regenerated on.
The earlier fix coerced reducedModel.rev to double on entry to
mergeLinear, which changed the class of rev in the model it returns --
a type the caller did not ask to have changed.

Comparing instead matches what every other use of rev in RAVEN already
does (rev == 1, rev ~= 0, find(rev)), works for a double, an integer or a
logical alike, and leaves the model untouched. On the released Human-GEM
2.1.0 .mat, whose rev is int64, both forms return the same 8751
reactions with the same S and the same rev values, but the comparison
leaves rev as int64 instead of silently making it double.

ftINIT had the same pattern and the same failure:

    any(prepData.refModel.S(metSel,:) < 0,1) & prepData.refModel.rev.'

a sparse logical combined with rev, which MATLAB refuses for an integer
array. It only runs when metabolomics data is supplied, which is why
#1143 did not reach it. Every other use of rev compares first and is
already class-agnostic.
@edkerk

edkerk commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Revised the rev fix after a review question: are there other places where the class of rev matters, and is coercing it in mergeLinear the right move?

I tested every pattern rev appears in, with a double, an int64 and a logical:

mergeLinear:71    sparse-logical | rev       double=2    int64=ERROR   logical=2
ftINIT:138        any(S<0) & rev.'           double=ok   int64=ERROR   logical=ok
ftINITMILP:119    logical & (rev == 1)       double=ok   int64=ok      logical=ok
ftINITInternal:88 rev ~= 0                   double=ok   int64=ok      logical=ok
mergeLinear:80    scalar && ~scalar          double=ok   int64=ok      logical=ok
mergeLinear:145   rev * rev                  double=ok   int64=ok      logical=ok
solveLP:189       find(rev)                  double=ok   int64=ok      logical=ok
checkModelStruct  rev == 0                   double=ok   int64=ok      logical=ok

Two changes follow.

ftINIT has the same bug. Line 138 combines a sparse logical with rev directly, exactly as mergeLinear did. It only runs when metabolomics data is supplied, which is why #1143 never reached it. Everything else compares rev first and is already class-agnostic.

Comparing is better than coercing. The previous commit set reducedModel.rev = double(reducedModel.rev) on entry, which changes the class of rev in the model mergeLinear returns — a type change the caller did not ask for. rev ~= 0 matches what the rest of RAVEN already does, works for all three classes, and leaves the model alone.

On the released Human-GEM 2.1.0 .mat, whose rev is int64:

input rev class: int64
mergeLinear on int64 rev:  OK, 8751 rxns out, rev class out = int64
mergeLinear on double rev:     8751 rxns out, rev class out = double
same reactions out: 1   same S: 1   rev values equal: 1

Same result either way, but rev keeps its class now. Documentation regenerated for both functions.

@edkerk
edkerk merged commit ee5f1bb into develop Oct 7, 2026
1 check failed
@edkerk
edkerk deleted the feat/raven2-read-raven3 branch October 7, 2026 19:38
@edkerk edkerk mentioned this pull request Oct 7, 2026
1 of 2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant