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
Loopless solutions for optlang [fix #385] #391
Loopless solutions for optlang [fix #385] #391
Conversation
cobra/flux_analysis/loopless.py
Outdated
Erratum in: Biophys J. 2011 Mar 2;100(5):1381. | ||
""" | ||
internal = [i for i, r in enumerate(model.reactions) if not r.boundary] | ||
Sint = model.S[:, np.array(internal)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we think this is an operation we'll do many times? Just thinking that we could speed things up if I had the array-generating function accept an optional argument to only give you certain columns...
In this case though, probably not a speed-limiting factor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now it's only used here...
cobra/flux_analysis/loopless.py
Outdated
coefs = {model.solver.variables[ | ||
"delta_g_" + model.reactions[ridx].id]: row[i] | ||
for i, ridx in enumerate(internal) if abs(row[i]) > zero_cutoff} | ||
model.solver.constraints[name].set_linear_coefficients(coefs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this something we'll want to context-manage eventually?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already is due to the add_to_solver above. Had to do it like that because you can't set linear coefficients for a Constraint that is not part of an optlang.Model yet.
def _(): | ||
with test_model: | ||
add_loopless(test_model) | ||
test_model.optimize(solver="optlang-glpk") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so I'm confused: glpk can handle the MILP formulation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Absolutely. GLPK includes a MIP solver. It does not support indicator variables like cplex or gurobi so you have to declare them by hand.
Codecov Report
@@ Coverage Diff @@
## devel-2 #391 +/- ##
===========================================
+ Coverage 65.35% 65.52% +0.16%
===========================================
Files 64 64
Lines 7716 7822 +106
Branches 1338 1359 +21
===========================================
+ Hits 5043 5125 +82
- Misses 2442 2459 +17
- Partials 231 238 +7
Continue to review full report at Codecov.
|
cobra/flux_analysis/loopless.py
Outdated
---------- | ||
.. [1] Elimination of thermodynamically infeasible loops in steady-state | ||
metabolic models. | ||
Schellenberger J, Lewis NE, Palsson BO. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this not be fixed by using u"""
rather than reverting to ASCII? Internationalization is a good thing in general, we're not in the 80s anymore :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in this case it's not that critical since Palsson is cited like that pretty often. If it would in actual code I would agree. But really have no preference. I can add the u"""
if you would like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, he is cited with O fairly often. Let's leave it then.
55fd021
to
3b2c241
Compare
cbbe3d1
to
4d96055
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- made some cosmetic changes hope it was ok
- Although haven't tried it live so don't not very informed opinion but I like the API you suggest, shall we proceed with it as is? Guess just need to remove the additional benchmarking code or you want to keep it?
@@ -143,7 +158,7 @@ def _fva_optlang(model, reaction_list, fraction): | |||
fva_old_objective = prob.Variable( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you need m.optimize()
above instead to save the solution? also, I reverted m.optimize()
to raise an exception when doing the pfba, was not intended as a final solution there but work-in-progress, with that change I guess you don't need to raise ValueError
here , think that's where the error comes from...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really. The thing is that everything involving constructing a Solution (even LazySolution) adds overhead. This is why I use the pure optlang interface and m.solver.objective.value
below (which is set by calling m.solver.optimize
already). In that instance it does not make so much difference except for small models. However, in the loop below that make quite a difference.
cobra/flux_analysis/loopless.py
Outdated
"""Add variables and constraints to make a model thermodynamically | ||
feasible. This removes flux loops. The used formulation is described | ||
in [1]_. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps could give an example of when to use add_loopless
versus loopless_solution
to help understand the reason for both of them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that is a good idea. Also note that zero_cutoff
becomes unnecessary if there would a way in optlang to get the tolerance for the solver.
So here's a random question: what if I did: add_loopless(model)
model.add_reaction(my_new_reaction)
model.optimize() or even add_moma(model)
model.add_reaction(my_new_reaction)
model.optimize() My point is that if we want these routines to return/modify model objects, maybe we should be superclassing model, and then either providing add_reaction wrappers that handle the new constraints, or return but then we might lose speed and the ability to context manage. Either way, probably not a huge concern... Maybe a simple docstring warning would be sufficient |
dda4101
to
0225482
Compare
@pstjohn That is a good point. For now mentioning it in the docstrings and the future docs is the easiest thing. We could add a check in add_reactions but I wouldn't know for what to check right now... |
Returning to WIP status since the changes in the Solution class broke the loopless functions. @hredestig I had to rebase due to some problems. I tried to integrate all your suggestions and some more. Could you check whether it's fine? |
@@ -143,7 +158,7 @@ def _fva_optlang(model, reaction_list, fraction): | |||
fva_old_objective = prob.Variable( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really. The thing is that everything involving constructing a Solution (even LazySolution) adds overhead. This is why I use the pure optlang interface and m.solver.objective.value
below (which is set by calling m.solver.optimize
already). In that instance it does not make so much difference except for small models. However, in the loop below that make quite a difference.
cobra/flux_analysis/loopless.py
Outdated
"""Add variables and constraints to make a model thermodynamically | ||
feasible. This removes flux loops. The used formulation is described | ||
in [1]_. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that is a good idea. Also note that zero_cutoff
becomes unnecessary if there would a way in optlang to get the tolerance for the solver.
cobra/flux_analysis/loopless.py
Outdated
""" | ||
internal = [i for i, r in enumerate(model.reactions) if not r.boundary] | ||
Sint = model.S[:, numpy.array(internal)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually forgot to throw an error here if there is no numpy :O
So here is the gist of the last commits: Model.objective still had the |
Looks great now, is it still wip? |
@hredestig Still figuring out whether loopless solution should have an argument Might be useful for flux sampling (convert sample to a loopless solution) or saved flux solutions. The problem there is how to get the objective value from a dict alone. Easy if |
* Remove unimplemented/incomplete optlang solvers from get_solver_name * Add test_choose_solver fix from #391 * fix typo
8ad4d00
to
4e63570
Compare
Ok, should be fine now. Added the fluxes arg as well. Note that this also fixes two bugs I found on the way (#416 and another one in util.solver_valid_atoms). Let me know if you want those in a separate PR. |
cobra/flux_analysis/loopless.py
Outdated
model.objective.set_linear_coefficients( | ||
{rxn.forward_variable: -1, rxn.reverse_variable: 1}) | ||
model.solver.objective.direction = "min" | ||
model.optimize(objective_sense=None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this raises a SolveError if status is not optimal - be in try/catch?
- implement review comment from hredestig - fix some errors in loopless_optlang when using fluxes arg - add a test for fluxes arg
4e63570
to
9ed85d7
Compare
Okay rebased onto devel-2 😅 and added fixes and tests. Also implemented the review from @hredestig. For now this PR also fixes #416 and #418. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice!
Up to now only implements the original method from Schellenberger. First time the optlang interface really shines. I could basically implement the original method as is. This requires less constraints than the prior version in cobrapy and brings a speed up (twice as fast).
Missing:
As expected a posteriori methods are faster. For FVA the loopless version is still slower than the non-loopless one for large problems (kind of expected). However it is much faster than using
add_loopless
orconstruct_loopless_model
followed by calling "normal" FVA. Faster meaning 2 seconds vs 2 minutes for textbook and a much larger gap (~1000x) for ecoli.API
This one I am still not sure about. So fire away with your comments
A priori:
A posteriori:
Maybe that one should take a flux distribution as argument alongside with the model? That way you could use it to convert flux samples or saved flux distributions to loopless ones as well...
Benchmarks
test_output.txt