Skip to content

Commit 1e08ecb

Browse files
authored
Merge pull request #11 from Jij-Inc/08-network
Introduce 08-network
2 parents 63c98d8 + 6a4953e commit 1e08ecb

File tree

4 files changed

+580
-0
lines changed

4 files changed

+580
-0
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import jijmodeling as jm
2+
3+
def build_ip_formulation() -> jm.Problem:
4+
"""Create integer programming formulation for the arc-based flow problem.
5+
6+
This model aligns with the ZPL (0-based) specification, formulating a
7+
multi-commodity flow problem with integer scaling and big-M constraints.
8+
9+
Sets:
10+
- N = {0..n−1}
11+
- A = {(i, j) | i ≠ j}
12+
- T = {(k, i, j) | i ≠ j and k ≠ j}
13+
14+
Parameters:
15+
- n (int): Number of nodes.
16+
- t (ndarray, shape (n, n)): Demand matrix, with zero diagonal.
17+
- M (int): Big-M constant for capacity constraints.
18+
- intscale (int): Integer scaling factor.
19+
20+
Variables:
21+
- x[i, j] ∈ {0, 1}: Binary arc selection variable.
22+
- f[k, i, j] ∈ ℤ, 0..intscale·M: Flow of commodity k on arc (i, j).
23+
- z ∈ ℤ, 0..intscale·M: Global upper bound on flow.
24+
25+
Objective:
26+
- Minimize z.
27+
28+
Constraints:
29+
- c1: ∀ i ∈ N: Σ_{j ≠ i} x[i, j] = 2 (out-degree = 2).
30+
- c2: ∀ j ∈ N: Σ_{i ≠ j} x[i, j] = 2 (in-degree = 2).
31+
- c11: ∀ (k, i), k ≠ i:
32+
Σ_{j ≠ i} f[k, j, i] − Σ_{j ≠ i, j ≠ k} f[k, i, j]
33+
= t[k, i]·intscale
34+
- c14: ∀ (k, i, j), i ≠ j, k ≠ j:
35+
f[k, i, j] ≤ M·intscale·x[i, j]
36+
- c100: ∀ (i, j), i ≠ j:
37+
Σ_{k ≠ j} f[k, i, j] ≤ z
38+
39+
Returns:
40+
jm.Problem: JijModeling problem instance with all variables,
41+
objective, and constraints defined.
42+
"""
43+
# ---- Placeholders ----
44+
n = jm.Placeholder("n")
45+
t = jm.Placeholder("t", ndim=2) # (n,n)
46+
M = jm.Placeholder("M")
47+
intscale = jm.Placeholder("intscale")
48+
49+
# ---- Indices ----
50+
i = jm.Element("i", belong_to=(0, n))
51+
j = jm.Element("j", belong_to=(0, n))
52+
k = jm.Element("k", belong_to=(0, n))
53+
54+
# ---- Vars ----
55+
x = jm.BinaryVar("x", shape=(n, n), description="arc i->j selected")
56+
f = jm.IntegerVar(
57+
"f", shape=(n, n, n),
58+
lower_bound=0, upper_bound=intscale * M,
59+
description="flow of commodity k on arc i->j"
60+
)
61+
z = jm.IntegerVar("z", lower_bound=0, upper_bound=intscale * M)
62+
63+
# ---- Problem & Objective ----
64+
problem = jm.Problem("ip_formulation", sense=jm.ProblemSense.MINIMIZE)
65+
problem += z
66+
67+
# c1: ∀ i ∈ N : Σ_{j ≠ i} x[i,j] = 2
68+
problem += jm.Constraint(
69+
"c1_outdeg_eq_2",
70+
jm.sum([(j, j != i)], x[i, j]) == 2,
71+
forall=[i]
72+
)
73+
74+
# c2: ∀ j ∈ N : Σ_{i ≠ j} x[i,j] = 2
75+
problem += jm.Constraint(
76+
"c2_indeg_eq_2",
77+
jm.sum([(i, i != j)], x[i, j]) == 2,
78+
forall=[j]
79+
)
80+
81+
# c11: flow balance
82+
problem += jm.Constraint(
83+
"c11_flow_balance",
84+
jm.sum([(j, j != i)], f[k, j, i])
85+
- jm.sum([(j, (j != i) & (j != k))], f[k, i, j])
86+
== t[k, i] * intscale,
87+
forall=[k, (i, k != i)]
88+
)
89+
90+
# c14: capacity bound
91+
problem += jm.Constraint(
92+
"c14_capacity_by_x",
93+
f[k, i, j] <= M * intscale * x[i, j],
94+
forall=[k, i, (j, (i != j) & (k != j))]
95+
)
96+
97+
# c100: z upper bound on flow
98+
problem += jm.Constraint(
99+
"c100_z_upper_bounds_flow",
100+
jm.sum([(k, k != j)], f[k, i, j]) <= z,
101+
forall=[i, (j, i != j)]
102+
)
103+
104+
return problem
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import os
2+
import jijmodeling as jm
3+
import glob
4+
from ommx.artifact import ArtifactBuilder
5+
from model import build_ip_formulation
6+
from sol_reader import parse_solution_zfx
7+
8+
9+
def _pick_solution_file(sol_dir: str, base: str) -> str | None:
10+
"""Pick first existing solution file for a basename."""
11+
candidates = [
12+
os.path.join(sol_dir, f"{base}.opt.sol"),
13+
os.path.join(sol_dir, f"{base}.best.sol"),
14+
os.path.join(sol_dir, f"{base}.bst.sol"),
15+
os.path.join(sol_dir, f"{base}.sol"),
16+
]
17+
for p in candidates:
18+
if os.path.exists(p):
19+
return p
20+
return None
21+
22+
23+
def batch_process(
24+
sol_root: str = "../../solutions",
25+
output_directory: str = "./ommx_output",
26+
):
27+
"""
28+
Process instances from a QBench JSON file and corresponding solution files,
29+
convert them into OMMX artifacts, and save them to the output directory.
30+
31+
Parameters:
32+
sol_root (str): Path to the root solutions directory.
33+
output_directory (str): Path to save the generated .ommx files.
34+
"""
35+
os.makedirs(output_directory, exist_ok=True)
36+
37+
problem = build_ip_formulation()
38+
39+
processed_count = 0
40+
error_count = 0
41+
42+
# scan all sol_root's .sol file and get the base name.
43+
sol_files = glob.glob(os.path.join(sol_root, "*.sol"))
44+
bases = set()
45+
for path in sol_files:
46+
name = os.path.basename(path)
47+
for suffix in [".opt.sol", ".best.sol", ".bst.sol", ".sol"]:
48+
if name.endswith(suffix):
49+
bases.add(name[: -len(suffix)])
50+
break
51+
52+
print(f"Found {len(bases)} bases: {sorted(bases)}")
53+
54+
def cut_matrix(t: list[list[int]], n: int) -> list[list[int]]:
55+
if not (5 <= n <= 24):
56+
raise ValueError("n must be between 5 and 24.")
57+
return [row[:n] for row in t[:n]]
58+
59+
your_t_0based = [
60+
[0, 24, 43, 23, 21, 41, 61, 21, 20, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 24, 19, 23, 0],
61+
[0, 0, 0, 0, 21, 18, 39, 23, 0, 0, 40, 19, 64, 19, 17, 64, 80, 0, 0, 22, 24, 18, 19, 0],
62+
[16, 0, 0, 20, 44, 0, 42, 22, 20, 0, 0, 23, 21, 0, 40, 21, 38, 97, 22, 17, 20, 37, 17, 20],
63+
[42, 20, 18, 0, 40, 57, 43, 42, 0, 0, 19, 17, 0, 39, 0, 0, 22, 41, 0, 0, 17, 42, 40, 43],
64+
[0, 0, 0, 40, 0, 83, 60, 0, 44, 0, 37, 60, 0, 40, 0, 0, 40, 19, 0, 39, 41, 17, 0, 0],
65+
[60, 18, 0, 37, 18, 0, 21, 41, 23, 39, 0, 63, 60, 39, 0, 19, 0, 16, 16, 0, 0, 40, 0, 16],
66+
[22, 0, 0, 0, 0, 0, 0, 61, 36, 80, 96, 19, 19, 41, 16, 0, 0, 0, 22, 0, 43, 0, 44, 22],
67+
[0, 0, 20, 19, 17, 20, 40, 0, 0, 60, 61, 0, 20, 62, 20, 0, 0, 38, 0, 0, 0, 0, 24, 22],
68+
[21, 22, 38, 0, 44, 20, 40, 39, 0, 36, 22, 21, 19, 39, 19, 0, 0, 21, 24, 16, 23, 21, 37, 0],
69+
[0, 24, 23, 39, 20, 0, 0, 41, 0, 0, 0, 22, 0, 0, 44, 42, 22, 42, 22, 19, 20, 58, 18, 0],
70+
[60, 57, 0, 0, 16, 0, 16, 37, 0, 0, 0, 44, 63, 0, 18, 0, 17, 18, 0, 0, 0, 100, 24, 23],
71+
[0, 44, 44, 0, 23, 17, 39, 21, 0, 17, 40, 0, 24, 78, 17, 24, 20, 18, 0, 24, 24, 0, 20, 0],
72+
[23, 16, 0, 0, 0, 23, 0, 0, 0, 0, 43, 58, 0, 0, 24, 60, 0, 0, 19, 0, 21, 0, 20, 0],
73+
[44, 20, 0, 19, 21, 0, 39, 19, 0, 0, 0, 39, 22, 0, 0, 64, 24, 22, 0, 39, 0, 43, 42, 16],
74+
[0, 60, 37, 18, 0, 0, 0, 20, 0, 41, 43, 16, 43, 24, 0, 0, 18, 18, 0, 44, 20, 0, 21, 37],
75+
[0, 0, 23, 39, 0, 24, 40, 0, 37, 0, 40, 20, 44, 43, 0, 0, 0, 0, 0, 16, 0, 59, 0, 0],
76+
[0, 42, 0, 0, 23, 24, 38, 19, 36, 0, 20, 60, 57, 0, 23, 40, 0, 0, 0, 16, 42, 0, 23, 0],
77+
[0, 41, 36, 43, 23, 41, 17, 0, 38, 0, 0, 21, 21, 17, 16, 16, 39, 0, 22, 0, 21, 23, 16, 23],
78+
[17, 0, 23, 23, 20, 0, 17, 58, 17, 0, 20, 0, 17, 24, 0, 0, 17, 42, 0, 58, 19, 22, 0, 24],
79+
[42, 0, 16, 0, 43, 0, 24, 36, 0, 16, 24, 41, 41, 0, 24, 0, 0, 0, 0, 0, 56, 38, 63, 19],
80+
[37, 23, 23, 0, 42, 16, 23, 76, 23, 0, 0, 24, 20, 41, 20, 24, 40, 23, 0, 0, 0, 39, 20, 0],
81+
[43, 20, 17, 17, 0, 20, 19, 0, 80, 0, 0, 0, 40, 0, 40, 16, 19, 0, 0, 0, 18, 0, 17, 0],
82+
[18, 20, 44, 40, 21, 18, 0, 20, 0, 0, 16, 24, 0, 0, 19, 18, 0, 17, 23, 0, 23, 44, 0, 42],
83+
[44, 0, 0, 0, 62, 0, 17, 41, 0, 0, 0, 63, 0, 37, 22, 0, 20, 0, 0, 19, 57, 18, 38, 0],
84+
]
85+
86+
for base in sorted(bases):
87+
# from base to obtain n, such like network05 → n=5
88+
try:
89+
n_str = "".join(ch for ch in base if ch.isdigit())
90+
n_val = int(n_str) if n_str else 5
91+
except Exception:
92+
print(f"[{base}] Cannot parse n from base, skip.")
93+
continue
94+
95+
try:
96+
instance_data = {
97+
"n": n_val,
98+
"t": cut_matrix(your_t_0based, n_val),
99+
"M": 1000,
100+
"intscale": 1000,
101+
}
102+
103+
interpreter = jm.Interpreter(instance_data)
104+
ommx_instance = interpreter.eval_problem(problem)
105+
106+
sol_path = _pick_solution_file(sol_root, base)
107+
solution = None
108+
if sol_path:
109+
try:
110+
print(f" → Evaluating solution: {sol_path}")
111+
solution_dict = parse_solution_zfx(sol_path, n_val)
112+
solution = ommx_instance.evaluate(solution_dict)
113+
if (
114+
solution.feasible
115+
and abs(solution.objective - solution_dict[0]) < 1e-6
116+
):
117+
print(
118+
f" objective={solution.objective}, feasible={solution.feasible}"
119+
)
120+
else:
121+
print(
122+
" ! Objective mismatch or infeasible; will save instance only."
123+
)
124+
solution = None
125+
except Exception as sol_err:
126+
print(f" ! Solution evaluation failed: {sol_err}")
127+
solution = None
128+
129+
out_path = os.path.join(output_directory, f"{base}.ommx")
130+
if os.path.exists(out_path):
131+
os.remove(out_path)
132+
133+
builder = ArtifactBuilder.new_archive_unnamed(out_path)
134+
builder.add_instance(ommx_instance)
135+
if solution is not None:
136+
builder.add_solution(solution)
137+
builder.build()
138+
139+
print(f" ✓ Created: {out_path}")
140+
print("-" * 50)
141+
processed_count += 1
142+
143+
except Exception as e:
144+
print(f"[{base}] Error: {e}")
145+
print("-" * 50)
146+
error_count += 1
147+
148+
print(f"Batch complete — processed: {processed_count}, errors: {error_count}")
149+
150+
151+
if __name__ == "__main__":
152+
batch_process(
153+
sol_root="../../solutions",
154+
output_directory="./ommx_output",
155+
)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import re
2+
3+
def parse_solution_zfx(file_path: str, n: int) -> dict[int, int]:
4+
"""Parse Jij solution (.sol) into {id: value} with strict z→f→x ordering.
5+
6+
Order:
7+
- id=0 : z
8+
- id=1.. : f[k,i,j] (k=0..n-1, i=0..n-1, j=0..n-1)
9+
- then: x[i,j] (i=0..n-1, j=0..n-1)
10+
11+
Args:
12+
file_path (str): Path to solution file
13+
n (int): Number of nodes
14+
15+
Returns:
16+
dict[int,int]: {id: value} in z→f→x order
17+
"""
18+
z_val = None
19+
f_vals = {}
20+
x_vals = {}
21+
22+
with open(file_path, "r") as f:
23+
for line in f:
24+
line = line.strip()
25+
if not line or line.startswith("#"):
26+
continue
27+
28+
# z
29+
m = re.match(r"^z\s+([-+]?\d+\.?\d*)$", line)
30+
if m:
31+
z_val = int(m.group(1))
32+
continue
33+
34+
# f#k#i#j
35+
m = re.match(r"^f#(\d+)#(\d+)#(\d+)\s+([-+]?\d+\.?\d*)$", line)
36+
if m:
37+
k, i, j, val = (
38+
int(m.group(1)) - 1,
39+
int(m.group(2)) - 1,
40+
int(m.group(3)) - 1,
41+
int(m.group(4)),
42+
)
43+
f_vals[(k, i, j)] = val
44+
continue
45+
46+
# x#i#j
47+
m = re.match(r"^x#(\d+)#(\d+)\s+([-+]?\d+\.?\d*)$", line)
48+
if m:
49+
i, j, val = int(m.group(1)) - 1, int(m.group(2)) - 1, int(m.group(3))
50+
x_vals[(i, j)] = val
51+
continue
52+
53+
if z_val is None:
54+
raise ValueError("No z found in solution file.")
55+
56+
# ---- Rebuild {id: value} ----
57+
sol_dict = {}
58+
idx = 0
59+
60+
# z
61+
sol_dict[idx] = z_val
62+
idx += 1
63+
64+
# f[k,i,j], dictionary order
65+
for k in range(n):
66+
for i in range(n):
67+
for j in range(n):
68+
sol_dict[idx] = f_vals.get((k, i, j), 0.0)
69+
idx += 1
70+
71+
# x[i,j], dictionary order
72+
for i in range(n):
73+
for j in range(n):
74+
sol_dict[idx] = x_vals.get((i, j), 0.0)
75+
idx += 1
76+
77+
return sol_dict

0 commit comments

Comments
 (0)