diff --git a/src/io/readYAMLmodel.m b/src/io/readYAMLmodel.m index 90a3333c..e8b5c351 100755 --- a/src/io/readYAMLmodel.m +++ b/src/io/readYAMLmodel.m @@ -195,6 +195,7 @@ end section = 0; +readList = ''; metMiriams=cell(100000,3); metMirNo=1; rxnMiriams=cell(100000,3); rxnMirNo=1; geneMiriams=cell(100000,3); genMirNo=1; @@ -346,6 +347,12 @@ % import metabolites: if section == 2 + % The lines of a notes map are indented below the entry's own + % fields; their keys are not entry fields, whatever their name. + if strcmp(readList, 'notes') && ~isempty(regexp(tline_raw, '^ {6,}', 'once')) + model = appendNote(model, 'metNotes', tline_raw, tline_key, tline_value, pos); + continue + end switch tline_key case 'id' pos = pos + 1; @@ -364,8 +371,14 @@ model = readFieldValue(model, 'metCharges', tline_value, pos); readList=''; miriamKey=''; case 'notes' - model = readFieldValue(model, 'metNotes', tline_value, pos); - readList=''; miriamKey=''; + % A notes map (other tools' keys, as cobrapy and + % raven-toolbox write them) is read into the note text. + if isempty(tline_value) || strcmp(tline_value, '!!omap') + readList = 'notes'; + else + model = readFieldValue(model, 'metNotes', tline_value, pos); + readList=''; miriamKey=''; + end case 'inchis' model = readFieldValue(model, 'inchis', tline_value, pos); readList=''; miriamKey=''; @@ -421,6 +434,12 @@ % import reactions: if section == 3 + % The lines of a notes map are indented below the entry's own + % fields; their keys are not entry fields, whatever their name. + if strcmp(readList, 'notes') && ~isempty(regexp(tline_raw, '^ {6,}', 'once')) + model = appendNote(model, 'rxnNotes', tline_raw, tline_key, tline_value, pos); + continue + end switch tline_key case 'id' pos = pos + 1; @@ -445,8 +464,12 @@ % `notes` is the canonical reaction-side key; `rxnNotes` % is the legacy RAVEN MATLAB key, kept here for % backward-compatible reads. - model = readFieldValue(model, 'rxnNotes', tline_value, pos); - readList=''; miriamKey=''; + if isempty(tline_value) || strcmp(tline_value, '!!omap') + readList = 'notes'; + else + model = readFieldValue(model, 'rxnNotes', tline_value, pos); + readList=''; miriamKey=''; + end case 'rxnFrom' model = readFieldValue(model, 'rxnFrom', tline_value, pos); readList=''; miriamKey=''; @@ -878,6 +901,47 @@ model.(fieldName)(pos,1) = {value}; end +function model = appendNote(model, fieldName, raw, key, value, pos) +% One line of a notes map: a 'note' entry adds its value (RAVEN's own note +% text), another entry adds 'key: value', a list item adds its value to the +% entry above it. Entries are separated by '; ', list items by ', '. A +% quoted list item is read whole, as the key detection would split it at +% a ': ' inside the quotes. +item = regexp(raw, '^ +- ([''"])(.*)\1$', 'tokens', 'once'); +if ~isempty(item) + key = ''; + value = item{2}; +end +if strcmp(value, '!!omap') + value = ''; +end +text = ''; +if numel(model.(fieldName)) >= pos + text = model.(fieldName){pos}; +end +if isempty(key) + if isempty(text) || endsWith(text, ':') + text = strtrim([text ' ' value]); + else + text = [text ', ' value]; + end +else + if isempty(value) + entry = [key ':']; + elseif strcmp(key, 'note') + entry = value; + else + entry = [key ': ' value]; + end + if isempty(text) + text = entry; + else + text = [text '; ' entry]; + end +end +model = readFieldValue(model, fieldName, text, pos); +end + function [miriams,miriamKey,entryNumber] = gatherAnnotation(pos,miriams,key,value,miriamKey,entryNumber) if isempty(key) key=miriamKey; diff --git a/tests/function_tests/tIO.m b/tests/function_tests/tIO.m index 86a06ae6..e56709ca 100644 --- a/tests/function_tests/tIO.m +++ b/tests/function_tests/tIO.m @@ -294,6 +294,45 @@ function writeReadYAMLPreservesUnknownAnnotationFields(testCase) testCase.verifyEqual(m2.annotation.geckopyVersion, '0.2.1'); end + function readYAMLmodelReadsNotesMaps(testCase) + % cobrapy and raven-toolbox write other tools' notes as a map + % under "notes"; its entries are read into the note text. + m = testCase.model; + m.metNotes = repmat({''}, numel(m.mets), 1); + m.rxnNotes = repmat({''}, numel(m.rxns), 1); + m.metNotes{1} = 'METPLACEHOLDER'; + m.rxnNotes{1} = 'RXNPLACEHOLDER'; + f = [tempname '.yml']; + testCase.addTeardown(@() delete(f)); + evalc('writeYAMLmodel(m, f);'); + txt = fileread(f); + % The metabolite's map has a key named like an entry field and a + % quoted list item with ': ', and is followed by an annotation. + txt = regexprep(txt, '( *)- notes: METPLACEHOLDER', ... + ['$1- notes: !!omap\n$1 - enzyme_usage: Enzyme-usage pseudometabolite\n' ... + '$1 - name: other\n$1 - refs:\n$1 - "PMID: 1"\n$1 - b\n' ... + '$1- annotation: !!omap\n$1 - sbo: SBO:0000252']); + % The reaction's map comes before other fields, has keys named + % like entry fields and a list value. + txt = regexprep(txt, '( *)- notes: RXNPLACEHOLDER', ... + ['$1- notes: !!omap\n$1 - geckopy_warning: no match for g1\n$1 - id: R99\n' ... + '$1 - name: other\n$1 - refs:\n$1 - a\n$1 - b\n$1 - note: curated\n' ... + '$1- rxnFrom: test']); + fid = fopen(f, 'w'); fwrite(fid, txt); fclose(fid); + evalc('m2 = readYAMLmodel(f);'); + testCase.verifyEqual(m2.metNotes{1}, ... + 'enzyme_usage: Enzyme-usage pseudometabolite; name: other; refs: PMID: 1, b'); + testCase.verifyEqual(m2.metNames, m.metNames); + testCase.verifyTrue(any(strcmp(m2.metMiriams{1}.name, 'sbo'))); + testCase.verifyEqual(m2.rxnNotes{1}, ... + 'geckopy_warning: no match for g1; id: R99; name: other; refs: a, b; curated'); + testCase.verifyEqual(m2.rxns, m.rxns); + testCase.verifyEqual(m2.rxnNames, m.rxnNames); + testCase.verifyEqual(m2.rxnFrom{1}, 'test'); + testCase.verifyEqual(numel(m2.mets), numel(m.mets)); + testCase.verifyEqual(numel(m2.rxns), numel(m.rxns)); + end + function exportForGitWritesRepo(testCase) d = [tempname filesep]; mkdir(d);