Drive Pareto Desk from your own code
A decision aid, not an approval. The model reads the Pareto analysis your browser (or your script) computed; it never recomputes dominance, crowding or hypervolume and never evaluates a design. A front says which designs are not beaten in the objectives you gave - not that any design is safe or right to build.
Everything the web page does is available over HTTP. Analyse your table of evaluated designs with
the page's own pareto.js (feasibility, fast non-dominated sorting, crowding distance,
hypervolume, pymoo pseudo-weights and ASF picks - checked against pymoo 0.6.2), send the facts, and
get back a verdict (recommend, close_call, rerun), a pick, and
either a shortlist with the measured moves between designs or a pymoo script that refines the
search and writes its results back in this page's format. The natural use is an optimisation loop:
run, analyse, decide, refine, run again.
Two lanes: the task field
| task | what you get | extra input |
|---|---|---|
tradeoff | A shortlist of 1-5 front designs (the pick, alternatives, extremes) with what each gives and gives up, up to 6 measured moves between designs, constraint notes and caveats. | none |
refine | An algorithm choice (NSGA2, NSGA3, RNSGA2, SMSEMOA or MOEAD), a search space with bounds taken from the front, run settings, a complete pymoo script with an evaluate_design(x) stub, assumptions and the checks to run on the new results. | decision: the text of an earlier tradeoff run (optional) |
Both lanes return the same envelope: lane, verdict, pick, headline, tldr, the lane body, next_steps and prescan_responses. Worked examples: tradeoff, refine.
Input fields
Every field is a string.
| field | required | meaning |
|---|---|---|
task | yes | tradeoff or refine. |
facts | yes | A JSON-encoded string with the browser's analysis - see below. Build it with ParetoKit.buildInput. |
title | no | A label for the problem, up to 160 characters. |
context | no | Your notes: what the objectives mean, units, what matters most, hard limits, how the designs were made. Up to 3,000 characters. |
decision | refine only | Plain text of an earlier trade-off (the page builds it with Recon.decisionText). Up to 6,000 characters. |
question | no | Answered in tldr as a bullet starting "Answer:". Up to 1,200 characters. |
retry_note | no | Only on a retry after a malformed reply. |
The facts string
settings (preference weights per objective, the hypervolume reference point, the
method); columns (name, role - min, max, le,
ge, g, var, label - and bound);
counts; objectives (best and worst on the front, the best design,
feasible range); constraints (limit, how many front designs sit on it);
variables (front and table ranges); hypervolume (normalised, plus a raw
value when you give a reference point); front - up to 40 non-dominated feasible designs,
ids S1.., with their line, objectives, pseudo-weights, crowding distance, constraint
slack and variables; picks (pseudo-weight pick, ASF pick, best per objective);
pick_deltas and neighbors (measured moves with change,
improves and worsens); infeasible_nearest (ids
I1..); correlations; flags (F1.. with severity,
category, message and the designs they concern); browser_verdict; and
clipped.
Building the body
The simplest way to get a body that matches the page byte for byte is to run the page's own module
in Node. pareto.js has no dependencies and exports itself with
module.exports. Tag the header of your CSV the way the page reads it:
cost[min], range[max], stress[<=250],
mass[>=10], g1[g], x1[var].
// make-body.js - build the exact body the page sends, with the page's own code.
// Save https://pareto-desk.skillsafe.ai/pareto.js next to this file, then:
// node make-body.js designs.csv tradeoff "Bracket sizing" "notes" "mass_kg:0.6, deflection_mm:0.4" > body.json
const fs = require("fs");
const K = require("./pareto.js");
const [csv, lane = "tradeoff", title = "", context = "", weights = "", decision = ""] = process.argv.slice(2);
const set = { lane, title, context, weights, decision, table: fs.readFileSync(csv, "utf8"), roles: "", ref_point: "", question: "" };
const A = K.analyze(set);
if (A.empty) throw new Error(A.errors.join("; ") || "need at least two rows and two tagged objectives");
const body = K.mustBeObject(K.buildInput(A, set));
console.error("browser verdict:", A.hint, "| front:", A.front.length, "of", A.feasible.length, "feasible | flags:", A.flags.map(f => f.id + " " + f.category).join(", "));
console.error("idempotency key: pareto-desk:" + lane + ":" + K.hashInput(body) + ":a1");
process.stdout.write(JSON.stringify(body));
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses
the same envelope, so one helper covers the whole API:
{"ok": true, "data": {"job_id": "job_...", "status": "queued"}}
{"ok": false, "error": {"code": "payment_required", "message": "..."}}
The token is minted for this app (the guest endpoint takes {"slug":"pareto-desk"} in
its body), so no slug header is needed afterwards. Send it as Authorization: Bearer ….
The input object IS the request body. There is no {"input": …}
wrapper. A wrapped body is answered with an unknown field 'input' warning, and the
model never sees your text.
Error codes
| status | code | what to do |
|---|---|---|
| 400 | validation_error | A field is missing or the wrong type. Every field is a string: facts must be a JSON-encoded string, not an object. |
| 401 | unauthorized | The token is missing, malformed or expired. Get a new one from the token page. |
| 402 | payment_required | The balance is below min_credits. Call /estimate first and top up. |
| 403 | forbidden | The token is valid but not for this app, or a guest token tried a metered run. A guest cannot run; sign in for a personal token. |
| 404 | not_found | Unknown job id, or the app slug does not exist. |
| 409 | conflict | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
| 429 | rate_limited | Too many requests. Back off and retry; do not tight-loop. |
| 5xx | internal | A server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice. |
1. A tiny client
One helper that sends the token, unwraps data and raises on ok: false.
The token comes from the token page (Copy token or
Copy shell export); step 2 covers the kinds of token and minting one from code.
# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="pareto-desk"
TOKEN="$SKILLSAFE_TOKEN" # from https://pareto-desk.skillsafe.ai/tokens.html
call() { # call <path> [json-body]
if [ -n "$2" ]; then
curl -sS -X POST "$BASE/$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
else
curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN"
fi
}
import json, os, urllib.error, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "pareto-desk"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://pareto-desk.skillsafe.ai/tokens.html
def call(path, body=None):
"""Returns the unwrapped `data`, or raises with the API error code."""
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(f"{BASE}/{path}", data=data, method="POST" if body is not None else "GET")
req.add_header("Authorization", f"Bearer {TOKEN}")
if body is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as e:
payload = json.load(e)
if not payload.get("ok"):
err = payload.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
return payload["data"]
import { readFileSync } from "node:fs";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "pareto-desk";
// Paste the token from https://pareto-desk.skillsafe.ai/tokens.html into a file named "token",
// or replace the fallback with it.
let TOKEN = "YOUR_TOKEN";
try { TOKEN = readFileSync("token", "utf8").trim(); } catch {}
async function call(path, body) {
const res = await fetch(`${BASE}/${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const payload = await res.json();
if (!payload.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
return payload.data;
}
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "pareto-desk"
)
var token = os.Getenv("SKILLSAFE_TOKEN") // from https://pareto-desk.skillsafe.ai/tokens.html
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if body != nil {
method = http.MethodPost
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+"/"+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class ParetoDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "pareto-desk";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN);
if (jsonBody != null) {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
b.GET();
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is always {"ok":true,"data":...} or {"ok":false,"error":...}.
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "pareto-desk"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://pareto-desk.skillsafe.ai/tokens.html
def call(path, body = nil)
uri = URI("#{BASE}/#{path}")
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "pareto-desk";
define("TOKEN", getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"); // from /tokens.html
function call(string $path, ?array $body = null) {
$ch = curl_init(BASE . "/" . $path);
$headers = ["Authorization: Bearer " . TOKEN];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
using System.Net.Http.Json;
using System.Text.Json;
static class ParetoDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "pareto-desk";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(string path, object? body = null)
{
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, $"{Base}/{path}");
req.Headers.Add("Authorization", $"Bearer {Token}");
if (body is not null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var payload = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!payload.GetProperty("ok").GetBoolean())
{
var e = payload.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return payload.GetProperty("data");
}
}
2. Get a token
The easiest route is the token page: it shows the token this browser
already holds, with Copy token and Copy shell export buttons, and
a sign-in button for a personal token. A guest token, minted with
POST /guest and {"slug":"pareto-desk"}, can call /me and
/estimate; the run is metered, so /run and /run-stream need
a personal token.
# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
# https://pareto-desk.skillsafe.ai/tokens.html
# export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead. A guest token is enough
# for /me and /estimate; a run needs a personal token from signing in.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" -d '{"slug":"pareto-desk"}'
# {"ok":true,"data":{"token":"…","subject_type":"guest"}}
# Open https://pareto-desk.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot start a metered run.
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest", data=b'{"slug": "pareto-desk"}', method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
// Open https://pareto-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "pareto-desk" }),
});
const TOKEN = (await res.json()).data.token;
// Open https://pareto-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
guestReq, _ := http.NewRequest(http.MethodPost,
"https://api.skillsafe.ai/v1/app-api/guest", bytes.NewReader([]byte(`{"slug":"pareto-desk"}`)))
guestReq.Header.Set("Content-Type", "application/json")
guestRes, err := http.DefaultClient.Do(guestReq)
if err != nil {
panic(err)
}
defer guestRes.Body.Close()
var guest struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
_ = json.NewDecoder(guestRes.Body).Decode(&guest)
fmt.Println(guest.Data.Token)
// Open https://pareto-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
var http = HttpClient.newHttpClient();
var guestReq = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"pareto-desk\"}"))
.build();
HttpResponse<String> guest = http.send(guestReq, HttpResponse.BodyHandlers.ofString());
System.out.println(guest.body()); // {"ok":true,"data":{"token":"…","subject_type":"guest"}}
# Open https://pareto-desk.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot start a metered run.
require "json"
require "net/http"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.generate({ slug: "pareto-desk" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
TOKEN = JSON.parse(res.body)["data"]["token"]
<?php
// Open https://pareto-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["slug" => "pareto-desk"]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$guest = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $guest["data"]["token"];
// Open https://pareto-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
using var http = new HttpClient();
var guestReq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/guest");
guestReq.Content = new StringContent("{\"slug\":\"pareto-desk\"}", Encoding.UTF8, "application/json");
var guestRes = await http.SendAsync(guestReq);
var guest = await guestRes.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(guest.GetProperty("data").GetProperty("token").GetString());
3. Check the session and the balance
call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
print(me["subject_type"], me.get("credits"))
const me = await call("me");
console.log(me.subject_type, me.credits);
raw, err := call("me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("me", null));
// {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var me = await ParetoDesk.Call("me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
4. Price the run (free)
/estimate returns the model binding and the credits a run would reserve. It creates no
job and charges nothing. Expect model_alias gpt-terra and
markup_bps 1000 (a 10% markup). hold_credits is a
reservation, not the price: it is held against your balance while the run executes
and released afterwards. min_credits is the least balance that can start a run. What
you actually pay is charged_credits, reported on the finished job and in the
done event, and it is usually far lower than the hold. The body is the input object
itself, with no {"input": …} wrapper. /estimate does not validate the
body, so check the shape yourself: an object whose every value is a string, task equal
to tradeoff or refine,
facts non-empty, and facts a JSON string that parses to an object (this is
what the page's own guard, ParetoKit.mustBeObject, refuses to spend without).
# body.json is the input object itself - no {"input": ...} wrapper. Build it with
# make-body.js above, or by hand. estimate does not validate it, so check the shape first:
python3 -c 'import json;b=json.load(open("body.json"));assert isinstance(b,dict) and b.get("task") in ("tradeoff","refine") and all(isinstance(v,str) for v in b.values()) and all(b.get(k,"").strip() for k in ("facts",)) and isinstance(json.loads(b["facts"]),dict)'
INPUT=$(cat body.json)
call estimate "$INPUT"
# {"ok":true,"data":{"model":"...","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":...,"min_credits":...,"sponsor_enabled":false,
# "warnings":[]}}
#
# estimate creates no job and charges nothing. hold_credits is RESERVED, not the
# price; charged_credits after the run is the actual cost, usually far lower.
INPUT = json.load(open("body.json")) # built by make-body.js above, or by hand
assert isinstance(INPUT, dict) and INPUT.get("task") in ("tradeoff", "refine")
assert all(isinstance(v, str) for v in INPUT.values())
assert all(INPUT.get(k, "").strip() for k in ("facts",))
assert isinstance(json.loads(INPUT["facts"]), dict) # facts is a JSON STRING
est = call("estimate", INPUT)
print(est["model_alias"], est["markup_bps"], est["hold_credits"], est.get("warnings"))
me = call("me")
if me.get("credits", 0) < est["min_credits"]:
raise SystemExit("top up first: balance is below min_credits")
const INPUT = JSON.parse(readFileSync("body.json", "utf8")); // built by make-body.js above
if (!INPUT || typeof INPUT !== "object" || !["tradeoff", "refine"].includes(INPUT.task)) throw new Error("task must be tradeoff or refine");
for (const [k, v] of Object.entries(INPUT)) if (typeof v !== "string") throw new Error(k + " must be a string");
for (const k of ["facts"]) if (!(INPUT[k] || "").trim()) throw new Error(k + " is required");
JSON.parse(INPUT.facts); // throws unless facts is a JSON string
const est = await call("estimate", INPUT);
console.log(est.model_alias, est.markup_bps, est.hold_credits, est.warnings);
const me = await call("me");
if ((me.credits ?? 0) < est.min_credits) throw new Error("top up first");
raw, _ := os.ReadFile("body.json") // built by make-body.js above
var input map[string]string // every field is a string, facts included
if err := json.Unmarshal(raw, &input); err != nil {
panic("body.json must be an object of strings: " + err.Error())
}
if input["task"] != "tradeoff" && input["task"] != "refine" {
panic("task must be tradeoff or refine")
}
for _, k := range []string{"facts"} {
if strings.TrimSpace(input[k]) == "" {
panic(k + " is required")
}
}
var facts map[string]any
if err := json.Unmarshal([]byte(input["facts"]), &facts); err != nil {
panic("facts must be a JSON string holding an object")
}
est, err := call("estimate", input)
if err != nil {
panic(err)
}
fmt.Println(string(est)) // model_alias gpt-terra, markup_bps 1000, hold_credits, min_credits
String input = Files.readString(Path.of("body.json")); // built by make-body.js above
if (!input.matches("(?s)\\s*\\{.*\"task\"\\s*:\\s*\"(tradeoff|refine)\".*\\}\\s*"))
throw new IllegalStateException("body.json must be an object with task tradeoff or refine");
String lane = input.replaceAll("(?s).*\"task\"\\s*:\\s*\"(tradeoff|refine)\".*", "$1");
for (String k : new String[] {"facts"})
if (!input.contains("\"" + k + "\"")) throw new IllegalStateException(k + " is required");
String est = call("estimate", input);
System.out.println(est); // model_alias gpt-terra, markup_bps 1000, hold_credits, min_credits
INPUT = JSON.parse(File.read("body.json")) # built by make-body.js above
raise "task must be tradeoff or refine" unless %w[tradeoff refine].include?(INPUT["task"])
INPUT.each { |k, v| raise "#{k} must be a string" unless v.is_a?(String) }
%w[facts].each { |k| raise "#{k} is required" if INPUT[k].to_s.strip.empty? }
raise "facts must hold an object" unless JSON.parse(INPUT["facts"]).is_a?(Hash)
est = call("estimate", INPUT)
puts est["model_alias"], est["markup_bps"], est["hold_credits"]
<?php
$input = json_decode(file_get_contents("body.json"), true); // built by make-body.js above
if (!is_array($input) || !in_array($input["task"] ?? "", ["tradeoff", "refine"], true)) { throw new Exception("task must be tradeoff or refine"); }
foreach ($input as $k => $v) { if (!is_string($v)) { throw new Exception("$k must be a string"); } }
foreach (["facts"] as $k) { if (trim($input[$k] ?? "") === "") { throw new Exception("$k is required"); } }
if (!is_array(json_decode($input["facts"], true))) { throw new Exception("facts must be a JSON string"); }
$est = call("estimate", $input);
echo $est["model_alias"], " ", $est["markup_bps"], " ", $est["hold_credits"], PHP_EOL;
var input = File.ReadAllText("body.json"); // built by make-body.js above
using var doc = JsonDocument.Parse(input);
var root = doc.RootElement;
var lane = root.GetProperty("task").GetString();
if (lane != "tradeoff" && lane != "refine") throw new Exception("task must be tradeoff or refine");
foreach (var p in root.EnumerateObject())
if (p.Value.ValueKind != JsonValueKind.String) throw new Exception($"{p.Name} must be a string");
JsonDocument.Parse(root.GetProperty("facts").GetString()!); // facts is a JSON string
var est = await ParetoDesk.Call("estimate", root);
Console.WriteLine(est); // model_alias gpt-terra, markup_bps 1000, hold_credits, min_credits
5. Run it, then poll
POST /run returns a job_id; poll GET /jobs/{id} until it is
terminal. The reply is a string at data.output.output: JSON.parse
it (step 7). Send an Idempotency-Key built from the lane, a hash of the input and the
attempt number, pareto-desk:<lane>:<hash>:a<attempt> (for example
pareto-desk:tradeoff:3osqch17liu9c:a1), so a retried request returns the same job instead
of billing a second run. Use one key per distinct input: a changed table, roles, weights or notes (so changed
facts) or a changed decision are a new hash, the same designs in the other lane are a new key, and replaying
an old key with a different body is a 409. The page uses
ParetoKit.hashInput(body) for the hash (it covers task, title,
context, facts, decision and question;
make-body.js prints the key); any stable digest of the body works from other
languages. Leave retry_note out of the hash and bump the attempt instead.
# Always send an Idempotency-Key derived from the input. A retried request with
# the same key returns the SAME job instead of billing a second run.
LANE=$(printf '%s' "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["task"])') # tradeoff or refine
KEY="pareto-desk:$LANE:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
while :; do
OUT=$(call "jobs/$JOB")
STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
sleep 2
done
# {"ok":true,"data":{"job_id":"job_...","status":"succeeded",
# "output":{"output":"{\"lane\":\"tradeoff\",\"verdict\":\"close_call\",\"headline\":\"...\", ...}"},
# "charged_credits":...,"truncated":false}}
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])' > reply.json
import hashlib, time
digest = hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:16]
key = f"pareto-desk:{INPUT['task']}:{digest}:a1"
req = urllib.request.Request(f"{BASE}/run", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call(f"jobs/{job_id}")
if job["status"] in ("succeeded", "failed"):
break
time.sleep(2)
if job["status"] == "failed":
raise RuntimeError(job.get("error"))
text = job["output"]["output"] # the reply, as a string
print("charged", job.get("charged_credits"), "truncated", job.get("truncated"))
import { createHash } from "node:crypto";
const digest = createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16);
const key = `pareto-desk:${INPUT.task}:${digest}:a1`;
const started = await fetch(`${BASE}/run`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify(INPUT),
}).then((r) => r.json());
if (!started.ok) throw new Error(`${started.error.code}: ${started.error.message}`);
let job = started.data;
while (job.status !== "succeeded" && job.status !== "failed") {
await new Promise((r) => setTimeout(r, 2000));
job = await call(`jobs/${job.job_id}`);
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const text = job.output.output; // the reply, as a string
console.log(job.charged_credits, job.truncated);
body, _ := json.Marshal(input)
sum := sha256.Sum256(body)
key := fmt.Sprintf("pareto-desk:%s:%x:a1", input["task"], sum[:8])
req, _ := http.NewRequest(http.MethodPost, base+"/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
var started struct {
Data struct {
JobID string `json:"job_id"`
} `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&started)
res.Body.Close()
var jobOutput string
for {
raw, err := call("jobs/"+started.Data.JobID, nil)
if err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
Charged int `json:"charged_credits"`
Truncated bool `json:"truncated"`
}
_ = json.Unmarshal(raw, &job)
if job.Status == "succeeded" {
jobOutput = job.Output.Output
fmt.Println(job.Charged, job.Truncated)
break
}
if job.Status == "failed" {
panic(string(raw))
}
time.Sleep(2 * time.Second)
}
String key = "pareto-desk:" + lane + ":" + sha256Hex(input).substring(0, 16) + ":a1";
HttpRequest run = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(input)).build();
String started = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
String jobId = started.replaceAll(".*\"job_id\":\"([^\"]+)\".*", "$1");
while (true) {
String job = call("jobs/" + jobId, null);
if (job.contains("\"status\":\"succeeded\"")) { System.out.println(job); break; }
if (job.contains("\"status\":\"failed\"")) throw new RuntimeException(job);
Thread.sleep(2000);
}
// Parse data.output.output (a string holding the reply JSON) with your JSON library.
// sha256Hex: HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(input.getBytes(UTF_8)))
require "digest"
key = "pareto-desk:#{INPUT['task']}:#{Digest::SHA256.hexdigest(JSON.generate(INPUT))[0, 16]}:a1"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = JSON.generate(INPUT)
job = JSON.parse(Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }.body)["data"]
until %w[succeeded failed].include?(job["status"])
sleep 2
job = call("jobs/#{job['job_id']}")
end
raise job.inspect if job["status"] == "failed"
text = job["output"]["output"] # the reply, as a string
puts job["charged_credits"], job["truncated"]
<?php
$key = "pareto-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json", "Idempotency-Key: " . $key],
CURLOPT_RETURNTRANSFER => true,
]);
$job = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
while (!in_array($job["status"], ["succeeded", "failed"], true)) {
sleep(2);
$job = call("jobs/" . $job["job_id"]);
}
if ($job["status"] === "failed") { throw new RuntimeException(json_encode($job)); }
$text = $job["output"]["output"]; // the reply, as a string
echo $job["charged_credits"], PHP_EOL;
using System.Security.Cryptography;
var json = input; // the body.json text from step 4
var key = $"pareto-desk:{lane}:" + Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(json)))[..16].ToLower() + ":a1";
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run");
req.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN"}");
req.Headers.Add("Idempotency-Key", key);
req.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var started = await (await new HttpClient().SendAsync(req)).Content.ReadFromJsonAsync<JsonElement>();
var jobId = started.GetProperty("data").GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await ParetoDesk.Call($"jobs/{jobId}");
var status = job.GetProperty("status").GetString();
if (status == "succeeded") break;
if (status == "failed") throw new Exception(job.ToString());
await Task.Delay(2000);
}
var output = job.GetProperty("output").GetProperty("output").GetString()!; // the reply, as a string
6. Or stream it
POST /run-stream takes the same body and headers and answers with server-sent events:
job (the job id), delta (chunks of the reply) and done (the
status, charged_credits, truncated and, when present, the full
output). A browser page may receive only tick heartbeats and then
done, never a delta, so take the reply from done.output.output
when it is there, fall back to the concatenated deltas, and fall back again to
GET /jobs/{id}.
# Server-sent events. `delta` events carry chunks of the reply; `done` carries the
# status, charged_credits and the truncated flag. Ignore `tick` heartbeats.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d "$INPUT"
# event: job {"job_id":"job_..."}
# event: delta {"text":"{\"lane\":\"tradeoff\",\"verdict\":\"close_call\",\"pick\":{\"ref\":\"S4"}
# event: done {"status":"succeeded","charged_credits":...,"truncated":false}
req = urllib.request.Request(f"{BASE}/run-stream", data=json.dumps(INPUT).encode(), method="POST")
for h, v in (("Authorization", f"Bearer {TOKEN}"), ("Content-Type", "application/json"),
("Idempotency-Key", key), ("Accept", "text/event-stream")):
req.add_header(h, v)
raw, done, event = "", {}, None
with urllib.request.urlopen(req) as stream:
for line in stream:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
raw += json.loads(line[6:]).get("text", "")
elif line.startswith("data: ") and event == "done":
done = json.loads(line[6:])
text = (done.get("output") or {}).get("output") or raw
print(done.get("status"), done.get("charged_credits"), done.get("truncated"))
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key, Accept: "text/event-stream" },
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null, done = null;
for (;;) {
const { value, done: end } = await reader.read();
if (end) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i); buf = buf.slice(i + 1);
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ") && event === "delta") raw += JSON.parse(line.slice(6)).text || "";
else if (line.startsWith("data: ") && event === "done") done = JSON.parse(line.slice(6));
}
}
const streamed = done?.output?.output || raw; // browsers may get only ticks + done
console.log(done, streamed.length);
req, _ = http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, err = http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var raw strings.Builder
event := ""
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = line[7:]
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct{ Text string `json:"text"` }
_ = json.Unmarshal([]byte(line[6:]), &d)
raw.WriteString(d.Text)
case strings.HasPrefix(line, "data: ") && event == "done":
fmt.Println("done:", line[6:])
}
}
HttpRequest stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(input)).build();
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
// "event: delta" lines are followed by "data: {\"text\":...}"; "event: done" by the status.
if (line.startsWith("data: ")) System.out.println(line.substring(6));
});
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
{ "Authorization" => "Bearer #{TOKEN}", "Content-Type" => "application/json",
"Idempotency-Key" => key, "Accept" => "text/event-stream" }.each { |k, v| req[k] = v }
req.body = JSON.generate(INPUT)
raw, event = +"", nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |h|
h.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event: ") then event = line[7..]
elsif line.start_with?("data: ") && event == "delta" then raw << JSON.parse(line[6..])["text"].to_s
elsif line.start_with?("data: ") && event == "done" then puts line[6..]
end
end
end
end
end
<?php
$raw = ""; $event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json", "Idempotency-Key: " . $key, "Accept: text/event-stream"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) $event = substr($line, 7);
elseif (str_starts_with($line, "data: ") && $event === "delta") $raw .= json_decode(substr($line, 6), true)["text"] ?? "";
elseif (str_starts_with($line, "data: ") && $event === "done") echo substr($line, 6), PHP_EOL;
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
var sreq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream");
sreq.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN"}");
sreq.Headers.Add("Idempotency-Key", key);
sreq.Headers.Add("Accept", "text/event-stream");
sreq.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
using var sres = await new HttpClient().SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var sr = new StreamReader(await sres.Content.ReadAsStreamAsync());
var raw = new System.Text.StringBuilder(); string? ev = null, line;
while ((line = await sr.ReadLineAsync()) != null)
{
if (line.StartsWith("event: ")) ev = line[7..];
else if (line.StartsWith("data: ") && ev == "delta") raw.Append(JsonSerializer.Deserialize<JsonElement>(line[6..]).GetProperty("text").GetString());
else if (line.StartsWith("data: ") && ev == "done") Console.WriteLine(line[6..]);
}
7. Parse the reply
The reply is a JSON object serialised as a string. Parse it, then check the lane.
# The reply is a JSON string inside data.output.output. Pull it out and parse it:
printf '%s' "$JOB" | python3 -c 'import sys,json;r=json.loads(json.load(sys.stdin)["output"]["output"]);print(r["verdict"],r["pick"]["ref"],r["headline"])'
reply = json.loads(job["output"]["output"])
assert reply["lane"] == INPUT["task"], "the model answered as another lane"
print(reply["verdict"], reply["pick"]["ref"], reply["headline"])
for d in reply.get("shortlist", []): # tradeoff lane
print(d["ref"], d["role"], d["why"])
open("refine.py", "w").write(reply.get("script", "")) # refine lane
const reply = JSON.parse(job.output.output);
if (reply.lane !== INPUT.task) throw new Error("the model answered as another lane");
console.log(reply.verdict, reply.pick.ref, reply.headline);
for (const d of reply.shortlist || []) console.log(d.ref, d.role, d.why); // tradeoff lane
if (reply.script) require("fs").writeFileSync("refine.py", reply.script); // refine lane
var reply struct {
Lane, Verdict, Headline, Script string
Pick struct{ Ref string }
Shortlist []struct{ Ref, Role, Why string }
}
if err := json.Unmarshal([]byte(job.Output.Output), &reply); err != nil { panic(err) }
fmt.Println(reply.Verdict, reply.Pick.Ref, reply.Headline)
// With any JSON library (Jackson shown): the reply is a string that holds a JSON object.
JsonNode reply = new ObjectMapper().readTree(outputString);
System.out.println(reply.get("verdict").asText() + " " + reply.get("pick").get("ref").asText()
+ " " + reply.get("headline").asText());
reply = JSON.parse(job["output"]["output"])
raise "the model answered as another lane" unless reply["lane"] == INPUT["task"]
puts [reply["verdict"], reply["pick"]["ref"], reply["headline"]].join(" ")
$reply = json_decode($job["output"]["output"], true);
if ($reply["lane"] !== $input["task"]) { throw new Exception("the model answered as another lane"); }
echo $reply["verdict"], " ", $reply["pick"]["ref"], " ", $reply["headline"], "\n";
var reply = JsonSerializer.Deserialize<JsonElement>(outputString);
Console.WriteLine($"{reply.GetProperty("verdict")} {reply.GetProperty("pick").GetProperty("ref")} {reply.GetProperty("headline")}");
Invariants worth asserting
pick.refis one of the front ids you sent (S1..); empty only forrerunwith no feasible design.- The verdict is never looser than
facts.browser_verdict(rerun < close_call < recommend) unless the medium or high flags that set it were dismissed. - Every flag id appears once in
prescan_responses; in the tradeoff lane the shortlist marks exactly onepick, the same design aspick.ref. - Every number in the prose exists in
factsor your notes (after rounding to 3 significant figures). - In the refine lane the script declares
n_obj,n_ieq_constrandn_varas your table has them, imports only the allowed pymoo modules, and writespareto_refined.csvwith this page's column tags; the search-space bounds are front or table min/max values fromfacts.variables. - The page's
recon.jschecks all of this; you can run it in Node the same way aspareto.js.
The output contract
{
"lane": "tradeoff" | "refine",
"verdict": "recommend" | "close_call" | "rerun",
"pick": {"ref": "S4"},
"headline": "...",
"tldr": ["..."],
// tradeoff:
"shortlist": [{"ref", "role": "pick|alternative|extreme", "why", "gives_up"}],
"tradeoffs": [{"from", "to", "statement"}],
"constraints_note": ["..."],
"caveats": ["..."],
// refine:
"algorithm": {"name": "NSGA2|NSGA3|RNSGA2|SMSEMOA|MOEAD", "why"},
"search_space": [{"variable", "lower", "upper", "basis"}],
"settings": [{"name", "value", "why"}],
"script": "import numpy as np ...",
"assumptions": ["..."],
"checks": ["..."],
// both:
"next_steps": ["..."],
"prescan_responses": [{"ref": "F1", "verdict": "confirmed|dismissed", "note": "..."}]
}
Worked example: tradeoff
The cantilever-beam example from the page: 48 illustrative designs, mass against tip deflection with a 250 MPa stress limit. The body, with facts shortened here:
{
"task": "tradeoff",
"title": "Cantilever bracket, 1 m, 5 kN tip load",
"context": "Steel bracket for a test rig. Rectangular section, b and h in mm. Lighter is better for handling, but tip deflection above about 15 mm makes the fixture wobble. Stress limit is the allowable bending stress we use for this steel.",
"facts": "{\"settings\":{\"weights\":{\"mass_kg\":0.5,\"deflection_mm\":0.5},\"weights_source\":\"equal\",\"reference_point\":{\"mass_kg\":50.6338,\"deflection_mm\":8.6351},\"reference_point_source\":\"default: nadir + 10% of the front's range in every objective\",\"method\":\"pymoo semantics: maximised objectives negated; constraints as g <= 0, CV = sum of positive parts; fast non-dominated sorting of feasible rows; crowding distance (cd); normalisation by the first front's ideal and nadir; exact hypervolume of the normalised front; PseudoWeights; ASF on normalised values with 1/weights\"},\"columns\":[{\"name\":\"design\",\"role\":\"la... (7099 characters in full)"
}
The reply (a saved run, the same one the page's example replays):
{
"lane": "tradeoff",
"verdict": "close_call",
"pick": {
"ref": "S4"
},
"headline": "Under equal weight on mass and deflection, the pick S4 and the alternative S3 are both defensible cantilever bracket designs, so the choice comes down to whether mass or stiffness should tip the balance.",
"tldr": [
"The pseudo-weight pick S4 (mass_kg 23.796, deflection_mm 3.428) and the ASF pick S3 (mass_kg 20.637, deflection_mm 3.551) disagree under the same equal weights (flag F2), which is why this is a close call.",
"All 10 front designs are feasible and the stress limit (<= 250 MPa) is inactive on the front (active_on_front 0); the nearest infeasible design I1 only exceeds it by cv 16.2.",
"The front runs from mass_kg 10.035 (S1) to 46.943 (S10) and deflection_mm 7.965 (S1) to 1.264 (S10) - every one of these stays well under the context's 15 mm wobble concern.",
"A gap between S2 and S3 (flag F1) means the region right around the alternative pick S3 was not densely sampled."
],
"shortlist": [
{
"ref": "S4",
"role": "pick",
"why": "Closest match to the equal weights (weight 0.5 mass_kg, weight 0.5 deflection_mm): pseudo_weights mass_kg 0.480866, deflection_mm 0.519134, at mass_kg 23.796 and deflection_mm 3.428.",
"gives_up": "Concedes 13.761 kg versus the lightest design S1 (mass_kg 10.035) to keep deflection far below S1's 7.965 mm, and concedes 2.164 mm versus the stiffest design S10 (deflection_mm 1.264) while carrying 23.147 kg less mass."
},
{
"ref": "S3",
"role": "alternative",
"why": "The ASF pick under the same weights, pseudo_weights mass_kg 0.519701, deflection_mm 0.480299, at mass_kg 20.637 and deflection_mm 3.551, 3.159 kg lighter than the pick.",
"gives_up": "Gives up 0.123 mm of deflection versus the pick S4 (3.551 mm vs 3.428 mm) in exchange for that lighter mass."
},
{
"ref": "S1",
"role": "extreme",
"why": "Lightest feasible design on the front, mass_kg 10.035, pseudo_weights mass_kg 1, deflection_mm 0, shown for contrast.",
"gives_up": "Concedes 7.965 mm of deflection, the worst on the front, versus the pick's 3.428 mm."
},
{
"ref": "S10",
"role": "extreme",
"why": "Stiffest feasible design on the front, deflection_mm 1.264, pseudo_weights mass_kg 0, deflection_mm 1, shown for contrast.",
"gives_up": "Concedes 23.147 kg of mass versus the pick S4 (46.943 kg vs 23.796 kg) for 2.164 mm less deflection."
}
],
"tradeoffs": [
{
"from": "S4",
"to": "S3",
"statement": "Moving from the pick S4 to the ASF alternative S3 saves 3.159 kg (mass_kg 23.796 to 20.637) but deflection worsens by 0.123 mm (3.428 to 3.551)."
},
{
"from": "S4",
"to": "S1",
"statement": "Moving from S4 to the lightest design S1 saves 13.761 kg (down to mass_kg 10.035) but deflection worsens by 4.537 mm (up to 7.965 mm)."
},
{
"from": "S4",
"to": "S10",
"statement": "Moving from S4 to the stiffest design S10 cuts deflection by 2.164 mm (down to 1.264 mm) but adds 23.147 kg of mass (up to 46.943 kg)."
},
{
"from": "S2",
"to": "S3",
"statement": "Across the gap the browser flagged (F1), moving from S2 to S3 adds 3.953 kg of mass (16.684 to 20.637) but cuts deflection by 3.328 mm (6.879 to 3.551), with no intermediate design sampled in between."
}
],
"constraints_note": [
"The stress limit (<= 250 MPa) is not active on the front - active_on_front is 0, and front designs keep slack from 13.2 (S1) up to 206.4 (S10).",
"The nearest infeasible designs violate the stress limit by cv 16.2 (I1) up to 58.5 (I5), so the boundary sits close in the design space even though no front design touches it.",
"The context's 15 mm deflection-wobble concern is not one of the facts constraints, but every front design, S1 through S10, falls between deflection_mm 1.264 and 7.965, well clear of it."
],
"caveats": [
"The gap between S2 and S3 (flag F1) means no design was evaluated in that region, so a preferred point there cannot be confirmed from this sample.",
"The table gives single evaluations only, so noise or manufacturing variation in mass_kg, deflection_mm or stress_MPa is not captured.",
"Only b_mm and h_mm were varied here; other cross-section shapes or steel grades are outside this sample."
],
"next_steps": [
"Evaluate one or two designs between S2 (b_mm 25.7, h_mm 82.7) and S3 (b_mm 25.4, h_mm 103.5) to close the gap the browser flagged (F1).",
"Decide whether mass or deflection should tip the balance: S1 favors mass (mass_kg 10.035) and S10 favors deflection (deflection_mm 1.264), both well under the context's 15 mm wobble concern.",
"If S4 and S3 remain close, run the refine lane centred on the pick to sample more densely around b_mm 25-31 and h_mm 98-104.",
"Check the stress slack on any new candidate design against the 250 MPa limit before adding it to the table."
],
"prescan_responses": [
{
"ref": "F1",
"verdict": "confirmed",
"note": "The gap sits between S2 and S3, right where the ASF alternative S3 lies, so a finer sample there could change which design looks best under these weights."
},
{
"ref": "F2",
"verdict": "confirmed",
"note": "S4 (pseudo-weight pick) and S3 (ASF pick) do differ under the equal weights, which is exactly why this is a close_call: both appear in the shortlist as pick and alternative."
}
]
}
Worked example: refine
The same beam problem handed to the refine lane with the decision above. The body, shortened:
{
"task": "refine",
"title": "Cantilever bracket, 1 m, 5 kN tip load",
"context": "Steel bracket for a test rig. Rectangular section, b and h in mm. Lighter is better for handling, but tip deflection above about 15 mm makes the fixture wobble. Stress limit is the allowable bending stress we use for this steel.",
"facts": "{\"settings\":{\"weights\":{\"mass_kg\":0.5,\"deflection_mm\":0.5},\"weights_source\":\"equal\",\"reference_point\":{\"mass_kg\":50.6338,\"deflection_mm\":8.6351},\"reference_point_source\":\"default: nadir + 10% of the front's range in every objective\",\"method\":\"pymoo semantics: maximised objectives negated; constraints as g <= 0, CV = sum of positive parts; fast non-dominated sorting of feasible rows; crowding distance (cd); normalisation by the first front's ideal and nadir; exact hypervolume of the normalised front; PseudoWeights; ASF on normalised values with 1/weights\"},\"columns\":[{\"name\":\"design\",\"role\":\"la... (7099 characters in full)",
"decision": "Verdict: close call. Pick: S4 (mass_kg 23.796, deflection_mm 3.428).\nUnder equal weight on mass and deflection, the pick S4 and the alternative S3 are both defensible cantilever bracket designs, so the choice comes down to whether mass or stiffness should tip the balance.\n- S4 (pick) (mass_kg 23.796, deflection_mm 3.428): Closest match to the equal weights (weight 0.5 mass_kg, weight 0.5 deflectio... (2896 characters in full)"
}
The reply (a saved run, the same one the page's example replays):
{
"lane": "refine",
"verdict": "close_call",
"pick": {
"ref": "S4"
},
"headline": "The refined search centers a new RNSGA2 run on S4 (mass_kg 23.796, deflection_mm 3.428), the close-call pick, resampling the current front's range to weigh S4 against S3 and test the S2-S3 gap the browser flagged.",
"tldr": [
"Pick stays S4 (mass_kg 23.796, deflection_mm 3.428), the preference pick under the equal weights (weight 0.5 mass_kg, weight 0.5 deflection_mm).",
"Verdict stays close_call, matching facts.browser_verdict; the ASF pick S3 (mass_kg 20.637, deflection_mm 3.551) remains a defensible alternative (F2).",
"RNSGA2 is used with a reference point at S4's own objective values to concentrate new designs around the pick rather than resample the whole front.",
"Search bounds reuse the current front's ranges (b_mm 12.9-52, h_mm 82.7-119.4), which already bracket the S2-S3 gap the browser flagged (F1)."
],
"algorithm": {
"name": "RNSGA2",
"why": "The decision is close_call and the aim is to fill in the region around the pick S4 (mass_kg 23.796, deflection_mm 3.428) rather than resample the whole front, so RNSGA2 with a reference point at S4's objective values fits better than a whole-front method like NSGA2."
},
"search_space": [
{
"variable": "b_mm",
"lower": 12.9,
"upper": 52,
"basis": "front_min/front_max: the current front's own range already contains the pick S4 (b_mm 30.9) and the S2-S3 gap (F1) between b_mm 25.7 and b_mm 25.4, so refining locally within it is enough."
},
{
"variable": "h_mm",
"lower": 82.7,
"upper": 119.4,
"basis": "front_min/front_max: keeps the search within the range that already produced the feasible front (S2 h_mm 82.7 to S9 h_mm 119.4), including the S2-S3 gap, instead of the whole table's wider all_min/all_max range."
}
],
"settings": [
{
"name": "pop_size",
"value": 40,
"why": "Large enough for RNSGA2 to spread points around the S4 reference point across 2 objectives without multiplying evaluate_design calls far beyond the original 48-row sweep."
},
{
"name": "n_gen",
"value": 60,
"why": "Enough generations for RNSGA2 to converge locally with only 2 variables and 2 objectives; raise it only if evaluate_design turns out to be cheap."
},
{
"name": "seed",
"value": 1,
"why": "Fixes the run so results are reproducible and so a repeat run used to compare hypervolume.raw_value uses the same conditions."
},
{
"name": "epsilon",
"value": 0.01,
"why": "Keeps RNSGA2's niching tight around the reference point (mass_kg 23.796, deflection_mm 3.428) instead of spreading across the whole front."
}
],
"script": "import numpy as np\nimport csv\nfrom pymoo.core.problem import ElementwiseProblem\nfrom pymoo.algorithms.moo.rnsga2 import RNSGA2\nfrom pymoo.operators.crossover.sbx import SBX\nfrom pymoo.operators.mutation.pm import PM\nfrom pymoo.termination import get_termination\nfrom pymoo.optimize import minimize\n\n\ndef evaluate_design(x):\n # AUTHOR_INPUT_NEEDED: replace with the real bracket model (beam\n # formula or FEA) that returns mass_kg, deflection_mm and stress_MPa\n # for a given (b_mm, h_mm) design.\n raise NotImplementedError(\n 'AUTHOR_INPUT_NEEDED: implement evaluate_design(x) to return '\n 'a dict with keys mass_kg, deflection_mm, stress_MPa for '\n 'x = [b_mm, h_mm]'\n )\n\n\nclass BracketProblem(ElementwiseProblem):\n def __init__(self):\n super().__init__(\n n_var=2,\n n_obj=2,\n n_ieq_constr=1,\n xl=np.array([12.9, 82.7]),\n xu=np.array([52.0, 119.4]),\n )\n\n def _evaluate(self, x, out, *args, **kwargs):\n result = evaluate_design(x)\n\n mass_kg = result['mass_kg']\n deflection_mm = result['deflection_mm']\n stress_MPa = result['stress_MPa']\n\n # both objectives are already minimised, no negation needed\n out['F'] = [mass_kg, deflection_mm]\n\n # stress_MPa <= 250 -> g = stress_MPa - 250 <= 0\n out['G'] = [stress_MPa - 250.0]\n\n\nproblem = BracketProblem()\n\nalgorithm = RNSGA2(\n ref_points=np.array([[23.796, 3.428]]),\n pop_size=40,\n epsilon=0.01,\n normalization='front',\n extreme_points_as_reference_points=False,\n crossover=SBX(prob=0.9, eta=15),\n mutation=PM(eta=20),\n)\n\ntermination = get_termination('n_gen', 60)\n\nres = minimize(problem, algorithm, termination, seed=1, verbose=True)\n\nwith open('pareto_refined.csv', 'w', newline='') as f:\n writer = csv.writer(f)\n writer.writerow([\n 'b_mm[var]',\n 'h_mm[var]',\n 'mass_kg[min]',\n 'deflection_mm[min]',\n 'stress_MPa[<=250]',\n ])\n for xi, fi, gi in zip(res.X, res.F, res.G):\n b_mm, h_mm = xi\n mass_kg, deflection_mm = fi\n stress_MPa = gi[0] + 250.0\n writer.writerow([b_mm, h_mm, mass_kg, deflection_mm, stress_MPa])\n",
"assumptions": [
"b_mm and h_mm are treated as continuous variables in millimetres, matching facts.columns; AUTHOR_INPUT_NEEDED if only discrete stock sizes are actually available.",
"evaluate_design(x) is assumed cheap enough to call many times over the run (pop_size 40 across n_gen 60 generations); AUTHOR_INPUT_NEEDED to lower these settings if each call is an expensive FEA solve.",
"The 250 MPa stress limit and the model behind mass_kg, deflection_mm and stress_MPa are assumed unchanged from the original sweep; AUTHOR_INPUT_NEEDED if a different steel grade or rig geometry applies now.",
"The RNSGA2 reference point is set to S4's own objective values (mass_kg 23.796, deflection_mm 3.428); AUTHOR_INPUT_NEEDED if the user wants it anchored elsewhere instead."
],
"checks": [
"Whether a design at S4's objective values (mass_kg 23.796, deflection_mm 3.428) is still non-dominated in the new front.",
"Whether the new front's mass_kg and deflection_mm near S4 improve on 23.796 kg and 3.428 mm.",
"Whether any new feasible design lands between S2 (mass_kg 16.684, deflection_mm 6.879) and S3 (mass_kg 20.637, deflection_mm 3.551), closing the gap F1 flagged.",
"hypervolume.raw_value only if a reference point is set in both runs; facts.hypervolume.raw_value is null here, so set reference_point.mass_kg and reference_point.deflection_mm the same way in this run and the new one before comparing."
],
"next_steps": [
"Implement evaluate_design(x) with the real bracket model, run the script, then paste pareto_refined.csv back into Pareto Desk.",
"Check whether S4 is still non-dominated and whether mass_kg/deflection_mm improved on 23.796 kg and 3.428 mm.",
"Look for new feasible designs between S2 (b_mm 25.7, h_mm 82.7) and S3 (b_mm 25.4, h_mm 103.5), the gap the browser flagged (F1).",
"Set reference_point.mass_kg and reference_point.deflection_mm the same way in this run and the current one so hypervolume.raw_value becomes comparable.",
"If S3 and S4 still disagree (F2) after the new front, decide manually whether mass or deflection should tip the balance, since both stay well under the context's 15 mm wobble limit."
],
"prescan_responses": [
{
"ref": "F1",
"verdict": "confirmed",
"note": "The refined search's bounds (b_mm 12.9-52, h_mm 82.7-119.4) cover the S2-S3 gap, so the new run can test whether a feasible design fills it; the checks above cover this."
},
{
"ref": "F2",
"verdict": "confirmed",
"note": "This is why the verdict stays close_call; RNSGA2 is centred on the preference pick S4 while the front-range bounds still let the search reach S3's neighbourhood too."
}
]
}
Truncation and partial results
If your balance sits between min_credits and hold_credits, the run still
executes with a smaller output cap and the job carries "truncated": true. The JSON may
then stop mid-object: close it (the page's Recon.closeJson does this) and show the
sections that arrived, saying how many of the lane's sections were recovered, rather than treating
a clipped reply as complete.