/** * Roboliq: Automation for liquid-handling robots * @copyright 2017, ETH Zurich, Ellis Whitehead * @license GPL-3.0 */ /** * Function to generate an MCMC model for use with [Stan](http://mc-stan.org/). * @module stanModel */ const _ = require('lodash'); const assert = require('assert'); const fs = require('fs'); const wellsParser = require('./parsers/wellsParser'); // PROBLEM WITH beta: // Will need to have a beta for each explicitly tested volume, // but also need to calculate the bias for other volumes // (e.g. we might add 3ul dye and 297ul water to a well, but 297ul isn't one of our test volumes). // We might need to have several lines for assigning to RV_VTIPASP: // // - RV_TIPASP[i1] = d * (1 + beta[pd]) + RV_TIPASP_raw * (sigma_v0 + d * sigma_v1[psub]) // - RV_TIPASP[i2] = d + RV_TIPASP_raw * (sigma_v0 + d * sigma_v1[psub]) // // The first line is for the nodal volumes we want beta estimate for. // The second line is for the volumes where we completely ignore beta. // A third line could be for interpolating volumes between two nodal volumes. // A fourth line could be for extrapolating beta to volumes beyond our nodal volumes. function Ref(name, i) { if (_.isPlainObject(name)) { const rv = name; return { name: rv.type, i: rv.i, idx: rv.idx }; } return { name, i, idx: (_.isNumber(i)) ? i + 1 : undefined }; } function RefRV(i) { return { name: "rvs", i, idx: (_.isNumber(i)) ? i + 1 : undefined }; } function lookup(model, ref) { return model[ref.name][ref.i]; } // Standard Normal variate using Box-Muller transform. function randn_bm(mean, sigma) { var u = 1 - Math.random(); // Subtraction to flip [0, 1) to (0, 1]. var v = 1 - Math.random(); return mean + sigma * Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v ); } /** * Create the initial empty model. * You will add pipetting and measurement actions will be added to. * @param {number[]} subclassNodes - sorted volumes after which a subclass starts (e.g. for subclasses 3-15,15.01-500,500.01-1000, subclassNodes = [3,15,500,1000]) * @param {number[]} betaDs - volumes for which we want a beta parameter (dispense bias) * @param {number[]} gammaDs - volumes for which we want a gamma parameter (unintended dilution) * @return {object} an object with mostly empty properties representing the model's random variables and labware. */ function createEmptyModel(subclassNodes, betaDs, gammaDs) { assert(!_.isEmpty(subclassNodes)); assert(!_.isEmpty(betaDs)); return { subclassNodes, betaDs, gammaDs, models: {}, // labware models liquids: {}, labwares: {}, wells: {}, tips: {}, // liquidClass/pipettingParameters (p) + dispense volume (d) combinations pds: {}, // liquidClass/pipettingParameters (p) + subclass (sub) combinations psubs: {}, gammas: {}, // Random variables RV_AL: [], RV_A0: [], RV_AV: [], RV_VTIPASP: [], RV_V: [], RV_U: [], RV_C: [], RV_A: [], RV_G0: [], // empty/starting weights RV_G: [], // weight // Pipetting operations pipOps: [], absorbanceMeasurements: [], weightMeasurements: [], }; } function addRv(model, rv) { rv.i = model.rvs.length; rv.idx = model.rvs.length; // FIXME: change this to i + 1 model.rvs.push(rv); return RefRV(rv.i); } function addRv2(model, group, rv) { const list = model[group]; rv.i = list.length; rv.idx = list.length + 1; list.push(rv); return Ref(group, rv.i); } /** * Add a liquid to the model. * @param {object} model - the model * @param {string} k - liquid name * @param {object} spec - concentration specification * @param {string} [spec.type="fixed"] - set to "fixed" for a concentration, "normal" for a normally distributed concentration. If set to "normal", you will need to supply the "loc" and "scale" values as input data when running Stan. * @param {number} [spec.value=0] - if fixed, this is the fixed concentration - otherwise the parameters will be estimated. */ function addLiquid(model, k, spec) { const liquidData = getLiquidData(model, k); liquidData.spec = spec; } function getLabwareData(model, l) { // console.log("getLabwareData: "+l); const m = "FIXME"; if (!model.labwares.hasOwnProperty(l)) { model.labwares[l] = { m }; } return model.labwares[l]; } function getLiquidData(model, k) { if (!model.liquids.hasOwnProperty(k)) { model.liquids[k] = {k, idx: model.liquids.length}; } return model.liquids[k]; } function getWellData(model, well) { if (!model.wells.hasOwnProperty(well)) { const {labware: l, wellId: wellPos} = wellsParser.parseOne(well); model.wells[well] = { l, pos: wellPos }; } return model.wells[well]; } function getTipData(model, t) { if (!model.tips.hasOwnProperty(t)) { model.tips[t] = { }; } return model.tips[t]; } function getRv_al(model, l) { const labwareData = getLabwareData(model, l); if (!labwareData.hasOwnProperty("ref_al")) { const ref = addRv2(model, "RV_AL", {type: "RV_AL", l}); labwareData.ref_al = ref; } return lookup(model, labwareData.ref_al); } function getRv_a0(model, well) { const wellData = getWellData(model, well); if (!wellData.hasOwnProperty("ref_a0")) { const rv = {type: "RV_A0", well}; const ref = addRv2(model, "RV_A0", rv); wellData.ref_a0 = ref; } return lookup(model, wellData.ref_a0); } function getRv_av(model, well) { const wellData = getWellData(model, well); if (!wellData.hasOwnProperty("ref_av")) { const rv = {type: "RV_AV", well}; const ref = addRv2(model, "RV_AV", rv); wellData.ref_av = ref; } return lookup(model, wellData.ref_av); } function getRv_g0(model, l) { const labwareData = getLabwareData(model, l); if (!labwareData.hasOwnProperty("ref_g0")) { const rv = {type: "RV_G0", l}; const ref = addRv2(model, "RV_G0", rv); labwareData.ref_g0 = ref; } return lookup(model, labwareData.ref_g0); } function absorbance_A0(model, wells) { _.forEach(wells, well => { const {labware: l, wellId: wellPos} = wellsParser.parseOne(well); const rv_al = getRv_al(model, l); const rv_a0 = getRv_a0(model, well); }); } function absorbance_AV(model, wells) { _.forEach(wells, well => { const {labware: l, wellId: wellPos} = wellsParser.parseOne(well); const rv_al = getRv_al(model, l); const rv_a0 = getRv_a0(model, well); const rv_av = getRv_av(model, well); }); } /** * Add an absorbance measurement to the model. * @param {object} model - the model * @param {string[]} wells - the names of the measured wells */ function measureAbsorbance(model, wells) { _.forEach(wells, well => { const {labware: l, wellId: wellPos} = wellsParser.parseOne(well); const wellData = getWellData(model, well); const rv_al = getRv_al(model, l); const rv_a0 = getRv_a0(model, well); // console.log("wellData: "+JSON.stringify(wellData)) // If the well already has an absorbance RV if (wellData.ref_a) { model.absorbanceMeasurements.push({ref_a: wellData.ref_a, well}); } // If there's some volume in the well else if (wellData.ref_vWell) { const rv_av = getRv_av(model, well); const ref_av = Ref(rv_av); // console.log({wellData}) // If there's some concentration in the well if (wellData.ref_cWell) { const rv_a = {type: "a", well, l, wellPos, ref_av, ref_vWell: wellData.ref_vWell, ref_cWell: wellData.ref_cWell}; const ref_a = addRv2(model, "RV_A", rv_a); model.absorbanceMeasurements.push({ref_a}); wellData.ref_a = ref_a; } // Otherwise the liquid is clear: else { const rv_a = {type: "a", well, l, wellPos, ref_av}; const ref_a = addRv2(model, "RV_A", rv_a); model.absorbanceMeasurements.push({ref_a}); wellData.ref_a = ref_a; } } // Otherwise, just measure A0 else { const ref_a0 = Ref(rv_a0); const rv_a = {type: "a", well, l, wellPos, ref_a0}; const ref_a = addRv2(model, "RV_A", rv_a); wellData.ref_a = ref_a; model.absorbanceMeasurements.push({ref_a}); } }); } /** * Add a weight measurement to the model. * @param {object} model - the model * @param {string} l - the labware name */ function measureWeight(model, l) { // console.log("measureWeight: "+l) const labwareData = getLabwareData(model, l); // console.log({labwareData}) let ref_g; // If we already have an RV for the current weight, use it. if (labwareData.ref_g) { ref_g = labwareData.ref_g; } // Otherwise, calculate a new RV else { // Ensure we have a variable for the empty/starting weight of the plate const rv_g0 = getRv_g0(model, l); const ref_g0 = Ref(rv_g0); // Find wells on the plate const wells = _.filter(model.wells, wellData => wellData.l == l && wellData.ref_vWell); // Get their volumes const ref_vs = wells.map(x => x.ref_vWell); const rv_g = {type: "RV_G", l, ref_g0, ref_vs}; // console.log({rv_g}) ref_g = addRv2(model, "RV_G", rv_g); labwareData.ref_g = ref_g; } // console.log({ref_g}) model.weightMeasurements.push({ref_g}); } /** * Assign a liquid to a well in the model. * @param {object} model - the model * @param {string} well - the well name * @param {string} k - the liquid name */ function assignLiquid(model, well, k) { const liquidData = getLiquidData(model, k); const wellData = getWellData(model, well); wellData.k = k; wellData.idx_k = liquidData.idx; } /** * Add an aspiration operation to the model. * @param {object} model - the model * @param {object} args * @param {string} args.p - the name of the pipetting parameters * @param {number|string} args.t - the tip identifier * @param {number} args.d - volume in microliters * @param {string} args.well - the well name */ function aspirate(model, {p, t, d, well}) { // create: RV for volume aspirated into tip // create: RV for concentration in tip // create: RV for new volume in src // input: previous volume of src // input: variable for k's concentration - we'll need a random variable for the original sources and a calculated variable for what we pipette together const wellData = getWellData(model, well); const labwareData = getLabwareData(model, wellData.l); const tipData = getTipData(model, t); const ref_vWell0 = wellData.ref_vWell; // volume of well before aspirating // console.log({p, t, d, well, wellData, tipData}) // Need a variable for the well/liquid concentration. // If one doesn't already exist, we'll need to create one. // If well's liquid is known: // if it's concentration is fixed, // add a RV_C item, but not an RV_C_raw. // if the concentration is user-defined or estimated, // add both RV_C_raw and RC_C items. // Otherwise add an RV_C item with fixed value 0 (and no RV_C_raw). if (!wellData.hasOwnProperty("ref_cWell")) { let ref_c; if (wellData.hasOwnProperty("k")) { const liquidData = getLiquidData(model, wellData.k); if (_.get(liquidData.spec, "type") === "fixed") { if (liquidData.spec.value > 0) { const rv_c = {type: "c", k: liquidData.k, value: liquidData.spec.value, of: liquidData.k}; ref_c = addRv2(model, "RV_C", rv_c); } } else { // const rv_c_raw = {type: "c", k, value: liquidData.spec.value}; // const ref_c_raw = addRv2(model, "RV_C_raw", rv_c_raw); const rv_c = {type: "c", k: liquidData.k, alpha_k: `alpha_k_${liquidData.k}`, of: liquidData.k}; ref_c = addRv2(model, "RV_C", rv_c); } } else { const rv_c = {type: "c", value: 0, well, of: well}; ref_c = addRv2(model, "RV_C", rv_c); } wellData.ref_cWell = ref_c; } const ref_cWell0 = wellData.ref_cWell; // concentration of well before aspirating // Add new psub (liquid class + subclass) data const sub = (model.subclassNodes[0] == d) ? 1 : _.findIndex(model.subclassNodes, v => d <= v); assert(sub > 0, `didn't find subclass: ${JSON.stringify({sub, d, x: model.subclassNodes})}`); const psub = p+sub; if (!model.psubs.hasOwnProperty(psub)) { const idx = _.size(model.psubs) + 1; model.psubs[psub] = {idx, psub, p, sub}; } const idx_psub = model.psubs[psub].idx; // Add new pd (liquid class + dispense volume) data, if d is in betaDs let idx_pd; if (_.includes(model.betaDs, d)) { const pd = p+d; if (!model.pds.hasOwnProperty(pd)) { const idx = _.size(model.pds) + 1; model.pds[pd] = {idx, idx_psub, psub, pd, p, sub, d}; } idx_pd = model.pds[pd].idx; // console.log({p, d, idx_pd}) } else { // console.log({d, betaDs: model.betaDs.join(",")}) } const idx_pip = model.pipOps.length; const rv_vTipAsp = {type: "vTipAsp", idx_pip, idx_psub, idx_pd}; // TODO: this is currently just calculated, so it'd be better not to have a // RV_raw entry for this, because that adds a superfluous parameter to the // model. We should differentiate between RV's that are calculated and RV's // that require their own parameter. const ref_vTipAsp = addRv2(model, "RV_VTIPASP", rv_vTipAsp); tipData.ref_vTipAsp = ref_vTipAsp; tipData.ref_cTipAsp = ref_cWell0; // // If we have information about the source concentration, // // then track the concentration in the tip too. // if (!_.isUndefined(ref_cWell0)) { // const idx_cTipAsp = model.rvs.length; // const rv_cTipAsp = {idx: idx_cTipAsp, type: "cTipAsp", ref_cWell0, idx_d}; // model.rvs.push(rv_cTipAsp); // tipData.ref_cTipAsp = RefRV(idx_cTipAsp); // } // If we already have information about well volume, // then update it by removing the aliquot. if (ref_vWell0) { const rv_vWellAsp = {type: "vWellAsp", well, ref_vWell0, ref_vTipAsp}; const ref_vWellAsp = addRv2(model, "RV_V", rv_vWellAsp); wellData.ref_vWell = ref_vWellAsp; } wellData.ref_a = undefined; labwareData.ref_g = undefined; const asp = { d // p, t, d, well, k, // idx_volTot0, idx_conc0, // idx_v, idx_c, idx_volTot, }; model.pipOps.push(asp); } /** * Add a dispense operation to the model. * @param {object} model - the model * @param {object} args * @param {string} args.p - the name of the pipetting parameters * @param {number|string} args.t - the tip identifier * @param {number} args.d - volume in microliters * @param {string} args.well - the well name */ function dispense(model, {p, t, d, well}) { if (d === 0) return; // input: RV for volume aspirated into tip // input: RV for concentration in tip // input: previous volume of dst // input: previous conc of dst // create: RV for new volume of dst // create: RV for new conc of dst const wellData = getWellData(model, well); const labwareData = getLabwareData(model, wellData.l); const tipData = getTipData(model, t); const ref_vWell0 = wellData.ref_vWell; // volume of well before dispensing const ref_cWell0 = wellData.ref_cWell; // concentration of well before dispensing const ref_vTipAsp = tipData.ref_vTipAsp; // volume in tip const ref_cTipAsp = tipData.ref_cTipAsp; // concentration in tip // Unintended dilution let ref_uWellDis; if (_.includes(model.gammaDs, d)) { const pd = p + d; if (!model.gammas.hasOwnProperty(pd)) { const idx = _.size(model.gammas) + 1; model.gammas[pd] = {idx, pd, p, d}; } const idx_gamma = model.gammas[pd].idx; const rv_uWellDis = {type: "RV_U", well, idx_gamma}; ref_uWellDis = addRv2(model, "RV_U", rv_uWellDis); } // Add aliquot to well const rv_vWellDis = {type: "vWellDis", well, ref_vWell0, ref_vTipAsp, ref_uWellDis}; const ref_vWellDis = addRv2(model, "RV_V", rv_vWellDis); tipData.ref_vTipAsp = undefined; wellData.ref_vWell = ref_vWellDis; // If we have information about the tip concentration, // then update the concentration in the destination well too. if (ref_cTipAsp || ref_cWell0) { const rv_cWellDis = {type: "cWellDis", ref_vWell0, ref_cWell0, ref_vWell1: ref_vWellDis, ref_vTipAsp, ref_cTipAsp, ref_uWellDis, well, of: well}; const ref_cWellDis = addRv2(model, "RV_C", rv_cWellDis); wellData.ref_cWell = ref_cWellDis; tipData.ref_cTipAsp = undefined; } wellData.ref_a = undefined; labwareData.ref_g = undefined; const asp = { d // p, t, d, well, k, // idx_volTot0, idx_conc0, // idx_v, idx_c, idx_volTot, }; model.pipOps.push(asp); } /** * Print the Stan model to stdout, and save a `basename`.R file that holds indexes to associate the random variables back to labware. * @param {string} model - the model * @param {string} [basename="stanModel"] - the basename for the R output */ function printModel(model, basename) { const output = { data: [], transformedData: { definitions: [], statements: [] }, parameters: [], transformedParameters: { definitions: [], statements: [] }, model: [], R: [] }; handle_transformed_data(model, output); handle_transformed_parameters(model, output); handle_model_psubs(model, output); handle_model_pds(model, output); handle_model_weightMeasurements(model, output); console.log(); console.log("data {"); output.data.forEach(s => console.log(s)); console.log(" real<lower=0> sigma_a_scale;"); if (model.RV_AL.length > 0) { //const NL = model.RV_AL.length; const NM = _.size(model.models) || 1; console.log(makeVectorOrRealVariable("alpha_l_loc", NM, "<lower=0>")); console.log(makeVectorOrRealVariable("alpha_l_scale", NM, "<lower=0>")); } if (model.RV_AV.length > 0) { const NM = _.size(model.models) || 1; console.log(makeVectorOrRealVariable("alpha_v_loc", NM, "")); console.log(makeVectorOrRealVariable("alpha_v_scale", NM, "<lower=0>")); console.log(makeVectorOrRealVariable("sigma_alpha_v_scale", NM, "<lower=0>")); } _.forEach(model.liquids, liquidData => { if (_.get(liquidData.spec, "type") === "normal") { console.log(` real<lower=0> alpha_k_${liquidData.k}_loc; // concentration of liquid ${liquidData.k}`); console.log(` real<lower=0> alpha_k_${liquidData.k}_scale; // concentration of liquid ${liquidData.k}`); } }); if (model.absorbanceMeasurements.length > 0) { console.log(); console.log(` vector<lower=0>[${model.absorbanceMeasurements.length}] A; // Absorbance measurements`); } console.log("}"); console.log(); console.log("transformed data {"); output.transformedData.definitions.forEach(s => console.log(s)); console.log(); output.transformedData.statements.forEach(s => console.log(s)); console.log("}"); console.log(); console.log("parameters {"); output.parameters.forEach(s => console.log(s)); if (model.RV_AL.length > 0) { console.log(" vector[NM] alpha_l_raw;"); const NM = 1; const times = (NM == 1) ? "*" : ".*"; output.transformedParameters.definitions.push(` vector<lower=0>[NM] alpha_l = alpha_l_loc + alpha_l_raw ${times} alpha_l_scale;`); } if (model.RV_AL.length > 1) { console.log(" vector<lower=0,upper=1>[NM] sigma_alpha_l;"); } console.log(" vector<lower=0,upper=1>[NM] sigma_alpha_i;"); if (model.RV_AV.length > 0) { console.log(" vector[NM] alpha_v_raw;"); console.log(" vector<lower=0>[NM] sigma_alpha_v_raw;"); const times = (model.RV_AL.length == 1 || (_.size(model.models) || 1) == 1) ? "*" : ".*"; // console.log({times, NL: model.RV_AL.length, NM: model.models.length}) output.transformedParameters.definitions.push(` vector[NM] alpha_v = alpha_v_loc + alpha_v_raw ${times} alpha_v_scale;`); output.transformedParameters.definitions.push(` vector<lower=0>[NM] sigma_alpha_v = sigma_alpha_v_raw ${times} sigma_alpha_v_scale;`); } console.log(); if (model.RV_AL.length > 0) console.log(` vector[${model.RV_AL.length}] RV_AL_raw;`); if (model.RV_A0.length > 0) console.log(` vector[${model.RV_A0.length}] RV_A0_raw;`); if (model.RV_AV.length > 0) console.log(` vector[${model.RV_AV.length}] RV_AV_raw;`); if (model.RV_VTIPASP.length > 0) console.log(` vector[${model.RV_VTIPASP.length}] RV_VTIPASP_raw;`); // if (model.RV_C_raw.length > 0) console.log(` vector[${model.RV_C_raw.length}] RV_C_raw;`) // console.log(" vector[NRV] RV_raw;"); _.forEach(model.liquids, liquidData => { if (_.get(liquidData.spec, "type") === "normal") { // console.log(` real<lower=${liquidData.spec.lower || 0}, upper=${liquidData.spec.upper}> alpha_k_${liquidData.k}; // concentration of liquid ${liquidData.k}`); console.log(` real alpha_k_${liquidData.k}_raw; // unscaled concentration variance of liquid ${liquidData.k}`); output.transformedParameters.definitions.push(` real alpha_k_${liquidData.k} = alpha_k_${liquidData.k}_loc + alpha_k_${liquidData.k}_raw * alpha_k_${liquidData.k}_scale;`); } }); if (model.absorbanceMeasurements.length > 0) { console.log(" real<lower=0> sigma_a_raw;"); } console.log("}"); console.log(); console.log("transformed parameters {"); output.transformedParameters.definitions.forEach(s => console.log(s)); console.log(); output.transformedParameters.statements.forEach(s => console.log(s)); console.log("}"); console.log(); console.log("model {"); output.model.forEach(s => console.log(s)); if (model.RV_AL.length > 0) console.log(" RV_AL_raw ~ normal(0, 1);"); if (model.RV_A0.length > 0) console.log(" RV_A0_raw ~ normal(0, 1);"); if (model.RV_AV.length > 0) console.log(" RV_AV_raw ~ normal(0, 1);"); if (model.RV_VTIPASP.length > 0) console.log(" RV_VTIPASP_raw ~ normal(0, 1);"); // if (model.RV_C_raw.length > 0) console.log(" RV_C_raw ~ normal(0, 1);"); // console.log(" RV_raw ~ normal(0, 1);"); if (model.absorbanceMeasurements.length > 0) { console.log(" sigma_a_raw ~ exponential(1);"); if (model.RV_AV.length > 0) { console.log(" alpha_v_raw ~ normal(0, 1);"); console.log(" sigma_alpha_v_raw ~ exponential(1);"); } console.log(); const idxsRv = model.absorbanceMeasurements.map(x => x.ref_a.idx); // console.log(` A ~ normal(RV_A[{${idxsRv}}], RV_A[{${idxsRv}}] * sigma_a);`); console.log(` A ~ normal(RV_A[A_i_A], RV_A[A_i_A] * sigma_a);`); } console.log("}"); fs.writeFileSync((basename || "stanModel")+".R", output.R.join("\n")+"\n"); } // There's appears to be a bug in RStan such that 1-element vectors // are not passed to stan. So we need to turn 1-element vectors into reals. function makeVectorOrRealVariable(name, n, modifier, value) { const type = (_.isString(n) || n > 1) ? `vector${modifier}[${n}]` : `real${modifier}`; return ` ${type} ${name}${_.isUndefined(value) ? "" : " = "+value};`; } /*function makeVectorVariable(name, n, modifier, value) { const type = `vector${modifier}[${n}]`; return ` ${type} ${name}${_.isUndefined(value) ? "" : " = "+value};`; }*/ function handle_transformed_data(model, output) { output.transformedData.definitions.push(` int NM = 1; // number of labware models`); output.transformedData.definitions.push(` int NL = ${_.size(model.labwares)}; // number of labwares`); output.transformedData.definitions.push(` int NI = ${_.size(model.wells)}; // number of wells`); output.transformedData.definitions.push(` int NT = ${_.size(model.tips)}; // number of tips`); // output.transformedData.definitions.push(` int NRV = ${_.size(model.rvs)}; // number of latent random variables`); output.transformedData.definitions.push(` int NJ = ${model.pipOps.length}; // number of pipetting operations`); _.forEach(model.liquids, liquidData => { if (_.get(liquidData.spec, "type") === "fixed") { output.transformedData.definitions.push(` real alpha_k_${liquidData.k} = ${liquidData.spec.value}; // concentration of liquid ${liquidData.k}`); } }); if (!_.isEmpty(model.pipOps)) { // console.log(` real d[NJ] = {${model.pipOps.map(x => x.d.toFixed(1))}};`); output.transformedData.definitions.push(` vector<lower=0>[NJ] d; // desired volumes`); output.transformedData.statements.push(` {`); output.transformedData.statements.push(` real d0[NJ] = {${model.pipOps.map(x => x.d.toFixed(1))}};`); output.transformedData.statements.push(` for (i in 1:NJ) d[i] = d0[i];`); output.transformedData.statements.push(` }`); } } function addCentralizedParameter_sigma(name, n, nScales, output) { assert(n > 0); assert(nScales == 1 || nScales == n); // Scale if (nScales == 1) { output.data.push(` real<lower=0> ${name}_scale;`); } else if (nScales > 1) { output.data.push(` vector<lower=0>[${nScales}] ${name}_scale;`); } if (n == 1) { output.parameters.push(` real<lower=0> ${name}_raw;`); } else if (n > 1) { output.parameters.push(` vector<lower=0>[${n}] ${name}_raw;`); } if (n == 1) { output.transformedParameters.definitions.push(` real<lower=0> ${name} = ${name}_raw * ${name}_scale;`); } else if (n > 1) { if (nScales == 1) { output.transformedParameters.definitions.push(` vector<lower=0>[${n}] ${name} = ${name}_raw * ${name}_scale;`); } else { output.transformedParameters.definitions.push(` vector<lower=0>[${n}] ${name} = ${name}_raw .* ${name}_scale;`); } } output.model.push(` ${name}_raw ~ exponential(1);`); } /** * Add variables for a centralized mean * @param {string} name - name of variable * @param {boolean} withLoc - whether to include a location data variable * @param {integer} n - array size, or 1 for a scalar * @param {integer} nScales - number of scale parameters; 1 = same scale for each item in array; n = one scale parameter for each item. * @param {string} [nName] - optional name for array size (to be used in place of n in some places) * @param {string} limits - a limits modifier ("" if no limits) */ function addCentralizedParameter_mean(name, withLoc, n, nScales, nName, limits, output) { assert(n > 0); assert(nScales == 1 || nScales == n); // Location if (withLoc) { output.data.push(makeVectorOrRealVariable(`${name}_loc`, nScales, limits)); } // Scale output.data.push(makeVectorOrRealVariable(`${name}_scale`, nScales, "<lower=0>")); const n2 = (_.isString(nName) && !_.isEmpty(nName)) ? nName : n; // Raw output.parameters.push(makeVectorOrRealVariable(`${name}_raw`, n2, "")); const times = (n == 1 || nScales == 1) ? "*" : ".*"; const value = `${name}_loc + ${name}_raw ${times} ${name}_scale`; output.transformedParameters.definitions.push(makeVectorOrRealVariable(name, n2, limits, value)); output.model.push(` ${name}_raw ~ normal(0, 1);`); } function handle_model_psubs(model, output) { const n = _.size(model.psubs); if (n == 0) return; addCentralizedParameter_sigma("sigma_v0", 1, 1, output); addCentralizedParameter_sigma("sigma_v1", n, 1, output); output.R.push("dfModel_psubs = tibble("); output.R.push(` idx = c(${_.map(model.psubs, "idx").join(", ")}),`); output.R.push(` p = c(\"${_.map(model.psubs, "p").join('", "')}\"),`); output.R.push(` sub = c(${_.map(model.psubs, "sub").join(", ")})`); output.R.push(")"); } function handle_model_pds(model, output) { const n = _.size(model.pds); if (n == 0) return; output.transformedData.definitions.push(` int NBETA = ${n}; // number of liquidClass+majorD combinations for beta`); // The second argument shouldn't be 'true', because it doesn't make // sense to have a single location variable for all betas. // Either none of them should have location, or all of them. // The problem with all of them having betas is that it's not so simple // for the user to know which order the location variables should be specified // in. addCentralizedParameter_mean("beta", true, n, 1, "NBETA", "", output); output.R.push("dfModel_pds = tibble("); output.R.push(` idx = c(${_.map(model.pds, "idx").join(", ")}),`); output.R.push(` idx_psub = c(${_.map(model.pds, "idx_psub").join(", ")}),`); output.R.push(` p = c(\"${_.map(model.pds, "p").join('", "')}\"),`); output.R.push(` sub = c(${_.map(model.pds, "sub").join(", ")}),`); output.R.push(` d = c(${_.map(model.pds, "d").join(", ")})`); output.R.push(")"); } function handle_model_weightMeasurements(model, output) { const n = model.weightMeasurements.length; if (n == 0) return; output.data.push(` vector<lower=0>[${n}] G; // weight measurements`); addCentralizedParameter_sigma("sigma_g", 1, 1, output); output.data.push(` real<lower=0> rho; // density of liquid`); output.parameters.push(` vector<lower=0>[${model.RV_G0.length}] RV_G0; // empty/starting weights`); output.transformedParameters.definitions.push(` vector<lower=0>[${n}] RV_G; // empty/starting weights`); _.forEach(model.RV_G, rv_g => { const idx_g0 = rv_g.ref_g0.idx; const idxs_v = rv_g.ref_vs.map(x => x.idx); let s = ` RV_G[${rv_g.idx}] = RV_G0[${idx_g0}]`; if (!_.isEmpty(rv_g.ref_vs)) s = s + ` + rho * sum(RV_V[{${idxs_v}}])`; s += `; // weight of ${rv_g.l}`; output.transformedParameters.statements.push(s); }); const idxs_g = model.weightMeasurements.map(x => x.ref_g.idx); output.model.push(` G ~ normal(RV_G[{${idxs_g}}], sigma_g);`); } function handle_transformed_parameters(model, output) { /* if (!_.isEmpty(model.betas)) { output.transformedParameters.definitions.push(""); output.transformedParameters.definitions.push(" vector[NBETA] beta0 = beta0_raw * beta_scale;"); output.transformedParameters.definitions.push(" vector[NBETA] beta1 = beta1_raw * beta_scale;"); output.transformedParameters.definitions.push(" vector<lower=0>[NBETA] sigma_v = sigma_v_raw * sigma_v_scale;"); const rvs_gamma = _.values(model.betas).filter(rv => rv.withGamma); if (!_.isEmpty(rvs_gamma)) { output.transformedData.definitions.push(` int<lower=0> NGAMMA = ${rvs_gamma.length};`); output.transformedData.definitions.push(` int<lower=1> gamma_i_raw[NGAMMA] = ${rvs_gamma.map(rv => rv.idx)};`); output.transformedParameters.definitions.push(" vector<lower=0>[NBETA] gamma = 0;"); // need the same number of gamma variables as betas, but we probably have fewer gamma_raw parameters, since it's difficult to identify gamma in many experiments with small volumes. output.transformedParameters.definitions.push(" real sigma_gamma = sigma_gamma_raw * sigma_gamma_scale;"); output.transformedParameters.statements.push(""); output.transformedParameters.statements.push(" for (i in 1:NGAMMA) gamma[gamma_i_raw[i]] = max({0, gamma_raw[i] * sigma_gamma});"); } } */ output.transformedParameters.definitions.push(" real<lower=0> sigma_a = sigma_a_raw * sigma_a_scale;"); // output.transformedParameters.definitions.push(` vector[NRV] RV;`); handle_transformed_parameters_RV_al(model, output); handle_transformed_parameters_RV_a0(model, output); handle_transformed_parameters_RV_av(model, output); handle_transformed_parameters_RV_vTipAsp(model, output); handle_transformed_parameters_RV_U(model, output); handle_transformed_parameters_RV_V(model, output); handle_transformed_parameters_RV_C(model, output); handle_transformed_parameters_RV_A(model, output); } function handle_transformed_parameters_RV_al(model, output) { const rvs = model.RV_AL; if (rvs.length == 0) return; const idxs_m = Array(rvs.length); for (let i = 0; i < rvs.length; i++) { const rv = rvs[i]; idxs_m[i] = 0 + 1; } output.transformedParameters.definitions.push(` vector<lower=0>[${rvs.length}] RV_AL; // average absorbance of labware`); output.transformedParameters.statements.push(""); output.transformedParameters.statements.push(" // AL[l] ~ normal(alpha_l[m], sigmal_alpha_l[m])") if (model.RV_AL.length > 1) { output.transformedParameters.statements.push(` RV_AL = alpha_l[{${idxs_m}}] + RV_AL_raw .* sigma_alpha_l[{${idxs_m}}];`); } else { output.transformedParameters.statements.push(` RV_AL = alpha_l[{${idxs_m}}];`); } } function handle_transformed_parameters_RV_a0(model, output) { const rvs = model.RV_A0; const idxs_al = Array(rvs.length); const idxs_m = Array(rvs.length); for (let i = 0; i < rvs.length; i++) { const rv = rvs[i]; const wellData = model.wells[rv.well]; const labwareData = model.labwares[wellData.l]; idxs_al[i] = labwareData.ref_al.idx; idxs_m[i] = 0 + 1; } output.transformedData.definitions.push(` int<lower=1> RV_A0_i_AL[${idxs_al.length}] = {${idxs_al}};`) output.transformedData.definitions.push(` int<lower=1> RV_A0_i_m[${idxs_m.length}] = {${idxs_m}};`) output.transformedParameters.definitions.push(` vector<lower=0>[${rvs.length}] RV_A0; // absorbance of empty wells`); output.transformedParameters.statements.push(""); output.transformedParameters.statements.push(" // A0[i] ~ normal(AL[m[i]], sigma_alpha_i[m[i]])"); output.transformedParameters.statements.push(` RV_A0 = RV_AL[RV_A0_i_AL] + RV_A0_raw .* sigma_alpha_i[RV_A0_i_m];`); } function handle_transformed_parameters_RV_av(model, output) { const rvs = model.RV_AV; if (rvs.length == 0) return; const idxs_a0 = Array(rvs.length); // const idxs_l = Array(rvs.length); const idxs_m = Array(rvs.length); for (let i = 0; i < rvs.length; i++) { const rv = rvs[i]; const wellData = model.wells[rv.well]; idxs_a0[i] = wellData.ref_a0.idx; // const labwareData = model.labwares[wellData.l]; // // console.log({rv, wellData, labwareData}) // idxs_l[i] = labwareData.idx_al; idxs_m[i] = 0 + 1; } output.transformedData.definitions.push(` int<lower=1> RV_AV_i_A0[${idxs_a0.length}] = {${idxs_a0}};`) output.transformedData.definitions.push(` int<lower=1> RV_AV_i_m[${idxs_m.length}] = {${idxs_m}};`) // There's a bug in Rstan for data vectors of length 1, so this check is necessary... // const NM = model.models.length || 1; // CONTINUE // if (NM == 1) { // output.transformedParameters.definitions.push(` vector[NM] alpha_v = alpha_v_loc + alpha_v_raw * alpha_v_scale;`); // output.transformedParameters.definitions.push(` vector<lower=0>[NM] sigma_alpha_v = sigma_alpha_v_raw * sigma_alpha_v_scale;`); // } // else if (NM > 1) { // output.transformedParameters.definitions.push(` vector[NM] alpha_v = alpha_v_loc + alpha_v_raw .* alpha_v_scale;`); // output.transformedParameters.definitions.push(` vector<lower=0>[NM] sigma_alpha_v = sigma_alpha_v_raw .* sigma_alpha_v_scale;`); // } output.transformedParameters.definitions.push(` vector<lower=0>[${rvs.length}] RV_AV; // absorbance of water-filled wells`); output.transformedParameters.statements.push(""); output.transformedParameters.statements.push(" // AV[i] ~ normal(A0[i] + alpha_v[m[i]], sigma_alpha_v[m[i]])"); output.transformedParameters.statements.push(` RV_AV = RV_A0[RV_AV_i_A0] + alpha_v[RV_AV_i_m] + RV_AV_raw .* sigma_alpha_v[RV_AV_i_m];`); } function handle_transformed_parameters_RV_vTipAsp(model, output) { const rvs = model.RV_VTIPASP; if (rvs.length == 0) return; const idxs_pip = rvs.map(rv => rv.idx_pip + 1); const idxs_psubsId = rvs.map(rv => rv.idx_psub); const rvs_withBeta = rvs.filter(rv => _.isNumber(rv.idx_pd)); const idxs_withBeta = rvs_withBeta.map(rv => rv.idx); const idxs_withBeta_beta = rvs_withBeta.map(rv => rv.idx_pd); output.transformedParameters.definitions.push(` vector<lower=0>[${rvs.length}] RV_VTIPASP; // volume aspirated into tip`); output.transformedParameters.statements.push(""); output.transformedData.definitions.push(` int<lower=1> RV_VTIPASP_i_d[${idxs_pip.length}] = {${idxs_pip}};`); if (_.size(model.psubs) > 1) { output.transformedData.definitions.push(` int<lower=1> RV_VTIPASP_i_psub[${idxs_pip.length}] = {${idxs_psubsId}};`); } output.transformedData.definitions.push(` int<lower=1> RV_VTIPASP_i_withBeta[${idxs_withBeta.length}] = {${idxs_withBeta}};`); output.transformedData.definitions.push(` int<lower=1> RV_VTIPASP_i_withBeta_beta[${idxs_withBeta_beta.length}] = {${idxs_withBeta_beta}};`); output.transformedParameters.statements.push(` {`); output.transformedParameters.statements.push(` // - RV_TIPASP[i1] = d * (1 + beta[pd]) + RV_TIPASP_raw * (sigma_v0 + d * sigma_v1[psub])`); output.transformedParameters.statements.push(` // - RV_TIPASP[i2] = d + RV_TIPASP_raw * (sigma_v0 + d * sigma_v1[psub])`); output.transformedParameters.statements.push(` vector[${model.RV_VTIPASP.length}] temp = d[RV_VTIPASP_i_d];`); output.transformedParameters.statements.push(` temp[RV_VTIPASP_i_withBeta] = temp[RV_VTIPASP_i_withBeta] .* (1 + beta[RV_VTIPASP_i_withBeta_beta]);`); const times = (_.size(model.psubs) > 1) ? ".*" : "*"; output.transformedParameters.statements.push(` RV_VTIPASP = temp + RV_VTIPASP_raw .* (sigma_v0 + temp ${times} sigma_v1${(_.size(model.psubs) > 1) ? "[RV_VTIPASP_i_psub]" : ""}); // volume aspirated into tip`); output.transformedParameters.statements.push(` }`); } function handle_transformed_parameters_RV_U(model, output) { const rvs = model.RV_U; if (rvs.length == 0) return; const n = _.size(model.gammas); output.transformedData.definitions.push(` int NGAMMA = ${n}; // Number of gamma parameters`); addCentralizedParameter_mean("gamma", true, n, 1, "NGAMMA", "<lower=0>", output); addCentralizedParameter_sigma("cv_gamma", 1, 1, output); output.parameters.push(` vector<lower=0>[${rvs.length}] RV_U_raw; // unintended dilution due to extra water dispense volumes`); output.transformedParameters.definitions.push(` vector<lower=0>[${rvs.length}] RV_U = gamma[{${rvs.map(rv => rv.idx_gamma)}}] .* (1 + RV_U_raw * cv_gamma); // unintended dilution due to extra water dispense volumes`) output.model.push(` RV_U_raw ~ normal(0, 1);`); } function handle_transformed_parameters_RV_V(model, output) { const rvs = model.RV_V; if (rvs.length == 0) return; output.transformedParameters.definitions.push(` vector<lower=0>[${rvs.length}] RV_V; // concentrations`); output.transformedParameters.statements.push(""); _.forEach(rvs, (rv, i) => { if (rv.type === "vWellAsp") { // if (rv.ref_vWell0) { // VolTot_t[j] = sum of volumes output.transformedParameters.statements.push(` RV_V[${rv.idx}] = RV_V[${rv.ref_vWell0.idx}] - RV_VTIPASP[${rv.ref_vTipAsp.idx}]; // volume in ${rv.well} after aspirating`); // } // else { // // VolTot_t[j] = sum of volumes // output.transformedParameters.statements.push(` RV[${idx + 1}] = -RV_VTIPASP[${rv.ref_vTipAsp.idx}]; // volume aspirated from well`); // } } else if (rv.type == "vWellDis") { // Summands for well volume const l = []; if (rv.ref_vWell0) { l.push(`RV_V[${rv.ref_vWell0.idx}]`); } l.push(`RV_VTIPASP[${rv.ref_vTipAsp.idx}]`); if (rv.ref_uWellDis) { l.push(`RV_U[${rv.ref_uWellDis.idx}]`); } // Add volume to well as sum of volumes output.transformedParameters.statements.push(` RV_V[${rv.idx}] = ${l.join(" + ")}; // volume in ${rv.well} after dispensing`); } }); } function handle_transformed_parameters_RV_C(model, output) { const rvs = model.RV_C; if (rvs.length == 0) return; output.transformedParameters.definitions.push(` vector<lower=0>[${rvs.length}] RV_C; // concentrations`); output.transformedParameters.statements.push(""); _.forEach(rvs, (rv, i) => { if (rv.type === "c") { if (rv.hasOwnProperty("value")) { output.transformedParameters.statements.push(` RV_C[${i+1}] = ${rv.value}; // concentration of ${rv.of}`); } else if (rv.hasOwnProperty("alpha_k")) { output.transformedParameters.statements.push(` RV_C[${i+1}] = ${rv.alpha_k}; // concentration of ${rv.of}`); } } else if (rv.type == "cWellDis") { // console.log({rv}) // C_t[j] = (c[i,j-1] * v[i,j-1] + cTip * vTip) / (v[i,j-1] + vTip) if (rv.ref_cWell0 && rv.ref_cTipAsp) { const cWell0 = `RV_C[${rv.ref_cWell0.idx}]`; output.transformedParameters.statements.push(` RV_C[${rv.idx}] = (${cWell0} * RV_V[${rv.ref_vWell0.idx}] + RV_C[${rv.ref_cTipAsp.idx}] * RV_VTIPASP[${rv.ref_vTipAsp.idx}]) / RV_V[${rv.ref_vWell1.idx}]; // concentration of ${rv.of}`); } else if (rv.ref_cWell0) { const cWell0 = `RV_C[${rv.ref_cWell0.idx}]`; output.transformedParameters.statements.push(` RV_C[${rv.idx}] = (${cWell0} * RV_V[${rv.ref_vWell0.idx}]) / RV_V[${rv.ref_vWell1.idx}]; // concentration of ${rv.of}`); } else if (rv.ref_uWellDis) { output.transformedParameters.statements.push(` RV_C[${rv.idx}] = (RV_C[${rv.ref_cTipAsp.idx}] * RV_VTIPASP[${rv.ref_vTipAsp.idx}]) / RV_V[${rv.ref_vWell1.idx}]; // concentration of ${rv.of}`); } else { output.transformedParameters.statements.push(` RV_C[${rv.idx}] = RV_C[${rv.ref_cTipAsp.idx}]; // concentration of ${rv.of}`); } } }); } function handle_transformed_parameters_RV_A(model, output) { const rvs = model.RV_A; if (rvs.length == 0) return; output.transformedParameters.definitions.push(` vector<lower=0>[${rvs.length}] RV_A; // absorbance measurements`); output.transformedParameters.statements.push(""); // A ~ normal(Av + vWell * cWell, (Av + vWell * cWell) * sigma_a) // A0 readouts const rvs_A0 = rvs.filter(rv => rv.ref_a0); if (rvs_A0.length > 0) { const idxs = rvs_A0.map(rv => rv.idx); const idxs_A0 = rvs_A0.map(rv => rv.ref_a0.idx); output.transformedData.definitions.push(` int<lower=1> RV_A_i1[${idxs.length}] = {${idxs}};`) output.transformedData.definitions.push(` int<lower=1> RV_A_i1_A0[${idxs_A0.length}] = {${idxs_A0}};`) output.transformedParameters.statements.push(` RV_A[RV_A_i1] = RV_A0[RV_A_i1_A0]; // absorbance of empty wells`); } // AV readouts const rvs_AV = rvs.filter(rv => rv.ref_av && !rv.ref_cWell); if (rvs_AV.length > 0) { const idxs = rvs_AV.map(rv => rv.idx); const idxs_AV = rvs_AV.map(rv => rv.ref_av.idx); output.transformedData.definitions.push(` int<lower=1> RV_A_i2[${idxs.length}] = {${idxs}};`) output.transformedData.definitions.push(` int<lower=1> RV_A_i2_AV[${idxs_AV.length}] = {${idxs_AV}};`) output.transformedParameters.statements.push(` RV_A[RV_A_i2] = RV_AV[RV_A_i2_AV]; // absorbance of water-filled wells`); } // A readouts const rvs_A = rvs.filter(rv => rv.ref_cWell); if (rvs_A.length > 0) { const idxs = rvs_A.map(rv => rv.idx); const idxs_AV = rvs_A.map(rv => rv.ref_av.idx); const idxs_V = rvs_A.map(rv => rv.ref_vWell.idx); const idxs_C = rvs_A.map(rv => rv.ref_cWell.idx); output.transformedData.definitions.push(` int<lower=1> RV_A_i3[${idxs.length}] = {${idxs}};`) output.transformedData.definitions.push(` int<lower=1> RV_A_i3_AV[${idxs_AV.length}] = {${idxs_AV}};`) output.transformedData.definitions.push(` int<lower=1> RV_A_i3_V[${idxs_V.length}] = {${idxs_V}};`) output.transformedData.definitions.push(` int<lower=1> RV_A_i3_C[${idxs_C.length}] = {${idxs_C}};`) output.transformedParameters.statements.push(` RV_A[RV_A_i3] = RV_AV[RV_A_i3_AV] + RV_V[RV_A_i3_V] .* RV_C[RV_A_i3_C]; // absorbance of wells with dye`); } // Indexes for A ~ normal(...) model if (model.absorbanceMeasurements.length > 0) { const idxs_A = model.absorbanceMeasurements.map(x => x.ref_a.idx); output.transformedData.definitions.push(` int<lower=1> A_i_A[${idxs_A.length}] = {${idxs_A}};`) output.R.push("df_RV_A = tribble("); output.R.push(" ~l, ~well, ~a, ~A, ~A0, ~AV, ~V, ~C"); _.forEach(model.absorbanceMeasurements, (x, i) => { const ref_a = x.ref_a; const rv = model.RV_A[ref_a.i]; if (rv.ref_a0) { output.R.push(` ,"${rv.l}", "${rv.wellPos}", ${i + 1}, ${rv.idx}, ${rv.ref_a0.idx}, NA, NA, NA`) } else if (rv.ref_av && !rv.ref_cWell) { output.R.push(` ,"${rv.l}", "${rv.wellPos}", ${i + 1}, ${rv.idx}, NA, ${rv.ref_av.idx}, NA, NA`) } else if (rv.ref_cWell) { output.R.push(` ,"${rv.l}", "${rv.wellPos}", ${i + 1}, ${rv.idx}, NA, ${rv.ref_av.idx}, ${rv.ref_vWell.idx}, ${rv.ref_cWell.idx}`) } }); output.R.push(")"); } } module.exports = { createEmptyModel, addLiquid, assignLiquid, measureAbsorbance, measureWeight, aspirate, dispense, printModel, };