labgate 0.5.31 → 0.5.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -2
- package/dist/cli.js +533 -0
- package/dist/cli.js.map +1 -1
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.js +45 -4
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/container.d.ts +3 -3
- package/dist/lib/container.js +144 -12
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/display-mcp.d.ts +10 -0
- package/dist/lib/display-mcp.js +160 -0
- package/dist/lib/display-mcp.js.map +1 -0
- package/dist/lib/display-store.d.ts +24 -0
- package/dist/lib/display-store.js +150 -0
- package/dist/lib/display-store.js.map +1 -0
- package/dist/lib/explorer-autopilot.d.ts +16 -0
- package/dist/lib/explorer-autopilot.js +573 -0
- package/dist/lib/explorer-autopilot.js.map +1 -0
- package/dist/lib/explorer-claude.d.ts +16 -0
- package/dist/lib/explorer-claude.js +361 -0
- package/dist/lib/explorer-claude.js.map +1 -0
- package/dist/lib/explorer-compare.d.ts +9 -0
- package/dist/lib/explorer-compare.js +190 -0
- package/dist/lib/explorer-compare.js.map +1 -0
- package/dist/lib/explorer-eval.d.ts +23 -0
- package/dist/lib/explorer-eval.js +161 -0
- package/dist/lib/explorer-eval.js.map +1 -0
- package/dist/lib/explorer-gc.d.ts +11 -0
- package/dist/lib/explorer-gc.js +304 -0
- package/dist/lib/explorer-gc.js.map +1 -0
- package/dist/lib/explorer-git.d.ts +14 -0
- package/dist/lib/explorer-git.js +136 -0
- package/dist/lib/explorer-git.js.map +1 -0
- package/dist/lib/explorer-lock.d.ts +5 -0
- package/dist/lib/explorer-lock.js +100 -0
- package/dist/lib/explorer-lock.js.map +1 -0
- package/dist/lib/explorer-mcp.d.ts +11 -0
- package/dist/lib/explorer-mcp.js +611 -0
- package/dist/lib/explorer-mcp.js.map +1 -0
- package/dist/lib/explorer-retention.d.ts +4 -0
- package/dist/lib/explorer-retention.js +58 -0
- package/dist/lib/explorer-retention.js.map +1 -0
- package/dist/lib/explorer-store.d.ts +77 -0
- package/dist/lib/explorer-store.js +950 -0
- package/dist/lib/explorer-store.js.map +1 -0
- package/dist/lib/explorer-types.d.ts +161 -0
- package/dist/lib/explorer-types.js +3 -0
- package/dist/lib/explorer-types.js.map +1 -0
- package/dist/lib/explorer.d.ts +31 -0
- package/dist/lib/explorer.js +247 -0
- package/dist/lib/explorer.js.map +1 -0
- package/dist/lib/results-store.js +37 -3
- package/dist/lib/results-store.js.map +1 -1
- package/dist/lib/test/integration-harness.js +1 -1
- package/dist/lib/test/integration-harness.js.map +1 -1
- package/dist/lib/ui.html +5115 -2052
- package/dist/lib/ui.js +906 -39
- package/dist/lib/ui.js.map +1 -1
- package/dist/lib/web-terminal.js +4 -3
- package/dist/lib/web-terminal.js.map +1 -1
- package/dist/mcp-bundles/dataset-mcp.bundle.mjs +0 -8
- package/dist/mcp-bundles/display-mcp.bundle.mjs +30209 -0
- package/dist/mcp-bundles/explorer-mcp.bundle.mjs +40036 -0
- package/dist/mcp-bundles/results-mcp.bundle.mjs +30 -4
- package/package.json +3 -2
- package/templates/tsp-lab/API_CONTRACT.md +20 -0
- package/templates/tsp-lab/EVAL.md +20 -0
- package/templates/tsp-lab/PROBLEM.md +18 -0
- package/templates/tsp-lab/data/generate_instances.py +51 -0
- package/templates/tsp-lab/data/instances.jsonl +12 -0
- package/templates/tsp-lab/eval.py +148 -0
- package/templates/tsp-lab/solver.py +88 -0
- package/templates/tsp-lab/stub-patches/enable_two_opt.patch +14 -0
|
@@ -29940,7 +29940,7 @@ var StdioServerTransport = class {
|
|
|
29940
29940
|
|
|
29941
29941
|
// src/lib/results-store.ts
|
|
29942
29942
|
import { randomUUID } from "crypto";
|
|
29943
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
29943
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
29944
29944
|
import { dirname, join as join2 } from "path";
|
|
29945
29945
|
|
|
29946
29946
|
// src/lib/config.ts
|
|
@@ -29971,6 +29971,10 @@ function getResultsDbPath() {
|
|
|
29971
29971
|
}
|
|
29972
29972
|
|
|
29973
29973
|
// src/lib/results-store.ts
|
|
29974
|
+
function isRenameFallbackError(err) {
|
|
29975
|
+
const code = err?.code;
|
|
29976
|
+
return code === "EBUSY" || code === "EXDEV";
|
|
29977
|
+
}
|
|
29974
29978
|
function nowIso() {
|
|
29975
29979
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
29976
29980
|
}
|
|
@@ -30285,12 +30289,34 @@ var ResultsStore = class {
|
|
|
30285
30289
|
}
|
|
30286
30290
|
}
|
|
30287
30291
|
writeFile(next) {
|
|
30288
|
-
const
|
|
30289
|
-
|
|
30292
|
+
const payload = JSON.stringify(next, null, 2) + "\n";
|
|
30293
|
+
const tmpPath = join2(dirname(this.dbPath), `.${Date.now()}.${process.pid}.${randomUUID()}.results.tmp`);
|
|
30294
|
+
writeFileSync(tmpPath, payload, {
|
|
30290
30295
|
encoding: "utf-8",
|
|
30291
30296
|
mode: PRIVATE_FILE_MODE
|
|
30292
30297
|
});
|
|
30293
|
-
|
|
30298
|
+
try {
|
|
30299
|
+
renameSync(tmpPath, this.dbPath);
|
|
30300
|
+
} catch (err) {
|
|
30301
|
+
if (!isRenameFallbackError(err)) {
|
|
30302
|
+
try {
|
|
30303
|
+
unlinkSync(tmpPath);
|
|
30304
|
+
} catch {
|
|
30305
|
+
}
|
|
30306
|
+
throw err;
|
|
30307
|
+
}
|
|
30308
|
+
try {
|
|
30309
|
+
writeFileSync(this.dbPath, payload, {
|
|
30310
|
+
encoding: "utf-8",
|
|
30311
|
+
mode: PRIVATE_FILE_MODE
|
|
30312
|
+
});
|
|
30313
|
+
} finally {
|
|
30314
|
+
try {
|
|
30315
|
+
unlinkSync(tmpPath);
|
|
30316
|
+
} catch {
|
|
30317
|
+
}
|
|
30318
|
+
}
|
|
30319
|
+
}
|
|
30294
30320
|
ensurePrivateFile(this.dbPath);
|
|
30295
30321
|
}
|
|
30296
30322
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "labgate",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.33",
|
|
4
4
|
"description": "Secure HPC wrapper for AI coding agents (Claude-first): policy controls, Apptainer sandboxes, and SLURM workflows — https://labgate.dev",
|
|
5
5
|
"homepage": "https://labgate.dev",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"bin/",
|
|
41
41
|
"dist/",
|
|
42
|
-
"sandbox/"
|
|
42
|
+
"sandbox/",
|
|
43
|
+
"templates/"
|
|
43
44
|
],
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# API Contract
|
|
2
|
+
|
|
3
|
+
Implement in `solver.py`:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
def solve(points):
|
|
7
|
+
...
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Input:
|
|
11
|
+
- `points`: list of `[x, y]` coordinates.
|
|
12
|
+
|
|
13
|
+
Output:
|
|
14
|
+
- A list of integer node indices representing a Hamiltonian cycle order.
|
|
15
|
+
- Must contain each node exactly once.
|
|
16
|
+
- `len(tour) == len(points)`.
|
|
17
|
+
|
|
18
|
+
Evaluation behavior:
|
|
19
|
+
- Evaluator treats output as a closed tour (last node reconnects to first).
|
|
20
|
+
- Invalid tours fail evaluation.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Evaluation Contract
|
|
2
|
+
|
|
3
|
+
Run:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python eval.py
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Output contract:
|
|
10
|
+
- The **last line of stdout** must be valid JSON.
|
|
11
|
+
- JSON must include at least:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{"score": 0.7812, "metrics": {"runtime_ms": 31.2}}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
- `score` is a float and **higher is better**.
|
|
19
|
+
- `metrics` is optional but recommended.
|
|
20
|
+
- Non-zero exit code or missing/invalid final JSON line is treated as evaluation failure.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# TSP Lab Problem
|
|
2
|
+
|
|
3
|
+
You are optimizing a Euclidean Traveling Salesperson Problem (TSP) solver.
|
|
4
|
+
|
|
5
|
+
Goal: maximize evaluation score over a fixed benchmark set of TSP instances.
|
|
6
|
+
|
|
7
|
+
Current baseline:
|
|
8
|
+
- `solver.py` uses nearest-neighbor construction.
|
|
9
|
+
|
|
10
|
+
What to improve:
|
|
11
|
+
- Better construction heuristics.
|
|
12
|
+
- Local search (for example 2-opt).
|
|
13
|
+
- Runtime-aware improvements (score includes a tiny runtime penalty).
|
|
14
|
+
|
|
15
|
+
Constraints:
|
|
16
|
+
- Keep the `solve(points)` API contract intact.
|
|
17
|
+
- Do not modify `eval.py` unless explicitly allowed.
|
|
18
|
+
- Keep runtime practical on the provided benchmark instances.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import random
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_instances(count: int, points_per_instance: int, seed: int):
|
|
10
|
+
for i in range(count):
|
|
11
|
+
instance_seed = seed + i * 1009
|
|
12
|
+
rng = random.Random(instance_seed)
|
|
13
|
+
|
|
14
|
+
# Mix uniform cloud + a weak center cluster so local search has room to improve.
|
|
15
|
+
points = []
|
|
16
|
+
for j in range(points_per_instance):
|
|
17
|
+
if j % 4 == 0:
|
|
18
|
+
x = rng.gauss(500.0, 180.0)
|
|
19
|
+
y = rng.gauss(500.0, 180.0)
|
|
20
|
+
else:
|
|
21
|
+
x = rng.uniform(0.0, 1000.0)
|
|
22
|
+
y = rng.uniform(0.0, 1000.0)
|
|
23
|
+
points.append([round(max(0.0, min(1000.0, x)), 3), round(max(0.0, min(1000.0, y)), 3)])
|
|
24
|
+
|
|
25
|
+
yield {
|
|
26
|
+
"id": f"inst_{i:03d}",
|
|
27
|
+
"seed": instance_seed,
|
|
28
|
+
"points": points,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> None:
|
|
33
|
+
parser = argparse.ArgumentParser(description="Generate deterministic TSP instances")
|
|
34
|
+
parser.add_argument("--count", type=int, default=12)
|
|
35
|
+
parser.add_argument("--points", type=int, default=48)
|
|
36
|
+
parser.add_argument("--seed", type=int, default=20260217)
|
|
37
|
+
parser.add_argument("--output", type=Path, default=Path(__file__).parent / "instances.jsonl")
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
|
|
40
|
+
rows = list(generate_instances(args.count, args.points, args.seed))
|
|
41
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
with args.output.open("w", encoding="utf-8") as f:
|
|
44
|
+
for row in rows:
|
|
45
|
+
f.write(json.dumps(row, separators=(",", ":")) + "\n")
|
|
46
|
+
|
|
47
|
+
print(f"wrote {len(rows)} instances to {args.output}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{"id":"inst_000","seed":20260217,"points":[[683.986,501.225],[48.285,616.705],[95.326,485.406],[544.661,644.98],[313.227,335.701],[451.726,736.463],[73.332,119.632],[295.362,488.05],[311.43,779.077],[357.013,188.147],[735.16,553.627],[848.313,875.275],[398.858,466.456],[226.985,616.271],[69.756,634.261],[265.573,105.813],[233.576,811.84],[312.43,718.167],[239.106,350.77],[339.89,171.019],[742.279,292.358],[764.341,400.574],[127.158,600.417],[568.032,274.537],[594.684,383.743],[639.212,67.937],[115.345,365.784],[736.814,311.384],[740.74,570.347],[195.542,181.487],[298.135,258.17],[665.133,359.811],[611.428,614.12],[465.802,327.782],[799.408,362.669],[265.021,972.475],[438.721,711.95],[475.786,986.801],[471.422,605.738],[441.864,435.809],[284.47,413.165],[760.204,741.432],[982.797,777.703],[955.065,578.705],[737.265,165.287],[578.27,391.485],[471.846,812.339],[13.765,200.115]]}
|
|
2
|
+
{"id":"inst_001","seed":20261226,"points":[[758.149,461.363],[851.146,805.955],[430.355,800.31],[971.87,405.734],[550.929,667.964],[859.758,602.053],[924.812,961.257],[600.163,822.698],[730.743,339.486],[9.34,419.11],[639.862,104.342],[973.502,557.276],[799.078,681.569],[547.909,532.516],[533.686,963.74],[963.989,822.733],[392.439,431.405],[74.622,679.043],[220.185,11.819],[3.289,538.228],[420.322,59.957],[143.799,240.156],[665.072,794.167],[670.759,630.144],[481.194,175.496],[966.604,504.127],[484.721,144.518],[843.851,483.177],[656.323,331.667],[252.814,282.728],[868.854,125.048],[949.193,310.906],[603.574,384.519],[773.865,354.254],[609.883,644.556],[11.029,210.353],[460.114,0.0],[499.468,626.871],[861.828,301.748],[462.611,608.424],[705.026,498.391],[477.975,839.622],[971.42,21.302],[238.133,188.17],[440.616,333.614],[134.819,416.892],[618.327,304.204],[465.456,505.684]]}
|
|
3
|
+
{"id":"inst_002","seed":20262235,"points":[[496.916,252.128],[96.719,288.477],[942.606,776.231],[688.433,568.783],[841.444,280.24],[240.097,357.931],[545.074,639.018],[373.152,830.405],[482.891,333.48],[899.981,432.971],[761.074,752.657],[891.145,461.803],[517.896,215.047],[690.35,2.163],[504.503,229.068],[191.792,579.626],[440.06,843.853],[487.079,320.721],[261.753,105.716],[346.012,314.983],[459.032,881.831],[494.771,54.279],[939.599,317.541],[160.038,781.17],[522.234,440.998],[822.49,113.743],[976.201,819.109],[33.871,934.614],[709.26,680.366],[477.231,83.252],[998.901,333.22],[829.078,137.085],[694.856,443.495],[303.571,121.059],[541.667,994.17],[933.671,868.551],[529.366,349.896],[474.558,537.771],[860.547,318.204],[751.796,473.336],[386.233,1000.0],[244.326,102.88],[280.562,576.871],[37.7,310.58],[533.925,222.075],[854.821,699.527],[815.267,762.728],[571.439,732.607]]}
|
|
4
|
+
{"id":"inst_003","seed":20263244,"points":[[301.076,315.01],[459.462,531.21],[188.241,177.274],[877.538,697.583],[447.279,322.692],[20.894,845.64],[117.812,758.162],[215.185,834.401],[371.434,564.083],[669.227,351.045],[960.485,754.741],[748.294,765.059],[551.814,588.54],[890.745,420.275],[582.675,897.548],[948.884,733.335],[472.147,575.595],[358.38,24.062],[196.822,809.055],[420.217,868.723],[286.741,406.368],[504.742,668.391],[448.042,388.253],[112.842,951.214],[288.67,560.341],[675.691,2.539],[646.583,90.151],[180.528,818.052],[408.731,477.44],[565.289,773.972],[173.542,994.296],[291.604,269.296],[680.216,477.521],[709.86,650.679],[234.329,334.261],[193.763,777.916],[556.573,426.064],[414.369,517.428],[263.123,662.036],[779.982,452.75],[579.311,483.535],[468.707,858.286],[478.315,161.212],[616.676,263.567],[422.942,596.815],[426.278,511.533],[742.47,463.636],[142.728,69.862]]}
|
|
5
|
+
{"id":"inst_004","seed":20264253,"points":[[477.275,445.019],[713.318,690.026],[162.492,172.005],[708.856,590.955],[294.784,220.955],[968.088,176.944],[295.353,663.014],[959.2,219.799],[0.0,132.236],[195.596,997.599],[571.184,434.251],[75.529,847.041],[333.996,671.191],[158.553,62.985],[823.791,35.811],[660.504,310.887],[533.609,389.294],[744.346,806.863],[134.658,142.041],[998.478,189.994],[323.262,865.75],[766.532,232.886],[604.468,284.027],[244.75,627.983],[343.15,703.776],[242.779,100.301],[738.63,791.076],[450.267,747.873],[452.002,427.453],[359.364,431.152],[826.691,261.663],[196.57,988.612],[182.785,348.418],[739.867,266.065],[624.33,493.646],[214.097,427.601],[831.911,555.777],[76.229,137.542],[791.282,318.506],[133.671,932.746],[419.426,779.566],[383.235,248.101],[842.463,164.796],[753.289,838.648],[325.607,716.687],[878.634,657.879],[842.152,180.623],[592.637,354.988]]}
|
|
6
|
+
{"id":"inst_005","seed":20265262,"points":[[157.518,371.13],[345.067,559.144],[4.414,133.838],[187.812,152.753],[447.618,633.502],[104.686,259.276],[903.362,163.241],[36.62,527.156],[447.772,489.729],[5.65,660.49],[223.545,672.598],[5.703,741.502],[299.445,581.059],[890.692,407.233],[410.673,66.233],[367.166,90.754],[805.714,821.309],[44.707,464.108],[356.679,909.184],[35.608,678.973],[606.104,724.143],[694.888,638.609],[111.129,286.524],[189.116,375.919],[450.877,654.209],[292.398,845.669],[596.806,290.811],[706.703,993.717],[395.052,378.591],[853.291,626.575],[685.965,850.644],[863.8,461.043],[376.231,121.579],[467.304,370.037],[310.814,728.899],[753.779,705.546],[725.532,272.545],[112.84,844.732],[212.528,685.117],[409.057,275.355],[729.216,345.717],[329.332,300.94],[623.699,117.554],[56.406,276.527],[368.941,520.93],[967.409,354.837],[193.038,790.596],[215.633,882.689]]}
|
|
7
|
+
{"id":"inst_006","seed":20266271,"points":[[369.18,520.158],[781.424,853.095],[805.066,404.252],[855.487,283.411],[405.009,789.122],[356.77,49.03],[937.196,815.471],[741.649,828.02],[379.573,381.529],[956.674,607.888],[739.591,340.122],[87.502,454.533],[124.863,493.345],[198.938,283.145],[594.598,630.482],[853.394,470.878],[626.752,403.06],[791.812,429.881],[689.167,926.958],[382.919,559.091],[463.865,348.772],[98.963,141.94],[900.643,457.124],[866.771,21.613],[335.004,189.301],[225.91,121.077],[526.673,395.053],[735.59,462.632],[287.721,393.298],[314.761,503.284],[186.22,245.111],[795.104,186.028],[417.321,447.111],[90.72,745.326],[560.205,876.282],[295.316,93.318],[459.482,618.948],[259.625,204.077],[219.822,615.395],[296.853,784.923],[627.916,635.752],[144.558,673.147],[682.152,823.043],[994.625,745.034],[772.634,216.363],[610.511,54.331],[980.268,724.8],[902.83,327.88]]}
|
|
8
|
+
{"id":"inst_007","seed":20267280,"points":[[524.239,321.274],[310.7,711.026],[129.47,457.573],[190.19,844.786],[277.811,479.541],[917.985,760.527],[921.935,429.161],[949.765,124.865],[560.536,641.004],[49.144,747.157],[401.924,45.803],[57.108,301.513],[419.857,270.514],[817.786,596.601],[193.859,955.26],[475.229,578.162],[462.166,415.531],[924.967,153.19],[819.08,506.761],[754.151,603.422],[392.487,373.325],[454.878,836.289],[143.077,970.386],[966.773,698.647],[674.642,266.247],[507.459,432.111],[284.105,350.058],[426.2,989.7],[666.184,189.744],[927.322,381.603],[31.2,600.569],[504.914,541.385],[794.168,371.21],[13.993,419.452],[91.18,365.595],[653.871,242.699],[353.611,335.591],[783.145,33.754],[115.445,992.465],[559.295,980.463],[289.921,320.239],[204.278,244.854],[891.546,856.759],[852.192,993.472],[872.517,480.099],[847.871,384.758],[29.051,113.588],[158.815,541.152]]}
|
|
9
|
+
{"id":"inst_008","seed":20268289,"points":[[626.028,687.447],[504.66,878.94],[868.05,860.11],[718.904,590.225],[453.606,571.127],[216.998,296.112],[930.59,491.676],[755.134,953.937],[368.473,747.776],[911.451,30.758],[960.926,343.573],[286.728,270.919],[541.846,503.02],[354.972,476.538],[884.738,499.398],[287.827,528.119],[417.682,376.11],[416.954,971.462],[675.428,771.707],[730.801,89.765],[355.811,232.117],[564.431,558.233],[542.062,582.815],[15.451,805.778],[627.626,378.726],[646.924,854.211],[784.669,247.572],[61.749,754.82],[654.68,565.584],[897.78,677.814],[705.336,632.456],[256.343,366.313],[216.039,545.031],[436.725,768.376],[289.669,663.62],[166.282,872.595],[425.369,950.71],[545.379,814.457],[544.404,723.767],[771.28,316.682],[523.788,376.575],[658.694,444.634],[213.996,283.628],[827.381,837.748],[372.559,528.4],[556.852,306.885],[886.203,884.83],[492.777,909.567]]}
|
|
10
|
+
{"id":"inst_009","seed":20269298,"points":[[753.864,514.102],[968.088,670.402],[153.493,149.5],[12.014,976.5],[281.651,536.746],[767.052,678.295],[472.5,671.237],[921.943,363.685],[1000.0,387.734],[940.096,560.942],[15.452,106.042],[12.961,846.44],[833.9,640.565],[174.537,845.716],[822.671,710.894],[314.718,999.232],[350.427,645.603],[954.735,463.851],[987.593,37.501],[619.551,85.026],[344.848,532.907],[808.037,812.685],[892.838,947.412],[868.442,830.687],[839.77,300.43],[937.261,168.978],[402.515,864.858],[442.578,934.554],[371.979,933.403],[437.1,754.582],[463.169,427.443],[951.892,470.011],[210.253,477.777],[664.592,13.714],[167.591,373.092],[296.046,968.374],[476.459,361.492],[330.979,113.478],[701.83,353.04],[595.925,961.351],[578.704,653.863],[68.055,729.963],[942.226,822.455],[965.433,531.93],[444.878,377.716],[233.479,894.353],[327.394,795.227],[303.043,524.204]]}
|
|
11
|
+
{"id":"inst_010","seed":20270307,"points":[[315.615,400.062],[503.968,236.176],[134.923,524.789],[925.554,241.383],[440.014,568.787],[134.199,532.952],[694.19,171.554],[456.662,363.543],[391.569,444.021],[592.26,801.592],[151.964,761.087],[456.951,905.873],[521.253,306.487],[809.057,659.169],[761.223,923.406],[538.494,320.908],[545.827,287.852],[299.502,776.694],[853.674,221.131],[862.68,919.452],[619.702,252.15],[377.518,895.258],[11.224,934.665],[419.355,39.437],[679.989,677.02],[639.098,738.968],[718.369,38.97],[339.119,154.48],[279.784,849.77],[311.958,792.925],[440.741,481.346],[659.838,762.043],[367.186,740.524],[750.092,941.72],[714.563,454.886],[594.889,394.258],[329.603,307.553],[771.642,219.997],[393.004,684.189],[593.515,473.802],[721.476,552.305],[472.337,61.207],[22.473,559.454],[370.486,786.227],[250.702,678.314],[775.339,442.749],[265.463,444.563],[20.119,98.301]]}
|
|
12
|
+
{"id":"inst_011","seed":20271316,"points":[[282.151,481.786],[10.994,916.475],[882.863,72.019],[205.716,130.974],[541.009,815.753],[569.001,455.751],[325.352,501.697],[968.888,889.175],[509.237,497.72],[727.008,376.312],[979.482,169.553],[834.617,55.585],[542.724,714.038],[183.634,242.702],[757.456,928.698],[695.503,819.934],[216.495,409.335],[385.031,892.905],[165.766,498.839],[316.598,854.91],[715.282,472.478],[458.152,332.918],[826.924,58.25],[222.041,655.671],[482.981,505.717],[291.735,45.164],[923.305,848.919],[288.386,26.115],[589.648,616.647],[766.389,352.905],[32.107,795.771],[159.765,375.757],[702.303,453.156],[556.369,549.713],[208.305,269.991],[685.616,690.388],[629.062,437.984],[713.096,390.803],[36.88,863.567],[220.44,601.364],[203.336,560.472],[154.731,367.911],[983.927,129.027],[299.802,356.034],[341.897,605.937],[783.284,105.391],[638.869,712.713],[396.054,926.216]]}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Evaluator for TSP Lab.
|
|
2
|
+
|
|
3
|
+
Contract: print a JSON object on the last stdout line:
|
|
4
|
+
{"score": <float>, "metrics": {...}}
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Iterable, List, Sequence, Tuple
|
|
13
|
+
|
|
14
|
+
import solver
|
|
15
|
+
|
|
16
|
+
Point = Tuple[float, float]
|
|
17
|
+
|
|
18
|
+
ALPHA_RUNTIME = 0.0005
|
|
19
|
+
INSTANCES_PATH = Path(__file__).parent / "data" / "instances.jsonl"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _euclid(a: Point, b: Point) -> float:
|
|
23
|
+
dx = a[0] - b[0]
|
|
24
|
+
dy = a[1] - b[1]
|
|
25
|
+
return (dx * dx + dy * dy) ** 0.5
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _distance_matrix(points: Sequence[Point]) -> List[List[float]]:
|
|
29
|
+
n = len(points)
|
|
30
|
+
dist = [[0.0] * n for _ in range(n)]
|
|
31
|
+
for i in range(n):
|
|
32
|
+
for j in range(i + 1, n):
|
|
33
|
+
d = _euclid(points[i], points[j])
|
|
34
|
+
dist[i][j] = d
|
|
35
|
+
dist[j][i] = d
|
|
36
|
+
return dist
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _mst_weight(dist: List[List[float]]) -> float:
|
|
40
|
+
n = len(dist)
|
|
41
|
+
if n <= 1:
|
|
42
|
+
return 0.0
|
|
43
|
+
|
|
44
|
+
in_tree = [False] * n
|
|
45
|
+
best = [float("inf")] * n
|
|
46
|
+
best[0] = 0.0
|
|
47
|
+
total = 0.0
|
|
48
|
+
|
|
49
|
+
for _ in range(n):
|
|
50
|
+
u = -1
|
|
51
|
+
best_val = float("inf")
|
|
52
|
+
for i in range(n):
|
|
53
|
+
if not in_tree[i] and best[i] < best_val:
|
|
54
|
+
best_val = best[i]
|
|
55
|
+
u = i
|
|
56
|
+
if u < 0:
|
|
57
|
+
raise RuntimeError("MST failed: disconnected candidate")
|
|
58
|
+
|
|
59
|
+
in_tree[u] = True
|
|
60
|
+
total += best_val
|
|
61
|
+
|
|
62
|
+
for v in range(n):
|
|
63
|
+
if in_tree[v]:
|
|
64
|
+
continue
|
|
65
|
+
w = dist[u][v]
|
|
66
|
+
if w < best[v]:
|
|
67
|
+
best[v] = w
|
|
68
|
+
|
|
69
|
+
return total
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _tour_length(tour: Sequence[int], dist: List[List[float]]) -> float:
|
|
73
|
+
n = len(tour)
|
|
74
|
+
if n <= 1:
|
|
75
|
+
return 0.0
|
|
76
|
+
|
|
77
|
+
total = 0.0
|
|
78
|
+
for i in range(n):
|
|
79
|
+
a = tour[i]
|
|
80
|
+
b = tour[(i + 1) % n]
|
|
81
|
+
total += dist[a][b]
|
|
82
|
+
return total
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _validate_tour(tour: Sequence[int], n: int) -> None:
|
|
86
|
+
if len(tour) != n:
|
|
87
|
+
raise ValueError(f"invalid tour length: expected {n}, got {len(tour)}")
|
|
88
|
+
seen = set(tour)
|
|
89
|
+
if len(seen) != n:
|
|
90
|
+
raise ValueError("tour contains duplicate nodes")
|
|
91
|
+
if min(seen) < 0 or max(seen) >= n:
|
|
92
|
+
raise ValueError("tour contains out-of-range node indices")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _load_instances(path: Path) -> Iterable[dict]:
|
|
96
|
+
if not path.exists():
|
|
97
|
+
raise FileNotFoundError(f"instances file not found: {path}")
|
|
98
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
99
|
+
raw = line.strip()
|
|
100
|
+
if not raw:
|
|
101
|
+
continue
|
|
102
|
+
yield json.loads(raw)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> None:
|
|
106
|
+
rows = list(_load_instances(INSTANCES_PATH))
|
|
107
|
+
if not rows:
|
|
108
|
+
raise RuntimeError("no instances found")
|
|
109
|
+
|
|
110
|
+
instance_scores: List[float] = []
|
|
111
|
+
total_runtime_ms = 0.0
|
|
112
|
+
|
|
113
|
+
for row in rows:
|
|
114
|
+
points = [(float(x), float(y)) for x, y in row["points"]]
|
|
115
|
+
n = len(points)
|
|
116
|
+
|
|
117
|
+
start = time.perf_counter()
|
|
118
|
+
tour = solver.solve(points)
|
|
119
|
+
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
|
120
|
+
total_runtime_ms += elapsed_ms
|
|
121
|
+
|
|
122
|
+
_validate_tour(tour, n)
|
|
123
|
+
|
|
124
|
+
dist = _distance_matrix(points)
|
|
125
|
+
mst = _mst_weight(dist)
|
|
126
|
+
length = _tour_length(tour, dist)
|
|
127
|
+
if length <= 0.0:
|
|
128
|
+
raise ValueError("tour length must be > 0")
|
|
129
|
+
|
|
130
|
+
instance_scores.append(mst / length)
|
|
131
|
+
|
|
132
|
+
mean_ratio = sum(instance_scores) / len(instance_scores)
|
|
133
|
+
runtime_penalty = ALPHA_RUNTIME * (total_runtime_ms / 1000.0)
|
|
134
|
+
score = mean_ratio - runtime_penalty
|
|
135
|
+
|
|
136
|
+
metrics = {
|
|
137
|
+
"instances": len(rows),
|
|
138
|
+
"mean_lb_over_len": mean_ratio,
|
|
139
|
+
"runtime_ms": total_runtime_ms,
|
|
140
|
+
"runtime_penalty": runtime_penalty,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
print(f"instances={metrics['instances']} mean_lb_over_len={mean_ratio:.6f} runtime_ms={total_runtime_ms:.2f}")
|
|
144
|
+
print(json.dumps({"score": score, "metrics": metrics}, separators=(",", ":")))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Baseline Euclidean TSP solver for the LabGate Solution Explorer demo.
|
|
2
|
+
|
|
3
|
+
API contract:
|
|
4
|
+
- solve(points) -> tour (list of node indices)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from math import hypot
|
|
10
|
+
from typing import List, Sequence, Tuple
|
|
11
|
+
|
|
12
|
+
Point = Tuple[float, float]
|
|
13
|
+
|
|
14
|
+
# Dummy-agent patch flips this to True for deterministic improvement tests.
|
|
15
|
+
ENABLE_TWO_OPT = False
|
|
16
|
+
MAX_TWO_OPT_PASSES = 2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _dist(a: Point, b: Point) -> float:
|
|
20
|
+
return hypot(a[0] - b[0], a[1] - b[1])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_distance_matrix(points: Sequence[Point]) -> List[List[float]]:
|
|
24
|
+
n = len(points)
|
|
25
|
+
dist = [[0.0] * n for _ in range(n)]
|
|
26
|
+
for i in range(n):
|
|
27
|
+
for j in range(i + 1, n):
|
|
28
|
+
d = _dist(points[i], points[j])
|
|
29
|
+
dist[i][j] = d
|
|
30
|
+
dist[j][i] = d
|
|
31
|
+
return dist
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _nearest_neighbor_tour(dist: List[List[float]]) -> List[int]:
|
|
35
|
+
n = len(dist)
|
|
36
|
+
if n <= 1:
|
|
37
|
+
return list(range(n))
|
|
38
|
+
|
|
39
|
+
unvisited = set(range(1, n))
|
|
40
|
+
tour = [0]
|
|
41
|
+
|
|
42
|
+
while unvisited:
|
|
43
|
+
last = tour[-1]
|
|
44
|
+
nxt = min(unvisited, key=lambda idx: dist[last][idx])
|
|
45
|
+
unvisited.remove(nxt)
|
|
46
|
+
tour.append(nxt)
|
|
47
|
+
|
|
48
|
+
return tour
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _apply_two_opt(tour: List[int], dist: List[List[float]], max_passes: int) -> List[int]:
|
|
52
|
+
n = len(tour)
|
|
53
|
+
if n < 5:
|
|
54
|
+
return tour
|
|
55
|
+
|
|
56
|
+
route = tour[:]
|
|
57
|
+
for _ in range(max_passes):
|
|
58
|
+
improved = False
|
|
59
|
+
for i in range(n - 1):
|
|
60
|
+
a = route[i]
|
|
61
|
+
b = route[(i + 1) % n]
|
|
62
|
+
for k in range(i + 2, n if i > 0 else n - 1):
|
|
63
|
+
c = route[k]
|
|
64
|
+
d = route[(k + 1) % n]
|
|
65
|
+
current = dist[a][b] + dist[c][d]
|
|
66
|
+
swapped = dist[a][c] + dist[b][d]
|
|
67
|
+
if swapped + 1e-12 < current:
|
|
68
|
+
route[i + 1:k + 1] = reversed(route[i + 1:k + 1])
|
|
69
|
+
improved = True
|
|
70
|
+
if not improved:
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
return route
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def solve(points: Sequence[Sequence[float]]) -> List[int]:
|
|
77
|
+
n = len(points)
|
|
78
|
+
if n == 0:
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
typed_points: List[Point] = [(float(p[0]), float(p[1])) for p in points]
|
|
82
|
+
dist = _build_distance_matrix(typed_points)
|
|
83
|
+
tour = _nearest_neighbor_tour(dist)
|
|
84
|
+
|
|
85
|
+
if ENABLE_TWO_OPT:
|
|
86
|
+
tour = _apply_two_opt(tour, dist, max_passes=MAX_TWO_OPT_PASSES)
|
|
87
|
+
|
|
88
|
+
return tour
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
diff --git a/solver.py b/solver.py
|
|
2
|
+
--- a/solver.py
|
|
3
|
+
+++ b/solver.py
|
|
4
|
+
@@ -12,8 +12,8 @@ from typing import List, Sequence, Tuple
|
|
5
|
+
Point = Tuple[float, float]
|
|
6
|
+
|
|
7
|
+
# Dummy-agent patch flips this to True for deterministic improvement tests.
|
|
8
|
+
-ENABLE_TWO_OPT = False
|
|
9
|
+
-MAX_TWO_OPT_PASSES = 2
|
|
10
|
+
+ENABLE_TWO_OPT = True
|
|
11
|
+
+MAX_TWO_OPT_PASSES = 4
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _dist(a: Point, b: Point) -> float:
|