JSONModel.jl 10.2 KB
Newer Older
1
"""
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
2
3
4
    struct JSONModel <: MetabolicModel
        json::Dict{String,Any}
    end
5

Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
6
7
8
A struct used to store the contents of a JSON model, i.e. a model read from a
file ending with `.json`. These model files typically store all the model
parameters in arrays of JSON objects (i.e. Julia dictionaries).
9

Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
10
11
Usually, not all of the fields of the input JSON can be easily represented when
converting to other models, care should be taken to avoid losing information.
12

Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
13
14
15
16
Direct work on this precise model type is not very efficient, as the accessor
functions need to repeatedly find the information in the JSON tree. This gets
very slow especially if calling many accessor functions sequentially. To avoid
that, convert to e.g. [`StandardModel`](@ref) as soon as possible.
17
18
19

# Example
````
20
model = load_json_model("some_model.json")
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
21
22
model.json # see the actual underlying JSON
reactions(model) # see the list of reactions
23
24
````
"""
25
struct JSONModel <: MetabolicModel
26
    json::Dict{String,Any}
27
28
end

29
30
31
32
33

# helper access macros, see examples below for usage
macro _json_sectionkey(namesConstant::Symbol, error)
    esc(:(
        begin
34
            _key = _guesskey(keys(model.json), _constants.keynames.$namesConstant)
35
36
37
38
39
40
41
42
43
44
45
46
            if isnothing(_key)
                $error
            end
            _key
        end
    ))
end

macro _json_section(namesConstant::Symbol, error)
    esc(:(
        begin
            _key = @_json_sectionkey $namesConstant ($error)
47
            model.json[_key]
48
49
50
51
52
53
54
55
        end
    ))
end

macro _json_section_firstid(namesConstant::Symbol, id::Symbol, error)
    return esc(:(
        begin
            _s = @_json_section $namesConstant ($error)
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
56
            _s[findfirst(x -> x["id"] == $id, _s)]
57
58
59
60
61
62
        end
    ))
end

function _parse_annotations(x)::Annotations
    Annotations([k => if typeof(v) == String
St. Elmo's avatar
St. Elmo committed
63
        [v]
64
65
66
67
68
69
70
    else
        convert(Vector{String}, v)
    end for (k, v) in x])
end

_parse_notes(x)::Notes = _parse_annotations(x)

71
72
73
74
75
76
"""
    reactions(model::JSONModel)

Extract reaction names (stored as `.id`) from JSON model.
"""
function reactions(model::JSONModel)
77
    rs = @_json_section rxns throw(
78
        DomainError(keys(model.json), "JSON model has no reaction keys"),
79
    )
Miroslav Kratochvil's avatar
draft    
Miroslav Kratochvil committed
80

81
    return [string(get(rs[i], "id", "rxn$i")) for i in eachindex(rs)]
Miroslav Kratochvil's avatar
draft    
Miroslav Kratochvil committed
82
83
end

84
"""
85
    metabolites(model::JSONModel)
86

87
Extract metabolite names (stored as `.id`) from JSON model.
88
"""
89
function metabolites(model::JSONModel)
90
    ms = @_json_section mets throw(
91
        DomainError(keys(model.json), "JSON model has no metabolite keys"),
92
    )
93

94
    return [string(get(ms[i], "id", "met$i")) for i in eachindex(ms)]
95
96
end

97
"""
98
    genes(model::JSONModel)
99

100
Extract gene names from a JSON model.
101
102
"""
function genes(model::JSONModel)
103
    gs = @_json_section genes return []
104

105
    return [string(get(gs[i], "id", "gene$i")) for i in eachindex(gs)]
106
107
end

108
109
"""
    stoichiometry(model::JSONModel)
110

111
112
113
Get the stoichiometry. Assuming the information is stored in reaction object
under key `.metabolites`.
"""
114
115
116
function stoichiometry(model::JSONModel)
    rxn_ids = reactions(model)
    met_ids = metabolites(model)
117

118
    rs = @_json_section rxns throw(
119
        DomainError(keys(model.json), "JSON model has no reaction keys"),
120
    )
121

122
    S = SparseArrays.spzeros(length(met_ids), length(rxn_ids))
123
    for (i, rid) in enumerate(rxn_ids)
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
124
        r = rs[findfirst(x -> x["id"] == rid, rs)]
125
126
        for (met_id, coeff) in r["metabolites"]
            j = findfirst(==(met_id), met_ids)
127
128
129
130
131
132
133
134
            if isnothing(j)
                throw(
                    DomainError(
                        met_id,
                        "Unknown metabolite found in stoichiometry of $(rxn_ids[i])",
                    ),
                )
            end
135
136
137
138
139
140
            S[j, i] = coeff
        end
    end
    return S
end

141
142
"""
    bounds(model::JSONModel)
143

144
145
146
Get the bounds for reactions, assuming the information is stored in
`.lower_bound` and `.upper_bound`.
"""
147
function bounds(model::JSONModel)
148
149
150
151
    rs = @_json_section rxns return (
        sparse(fill(-_constants.default_reaction_bound, n_reactions(model))),
        sparse(fill(_constants.default_reaction_bound, n_reactions(model))),
    )
152
    return (
153
154
        sparse([get(rxn, "lower_bound", -_constants.default_reaction_bound) for rxn in rs]),
        sparse([get(rxn, "upper_bound", _constants.default_reaction_bound) for rxn in rs]),
155
    )
156
157
end

158
159
"""
    objective(model::JSONModel)
160

161
162
Collect `.objective_coefficient` keys from model reactions.
"""
163
function objective(model::JSONModel)
164
    rs = @_json_section rxns return spzeros(n_reactions(model))
165

166
    return sparse([float(get(rxn, "objective_coefficient", 0.0)) for rxn in rs])
167
168
end

169
170
171
172
173
"""
    reaction_gene_associaton(model::JSONModel, rid::String)

Parses the `.gene_reaction_rule` from reactions.
"""
174
function reaction_gene_association(model::JSONModel, rid::String)
175
    rxn = @_json_section_firstid rxns rid return nothing
176
    return _maybemap(_parse_grr, get(rxn, "gene_reaction_rule", nothing))
177
178
end

179
180
181
182
183
184
185
186
187
188
"""
    reaction_subsystem(model::JSONModel, rid::String)

Parses the `.subsystem` out from reactions.
"""
function reaction_subsystem(model::JSONModel, rid::String)
    rxn = @_json_section_firstid rxns rid return nothing
    return get(rxn, "subsystem", nothing)
end

189
190
191
192
193
194
"""
    metabolite_formula(model::JSONModel, mid::String)

Parse and return the metabolite `.formula`
"""
function metabolite_formula(model::JSONModel, mid::String)
195
    met = @_json_section_firstid mets mid return nothing
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
196
    return _maybemap(_parse_formula, get(met, "formula", nothing))
197
198
end

199
"""
200
    metabolite_charge(model::JSONModel, mid::String)
201

202
Return the metabolite `.charge`
203
"""
204
function metabolite_charge(model::JSONModel, mid::String)
205
    met = @_json_section_firstid mets mid return nothing
206
    return get(met, "charge", 0)
St. Elmo's avatar
St. Elmo committed
207
208
end

209
210
211
212
213
214
215
216
217
218
"""
    metabolite_compartment(model::JSONModel, mid::String)

Return the metabolite `.compartment`
"""
function metabolite_compartment(model::JSONModel, mid::String)
    met = @_json_section_firstid mets mid return nothing
    return get(met, "compartment", nothing)
end

219
220
221
222
223
"""
    gene_annotations(model::JSONModel, gid::String)::Annotations

Gene annotations from the [`JSONModel`](@ref).
"""
224
225
function gene_annotations(model::JSONModel, gid::String)::Annotations
    gene = @_json_section_firstid genes gid return Dict()
226
    _maybemap(_parse_annotations, get(gene, "annotation", nothing))
227
228
end

229
230
231
232
233
"""
    gene_notes(model::JSONModel, gid::String)::Notes

Gene notes from the [`JSONModel`](@ref).
"""
234
235
function gene_notes(model::JSONModel, gid::String)::Notes
    gene = @_json_section_firstid genes gid return Dict()
236
    _maybemap(_parse_notes, get(gene, "notes", nothing))
237
238
end

239
240
241
242
243
"""
    reaction_annotations(model::JSONModel, rid::String)::Annotations

Reaction annotations from the [`JSONModel`](@ref).
"""
244
245
function reaction_annotations(model::JSONModel, rid::String)::Annotations
    rxn = @_json_section_firstid rxns rid return Dict()
246
    _maybemap(_parse_annotations, get(rxn, "annotation", nothing))
247
248
end

249
250
251
252
253
"""
    reaction_notes(model::JSONModel, rid::String)::Notes

Reaction notes from the [`JSONModel`](@ref).
"""
254
255
function reaction_notes(model::JSONModel, rid::String)::Notes
    rxn = @_json_section_firstid rxns rid return Dict()
256
    _maybemap(_parse_notes, get(rxn, "notes", nothing))
257
258
end

259
260
261
262
263
"""
    metabolite_annotations(model::JSONModel, mid::String)::Annotations

Metabolite annotations from the [`JSONModel`](@ref).
"""
264
265
function metabolite_annotations(model::JSONModel, mid::String)::Annotations
    met = @_json_section_firstid mets mid return Dict()
266
    _maybemap(_parse_annotations, get(met, "annotation", nothing))
267
268
end

269
270
271
272
273
"""
    metabolite_notes(model::JSONModel, mid::String)::Notes

Metabolite notes from the [`JSONModel`](@ref).
"""
274
275
function metabolite_notes(model::JSONModel, mid::String)::Notes
    met = @_json_section_firstid mets mid return Dict()
276
    _maybemap(_parse_notes, get(met, "notes", nothing))
277
278
end

St. Elmo's avatar
St. Elmo committed
279
"""
St. Elmo's avatar
St. Elmo committed
280
    reaction_stoichiometry(model::JSONModel, rxn_id::String)::Dict{String, Float64}
St. Elmo's avatar
St. Elmo committed
281
282
283
284

Return the reaction equation of reaction with id `rxn_id` in model. The reaction
equation maps metabolite ids to their stoichiometric coefficients.
"""
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
285
function reaction_stoichiometry(m::JSONModel, rxn_id::String)::Dict{String,Float64}
286
    ind = findfirst(x -> x["id"] == rxn_id, m.json["reactions"])
St. Elmo's avatar
St. Elmo committed
287
288
    m.json["reactions"][ind]["metabolites"]
end
289

290
291
292
293
294
"""
    Base.convert(::Type{JSONModel}, mm::MetabolicModel)

Convert any [`MetabolicModel`](@ref) to [`JSONModel`](@ref).
"""
295
function Base.convert(::Type{JSONModel}, mm::MetabolicModel)
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
296
297
298
299
    if typeof(mm) == JSONModel
        return mm
    end

300
    rxn_ids = reactions(mm)
301
    met_ids = metabolites(mm)
302
303
304
    gene_ids = genes(mm)
    S = stoichiometry(mm)
    lbs, ubs = bounds(mm)
305
    ocs = objective(mm)
306
307

    json_model = JSONModel(Dict{String,Any}())
308
309
    json = Dict{String,Any}()
    json["id"] = "model" # default
310
311
312

    #TODO: add notes, names and similar fun stuff when they are available

313
    json[first(_constants.keynames.genes)] = [
314
315
316
317
318
319
        Dict([
            "id" => gid,
            "annotation" => gene_annotations(mm, gid),
            "notes" => gene_notes(mm, gid),
        ],) for gid in gene_ids
    ]
320

321
    json[first(_constants.keynames.mets)] = [
322
323
        Dict([
            "id" => mid,
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
324
            "formula" => _maybemap(_unparse_formula, metabolite_formula(mm, mid)),
325
            "charge" => metabolite_charge(mm, mid),
326
            "compartment" => metabolite_compartment(mm, mid),
327
328
329
            "annotation" => metabolite_annotations(mm, mid),
            "notes" => metabolite_notes(mm, mid),
        ]) for mid in met_ids
330
331
    ]

332
    json[first(_constants.keynames.rxns)] = [
333
334
335
        begin
            res = Dict{String,Any}()
            res["id"] = rid
336
            res["subsystem"] = reaction_subsystem(mm, rid)
337
338
339
            res["annotation"] = reaction_annotations(mm, rid)
            res["notes"] = reaction_notes(mm, rid)

340
            grr = reaction_gene_association(mm, rid)
341
            if !isnothing(grr)
342
                res["gene_reaction_rule"] = _unparse_grr(String, grr)
343
            end
344

345
346
347
            res["lower_bound"] = lbs[ri]
            res["upper_bound"] = ubs[ri]
            res["objective_coefficient"] = ocs[ri]
348
            I, V = findnz(S[:, ri])
349
350
351
352
353
354
            res["metabolites"] =
                Dict{String,Float64}([met_ids[ii] => vv for (ii, vv) in zip(I, V)])
            res
        end for (ri, rid) in enumerate(rxn_ids)
    ]

355
    return JSONModel(json)
356
end