AI4deeperScience's picture
Deploy browser-native endpoint-audited PHBV predictor
149614b verified
Raw
History Blame Contribute Delete
5.31 kB
"use strict";
(function exposePredictor(root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
else root.PHBVPredictor = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function buildPredictor() {
const clip = value => Math.max(0, Math.min(100, value));
function validateRelationships(record) {
const percentages = [1, 2, 3].map(index => {
const raw = record[`additive${index}_percentage_wt`];
return raw === null || raw === undefined || raw === "" ? 0 : Number(raw);
});
const types = [1, 2, 3].map(index => String(record[`additive_type_${index}`] ?? "not_applicable"));
if (String(record.additives ?? "no") === "no" && percentages.some(value => value > 0)) {
throw new Error("Additives are marked absent, but an additive percentage is nonzero.");
}
percentages.forEach((value, index) => {
if (value > 0 && types[index] === "not_applicable") {
throw new Error(`Additive ${index + 1} has a nonzero percentage but its type is not applicable.`);
}
});
}
function transform(model, record) {
const vector = [];
model.numeric_features.forEach((feature, index) => {
const raw = record[feature];
const parsed = raw === null || raw === undefined || raw === "" ? NaN : Number(raw);
const value = Number.isFinite(parsed) ? parsed : model.numeric_imputer_statistics[index];
const mean = model.numeric_scaler_with_mean ? model.numeric_scaler_mean[index] : 0;
vector.push((value - mean) / model.numeric_scaler_scale[index]);
});
model.categorical_features.forEach((feature, index) => {
const raw = record[feature];
const selected = raw === null || raw === undefined || raw === ""
? String(model.categorical_imputer_statistics[index])
: String(raw);
model.encoder_categories[index].forEach(category => {
vector.push(selected === String(category) ? 1 : 0);
});
});
if (vector.length !== model.num_transformed_features) {
throw new Error("The browser preprocessing map is inconsistent with the model.");
}
return vector;
}
function estimate(model, record) {
validateRelationships(record);
const vector = transform(model, record);
let total = 0;
model.trees.forEach(tree => {
let node = 0;
while (tree.left[node] !== -1) {
node = vector[tree.feature[node]] <= tree.threshold[node]
? tree.left[node]
: tree.right[node];
}
total += tree.value[node];
});
return clip(total / model.n_estimators);
}
function globalBaseline(model, timeDays) {
const parameters = model.global_first_order_reference;
if (!parameters) return null;
return clip(parameters.asymptote * (1 - Math.exp(-parameters.rate_per_day * Number(timeDays))));
}
function intervalResult(model, prediction) {
const represented = Number(model.uncertainty.represented_study_curve_radius);
const crossStudy = Number(model.uncertainty.cross_study_stress_radius);
return {
represented_lower: clip(prediction - represented),
represented_upper: clip(prediction + represented),
cross_study_lower: clip(prediction - crossStudy),
cross_study_upper: clip(prediction + crossStudy),
};
}
function predictRecord(model, record) {
const prediction = estimate(model, record);
return {
estimate: prediction,
...intervalResult(model, prediction),
global_baseline: globalBaseline(model, record.biodegradation_time_days),
};
}
function predictCurve(model, record, options = {}) {
const startDay = Number(options.startDay);
const endDay = Number(options.endDay);
const points = Number(options.points ?? 100);
const monotone = options.monotone !== false;
if (!Number.isFinite(startDay) || !Number.isFinite(endDay) || endDay <= startDay) {
throw new Error("End day must exceed start day.");
}
if (!Number.isInteger(points) || points < 2 || points > 500) {
throw new Error("Curve points must be an integer from 2 to 500.");
}
const output = {
time_days: [], estimate: [], represented_lower: [], represented_upper: [],
cross_study_lower: [], cross_study_upper: [], global_baseline: [],
};
let runningMaximum = 0;
for (let index = 0; index < points; index += 1) {
const day = startDay + ((endDay - startDay) * index) / (points - 1);
const curveRecord = {...record, biodegradation_time_days: day};
let prediction = estimate(model, curveRecord);
if (monotone) {
prediction = Math.max(runningMaximum, prediction);
runningMaximum = prediction;
}
const intervals = intervalResult(model, prediction);
output.time_days.push(day);
output.estimate.push(prediction);
output.represented_lower.push(intervals.represented_lower);
output.represented_upper.push(intervals.represented_upper);
output.cross_study_lower.push(intervals.cross_study_lower);
output.cross_study_upper.push(intervals.cross_study_upper);
output.global_baseline.push(globalBaseline(model, day));
}
return output;
}
return {transform, estimate, predictRecord, predictCurve, validateRelationships};
});