Skip to content

Commit ec5f260

Browse files
committed
Fixed failing tests
1 parent bd81863 commit ec5f260

File tree

6 files changed

+210
-97
lines changed

6 files changed

+210
-97
lines changed

pymgpipe/nmpc.py

+74-39
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .fva import regularFVA
1010
from .utils import load_dataframe, load_model, set_objective
1111
from .io import suppress_stdout
12+
from .vffva import veryFastFVA
1213

1314

1415
def compute_nmpcs(
@@ -27,7 +28,13 @@ def compute_nmpcs(
2728
force=False,
2829
threshold=1e-5,
2930
write_to_file=True,
31+
fva_type="regular",
32+
obj_optimality=100,
3033
):
34+
assert fva_type == "regular" or fva_type == "fast", (
35+
"FVA type must be either `regular` or `fast`! Received %s" % fva_type
36+
)
37+
3138
start = time.time()
3239
out_dir = out_dir + "/" if out_dir[-1] != "/" else out_dir
3340
Path(out_dir).mkdir(exist_ok=True)
@@ -36,9 +43,15 @@ def compute_nmpcs(
3643
objective_out_file = out_dir + objective_out_file
3744
fluxes_out_file = out_dir + fluxes_out_file
3845

39-
nmpcs = pd.DataFrame() if force or not write_to_file else load_dataframe(out_file, return_empty=True)
46+
nmpcs = (
47+
pd.DataFrame()
48+
if force or not write_to_file
49+
else load_dataframe(out_file, return_empty=True)
50+
)
4051
all_fluxes = (
41-
pd.DataFrame() if force or not write_to_file else load_dataframe(fluxes_out_file, return_empty=True)
52+
pd.DataFrame()
53+
if force or not write_to_file
54+
else load_dataframe(fluxes_out_file, return_empty=True)
4255
)
4356
obj_values = (
4457
pd.DataFrame()
@@ -51,6 +64,11 @@ def compute_nmpcs(
5164

5265
try:
5366
models = [load_model(samples)]
67+
68+
# Skip models that already exist
69+
if models[0].name in list(nmpcs.columns) and not force:
70+
print("NMPCs for %s already exist in file!" % models[0].name)
71+
return
5472
except Exception:
5573
models = (
5674
samples
@@ -60,12 +78,14 @@ def compute_nmpcs(
6078
for m in os.listdir(os.path.dirname(samples))
6179
]
6280
)
63-
models = [
64-
f
65-
for f in models
66-
if not isinstance(f, str)
67-
or os.path.basename(f).split(".")[0] not in list(nmpcs.columns)
68-
]
81+
82+
# Skip models that already exist
83+
models = [
84+
f
85+
for f in models
86+
if not isinstance(f, str)
87+
or os.path.basename(f).split(".")[0] not in list(nmpcs.columns)
88+
]
6989
threads = os.cpu_count() - 1 if threads == -1 else threads
7090

7191
print("Computing NMPCs on %s models..." % len(models))
@@ -80,6 +100,11 @@ def compute_nmpcs(
80100
print("------------------------------------------")
81101

82102
for s in tqdm.tqdm(models, total=len(models)):
103+
if fva_type == "fast":
104+
assert isinstance(
105+
s, str
106+
), "For fast fva, `samples` param must be directory or list of model paths."
107+
model_path = s
83108
with suppress_stdout():
84109
m = load_model(path=s, solver=solver)
85110
if not isinstance(m, optlang.interface.Model):
@@ -93,43 +118,53 @@ def compute_nmpcs(
93118
m.variables["communityBiomass"].set_bounds(0.4, 1)
94119
set_objective(m, m.variables["communityBiomass"], direction="max")
95120

96-
with suppress_stdout():
97-
m.optimize()
98-
if m.status == "infeasible":
99-
logging.warning("%s model is infeasible!" % m.name)
100-
continue
101-
102-
obj_val = round(m.objective.value, 5)
103-
obj_values.loc[m.name] = obj_val
104-
if "ObjectiveConstraint" in m.constraints:
105-
m.remove(m.constraints["ObjectiveConstraint"])
106-
m.update()
107-
obj_const = m.interface.Constraint(
108-
expression=m.objective.expression,
109-
lb=obj_val,
110-
ub=obj_val,
111-
name="ObjectiveConstraint",
112-
)
113-
m.add(obj_const)
114-
m.update()
115-
116121
# Now perform FVA under constrained objective value
117-
with suppress_stdout():
118-
try:
122+
try:
123+
if fva_type == "regular":
124+
with suppress_stdout():
125+
m.optimize()
126+
if m.status == "infeasible":
127+
logging.warning("%s model is infeasible!" % m.name)
128+
continue
129+
130+
obj_val = round(m.objective.value, 5)
131+
obj_values.loc[m.name] = obj_val
132+
if "ObjectiveConstraint" in m.constraints:
133+
m.remove(m.constraints["ObjectiveConstraint"])
134+
m.update()
135+
obj_const = m.interface.Constraint(
136+
expression=m.objective.expression,
137+
lb=obj_val * (obj_optimality / 100),
138+
ub=obj_val,
139+
name="ObjectiveConstraint",
140+
)
141+
m.add(obj_const)
142+
m.update()
119143
res = regularFVA(
120144
m,
121145
reactions=reactions,
122146
regex=regex,
123147
ex_only=ex_only,
124148
solver=solver,
125-
threads=threads,
149+
threads=threads if parallel else 1,
126150
parallel=parallel,
127151
write_to_file=False,
128-
threshold=threshold
152+
threshold=threshold,
129153
)
130-
except Exception:
131-
logging.warning("Cannot solve %s model!" % m.name)
132-
continue
154+
elif fva_type == "fast":
155+
res = veryFastFVA(
156+
model=m,
157+
path=model_path,
158+
reactions=reactions,
159+
regex=regex,
160+
nCores=threads if parallel else 1,
161+
nThreads=1,
162+
optPerc=obj_optimality,
163+
threshold=threshold,
164+
)
165+
except Exception as e:
166+
logging.warning(f"Cannot solve {m.name} model!\n{e}")
167+
continue
133168
if res is None:
134169
return
135170
res["sample_id"] = m.name
@@ -158,9 +193,9 @@ def compute_nmpcs(
158193
all_fluxes.to_csv(fluxes_out_file)
159194

160195
res = namedtuple("res", "nmpc objectives fluxes")
161-
196+
162197
print("-------------------------------------------------------")
163-
print('Finished computing NMPCs!')
164-
print('Process took %s minutes to run...'%round((time.time()-start)/60,3))
198+
print("Finished computing NMPCs!")
199+
print("Process took %s minutes to run..." % round((time.time() - start) / 60, 3))
165200

166-
return res(nmpcs, obj_values, all_fluxes)
201+
return res(nmpcs, obj_values, all_fluxes)

pymgpipe/tests/test_build.py

-5
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def test_build_diet_fecal():
3030
taxonomy=sample_df,
3131
rel_threshold=1e-6,
3232
solver="gurobi",
33-
coupling_constraints=True,
3433
diet_fecal_compartments=True,
3534
)
3635

@@ -39,7 +38,6 @@ def test_build_diet_fecal():
3938
assert (
4039
"fe" in pymgpipe_model.compartments and "d" in pymgpipe_model.compartments
4140
)
42-
assert len([k for k in pymgpipe_model.constraints if re.match(".*_cp$", k.name)]) > 0
4341

4442
built_abundances = get_abundances(pymgpipe_model).to_dict()["A test model"]
4543
true_abundances = sample_df.set_index("strain")["abundance"].to_dict()
@@ -68,7 +66,6 @@ def test_build():
6866
taxonomy=sample_df,
6967
rel_threshold=1e-6,
7068
solver="gurobi",
71-
coupling_constraints=True,
7269
diet_fecal_compartments=False,
7370
)
7471

@@ -78,8 +75,6 @@ def test_build():
7875
"fe" not in pymgpipe_model.compartments
7976
and "d" not in pymgpipe_model.compartments
8077
)
81-
assert len([k for k in pymgpipe_model.constraints if re.match(".*_cp$", k.name)]) > 0
82-
8378

8479
built_abundances = get_abundances(pymgpipe_model).to_dict()["A test model"]
8580
true_abundances = sample_df.set_index("strain")["abundance"].to_dict()

pymgpipe/tests/test_e2e.py

+84-18
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@
44
import numpy as np
55
import cobra
66
from pkg_resources import resource_filename
7-
from pymgpipe import add_coupling_constraints, compute_nmpcs, get_abundances, build_models
7+
from pymgpipe import (
8+
add_coupling_constraints,
9+
compute_nmpcs,
10+
get_abundances,
11+
build_models,
12+
remove_reverse_vars,
13+
load_dataframe,
14+
get_reverse_id,
15+
)
816
from pymgpipe.build import _build
917
from pytest_check import check
1018
import re
1119
import tempfile
1220

1321

14-
1522
def test_build_models():
16-
samples = ['sample%s'%i for i in range(5)]
17-
cov = pd.DataFrame(columns=samples,index=['TaxaA','TaxaB','TaxaC','TaxaD'])
23+
samples = ["sample%s" % i for i in range(5)]
24+
cov = pd.DataFrame(columns=samples, index=["TaxaA", "TaxaB", "TaxaC", "TaxaD"])
1825

1926
for t in cov.columns:
20-
cov[t]=np.random.dirichlet(np.ones(4),size=1)[0]
27+
cov[t] = np.random.dirichlet(np.ones(4), size=1)[0]
2128

2229
with tempfile.TemporaryDirectory() as tmpdirname:
2330
build_models(
@@ -30,18 +37,19 @@ def test_build_models():
3037
coupling_constraints=True,
3138
compute_metrics=True,
3239
compress=True,
33-
write_lp=True
3440
)
35-
models_out = os.listdir(tmpdirname+'/models/')
36-
problems_out = os.listdir(tmpdirname+'/problems/')
41+
models_out = os.listdir(tmpdirname + "/models/")
42+
problems_out = os.listdir(tmpdirname + "/problems/")
43+
44+
assert len(models_out) == 5 and len(problems_out) == 5
3745

38-
assert len(models_out)==5 and len(problems_out) == 5
46+
assert (
47+
os.path.exists(tmpdirname + "/metabolic_diversity.png")
48+
and os.path.exists(tmpdirname + "/reaction_abundance.csv")
49+
and os.path.exists(tmpdirname + "/reaction_content.csv")
50+
and os.path.exists(tmpdirname + "/sample_label_conversion.csv")
51+
)
3952

40-
assert \
41-
os.path.exists(tmpdirname+'/metabolic_diversity.png') and \
42-
os.path.exists(tmpdirname+'/reaction_abundance.csv') and \
43-
os.path.exists(tmpdirname+'/reaction_content.csv') and \
44-
os.path.exists(tmpdirname+'/sample_label_conversion.csv')
4553

4654
def test_full_diet_fecal_compartments():
4755
sample_data = [
@@ -62,12 +70,13 @@ def test_full_diet_fecal_compartments():
6270
taxonomy=sample_df,
6371
rel_threshold=1e-6,
6472
solver="gurobi",
65-
coupling_constraints=False,
6673
diet_fecal_compartments=True,
6774
)
6875

6976
add_coupling_constraints(pymgpipe_model)
70-
assert len([k for k in pymgpipe_model.constraints if re.match(".*_cp$", k.name)]) > 0
77+
assert (
78+
len([k for k in pymgpipe_model.constraints if re.match(".*_cp$", k.name)]) > 0
79+
)
7180

7281
built_abundances = get_abundances(pymgpipe_model).to_dict()["A test model"]
7382
true_abundances = sample_df.set_index("strain")["abundance"].to_dict()
@@ -108,12 +117,13 @@ def test_full_single_compartment():
108117
taxonomy=sample_df,
109118
rel_threshold=1e-6,
110119
solver="gurobi",
111-
coupling_constraints=False,
112120
diet_fecal_compartments=False,
113121
)
114122

115123
add_coupling_constraints(pymgpipe_model)
116-
assert len([k for k in pymgpipe_model.constraints if re.match(".*_cp$", k.name)]) > 0
124+
assert (
125+
len([k for k in pymgpipe_model.constraints if re.match(".*_cp$", k.name)]) > 0
126+
)
117127

118128
built_abundances = get_abundances(pymgpipe_model).to_dict()["A test model"]
119129
true_abundances = sample_df.set_index("strain")["abundance"].to_dict()
@@ -133,3 +143,59 @@ def test_full_single_compartment():
133143
os.remove("community_objectives.csv")
134144

135145
assert len(nmpc_res.nmpc) == 20
146+
147+
148+
def test_remove_variables():
149+
sample_data = [
150+
["mc1", 0.1, "TaxaA"],
151+
["mc1", 0.2, "TaxaB"],
152+
["mc1", 0.3, "TaxaC"],
153+
["mc1", 0.4, "TaxaD"],
154+
]
155+
156+
sample_df = pd.DataFrame(sample_data, columns=["sample_id", "abundance", "strain"])
157+
sample_df["id"] = sample_df["strain"]
158+
sample_df["file"] = (
159+
resource_filename("pymgpipe", "resources/miniTaxa/") + sample_df.id + ".xml.gz"
160+
)
161+
162+
pymgpipe_model = _build(
163+
name="A test model",
164+
taxonomy=sample_df,
165+
rel_threshold=1e-6,
166+
solver="gurobi",
167+
diet_fecal_compartments=True,
168+
)
169+
some_var = pymgpipe_model.variables[100]
170+
reverse_var_id = get_reverse_id(some_var.name)
171+
172+
res1 = compute_nmpcs(samples=pymgpipe_model, write_to_file=False, threads=-1).nmpc
173+
remove_reverse_vars(pymgpipe_model, hard_remove=False)
174+
175+
assert pymgpipe_model.variables[reverse_var_id].lb == 0 and pymgpipe_model.variables[reverse_var_id].ub == 0
176+
177+
res2 = compute_nmpcs(samples=pymgpipe_model, write_to_file=False, threads=-1).nmpc
178+
remove_reverse_vars(pymgpipe_model, hard_remove=True)
179+
180+
assert reverse_var_id not in pymgpipe_model.variables
181+
182+
res3 = compute_nmpcs(samples=pymgpipe_model, write_to_file=False, threads=-1).nmpc
183+
184+
assert (
185+
len(_compare(res1, res2)) == 0
186+
and len(_compare(res1, res3)) == 0
187+
)
188+
189+
190+
def _compare(first, second, threshold=1e-10):
191+
first = load_dataframe(first)
192+
second = load_dataframe(second)
193+
bad = []
194+
for i, row in first.iterrows():
195+
second_row = second.loc[i].to_dict()
196+
for x in row.to_dict():
197+
first_x = row[x]
198+
second_x = second_row[x]
199+
if abs(first_x - second_x) > threshold:
200+
bad.append((i, x, abs(first_x - second_x)))
201+
return bad

pymgpipe/tests/test_utils.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ def test_remove_reverse_reactions(mini_optlang_model):
66
reverse_var_id = get_reverse_id(some_var.name)
77
assert reverse_var_id in mini_optlang_model.variables
88

9-
remove_reverse_vars(mini_optlang_model)
10-
new_reactions = len(mini_optlang_model.variables)
9+
remove_reverse_vars(mini_optlang_model,hard_remove=False)
10+
assert len(mini_optlang_model.variables) == num_reactions and \
11+
mini_optlang_model.variables[reverse_var_id].lb == 0 and mini_optlang_model.variables[reverse_var_id].ub == 0
12+
13+
remove_reverse_vars(mini_optlang_model,hard_remove=True)
14+
assert len(mini_optlang_model.variables) == num_reactions/2 and reverse_var_id not in mini_optlang_model.variables
15+
1116

12-
assert new_reactions == num_reactions/2 and reverse_var_id not in mini_optlang_model.variables

pymgpipe/utils.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ def _get_fluxes_from_model(model, reactions=None, regex=None, threshold=1e-5):
6868
reverse = model.variables[r_id]
6969
flux = float(forward.primal - reverse.primal)
7070
flux = 0 if flux == -0.0 else flux
71-
flux = flux if abs(flux) > threshold else 0
71+
if threshold is not None:
72+
flux = flux if abs(flux) > threshold else 0
7273
fluxes[forward.name] = flux
7374
return fluxes
7475

0 commit comments

Comments
 (0)