Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8bf11f4
fillCarbonNum: warn and weight 1 for a model without metabolite formulas
edkerk Oct 31, 2026
c39063f
ecStatsFromBrenda gives the median and the maximum per EC and keeps t…
edkerk Oct 31, 2026
a71a026
loadBrendaMeasurements reads measurements.tsv.xz
edkerk Oct 31, 2026
cc6b106
getECfromDatabase warns on multiplicities with action display and sea…
edkerk Oct 31, 2026
3028b24
Record the new tests in the verification matrix
edkerk Oct 31, 2026
c1941df
addNewRxnsToEC adds reactions whose genes already have enzymes
edkerk Oct 31, 2026
7ee7af7
applyComplexData counts a subunit without stoichiometry as one subunit
edkerk Oct 31, 2026
76238c7
calculateFfactor and relaxProteomicsGreedy read an empty cell array a…
edkerk Oct 31, 2026
77e7dd6
constrainEnzConcs and fillEnzConcs raise for a negative concentration
edkerk Oct 31, 2026
6f773b5
constrainFluxData raises for an invalid maxMinGrowth, a missing react…
edkerk Oct 31, 2026
e425e5e
loadFluxData raises for a missing file, a ragged row and no exchange …
edkerk Oct 31, 2026
7d976fe
getEnzymeUsage leaves out enzymes without usage reaction; reportEnzym…
edkerk Oct 31, 2026
3201183
truncateValues returns infinite values unchanged
edkerk Oct 31, 2026
6ee575a
Add tests for the contract changes of round 5 group 3
edkerk Oct 31, 2026
78e1757
getECfromDatabase treats an empty logical ecRxns as no selection
edkerk Oct 31, 2026
8ab8bd7
collectKcats counts each source file once, and mergeKcats matches OKP…
edkerk Oct 31, 2026
df8e34c
loadKcatSource skips a row of empty quoted fields and raises for a fi…
edkerk Oct 31, 2026
406c18e
readDLKcatOutput drops rows without a finite kcat before checking rea…
edkerk Oct 31, 2026
1698161
kcatAssignmentConfig rejects invalid values; kcatsFromECTree takes an…
edkerk Oct 31, 2026
01eca56
Merge fix/parity-round-5-g1
edkerk Oct 31, 2026
b9502db
Merge fix/parity-round-5-g2
edkerk Oct 31, 2026
63374c4
Merge fix/parity-round-5-g3
edkerk Oct 31, 2026
838f5ed
Merge fix/parity-round-5-g4
edkerk Oct 31, 2026
32a49d7
saveEcModel writes a default metaData description when there is none
edkerk Oct 31, 2026
97e8894
applyKcatConstraints takes cobra and ec reaction ids on a light model…
edkerk Oct 31, 2026
73c52b4
assignKcatValues writes ec.source as the lower case source followed b…
edkerk Oct 31, 2026
482e4cd
resolveTunableContext puts a kcat in the group of its source name bef…
edkerk Oct 31, 2026
1903908
findMetSmiles uses the last row of a duplicated cache name and skips …
edkerk Oct 31, 2026
4ea4fae
getECfromDatabase raises for actions other than display and ignore
edkerk Oct 31, 2026
776b79f
reviewKcatAssignment help text states the case rule for sources
edkerk Oct 31, 2026
5437f4a
Record tc0407 to tc0409 in the verification matrix
edkerk Oct 31, 2026
e0b2b1e
tc0009: the reloaded annotation carries the default description
edkerk Oct 31, 2026
eb727bd
saveEcModel: a note counts as the description; tc0009 expects the note
edkerk Oct 31, 2026
9c21494
Merge origin/develop
edkerk Oct 31, 2026
076f490
Merge fix/parity-round-5-g1
edkerk Oct 31, 2026
0e0d7d9
Merge fix/parity-round-5-g2
edkerk Oct 31, 2026
aa854e1
Merge fix/parity-round-5-g3
edkerk Oct 31, 2026
ea90879
Merge fix/parity-round-5-g4
edkerk Oct 31, 2026
7670047
Test saveEcModel and loadEcModel with an ec reaction without enzymes …
edkerk Nov 1, 2026
9f53a13
Merge fix/ec-rxn-without-enzymes
edkerk Nov 1, 2026
87017dc
ecFSEOF orders targets by the rounded score; getECfromGEM warns when …
edkerk Nov 1, 2026
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
35 changes: 20 additions & 15 deletions src/change_model/addNewRxnsToEC.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
% Note that the reversibility defined in the equation will be used to
% split to construct irreversible reactions (add _REV) and the rules
% defined will be used to expand the model (add _EXP_n).
% 2. After adding the new reactions, setKcatForReactions or
% 2. When all the genes of the new reactions already have enzymes, no
% newEnzymes are needed.
% 3. After adding the new reactions, setKcatForReactions or
% applyCustomKcats should be run to add the kcat to the reactions, and
% subsequently applyKcatConstraints.
%
Expand Down Expand Up @@ -78,8 +80,9 @@
end
params = modelAdapter.getParameters();

enzAdded = {};
if isempty(newEnzymes)
newEnzymes = [];
newEnzymes = struct('enzymes', {{}}, 'genes', {{}}, 'mw', []);
end

% Validate if GECKO version 3
Expand All @@ -91,7 +94,7 @@
error('addNewRxnsToEC only works on non-light ecModels')
else
% Check if the new proteins are not already present in the model
if ~isempty(newEnzymes)
if ~isempty(newEnzymes.enzymes)
[toRemove,~] = ismember(newEnzymes.enzymes, model.ec.enzymes);

% Remove from the list those enzymes that already present
Expand Down Expand Up @@ -213,19 +216,21 @@
model = addRxns(model, newRxns, 3, '', true, true);

% 4. Add protein usage reactions.
usageRxns.rxns = strcat('usage_',proteinMets.mets);
usageRxns.rxnNames = usageRxns.rxns;
usageRxns.mets = cell(numel(usageRxns.rxns),1);
usageRxns.stoichCoeffs = cell(numel(usageRxns.rxns),1);
for i=1:numel(usageRxns.mets)
usageRxns.mets{i} = {proteinMets.mets{i}, 'prot_pool'};
usageRxns.stoichCoeffs{i} = [1,-1];
if ~isempty(newEnzymes.enzymes)
usageRxns.rxns = strcat('usage_',proteinMets.mets);
usageRxns.rxnNames = usageRxns.rxns;
usageRxns.mets = cell(numel(usageRxns.rxns),1);
usageRxns.stoichCoeffs = cell(numel(usageRxns.rxns),1);
for i=1:numel(usageRxns.mets)
usageRxns.mets{i} = {proteinMets.mets{i}, 'prot_pool'};
usageRxns.stoichCoeffs{i} = [1,-1];
end
usageRxns.lb = zeros(numel(usageRxns.rxns),1);
usageRxns.ub = zeros(numel(usageRxns.rxns),1) + 1000;
usageRxns.rev = ones(numel(usageRxns.rxns),1);
usageRxns.grRules = newEnzymes.genes;
model = addRxns(model,usageRxns);
end
usageRxns.lb = zeros(numel(usageRxns.rxns),1);
usageRxns.ub = zeros(numel(usageRxns.rxns),1) + 1000;
usageRxns.rev = ones(numel(usageRxns.rxns),1);
usageRxns.grRules = newEnzymes.genes;
model = addRxns(model,usageRxns);

% 5. Update .ec field with enzyme data
model.ec.enzymes = [model.ec.enzymes; newEnzymes.enzymes(:)];
Expand Down
9 changes: 5 additions & 4 deletions src/change_model/applyComplexData.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
% Notes
% -----
% The subunit stoichiometry is assigned per protein ID: the proteins of a
% complex need not be listed in the order of model.ec.enzymes.
% complex need not be listed in the order of model.ec.enzymes. A subunit with
% stoichiometry 0 has no stated stoichiometry; it counts as a subunit with
% stoichiometry 1.
%
% foundComplex and proposedComplex share the following columns: rxn
% (reaction identifier), complexID and name (Complex Portal identifiers),
Expand Down Expand Up @@ -113,9 +115,8 @@
protMatIdx(~isOld) = lastProt+1 : lastProt+numel(newProts);
lastProt = lastProt + numel(newProts);
% Add subunit numbers
if all(complexData(i).stochiometry == 0)
complexData(i).stochiometry(:) = 1;
end
% A subunit without a stated stoichiometry (0) counts as one subunit
complexData(i).stochiometry(complexData(i).stochiometry == 0) = 1;
complexMatrix(i,protMatIdx) = complexData(i).stochiometry;
end

Expand Down
39 changes: 28 additions & 11 deletions src/change_model/applyKcatConstraints.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
%
% Existing enzyme constraints are first removed (unless updateRxns is
% provided), and new constraints are defined based on the content of
% model.ec.kcat. When every kcat is 0, the existing constraints of a full
% ecModel are removed and no new ones are written; a light ecModel is
% returned unchanged.
% model.ec.kcat. When every kcat is 0, the existing constraints are removed
% and no new ones are written.
%
% Parameters
% ----------
Expand All @@ -18,8 +17,13 @@
% if not all enzyme constraints should be updated, this can be given as
% either a logical vector of length model.ec.rxns, a vector of
% model.ec.rxns indices, or a (cell array of) string(s) with
% model.ec.rxns identifiers. For light models, these reactions should
% match model.rxns.
% model.ec.rxns identifiers. For light models, a (cell array of) string(s)
% may hold reaction identifiers of model.rxns or of model.ec.rxns (with
% the ###_ prefix); a reaction is then updated as a whole, with all its
% isozymes. An identifier that is in both or in neither raises the
% error applyKcatConstraints:ambiguousReaction or
% applyKcatConstraints:unknownReaction. A logical vector or indices
% refer to model.rxns for light models.
%
% Returns
% -------
Expand Down Expand Up @@ -56,6 +60,23 @@
updateRxns = updateRxnsLog;
elseif iscellstr(updateRxns) || ischar(updateRxns) || isstring(updateRxns)
updateRxnsIds = convertCharArray(updateRxns);
updateRxnsIds = updateRxnsIds(:);
if model.ec.geckoLight
isCobra = ismember(updateRxnsIds,model.rxns);
isEc = ismember(updateRxnsIds,model.ec.rxns);
if any(isCobra & isEc)
error('applyKcatConstraints:ambiguousReaction', ...
'The identifiers %s are in both model.rxns and model.ec.rxns.', ...
strjoin(updateRxnsIds(isCobra & isEc),', '))
end
if any(~isCobra & ~isEc)
error('applyKcatConstraints:unknownReaction', ...
'The identifiers %s are in neither model.rxns nor model.ec.rxns.', ...
strjoin(updateRxnsIds(~isCobra & ~isEc),', '))
end
updateRxnsIds = [updateRxnsIds(isCobra); extractAfter(updateRxnsIds(isEc),4)];
updateRxnsIds = updateRxnsIds(:);
end
updateRxns = ismember(rxns,updateRxnsIds);
end

Expand All @@ -68,12 +89,8 @@
' not a valid GECKO3 ecModel. First run makeEcModel(model).'])
end
if all(model.ec.kcat==0)
if model.ec.geckoLight
printOrange('WARNING: No kcat values are provided in model.ec.kcat, model remains unchanged.\n');
return
end
% In a full model the existing enzyme constraints of the selected
% reactions are still cleared below, and no new ones are written.
% The existing enzyme constraints of the selected reactions are still
% cleared below, and no new ones are written.
printOrange('WARNING: No kcat values are provided in model.ec.kcat, existing enzyme constraints are cleared and no new ones are written.\n');
end

Expand Down
14 changes: 11 additions & 3 deletions src/change_model/findMetSmiles.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
%
% Matches are cached in tutorials/***/data/smilesDB.tsv, which is checked
% first on subsequent runs so repeated queries are avoided. If the model
% already has a metSmiles field, non-empty entries are left unchanged.
% already has a metSmiles field, non-empty entries are left unchanged. When
% the cache holds a name more than once, the last row is used. A metabolite
% without a name is skipped with a warning (findMetSmiles:emptyName) and
% gets no SMILES.
%
% Parameters
% ----------
Expand Down Expand Up @@ -39,6 +42,10 @@
end
params = modelAdapter.params;

nEmpty = sum(cellfun(@isempty,model.metNames));
if nEmpty > 0
warning('findMetSmiles:emptyName','%d metabolite(s) have no name and get no SMILES.',nEmpty)
end
[uniqueNames, ~, uniqueIdx] = unique(regexprep(model.metNames,'^prot_.*',''));
uniqueSmiles(1:numel(uniqueNames),1) = {''};
metMatch = false(length(uniqueNames),1);
Expand All @@ -49,8 +56,9 @@
fID = fopen(smilesDBfile,'r');
raw = textscan(fID,'%s %s','Delimiter','\t','HeaderLines',0);
fclose(fID);
smilesDB.names = raw{1};
smilesDB.smile = raw{2};
%The last row of a name that occurs more than once is used
smilesDB.names = flipud(raw{1});
smilesDB.smile = flipud(raw{2});
[metMatch, metIdx] = ismember(uniqueNames,smilesDB.names);
uniqueSmiles(metMatch) = smilesDB.smile(metIdx(metMatch));
if verbose; fprintf('done.\n'); end
Expand Down
12 changes: 11 additions & 1 deletion src/enzyme_usage/getEnzymeUsage.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
% - capUsage : vector of enzyme capacity usages.
% - absUsage : vector of absolute enzyme usages.
% - UB : vector of enzyme exchange reaction upper bounds.
% - protID : string array of matching protein IDs.
% - protID : string array of matching protein IDs. An enzyme without a
% usage_prot_<id> reaction in the model is left out, with the warning
% getEnzymeUsage:missingUsageReaction.
% - fluxes : vector of fluxes, copy of input fluxes.
%
% Examples
Expand All @@ -60,6 +62,14 @@
end
usageData.protID = ecModel.ec.enzymes;
[~,rxnIdx] = ismember(strcat('usage_prot_',ecModel.ec.enzymes),ecModel.rxns);
noUsageRxn = rxnIdx == 0;
if any(noUsageRxn)
warning('getEnzymeUsage:missingUsageReaction', ...
'The model has no usage reaction for the enzymes %s; they are left out.', ...
strjoin(usageData.protID(noUsageRxn), ', '))
usageData.protID = usageData.protID(~noUsageRxn);
rxnIdx = rxnIdx(~noUsageRxn);
end

usageData.UB = ecModel.ub(rxnIdx);
usageData.absUsage = abs(fluxes(rxnIdx));
Expand Down
15 changes: 9 additions & 6 deletions src/enzyme_usage/reportEnzymeUsage.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
% how many top enzymes by absolute usage to list (default 10). Use 0
% or Inf to list all enzymes instead. Enzymes whose reactions carry no
% flux are never listed here, as they do not contribute to the
% solution.
% solution. A negative value raises reportEnzymeUsage:invalidTopAbsUsage.
%
% Returns
% -------
Expand Down Expand Up @@ -54,6 +54,9 @@
% values it used to reach the indexing below unclamped: usageData.protID(topUse(1:N))
% with N > numel(topUse) is an out-of-bounds index, so any model with fewer than 10
% enzymes (ecTestGEM has 5) crashed on the default call.
if ~(isnumeric(topAbsUsage) && isscalar(topAbsUsage) && topAbsUsage >= 0)
error('reportEnzymeUsage:invalidTopAbsUsage', 'topAbsUsage must be a non-negative number.')
end
if topAbsUsage == 0 || isinf(topAbsUsage) || topAbsUsage > numel(usageData.protID)
topAbsUsage = numel(usageData.protID);
end
Expand Down Expand Up @@ -84,8 +87,8 @@
if isscalar(find(carriedFlux))
highUsage.protID(end+1,1) = highEnzyme(i);
highUsage.geneID(end+1,1) = geneIDs(i);
highUsage.absUsage(end+1,1) = usageData.absUsage(enzIdx(i));
highUsage.capUsage(end+1,1) = usageData.capUsage(enzIdx(i));
highUsage.absUsage(end+1,1) = usageData.absUsage(highUsageProt(i));
highUsage.capUsage(end+1,1) = usageData.capUsage(highUsageProt(i));
highUsage.kcat(end+1,1) = kcat(carriedFlux);
highUsage.source(end+1,1) = ecModel.ec.source(idx(carriedFlux));
highUsage.rxnID(end+1,1) = rxns(carriedFlux);
Expand All @@ -95,8 +98,8 @@
% Add one entry for combined usage
highUsage.protID(end+1,1) = highEnzyme(i);
highUsage.geneID(end+1,1) = geneIDs(i);
highUsage.absUsage(end+1,1) = usageData.absUsage(enzIdx(i));
highUsage.capUsage(end+1,1) = usageData.capUsage(enzIdx(i));
highUsage.absUsage(end+1,1) = usageData.absUsage(highUsageProt(i));
highUsage.capUsage(end+1,1) = usageData.capUsage(highUsageProt(i));
highUsage.kcat(end+1,1) = nan;
highUsage.source{end+1,1} = '===';
highUsage.rxnID{end+1,1} = '===';
Expand All @@ -109,7 +112,7 @@
enzMet = strcat('prot_',highEnzyme{i});
[~, enzEcIdx] = ismember(enzMet,ecModel.mets);
indAbsUse = full(transpose(-ecModel.S(enzEcIdx,rIdx)).*enzFlux);
indCapUse = (indAbsUse /sum(indAbsUse)) * usageData.capUsage(enzIdx(i));
indCapUse = (indAbsUse /sum(indAbsUse)) * usageData.capUsage(highUsageProt(i));

rxnNumber = length(rIdx);
highUsage.protID(end+1:end+rxnNumber,1) = highEnzyme(i);
Expand Down
6 changes: 4 additions & 2 deletions src/flux_analysis/ecFSEOF.m
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,10 @@
corr_rxns = corr_rxns(targets);
target_type = target_type(targets);

% Order from highest to lowest score, the product of slope and correlation
[~,order] = sort(slope_rxns .* corr_rxns,'descend');
% Order from highest to lowest score, the product of slope and correlation,
% rounded to 9 significant digits so that scores equal up to solver noise keep
% the order of the model
[~,order] = sort(round(slope_rxns .* corr_rxns,9,'significant'),'descend');
target_rxns = target_rxns(order);
v_matrix = v_matrix(order,:);
rxnGeneM = rxnGeneM(order,:);
Expand Down
33 changes: 32 additions & 1 deletion src/gather_kcats/assignKcatValues.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
% 'median'/'mean', where the selected value need not equal any single
% input, it is taken from the first entry instead (arbitrary but
% deterministic, since an aggregate has no single "winning" source).
% The source is written in lower case snake_case, with the wildcard
% level and origin of the entry in brackets when it has them
% (kcatList.wildcardLvl and kcatList.origin), for example
% 'brenda (wc=0, origin=1)'. Functions that compare model.ec.source with
% a source name use the part before ' (' (see resolveTunableContext).
% overwrite : char
% whether existing kcat values should be overwritten. Options: 'true',
% 'false', 'ifHigher'. The last option will overwrite only if the new
Expand Down Expand Up @@ -77,6 +82,17 @@

% Remove zero kcat values. Only adjusting fields that are used later.
removeZero = kcatList.kcats == 0;
nEntries = numel(removeZero);
wildcardLvl = nan(nEntries, 1);
origin = nan(nEntries, 1);
if isfield(kcatList,'wildcardLvl') && numel(kcatList.wildcardLvl) == nEntries
wildcardLvl = double(kcatList.wildcardLvl(:));
end
if isfield(kcatList,'origin') && numel(kcatList.origin) == nEntries
origin = double(kcatList.origin(:));
end
wildcardLvl(removeZero) = [];
origin(removeZero) = [];
kcatList.kcats(removeZero) = [];
kcatList.rxns(removeZero) = [];
if isfield(kcatList,'kcatSource')
Expand Down Expand Up @@ -114,7 +130,8 @@
otherwise
error('Invalid criteria specified')
end
selectedSource(i) = kcatList.kcatSource(idxMatch(j));
selectedSource{i} = formatKcatSource(kcatList.kcatSource{idxMatch(j)}, ...
wildcardLvl(idxMatch(j)), origin(idxMatch(j)));
end

% Populate model.ec.kcat
Expand All @@ -140,3 +157,17 @@
end
rxnIdx = idxInModelUnique;
end

function text = formatKcatSource(source, wildcardLvl, origin)
% The provenance text of model.ec.source: the source label in lower case
% snake_case, followed by the wildcard level and origin of a fuzzy BRENDA
% match in brackets, e.g. 'brenda (wc=0, origin=1)'.
text = lower(strtrim(char(source)));
text = regexprep(text, '[^0-9a-z]+', '_');
text = regexprep(text, '^_+|_+$', '');
if ~isnan(wildcardLvl) && ~isnan(origin)
text = sprintf('%s (wc=%d, origin=%d)', text, fix(wildcardLvl), fix(origin));
elseif ~isnan(wildcardLvl)
text = sprintf('%s (wc=%d)', text, fix(wildcardLvl));
end
end
8 changes: 6 additions & 2 deletions src/gather_kcats/collectKcats.m
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
continue
end
lists = {};
usedFiles = {};
priority = {};
for t = 1:numel(st.files)
token = st.files{t};
Expand All @@ -209,8 +210,9 @@
if isempty(list)
continue
end
if ~any(cellfun(@(l) isequal(l, list), lists))
if ~ismember(name, usedFiles)
lists{end+1} = list; %#ok<AGROW>
usedFiles{end+1} = name; %#ok<AGROW>
end
if startsWith(token, 'database_')
priority{end+1} = token; %#ok<AGROW>
Expand Down Expand Up @@ -284,10 +286,12 @@
end

function s = normalizeSource(label)
% As mergeKcats folds source labels: lower case, other characters as '_'.
% As mergeKcats folds source labels: lower case, other characters as '_',
% without a leading okp_ (the tag of OpenKineticsPredictor rows).
s = lower(strtrim(char(label)));
s = regexprep(s, '[^0-9a-z]+', '_');
s = regexprep(s, '^_+|_+$', '');
s = regexprep(s, '^okp_', '');
end

function name = sourceFile(token)
Expand Down
Loading
Loading