Resolve UNRESOLVED.initConc and UNRESOLVED.Size in SBML import - #1985
Merged
Conversation
…sing them
SBML has one flat namespace; VCell separates physiology from application. An SBML global
parameter becomes a Model parameter, while a species' initial concentration is a
SpeciesContextSpec parameter under the SimulationContext. Those two name scopes are unrelated
roots -- ModelNameScope.getParent() and SimulationContextNameScope.getParent() both return
null, and neither is the other's peer -- so a Model parameter cannot name an initial
concentration, getRelativeScopePrefix yields the UNRESOLVED. marker, and the import dies much
later with
Error binding global parameter 'beta' to model: 'UNRESOLVED.initConc' is either not found
in your model or is not allowed to be used in the current context.
31 curated BioModels fail this way (issue #803, open since 2023).
Rather than dropping the dependency or freezing it as a number, invert it. For SBML
beta = c1/(N1*s4) where s4 is a species:
before s4.initConc = 250000 beta = c1/(N1 * UNRESOLVED.initConc) [broken]
after s4_initConc = 250000 (global) s4.initConc = s4_initConc
beta = c1/(N1 * s4_initConc) [exact]
Nothing is lost. The relationship stays symbolic, so scanning s4_initConc moves the initial
condition and beta together, which is what the SBML meant. Everything stays a global
parameter, which matters: global parameters already round-trip through SBMLExporter, whereas
SimulationContextParameter does not (#1984). Hoisting once per species, so N dependents
produce one parameter, not N.
The reference is written as a PLAIN NAME on purpose. A species' initial condition resolves it
through SimulationContext.getLocalEntry(), which falls through to getModel().getLocalEntry();
writing it as new Expression(ste, namescope) would ask the scope machinery for a prefix and
get UNRESOLVED. straight back. Verified both directions bind before building on it.
An earlier attempt inlined the constant value instead. Rejected: it freezes the dependency, so
the imported model reads as a magic number and an export no longer reproduces the source. It
was also strictly weaker -- it could only act when the initial condition was a literal, so
model 632, whose species initial condition is itself computed, stayed broken. Hoisting handles
it because it moves the expression, not the value.
Compartment sizes are deliberately NOT hoisted. A StructureMapping size must remain constant:
StructureSizeSolver (775, 786, 789), GeometryContext (419) and SBMLExporter (373, 387) all
call evaluateConstant() on it, so a symbol there would break the size solver and export. Those
models still fail, now with an explanation rather than a leaked UNRESOLVED marker.
Verified against the models: 599, 632, 705, 872 import (632 is the one inlining could not
do); 627 still fails, on a reaction-rate reference (#1983) that was hidden behind this one.
BMDB_SBMLImportTest 27 tests 0 failures, with 696 removed from the fault table because it now
passes. vcell-core Fast 547 tests, 1 error (VCellDataTest poetry noise, environmental).
AbstractNameScope gains a named constant for the "UNRESOLVED." literal so callers that can do
better on that path can test for it.
Refs #803, #1984
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The List<String> threaded through adjustExpression existed solely so one LowPriority vcLogger
message could name the hoisted initial conditions. It had no functional role -- hoisting
happens inside hoistInitialConditionToGlobalParameter whether or not a collector is passed.
Removing it because it was the wrong shape three ways:
- inconsistent: wired into 2 of adjustExpression's ~12 call sites, so hoists reached from any
other path reported nothing, with no principle behind which ones did;
- duplicative: SBMLSymbolMapping already records SBase->STE provenance for both initial and
runtime contexts, and the hoisted parameter carries a description explaining itself;
- expensive for what it was: an extra overload and parameter on a method called from a dozen
places, to produce a log line.
The logger.info inside the hoist itself stays, and is strictly better -- it fires for every
hoist from every call site rather than two. Naming conflicts never depended on the list either;
uniqueGlobalParameterName checks getModelParameter and getLocalEntry directly.
adjustExpression returns to a single five-argument method. Behaviour is unchanged: 599, 632,
705 and 872 still import, 627 still fails on its reaction-rate reference, BMDB_SBMLImportTest
27 tests 0 failures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion A compartment's size has two representations in VCell: the StructureMapping Size parameter, which belongs to the application, and Structure.StructureSize, a ModelQuantity that belongs to the physiology and that ModelNameScope names directly. adjustExpression already swapped to the latter when the target was a species initial condition; it did not when the target was in Model scope, so a global parameter referencing a compartment size resolved to UNRESOLVED.Size and failed at binding. Seven of the 31 models in #803 fail that way. Fixed by making the swap unconditional -- anything outside the application needs the model-level quantity. BIOMD0000000457 now imports. 429 and 1027 still fail, on a compartment with constant="false", which is a different unsupported feature that this failure was hiding. This replaces the reporting added earlier for the same case. Reporting an unresolved reference was the wrong shape: better to resolve it. With initial conditions hoisted and sizes using StructureSize, no UNRESOLVED. marker survives anywhere in the 27-model import suite, so the report was dead code. What remains is a single logger.error inside adjustExpression for a scope mismatch we have not seen, since the eventual binding failure names only the marker and not what produced it. No collector, no call-site plumbing, and it covers all twelve call sites rather than two. Verified with the check that actually tests semantics rather than parseability: the SBML Test Suite, which compares computed results against reference CSVs within tolerance, passes 1382 tests 0 failures. BMDB_SBMLImportTest 27 tests 0 failures. Refs #803 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #803 — both halves.
initConcby hoisting into a global parameter,Sizeby using the model-levelStructureSize.The problem
SBML has one flat namespace; VCell separates physiology from application. An SBML global parameter becomes a Model parameter, while a species' initial concentration is a
SpeciesContextSpecparameter under the SimulationContext. Those two name scopes are unrelated roots —ModelNameScope.getParent()andSimulationContextNameScope.getParent()both returnnull, and neither is the other's peer — so a Model parameter cannot name an initial concentration, andgetRelativeScopePrefixyields theUNRESOLVED.marker. The import then dies much later with:31 curated BioModels fail this way. #803 has been open since 2023.
The fix: invert the dependency, don't drop it
For SBML
beta = c1/(N1*s4)wheres4is a species:Verified on the imported model rather than from the log:
Nothing is lost. The relationship stays symbolic, so scanning
s4_initConcmoves the initial condition andbetatogether — which is what the SBML meant, and is the VCell idiom for varying an initial condition. Hoisting happens once per species, so N dependents produce one parameter, not N.Everything stays a global parameter, which matters: global parameters already round-trip through
SBMLExporter, whereasSimulationContextParameterdoes not (#1984). This fix therefore does not wait on that work.One implementation detail worth knowing
The reference is written as a plain name, deliberately. A species' initial condition resolves it through
SimulationContext.getLocalEntry(), which falls through togetModel().getLocalEntry(). Writing it asnew Expression(ste, namescope)would ask the scope machinery for a prefix and getUNRESOLVED.straight back. I probed that both directions bind before building on it.An earlier attempt, rejected
I first inlined the constant value (
beta = 1.539e-7). Rejected on review — it freezes the dependency, so the imported model reads as a magic number and an export no longer reproduces the source.It was also strictly weaker: it could only act when the initial condition was a literal, so model 632 — whose species initial condition is itself computed — stayed broken. Hoisting handles it because it moves the expression, not the value.
Compartment Size: fixed, not deferred
A compartment size has two representations: the
StructureMappingSizeparameter, which belongs to the application, andStructure.StructureSize, aModelQuantitythat belongs to the physiology and thatModelNameScopenames directly.adjustExpressionalready swapped to the latter when the target was a species initial condition — it just didn't when the target was in Model scope. Making that swap unconditional fixes the 7UNRESOLVED.Sizemodels. No hoisting needed, and the size stays a constant, soStructureSizeSolver,GeometryContextandSBMLExporter— all of which callevaluateConstant()on it — are unaffected.BIOMD0000000457 now imports. 429 and 1027 still fail, on a compartment with
constant="false"— a different unsupported feature this failure was hiding.No reporting left
An earlier revision reported unresolved references instead of fixing them. Resolving them is better, and with both classes handled no
UNRESOLVED.marker survives anywhere in the 27-model import suite — the report was dead code and is gone, along with the collector list that fed the initial-condition message.What remains is one
logger.errorinsideadjustExpressionfor a scope mismatch we have not seen, since the eventual binding failure names only the marker and not what produced it. No collector, no call-site plumbing, and it covers all twelve call sites rather than two.Verification
UNRESOLVED.initConcUNRESOLVED.initConcUNRESOLVED.initConcUNRESOLVED.initConcUNRESOLVED.SizeUNRESOLVED.Sizeconstant="false"compartments — a different feature this was hidingSBMLTestSuiteTest— computed results vs reference CSVs, within toleranceBMDB_SBMLImportTest(SBML_IT)vcell-coreFast, CI parallel flagsVCellDataTestpoetry noise, environmental)vcell-mathFastThe first row is the one that matters. "It imports" says nothing about whether the math is still right; the SBML Test Suite compares simulated output against reference results, so it is the check that these scope changes preserve semantics rather than just parseability.
AbstractNameScopegains a named constant for the"UNRESOLVED."literal, so callers that can do better on that path can test for it rather than matching a magic string.Refs #803, #1984
🤖 Generated with Claude Code