diff --git a/src/change_model/upgradeEcModel.m b/src/change_model/upgradeEcModel.m index 8cb9ecb9..318ff104 100644 --- a/src/change_model/upgradeEcModel.m +++ b/src/change_model/upgradeEcModel.m @@ -10,6 +10,8 @@ % objective coefficient change sign. Reactions already forward are left as % they are, so calling it again changes nothing. Flux values of the model % (fluxes of usage reactions and the pool exchange) change sign with it. +% The ecModel format (annotation.gecko_format, see saveEcModel) becomes 4 +% when it is missing or lower. % geckopy has the same (upgrade_ec_model). % % Parameters @@ -26,7 +28,7 @@ % % Examples % -------- -% ecModel = upgradeEcModel(loadEcModel('ecYeastGEM_GECKO3.yml')); +% ecModel = upgradeEcModel(readYAMLmodel('ecYeastGEM_GECKO3.yml')); % % See also % -------- @@ -37,6 +39,10 @@ if isempty(pool) return end +if ~isfield(model, 'annotation') || ~isfield(model.annotation, 'gecko_format') ... + || str2double(model.annotation.gecko_format) < 4 + model.annotation.gecko_format = '4'; +end usage = find(startsWith(model.rxns, 'usage_prot_')); reverse = usage(full(model.S(pool, usage)) > 0); exchange = find(strcmp(model.rxns, 'prot_pool_exchange')); diff --git a/src/io/loadEcModel.m b/src/io/loadEcModel.m index e0ed39f4..9ce5e43f 100644 --- a/src/io/loadEcModel.m +++ b/src/io/loadEcModel.m @@ -7,6 +7,11 @@ % providing the appropriate filename. If loading models from other % locations, one can directly use readYAMLmodel. % +% An ecModel whose protein usage and pool reactions run in the GECKO 3 +% direction is upgraded in the loaded model with upgradeEcModel, with a +% warning. An ecModel format (gecko_format in the file's metaData, see +% saveEcModel) above 4 is an error. +% % Name-Value Arguments % -------------------- % filename : char @@ -20,7 +25,7 @@ % Returns % ------- % model : struct -% an ecModel in GECKO 3 format (with ecModel.ec structure). +% an ecModel (with ecModel.ec structure). % % Examples % -------- @@ -47,7 +52,6 @@ params = modelAdapter.getParameters(); if isempty(filename) filename = 'ecModel.yml'; -else end if ~endsWith(filename,{'yml','yaml'}) @@ -63,4 +67,21 @@ elseif endsWith(filename,{'xml','sbml'}) model = importModel(filename); end + +if isfield(model, 'annotation') && isfield(model.annotation, 'gecko_format') ... + && str2double(model.annotation.gecko_format) > 4 + writer = 'an unknown release'; + if isfield(model.annotation, 'generator') + writer = model.annotation.generator; + end + error('loadEcModel:format', ['%s has ecModel format %s (written by %s); ' ... + 'this GECKO reads format 4 and older. Update GECKO to load it.'], ... + filename, model.annotation.gecko_format, writer) +end +[model, changed] = upgradeEcModel(model); +if ~isempty(changed) + warning('loadEcModel:upgraded', ['%s has %d protein usage and pool reactions in the GECKO 3 ' ... + 'direction; they are turned forward in the loaded model. upgradeGECKOproject upgrades the ' ... + 'files of the project.'], filename, numel(changed)) +end end diff --git a/src/io/saveEcModel.m b/src/io/saveEcModel.m index fb3a3c00..ae51dd54 100644 --- a/src/io/saveEcModel.m +++ b/src/io/saveEcModel.m @@ -7,6 +7,10 @@ function saveEcModel(ecModel,varargin) % simulations like FBA etc., but this model cannot be loaded back into % MATLAB for applying further GECKO functions, as model.ec is lost). % +% A YAML file records its ecModel format (gecko_format, see loadEcModel) +% and the GECKO release that wrote it (generator) in its metaData, in +% place of the geckopy_version that earlier geckopy releases wrote. +% % The ecModel is saved to the ecModel-specific models/ subfolder. For % saving to other locations, writeYAMLmodel or exportModel are more % suitable. @@ -14,7 +18,7 @@ function saveEcModel(ecModel,varargin) % Parameters % ---------- % ecModel : struct -% an ecModel in GECKO 3 format (with ecModel.ec structure). +% an ecModel (with ecModel.ec structure). % % Name-Value Arguments % -------------------- @@ -82,6 +86,22 @@ function saveEcModel(ecModel,varargin) fclose('all'); delete('backup.xml'); otherwise % Assume YAML + % The ecModel format the file follows (as project_format in + % model_adapter.toml) and the release that wrote it. + ecModel.annotation.gecko_format = '4'; + ecModel.annotation.generator = ['GECKO ' geckoVersion()]; + if isfield(ecModel.annotation, 'geckopy_version') + ecModel.annotation = rmfield(ecModel.annotation, 'geckopy_version'); + end writeYAMLmodel(ecModel,filename); end end + +function v = geckoVersion() +file = fullfile(findGECKOroot, 'version.txt'); +if isfile(file) + v = strtrim(fileread(file)); +else + v = 'unknown'; +end +end diff --git a/src/utilities/upgradeGECKOproject.m b/src/utilities/upgradeGECKOproject.m index 0a68d070..56a22661 100644 --- a/src/utilities/upgradeGECKOproject.m +++ b/src/utilities/upgradeGECKOproject.m @@ -162,12 +162,23 @@ function keepOriginal(file) if ~contains(fileread(file), 'prot_pool') continue end - model = readYAMLmodel(file); + try + model = readYAMLmodel(file); + catch err + add('saved ecModel', 'failed', sprintf('%s: %s', relative(file, projectPath), err.message)); + continue + end + if isfield(model.annotation, 'gecko_format') && str2double(model.annotation.gecko_format) > 4 + add('saved ecModel', 'failed', sprintf('%s: ecModel format %s is newer than this GECKO', ... + relative(file, projectPath), model.annotation.gecko_format)); + continue + end [model, changed] = upgradeEcModel(model); if isempty(changed) continue end if apply + model.annotation.generator = ['GECKO ' geckoVersion()]; keepOriginal(file); writeYAMLmodel(model, file); end @@ -287,3 +298,12 @@ function checkOptions(args, names, functionName) strjoin(names, ', ')) end end + +function v = geckoVersion() +file = fullfile(findGECKOroot, 'version.txt'); +if isfile(file) + v = strtrim(fileread(file)); +else + v = 'unknown'; +end +end diff --git a/test/VerificationMatrix.tsv b/test/VerificationMatrix.tsv index 7b060f14..d027bbbe 100644 --- a/test/VerificationMatrix.tsv +++ b/test/VerificationMatrix.tsv @@ -31,8 +31,8 @@ gather_kcats/kcatsFromECTree.m tc0086, tc0087 gather_kcats/kcatsFromHomologs.m tc0095, tc0096 gather_kcats/extractEnzymeSubstratePairs.m tc0096, tc0015 gather_kcats/convertCustomKcats.m tc0091, tc0092, tc0094 -change_model/upgradeEcModel.m tc0093, tc0094 -utilities/upgradeGECKOproject.m tc0094 +change_model/upgradeEcModel.m tc0093, tc0094, tc0099 +utilities/upgradeGECKOproject.m tc0094, tc0099 gather_kcats/readOpenKineticsPredictorOutput.m tc0030, tc0069 gather_kcats/reviewKcatAssignment.m Has only been tested by running it manually, not tested by test cases gather_kcats/ecStatsFromBrenda.m Not verified against real BRENDA data in this environment -- see its own TODO comment @@ -70,8 +70,8 @@ model_adapter/adapterParameters.tsv tc0064, tc0065 model_adapter/adapterTemplate.toml tc0068 utilities/* Not explicitly tested - The code has existed for a long time. There are a lot of functions in here, unclear which of them are still useful and functional. io/loadConventionalGEM.m tc0009 -io/loadEcModel.m tc0009 -io/saveEcModel.m tc0009 +io/loadEcModel.m tc0009, tc0099 +io/saveEcModel.m tc0009, tc0099 tutorials/tutorial_Human-GEM/HumanGEMAdapter.m tcm0002 - does not test all aspects of the adapter, but some. tutorials/full_ecModel/model_adapter.toml tcm0001 - does not test all aspects of the adapter, but some. tutorials/tutorial_yeast-GEM/code/changeMedia_batch.m Not explicitly tested - The code has existed for a long time diff --git a/test/unit_tests/geckoCoreFunctionTests.m b/test/unit_tests/geckoCoreFunctionTests.m index a9b12832..d0a0e7fd 100644 --- a/test/unit_tests/geckoCoreFunctionTests.m +++ b/test/unit_tests/geckoCoreFunctionTests.m @@ -240,6 +240,9 @@ function testsaveECModel_tc0009(testCase) saveEcModel(ecModel, [], adapter); loadedEcModel = loadEcModel([],adapter); delete(fullfile(adapter.params.path,'models','ecModel.yml')); + verifyEqual(testCase, loadedEcModel.annotation.gecko_format, '4') + verifyTrue(testCase, startsWith(loadedEcModel.annotation.generator, 'GECKO ')) + loadedEcModel.annotation = rmfield(loadedEcModel.annotation, {'gecko_format', 'generator'}); verifyEqual(testCase, ecModel, loadedEcModel) % Test loading of conventional GEM diff --git a/test/unit_tests/upgradeTests.m b/test/unit_tests/upgradeTests.m index 7be17179..0bdf71b7 100644 --- a/test/unit_tests/upgradeTests.m +++ b/test/unit_tests/upgradeTests.m @@ -90,6 +90,8 @@ function testUpgradeGECKOproject_tc0094(testCase) saved = readYAMLmodel(fullfile(project, 'models', 'ecModel.yml')); [~, changed] = upgradeEcModel(saved); verifyEmpty(testCase, changed) + verifyEqual(testCase, saved.annotation.gecko_format, '4') + verifyTrue(testCase, startsWith(saved.annotation.generator, 'GECKO ')) params = loadAdapterParams(fullfile(project, 'model_adapter.toml')); verifyEqual(testCase, params.projectFormat, 4) @@ -97,6 +99,34 @@ function testUpgradeGECKOproject_tc0094(testCase) verifyTrue(testCase, all(ismember(again.status, {'by hand'}))) end +function testEcModelFormat_tc0099(testCase) + % saveEcModel records the format and the release; loadEcModel upgrades + % a GECKO 3 file without them and refuses a newer format. + [model, adapter, project] = buildProject(); + testCase.addTeardown(@() rmdir(project, 's')); + mkdir(fullfile(project, 'models')); + saveEcModel(model, 'ecModel.yml', adapter); + text = fileread(fullfile(project, 'models', 'ecModel.yml')); + verifyNotEmpty(testCase, regexp(text, '- gecko_format: "4"', 'once')) + verifyNotEmpty(testCase, regexp(text, '- generator: GECKO \S+', 'once')) + loaded = verifyWarningFree(testCase, @() loadEcModel('ecModel.yml', adapter)); + verifyEqual(testCase, loaded.S, model.S) + + writeYAMLmodel(gecko3(model), fullfile(project, 'models', 'gecko3.yml')); + up = verifyWarning(testCase, @() loadEcModel('gecko3.yml', adapter), 'loadEcModel:upgraded'); + verifyEqual(testCase, up.S, model.S) + verifyEqual(testCase, [up.lb up.ub], [model.lb model.ub]) + verifyEqual(testCase, up.annotation.gecko_format, '4') + + writeLines(fullfile(project, 'models', 'newer.yml'), ... + strsplit(strrep(text, '- gecko_format: "4"', '- gecko_format: "5"'), newline)); + verifyError(testCase, @() loadEcModel('newer.yml', adapter), 'loadEcModel:format') + newer = readYAMLmodel(fullfile(project, 'models', 'newer.yml')); + verifyEqual(testCase, getfield(upgradeEcModel(newer), 'annotation', 'gecko_format'), '5') + report = upgradeGECKOproject(project); + verifyEqual(testCase, report.status(contains(report.note, 'newer.yml')), {'failed'}) +end + function old = gecko3(model) % The model as GECKO 3 defined it: usage and pool exchange reversed. r = startsWith(model.rxns, 'usage_prot_') | strcmp(model.rxns, 'prot_pool_exchange');