Spaces:
Running
Running
File size: 12,336 Bytes
3273d7f 1498cc9 3273d7f a0299f2 3273d7f 81a12e6 3273d7f a0299f2 3273d7f 1498cc9 3273d7f 055bfa0 3273d7f eeb45c5 3273d7f eeb45c5 3273d7f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | /* Delta-V /flow API client and display logic for the study workbench.
*
* The browser keeps only a session token and the open study id (per-viewer
* conveniences). Every study, job, model, prediction, recommendation and report
* is read back from the server, so a refresh restores the real state. Failures
* are thrown as ApiError and shown; nothing is computed locally instead.
*/
(function (root) {
"use strict";
const TOKEN_KEY = "deltav-flow-token";
const STUDY_KEY = "deltav-flow-study";
class ApiError extends Error {
constructor(status, message, issues, payload) {
super(message);
this.status = status;
this.issues = issues || [];
this.payload = payload || null;
}
}
function safeStore(store) {
return {
get: (k) => {
try {
return store ? store.getItem(k) : null;
} catch (e) {
return null;
}
},
set: (k, v) => {
try {
if (store) v == null ? store.removeItem(k) : store.setItem(k, v);
} catch (e) {
/* storage blocked: the session lasts for this page only */
}
},
};
}
async function errorFrom(res) {
let body = null;
try {
body = await res.json();
} catch (e) {
body = null;
}
const detail = body && (typeof body.detail === "string" ? body.detail : Array.isArray(body.detail) ? body.detail.map((d) => d.msg).join("; ") : null);
const fallback = {
401: "Your session is not recognised by this server.",
404: "Not found for this session. It may have been deleted, or created in another browser.",
409: "That conflicts with the current state. Reload and try again.",
410: "This model is no longer available. Refit the study version.",
413: "This file is larger than this copy of the site can take.",
429: "The server is busy or rate-limited. Wait a moment and try again.",
503: "The server is not ready for this action.",
}[res.status];
return new ApiError(res.status, detail || fallback || "The server returned an error (" + res.status + ").", body && body.issues, body);
}
function createClient(opts) {
const fetchFn = (opts && opts.fetch) || root.fetch.bind(root);
const store = safeStore(opts && "storage" in opts ? opts.storage : root.localStorage);
const sleep = (opts && opts.sleep) || ((ms) => new Promise((r) => setTimeout(r, ms)));
let token = store.get(TOKEN_KEY);
async function session() {
const res = await fetchFn("/flow/session", { method: "POST" });
if (!res.ok) throw await errorFrom(res);
token = (await res.json()).token;
store.set(TOKEN_KEY, token);
return token;
}
async function request(path, o) {
o = o || {};
if (!token) await session();
const send = () =>
fetchFn(path, {
method: o.method || "GET",
body: o.body,
headers: Object.assign({ Authorization: "Bearer " + token }, o.json !== undefined ? { "Content-Type": "application/json" } : {}, o.headers || {}),
...(o.json !== undefined ? { body: JSON.stringify(o.json) } : {}),
signal: o.signal,
});
let res = await send();
if (res.status === 401 && !o.noRetry) {
// Server data was reset or the token expired: start a new, empty session.
store.set(STUDY_KEY, null);
await session();
res = await send();
}
if (!res.ok) throw await errorFrom(res);
if (o.raw) return res;
return res.status === 204 ? null : res.json();
}
async function job(jobId, o) {
return request("/flow/jobs/" + encodeURIComponent(jobId), o);
}
/** Resolve a 200/202 job response to its finished job, polling the status URL. */
async function settle(view, o) {
o = o || {};
let current = view;
while (current.status === "queued" || current.status === "running") {
if (o.onUpdate) o.onUpdate(current);
await sleep(o.interval || 1500);
if (o.signal && o.signal.aborted) throw new ApiError(0, "Cancelled");
current = await job(current.job_id, { signal: o.signal });
}
if (current.status === "failed") throw new ApiError(409, (current.error && current.error.message) || "The job failed.", [], current);
if (current.status === "cancelled") throw new ApiError(409, "The job was cancelled.", [], current);
return current;
}
return {
request,
job,
settle,
get token() {
return token;
},
studyId: () => store.get(STUDY_KEY),
setStudyId: (id) => store.set(STUDY_KEY, id),
capabilities: () => request("/flow/capabilities"),
example: () => request("/flow/examples/worked"),
startExample: () => request("/flow/examples/worked/start", { method: "POST" }),
upload: (file) => request("/flow/studies", { method: "POST", body: file, headers: { "Content-Type": "application/zip" } }),
study: (id) => request("/flow/studies/" + encodeURIComponent(id)),
studies: () => request("/flow/studies"),
runs: (id) => request("/flow/studies/" + encodeURIComponent(id) + "/runs"),
fit: (id, body) => request("/flow/studies/" + encodeURIComponent(id) + "/fits", { method: "POST", json: body || {} }),
cancel: (jobId) => request("/flow/jobs/" + encodeURIComponent(jobId) + "/cancel", { method: "POST" }),
predict: (modelId, points, fields) =>
request("/flow/models/" + encodeURIComponent(modelId) + "/predict", { method: "POST", json: fields ? { points, fields: true, field_format: "uint8" } : { points, fields: false } }),
next: (modelId, body) => request("/flow/models/" + encodeURIComponent(modelId) + "/next", { method: "POST", json: body }),
addRuns: (id, parent, file) =>
request("/flow/studies/" + encodeURIComponent(id) + "/versions?parent_version_id=" + encodeURIComponent(parent), {
method: "POST",
body: file,
headers: { "Content-Type": "application/zip" },
}),
exampleCompletions: (ids) => request("/flow/examples/worked/completions?ids=" + encodeURIComponent(ids.join(",")), { raw: true }),
report: (id) => request("/flow/studies/" + encodeURIComponent(id) + "/report"),
scenario: (id, body) => request("/flow/studies/" + encodeURIComponent(id) + "/scenario", { method: "PUT", json: body }),
charge: (id, body) => request("/flow/studies/" + encodeURIComponent(id) + "/charges", { method: "POST", json: body }),
artifacts: (id) => request("/flow/studies/" + encodeURIComponent(id) + "/artifacts"),
artifact: (id, name) => request("/flow/studies/" + encodeURIComponent(id) + "/artifacts/" + encodeURIComponent(name), { raw: true }),
remove: (id) => request("/flow/studies/" + encodeURIComponent(id), { method: "DELETE" }),
};
}
/* ---------- display logic (pure) ---------- */
const STATE_TEXT = {
queued: "Queued",
starting: "Starting the GPU. This can take a few minutes",
loading: "Loading verified inputs and source checkpoint",
training: "Training ensemble members",
validating: "Checking saved members",
calibrating: "Calibrating intervals",
ready: "Ready",
done: "Done",
failed: "Failed",
cancelled: "Cancelled",
};
const DATA_LABEL = {
generated_research_simulation: "Live prediction",
customer_simulation: "Customer simulation data",
other_simulation: "Other simulation data",
};
const OUTCOME_LABEL = { kinetic_energy: "Kinetic energy", pressure_rms: "Pressure RMS" };
function jobText(job) {
if (!job) return "";
let text = STATE_TEXT[job.state] || job.state;
const p = job.progress || {};
if (job.state === "training" && p.member) {
text += ": member " + p.member + " of " + p.members;
if (Number.isFinite(p.step)) text += ", step " + p.step + " of " + p.steps;
}
return text;
}
/** Fraction of a fit done: members weigh equally; calibration finishes it. Never invents progress. */
function fitFraction(job) {
if (!job) return 0;
if (job.state === "ready") return 1;
const p = job.progress || {};
if (job.state === "calibrating" || job.state === "validating") return 0.97;
if (!p.member || !p.members) return 0;
const done = p.member_complete ? p.member : p.member - 1 + (Number.isFinite(p.step) && p.steps ? p.step / p.steps : 0);
return Math.max(0, Math.min(0.96, (done / p.members) * 0.96));
}
/** How one fitted model's recorded probe intervals compare with a tolerance. A setting counts as predicted well
* enough when every outcome's 90% half-width is inside its tolerance. Uses the server's recorded bands only. */
function probeCheck(bands, target) {
if (!bands || !Array.isArray(bands.halfwidth) || !bands.halfwidth.length || !target || !target.every((t) => t > 0)) return null;
const ratios = bands.halfwidth.map((h) => Math.max(...h.map((v, j) => v / target[j])));
const sorted = ratios.slice().sort((a, b) => a - b);
const mid = sorted.length >> 1;
const median = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
return { n: ratios.length, ready: ratios.filter((r) => r <= 1).length, median, widest: sorted[sorted.length - 1], calibrated: !!bands.calibrated, level: bands.level };
}
function intervalText(calibration) {
if (!calibration) return "No model";
if (calibration.status === "calibrated")
return "Calibrated joint " + Math.round(calibration.level * 100) + "% interval (normalized split conformal, " + calibration.n +
" calibration runs). Marginal over future queries like the calibration runs; not a per-query guarantee.";
return "Uncalibrated: raw ensemble spread only (" + calibration.n + " calibration runs supplied, 39 needed). Not a confidence interval.";
}
/** Requirements still missing before a calibrated fit, from the server's role counts and limits. */
function missingRequirements(version, capability) {
const out = [];
const roles = capability.roles;
const c = version.role_counts;
if (c.train < roles.train.min) out.push("Add " + (roles.train.min - c.train) + " more train runs (minimum " + roles.train.min + ").");
if (c.development < roles.development.min)
out.push("Add " + (roles.development.min - c.development) + " more development runs (minimum " + roles.development.min + ").");
if (c.calibration < roles.calibration.min_for_calibrated)
out.push("Calibrated intervals need " + roles.calibration.min_for_calibrated + " calibration runs; " + c.calibration +
" supplied. You can still request an explicitly uncalibrated, prediction-only fit.");
return out;
}
function labelsSupplied(c) {
return c.train + c.development + c.calibration + c.verification;
}
/** Rows for the budget panel. Negative differences are shown as added cost; unknowns stay unknown. */
function budgetRows(sc) {
if (!sc || sc.status === "not_set") return [];
const rows = [["Runs you had at the start", String(sc.E_existing_runs)], ["Runs added since", String(sc.A_added_runs)]];
if (Number.isFinite(sc.baseline_future_runs)) {
rows.push(["Planned runs still to do", String(sc.baseline_future_runs)]);
rows.push(["Against your plan", signed(sc.run_difference, "run")]);
}
if (sc.money) {
const m = sc.money;
rows.push([m.budget_difference >= 0 ? "Budget not yet spent" : "Extra cost", money(Math.abs(m.budget_difference), m.currency) + (m.incomplete ? " (incomplete)" : "")]);
}
return rows;
}
function signed(v, unit) {
const n = Math.abs(v);
const word = n === 1 ? unit : unit + "s";
return v >= 0 ? n + " fewer " + word + " so far" : n + " more " + word + " than planned";
}
function money(v, currency) {
const s = Math.abs(v).toLocaleString("en-US", { maximumFractionDigits: 2 });
return (v < 0 ? "−" : "") + s + " " + currency;
}
const api = { ApiError, createClient, jobText, fitFraction, probeCheck, intervalText, missingRequirements, labelsSupplied, budgetRows, money, DATA_LABEL, OUTCOME_LABEL, STATE_TEXT };
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.DeltaFlowApi = api;
})(typeof window !== "undefined" ? window : globalThis);
|