Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the SBML annotations work just like with JSON models #676

Merged
merged 3 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "COBREXA"
uuid = "babc4406-5200-4a30-9033-bf5ae714c842"
authors = ["The developers of COBREXA.jl"]
version = "1.4.0"
version = "1.4.1"

[deps]
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
Expand Down Expand Up @@ -32,7 +32,7 @@ JuMP = "1"
MAT = "0.10"
MacroTools = "0.5.6"
OrderedCollections = "1.4"
SBML = "~1.1, ~1.2"
SBML = "~1.3"
StableRNGs = "1.0"
Tulip = "0.7.0, 0.8.0, 0.9.2"
julia = "1.5"
Expand Down
133 changes: 114 additions & 19 deletions src/base/types/SBMLModel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,54 @@ Get charge of a chosen metabolite from [`SBMLModel`](@ref).
metabolite_charge(model::SBMLModel, mid::String)::Maybe{Int} =
model.sbml.species[mid].charge

function _sbml_export_annotation(annotation)::Maybe{String}
if isnothing(annotation) || isempty(annotation)
nothing
elseif length(annotation) != 1 || first(annotation).first != ""
@_io_log @warn "Possible data loss: multiple annotations converted to text for SBML" annotation
join(["$k: $v" for (k, v) in annotation], "\n")
else
@_io_log @warn "Possible data loss: trying to represent annotation in SBML is unlikely to work " annotation
first(annotation).second
function _parse_sbml_identifiers_org_uri(uri::String)::Tuple{String,String}
m = match(r"^http://identifiers.org/([^/]+)/(.*)$", uri)
isnothing(m) ? ("RESOURCE_URI", uri) : (m[1], m[2])
end

function _sbml_import_cvterms(sbo::Maybe{String}, cvs::Vector{SBML.CVTerm})::Annotations
res = Annotations()
isnothing(sbo) || (res["sbo"] = [sbo])
for cv in cvs
cv.biological_qualifier == :is || continue
for (id, val) in _parse_sbml_identifiers_org_uri.(cv.resource_uris)
push!(get!(res, id, []), val)
end
end
return res
end

function _sbml_export_cvterms(annotations::Annotations)::Vector{SBML.CVTerm}
isempty(annotations) && return []
length(annotations) == 1 && haskey(annotations, "sbo") && return []
[
SBML.CVTerm(
biological_qualifier = :is,
resource_uris = [
id == "RESOURCE_URI" ? val : "http://identifiers.org/$id/$val" for
(id, vals) = annotations if id != "sbo" for val in vals
],
),
]
end

const _sbml_export_notes = _sbml_export_annotation
function _sbml_export_sbo(annotations::Annotations)::Maybe{String}
haskey(annotations, "sbo") || return nothing
if length(annotations["sbo"]) != 1
@_io_log @error "Data loss: SBO term is not unique for SBML export" annotations["sbo"]
return
end
return annotations["sbo"][1]
end

function _sbml_import_notes(notes::Maybe{String})::Notes
isnothing(notes) ? Notes() : Notes("" => [notes])
end

function _sbml_export_notes(notes::Notes)::Maybe{String}
isempty(notes) || @_io_log @error "Data loss: notes not exported to SBML" notes
nothing
end

"""
$(TYPEDSIGNATURES)
Expand Down Expand Up @@ -180,6 +215,56 @@ gene_name(model::SBMLModel, gid::String) = model.sbml.gene_products[gid].name
"""
$(TYPEDSIGNATURES)

Return the annotations of reaction with ID `rid`.
"""
reaction_annotations(model::SBMLModel, rid::String) =
_sbml_import_cvterms(model.sbml.reactions[rid].sbo, model.sbml.reactions[rid].cv_terms)

"""
$(TYPEDSIGNATURES)

Return the annotations of metabolite with ID `mid`.
"""
metabolite_annotations(model::SBMLModel, mid::String) =
_sbml_import_cvterms(model.sbml.species[mid].sbo, model.sbml.species[mid].cv_terms)

"""
$(TYPEDSIGNATURES)

Return the annotations of gene with ID `gid`.
"""
gene_annotations(model::SBMLModel, gid::String) = _sbml_import_cvterms(
model.sbml.gene_products[gid].sbo,
model.sbml.gene_products[gid].cv_terms,
)

"""
$(TYPEDSIGNATURES)

Return the notes about reaction with ID `rid`.
"""
reaction_notes(model::SBMLModel, rid::String) =
_sbml_import_notes(model.sbml.reactions[rid].notes)

"""
$(TYPEDSIGNATURES)

Return the notes about metabolite with ID `mid`.
"""
metabolite_notes(model::SBMLModel, mid::String) =
_sbml_import_notes(model.sbml.species[mid].notes)

"""
$(TYPEDSIGNATURES)

Return the notes about gene with ID `gid`.
"""
gene_notes(model::SBMLModel, gid::String) =
_sbml_import_notes(model.sbml.gene_products[gid].notes)

"""
$(TYPEDSIGNATURES)

Convert any metabolic model to [`SBMLModel`](@ref).
"""
function Base.convert(::Type{SBMLModel}, mm::MetabolicModel)
Expand All @@ -194,38 +279,44 @@ function Base.convert(::Type{SBMLModel}, mm::MetabolicModel)
comps = _default.("compartment", metabolite_compartment.(Ref(mm), mets))
compss = Set(comps)

metid(x) = startswith(x, "M_") ? x : "M_$x"
rxnid(x) = startswith(x, "R_") ? x : "R_$x"
gprid(x) = startswith(x, "G_") ? x : "G_$x"

return SBMLModel(
SBML.Model(
compartments = Dict(
comp => SBML.Compartment(constant = true) for comp in compss
),
species = Dict(
mid => SBML.Species(
metid(mid) => SBML.Species(
name = metabolite_name(mm, mid),
compartment = _default("compartment", comps[mi]),
formula = metabolite_formula(mm, mid),
formula = _maybemap(_unparse_formula, metabolite_formula(mm, mid)),
charge = metabolite_charge(mm, mid),
constant = false,
boundary_condition = false,
only_substance_units = false,
sbo = _sbml_export_sbo(metabolite_annotations(mm, mid)),
notes = _sbml_export_notes(metabolite_notes(mm, mid)),
annotation = _sbml_export_annotation(metabolite_annotations(mm, mid)),
metaid = metid(mid),
cv_terms = _sbml_export_cvterms(metabolite_annotations(mm, mid)),
) for (mi, mid) in enumerate(mets)
),
reactions = Dict(
rid => SBML.Reaction(
rxnid(rid) => SBML.Reaction(
name = reaction_name(mm, rid),
reactants = [
SBML.SpeciesReference(
species = mets[i],
species = metid(mets[i]),
stoichiometry = -stoi[i, ri],
constant = true,
) for
i in SparseArrays.nonzeroinds(stoi[:, ri]) if stoi[i, ri] <= 0
],
products = [
SBML.SpeciesReference(
species = mets[i],
species = metid(mets[i]),
stoichiometry = stoi[i, ri],
constant = true,
) for
Expand All @@ -242,16 +333,20 @@ function Base.convert(::Type{SBMLModel}, mm::MetabolicModel)
reaction_gene_association(mm, rid),
),
reversible = true,
sbo = _sbml_export_sbo(reaction_annotations(mm, rid)),
notes = _sbml_export_notes(reaction_notes(mm, rid)),
annotation = _sbml_export_annotation(reaction_annotations(mm, rid)),
metaid = rxnid(rid),
cv_terms = _sbml_export_cvterms(reaction_annotations(mm, rid)),
) for (ri, rid) in enumerate(rxns)
),
gene_products = Dict(
gid => SBML.GeneProduct(
gprid(gid) => SBML.GeneProduct(
label = gid,
name = gene_name(mm, gid),
sbo = _sbml_export_sbo(gene_annotations(mm, gid)),
notes = _sbml_export_notes(gene_notes(mm, gid)),
annotation = _sbml_export_annotation(gene_annotations(mm, gid)),
metaid = gprid(gid),
cv_terms = _sbml_export_cvterms(gene_annotations(mm, gid)),
) for gid in genes(mm)
),
active_objective = "objective",
Expand Down