From d15544d9262dc4ff835a80f93ffb52876a38182e Mon Sep 17 00:00:00 2001 From: Eduard Kerkhoven Date: Sun, 25 Oct 2026 09:36:28 +0100 Subject: [PATCH 1/2] fix: keep each subsystem with its reaction when readYAMLmodel flattens them A reaction without a subsystem held an empty entry in model.subSystems, and flattening the entries with [model.subSystems{:}] dropped the empty ones, so the vector was shorter than model.rxns and every subsystem after a gap sat on the wrong reaction. A reaction without a subsystem now gets an empty entry first, and the entries are flattened one by one, so model.subSystems keeps one entry per reaction. --- src/io/readYAMLmodel.m | 5 ++++- tests/function_tests/tIO.m | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/io/readYAMLmodel.m b/src/io/readYAMLmodel.m index e8b5c351..3db80cd6 100755 --- a/src/io/readYAMLmodel.m +++ b/src/io/readYAMLmodel.m @@ -813,9 +813,12 @@ % end if isfield(model,'subSystems') +% A reaction without subsystem holds [] or '' here: give it an empty entry, +% so every reaction keeps its place when the entries are flattened + model.subSystems(cellfun(@isempty, model.subSystems)) = {{''}}; % If all entries are 1x1, then flatten if all(cellfun(@(x) numel(x) <= 1, model.subSystems)) - model.subSystems = transpose([model.subSystems{:}]); + model.subSystems = cellfun(@(x) x{1}, model.subSystems, 'UniformOutput', false); end end diff --git a/tests/function_tests/tIO.m b/tests/function_tests/tIO.m index fe5cb78c..0a865508 100644 --- a/tests/function_tests/tIO.m +++ b/tests/function_tests/tIO.m @@ -207,6 +207,37 @@ function writeReadYAMLRoundTrip(testCase) testCase.verifyEqual(numel(m2.rxns), numel(testCase.model.rxns)); end + function readYAMLmodelKeepsSubsystemsAligned(testCase) + % Reactions without a subsystem sit between, before and after + % those with one: each subsystem stays with its own reaction. + m = testCase.model; + n = numel(m.rxns); + m.subSystems = repmat({{''}}, n, 1); + withSubsystem = [2 5 n-3]; + names = {'Alpha'; 'Beta'; 'Gamma'}; + for i = 1:numel(withSubsystem) + m.subSystems{withSubsystem(i)} = names(i); + end + f = [tempname '.yml']; + testCase.addTeardown(@() delete(f)); + evalc('writeYAMLmodel(m, f);'); + evalc('m2 = readYAMLmodel(f);'); + testCase.verifyEqual(numel(m2.subSystems), n); + read = strings(n,1); + for i = 1:n + entry = m2.subSystems{i}; + if iscell(entry) && ~isempty(entry) + entry = entry{1}; + end + if ischar(entry) + read(i) = string(entry); + end + end + expected = strings(n,1); + expected(withSubsystem) = string(names); + testCase.verifyEqual(read, expected); + end + function writeYAMLmodelQuotesYAML11Booleans(testCase) % A YAML 1.1 loader such as PyYAML reads a plain y, n, yes, no, % on or off in any case as a boolean, so these values are From 6679f96158d482c706d37ea94a06ddd802eba352 Mon Sep 17 00:00:00 2001 From: Eduard Kerkhoven Date: Sun, 25 Oct 2026 16:43:45 +0100 Subject: [PATCH 2/2] test: check the subsystems that readYAMLmodel returns against checkModelStruct, for single and several subsystems per reaction --- tests/function_tests/tIO.m | 62 +++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/tests/function_tests/tIO.m b/tests/function_tests/tIO.m index 0a865508..4c5e712d 100644 --- a/tests/function_tests/tIO.m +++ b/tests/function_tests/tIO.m @@ -209,7 +209,9 @@ function writeReadYAMLRoundTrip(testCase) function readYAMLmodelKeepsSubsystemsAligned(testCase) % Reactions without a subsystem sit between, before and after - % those with one: each subsystem stays with its own reaction. + % those with one: each subsystem stays with its own reaction, + % and the result is a column of chars that checkModelStruct + % accepts. m = testCase.model; n = numel(m.rxns); m.subSystems = repmat({{''}}, n, 1); @@ -222,20 +224,58 @@ function readYAMLmodelKeepsSubsystemsAligned(testCase) testCase.addTeardown(@() delete(f)); evalc('writeYAMLmodel(m, f);'); evalc('m2 = readYAMLmodel(f);'); + testCase.verifyTrue(iscolumn(m2.subSystems)); testCase.verifyEqual(numel(m2.subSystems), n); - read = strings(n,1); + testCase.verifyTrue(iscellstr(m2.subSystems)); + expected = repmat({''}, n, 1); + expected(withSubsystem) = names; + testCase.verifyEqual(string(m2.subSystems), string(expected)); + ws = warning('off', 'RAVEN:modelError'); + testCase.addTeardown(@() warning(ws)); + try + evalc('checkModelStruct(m2, ''throwErrors'', true);'); + catch err + testCase.verifyFail(['checkModelStruct refused the model: ' err.message]); + end + end + + function readYAMLmodelKeepsNestedSubsystemsAligned(testCase) + % Reactions with several subsystems sit next to reactions with + % one and with none: every entry is a cell of chars, each stays + % with its own reaction, and checkModelStruct accepts the model. + m = testCase.model; + n = numel(m.rxns); + m.subSystems = repmat({{''}}, n, 1); + nested = {3, {'Alpha'; 'Delta'}; ... + 6, {'Beta'}; ... + n-2, {'Gamma'; 'Epsilon'; 'Zeta'}}; + for i = 1:size(nested,1) + m.subSystems{nested{i,1}} = nested{i,2}; + end + f = [tempname '.yml']; + testCase.addTeardown(@() delete(f)); + evalc('writeYAMLmodel(m, f);'); + evalc('m2 = readYAMLmodel(f);'); + testCase.verifyTrue(iscolumn(m2.subSystems)); + testCase.verifyEqual(numel(m2.subSystems), n); + testCase.verifyTrue(all(cellfun(@iscell, m2.subSystems))); for i = 1:n - entry = m2.subSystems{i}; - if iscell(entry) && ~isempty(entry) - entry = entry{1}; - end - if ischar(entry) - read(i) = string(entry); + row = find([nested{:,1}] == i); + if isempty(row) + testCase.verifyTrue(isempty([m2.subSystems{i}{:}]), ... + sprintf('reaction %d has no subsystem', i)); + else + testCase.verifyEqual(string(m2.subSystems{i}), ... + string(nested{row,2}), sprintf('reaction %d', i)); end end - expected = strings(n,1); - expected(withSubsystem) = string(names); - testCase.verifyEqual(read, expected); + ws = warning('off', 'RAVEN:modelError'); + testCase.addTeardown(@() warning(ws)); + try + evalc('checkModelStruct(m2, ''throwErrors'', true);'); + catch err + testCase.verifyFail(['checkModelStruct refused the model: ' err.message]); + end end function writeYAMLmodelQuotesYAML11Booleans(testCase)