Skip to content

Commit 91c2b0d

Browse files
committed
fix(acyclic): enforce reverse-edge ordering and add small-graph regression tests
1 parent 08c742a commit 91c2b0d

2 files changed

Lines changed: 95 additions & 5 deletions

File tree

corneto/backend/_base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,8 +1048,8 @@ def Acyclic0(
10481048
if In is not None:
10491049
# NOTE: Negative flows eq. to reversed directed edge
10501050
# Get edges s->t that can have a positive flow
1051-
if hasattr(In, "lb"):
1052-
e_neg = [(i, g.get_edge(i)) for i in np.flatnonzero(In.lb < 0)]
1051+
if hasattr(In, "ub"):
1052+
e_neg = [(i, g.get_edge(i)) for i in np.flatnonzero(In.ub > 0)]
10531053
e_ix = np.array([i for i, (s, t) in e_neg if len(s) > 0 and len(t) > 0])
10541054
else:
10551055
e_ix = np.array([i for i, (s, t) in enumerate(g.E) if len(s) > 0 and len(t) > 0])
@@ -1192,9 +1192,9 @@ def Acyclic(
11921192

11931193
if In is not None:
11941194
# Negative flows are handled as reversed directed edges.
1195-
if hasattr(In, "lb"):
1196-
lb = In.lb if len(In.shape) == 1 else In.lb[:, i_sample]
1197-
e_neg = [(i, g.get_edge(i)) for i in np.flatnonzero(lb < 0)]
1195+
if hasattr(In, "ub"):
1196+
ub = In.ub if len(In.shape) == 1 else In.ub[:, i_sample]
1197+
e_neg = [(i, g.get_edge(i)) for i in np.flatnonzero(ub > 0)]
11981198
e_ix = np.array([i for i, (s, t) in e_neg if s and t])
11991199
else:
12001200
e_ix = np.array([i for i, (s, t) in enumerate(g.E) if s and t])

tests/test_acyclic_small_graphs.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import numpy as np
2+
3+
from corneto._graph import EdgeType, Graph
4+
from corneto.backend import PicosBackend
5+
6+
7+
def _build_acyclic_problem(backend, graph, lb, ub):
8+
problem = backend.Flow(graph, lb=lb, ub=ub)
9+
problem += backend.NonZeroIndicator(problem.expr._flow, tolerance=1e-6)
10+
problem = backend.Acyclic(
11+
graph,
12+
problem,
13+
indicator_positive_var_name="_flow_ipos",
14+
indicator_negative_var_name="_flow_ineg",
15+
)
16+
return problem
17+
18+
19+
def _solve_and_assert_infeasible(problem, backend):
20+
if isinstance(backend, PicosBackend):
21+
problem.solve(solver="glpk", primals=False)
22+
else:
23+
problem.solve()
24+
assert problem.expr._flow.value is None
25+
26+
27+
def test_acyclic_blocks_forced_directed_2cycle(backend):
28+
graph = Graph()
29+
graph.add_edge("A", "B", type=EdgeType.DIRECTED)
30+
graph.add_edge("B", "A", type=EdgeType.DIRECTED)
31+
32+
problem = _build_acyclic_problem(backend, graph, lb=0, ub=10)
33+
problem += problem.expr._flow_ipos[0] == 1
34+
problem += problem.expr._flow_ipos[1] == 1
35+
36+
_solve_and_assert_infeasible(problem, backend)
37+
38+
39+
def test_acyclic_blocks_forced_directed_plus_reversible_2cycle(backend):
40+
graph = Graph()
41+
graph.add_edge("A", "B", type=EdgeType.DIRECTED)
42+
graph.add_edge("A", "B", type=EdgeType.UNDIRECTED)
43+
44+
lb = np.array([0, -10])
45+
ub = np.array([10, 10])
46+
problem = _build_acyclic_problem(backend, graph, lb=lb, ub=ub)
47+
problem += problem.expr._flow_ipos[0] == 1
48+
problem += problem.expr._flow_ineg[1] == 1
49+
50+
_solve_and_assert_infeasible(problem, backend)
51+
52+
53+
def test_acyclic_blocks_forced_mixed_3cycle(backend):
54+
graph = Graph()
55+
graph.add_edge("A", "B", type=EdgeType.DIRECTED)
56+
graph.add_edge("B", "C", type=EdgeType.DIRECTED)
57+
graph.add_edge("A", "C", type=EdgeType.UNDIRECTED)
58+
59+
lb = np.array([0, 0, -10])
60+
ub = np.array([10, 10, 10])
61+
problem = _build_acyclic_problem(backend, graph, lb=lb, ub=ub)
62+
# Force A->B, B->C and C->A (negative use of A<->C).
63+
problem += problem.expr._flow_ipos[0] == 1
64+
problem += problem.expr._flow_ipos[1] == 1
65+
problem += problem.expr._flow_ineg[2] == 1
66+
67+
_solve_and_assert_infeasible(problem, backend)
68+
69+
70+
def test_acyclic_enforces_constraints_per_sample(backend):
71+
graph = Graph()
72+
graph.add_edge("A", "B", type=EdgeType.DIRECTED)
73+
graph.add_edge("A", "B", type=EdgeType.UNDIRECTED)
74+
75+
lb = np.array([0, -10])
76+
ub = np.array([10, 10])
77+
problem = backend.Flow(graph, lb=lb, ub=ub, n_flows=2)
78+
problem += backend.NonZeroIndicator(problem.expr._flow, tolerance=1e-6)
79+
problem = backend.Acyclic(
80+
graph,
81+
problem,
82+
indicator_positive_var_name="_flow_ipos",
83+
indicator_negative_var_name="_flow_ineg",
84+
)
85+
86+
# Force A->B and B->A (negative use of reversible edge) only in sample 0.
87+
problem += problem.expr._flow_ipos[0, 0] == 1
88+
problem += problem.expr._flow_ineg[1, 0] == 1
89+
90+
_solve_and_assert_infeasible(problem, backend)

0 commit comments

Comments
 (0)