screening.jl 10.1 KB
Newer Older
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
1

2
"""
3
    _screen_args(argtuple, kwargtuple, modsname)
4
5
6
7

Internal helper to check the presence and shape of modification and argument
arrays in [`screen`](@ref) and pals.
"""
8
9
10
11
12
function _screen_args(argtuple, kwargtuple, modsname)

    mods = get(kwargtuple, modsname, nothing)
    args = get(kwargtuple, :args, nothing)

13
14
15
    if isnothing(mods)
        if isnothing(args)
            throw(
16
                DomainError(args, "at least one of `$modsname` and `args` must be defined"),
17
18
            )
        end
19
        return NamedTuple{(modsname,)}(Ref([[] for _ in args]))
20
    elseif isnothing(args)
21
        return (args = [() for _ in mods],)
22
23
24
25
26
27
28
29
30
    else
        if size(mods) != size(args)
            throw(
                DomainError(
                    "$(size(mods)) != $(size(args))",
                    "sizes of `$modsname` and `args` differ",
                ),
            )
        end
31
        return ()
32
33
34
    end
end

Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
35
"""
36
    screen(
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
37
        model::MetabolicModel;
38
39
40
        variants::Array{Vector}, N},
        analysis::Function,
        args::Array{Vector{Tuple}, N},
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
41
        workers = [myid()],
42
    )::Array where {N}
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

Take an array of model-modifying function vectors in `variants`, and execute
the function `analysis` on all variants of the `model` specified by `variants`.
The computation is distributed over worker IDs in `workers`. If `args` are
supplied (as an array of the same size as the `variants`), they are forwarded
as arguments to the corresponding analysis function calls.

The array of variants must contain vectors of single-parameter functions, these
are applied to model in order. The functions must *not* modify the model, but
rather return a modified copy. The copy should be made as shallow as possible,
to increase memory efficiency of the process. Variant generators that modify
the argument model in-place will cause unpredictable results. Refer to the
definition of [`screen_variant`](@ref) for details.

The function `analysis` will receive a single argument (the modified model),
58
59
60
61
together with arguments from `args` expanded by `...`. Supply an array of
tuples or vectors to pass in multiple arguments to each function. If the
argument values should be left intact (not expanded to multiple arguments),
they must be wrapped in single-item tuple or vector.
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

The modification and analysis functions are transferred to `workers` as-is; all
packages required to run them (e.g. the optimization solvers) must be loaded
there. Typically, you want to use the macro `@everywhere using
MyFavoriteSolver` from `Distributed` package for loading the solvers.

# Return value

The results of running `analysis` are collected in to the resulting array, in a
way that preserves the shape of the `variants`, similarly as with `pmap`.

The results of `analysis` function must be serializable, preferably made only
from pure Julia structures, because they may be transferred over the network
between the computation nodes. For that reason, functions that return whole
JuMP models that contain pointers to allocated C structures (such as
[`flux_balance_analysis`](@ref) used with `GLPK` or `Gurobi` otimizers) will
generally not in this context.

80
81
82
Note: this function is a thin argument-handling wrapper around
[`_screen_impl`](@ref).

Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
83
84
85
86
87
88
89
90
91
92
93
94
# Example
```
function reverse_reaction(i::Int)
    (model::CoreModel) -> begin
        mod = copy(model)
        mod.S[:,i] .*= -1   # this is unrealistic but sufficient for demonstration
        mod
    end
end

m = load_model(CoreModel, "e_coli_core.xml")

95
96
97
98
99
100
screen(m,
    variants = [
        [reverse_reaction(5)],
        [reverse_reaction(3), reverse_reaction(6)]
    ],
    analysis = mod -> mod.S[:,3])  # observe the changes in S
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
101

102
103
screen(m,
    variants = [
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
104
105
106
        [reverse_reaction(5)],
        [reverse_reaction(3), reverse_reaction(6)]
    ],
107
    analysis = mod -> flux_balance_analysis_vec(mod, GLPK.Optimizer))
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
108
```
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
109
"""
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
screen(args...; kwargs...) =
    _screen_impl(args...; kwargs..., _screen_args(args, kwargs, :variants)...)

"""
    _screen_impl(
        model::MetabolicModel;
        variants::Array{Vector, N},
        analysis::Function,
        args::Array{Tuple, N},
        workers = [myid()],
    )::Array where {N}

The actual implementation of [`screen`](@ref).
"""
function _screen_impl(
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
125
    model::MetabolicModel;
126
    variants::Array{V,N},
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
127
    analysis,
128
    args::Array{A,N},
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
129
    workers = [myid()],
130
131
)::Array where {V<:AbstractVector,A,N}

Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
132
133
    map(fetch, save_at.(workers, :cobrexa_screen_variants_model, Ref(model)))
    map(fetch, save_at.(workers, :cobrexa_screen_variants_analysis_fn, Ref(analysis)))
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
134
    map(fetch, get_from.(workers, Ref(:(precache!(cobrexa_screen_variants_model)))))
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
135

136
137
138
139
140
141
142
    res = pmap(
        (vars, args)::Tuple -> screen_variant(
            (@remote cobrexa_screen_variants_model),
            vars,
            (@remote cobrexa_screen_variants_analysis_fn),
            args,
        ),
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
        CachingPool(workers),
        zip(variants, args),
    )

    map(fetch, remove_from.(workers, :cobrexa_screen_variants_model))
    map(fetch, remove_from.(workers, :cobrexa_screen_variants_analysis_fn))

    return res
end

"""
    screen_variant(model::MetabolicModel, variant::Vector, analysis, args = ())

Helper function for [`screen`](@ref) that applies all single-argument
functions in `variant` to the `model` (in order from "first" to
"last"), and executes `analysis` on the result.

Can be used to test model variants locally.
"""
function screen_variant(model::MetabolicModel, variant::Vector, analysis, args = ())
    for fn in variant
        model = fn(model)
    end
    analysis(model, args...)
end

"""
    screen_variants(model, variants, analysis; workers=[myid()])

A shortcut for [`screen`](@ref) that only works with model variants.
"""
screen_variants(model, variants, analysis; workers = [myid()]) =
    screen(model; variants = variants, analysis = analysis, workers = workers)
176

Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
177
178
179
180
181
182
"""
    screen_optimize_objective(_, optmodel)::Union{Float64,Nothing}

A variant of [`optimize_objective`](@ref) directly usable in
[`screen_optmodel_modifications`](@ref).
"""
183
screen_optimize_objective(_, optmodel)::Maybe{Float64} = optimize_objective(optmodel)
184

185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
"""
    _screen_optmodel_prepare(model, optimizer, common_modifications)

Internal helper for [`screen_optmodel_modifications`](@ref) that creates the
model and applies the modifications.
"""
function _screen_optmodel_prepare(model, optimizer, common_modifications)
    precache!(model)
    optmodel = make_optimization_model(model, optimizer)
    for mod in common_modifications
        mod(model, optmodel)
    end
    return (model, optmodel)
end

"""
    _screen_optmodel_item((mods, args))

Internal helper for [`screen_optmodel_modifications`](@ref) that computes one
item of the screening task.
"""
function _screen_optmodel_item((mods, args))
    (model, optmodel) = @remote cobrexa_screen_optmodel_modifications_data
    for mod in mods
        mod(model, optmodel)
    end
    (@remote cobrexa_screen_optmodel_modifications_fn)(model, optmodel, args...)
end

Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
214
"""
215
    screen_optmodel_modifications(
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
216
217
        model::MetabolicModel,
        optimizer;
218
219
220
221
        common_modifications::Vector = [],
        modifications::Array{Vector,N},
        args::Array{Vector{Tuple},N},
        analysis::Function,
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
222
        workers = [myid()],
223
    ) where N
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
224
225
226
227
228

Screen multiple modifications of the same optimization model.

This function is potentially more efficient than [`screen`](@ref) because it
avoids making variants of the model structure and remaking of the optimization
229
230
231
model. On the other hand, modification functions need to keep the optimization
model in a recoverable state (one that leaves the model usable for the next
modification), which limits the possible spectrum of modifications applied.
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
232
233

Internally, `model` is distributed to `workers` and transformed into the
234
235
236
237
238
239
optimization model using [`make_optimization_model`](@ref).
`common_modifications` are applied to the models at that point. Next, vectors
of functions in `modifications` are consecutively applied, and the result of
`analysis` function called on model are collected to an array of the same
extent as `modifications`. Calls of `analysis` are optionally supplied with
extra arguments from `args` expanded with `...`, just like in [`screen`](@ref).
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
240
241

Both the modification functions (in vectors) and the analysis function here
242
243
244
have 2 base parameters (as opposed to 1 with [`screen`](@ref)): first is the
`model` (carried through as-is), second is the prepared JuMP optimization model
that may be modified and acted upon. As an example, you can use modification
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
245
[`change_constraint`](@ref) and analysis [`screen_optimize_objective`](@ref).
246
247
248
249
250
251
252
253
254
255

Note: This function is a thin argument-handling wrapper around
[`_screen_optmodel_modifications_impl`](@ref).
"""
screen_optmodel_modifications(args...; kwargs...) = _screen_optmodel_modifications_impl(
    args...;
    kwargs...,
    _screen_args(args, kwargs, :modifications)...,
)

Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
256
"""
257
258
259
260
261
262
263
264
265
266
267
268
269
    _screen_optmodel_modifications_impl(
        model::MetabolicModel,
        optimizer;
        common_modifications::Vector = [],
        modifications::Array{Vector,N},
        args::Array{Tuple,N},
        analysis::Function,
        workers = [myid()],
    ) where N

The actual implementation of [`screen_optmodel_modifications`](@ref).
"""
function _screen_optmodel_modifications_impl(
Miroslav Kratochvil's avatar
Miroslav Kratochvil committed
270
    model::MetabolicModel,
271
    optimizer;
272
273
274
275
    common_modifications::VF = [],
    modifications::Array{V,N},
    args::Array{A,N},
    analysis::Function,
276
    workers = [myid()],
277
)::Array where {V<:AbstractVector,VF<:AbstractVector,A,N}
278

279
280
    map(
        fetch,
281
282
283
284
285
286
287
288
289
290
291
        save_at.(
            workers,
            :cobrexa_screen_optmodel_modifications_data,
            Ref(
                :($COBREXA._screen_optmodel_prepare(
                    $model,
                    $optimizer,
                    $common_modifications,
                )),
            ),
        ),
292
293
294
    )
    map(fetch, save_at.(workers, :cobrexa_screen_optmodel_modifications_fn, Ref(analysis)))

295
    res = pmap(_screen_optmodel_item, CachingPool(workers), zip(modifications, args))
296
297
298
299
300
301

    map(fetch, remove_from.(workers, :cobrexa_screen_optmodel_modifications_data))
    map(fetch, remove_from.(workers, :cobrexa_screen_optmodel_modifications_fn))

    return res
end