pilotr (development version)
Read this first
power_design()refuses designs whose rows are correlated. It t-tests every row as an independent observation, yet it accepted any Gaussian design with a two-level between factor. That included designs crossed with items, designs with a within factor and subjects nested in sites. Take 30 subjects crossed with 20 items, with by-subject and residual standard deviations of 1 and a by-item one of 0.3. With no true effect, the row-wise t-test was significant in 105 of 200 replicates, against 16 for a t-test of the subject means on the same data. The power it reported was inflated and its Type M understated. It now takes one row per subject and no clustering, and otherwise names the item unit, within factor or grouping factor it found and points topower_mixed(). The no-code app, installed or in the browser, applies the same rule.brms_bridge()andgenerate_design_analysis()emit models that brms accepts, with priors on the data’s scale. A prior on random-effect standard deviations was emitted for designs without random effects, which brms rejects, so four of the eight shipped examples failed before sampling. An interaction key was used as written,cond:age, where brms names the coefficientage:cond, so brms refused a fifth example’s prior on it. The priors were also fixed at a unit scale described as standardised, although nothing standardised the data. On a Gaussian design with a residual SD of 10, the effect’s prior was N(0, 0.5) and the Savage-Dickey Bayes factor was about 1 whatever the data. Priors for the continuous families now scale with the response and with each column’s realised standard deviation, the intercept takes brms’s default, and the emitted call says that its data come frommodel_data(). Emitted scripts, priors and Bayes factors change.generate_design_analysis()accepts a focal interaction with its parts in either order and writes it into the script under the name brms gives it.brms_bridge()validates a specification given as a list, as the other entry points do, since its priors are now measured on the data that specification simulates. It refuses aprior_scaleorinteraction_scalethat is not a single positive number, and a specification that gives one interaction two keys, such ascond:ageandage:cond, since brms estimates one coefficient for both. A coefficient whose column does not vary is reported in a warning.A specification with two or more factors between the same unit is refused unless it declares specification version 0.4, which allocates such factors jointly. pilotr assigned the levels of each between factor to blocks of units on its own, so two such factors fell into the same or overlapping blocks. A 2 × 2 between-subjects design over 40 subjects produced cells of 20, 0, 0 and 20, and the second effect and the interaction could not be estimated. The refusal names the encoding that works in every version, one between factor whose levels are the cells.
spec_from_model()now builds that factor itself when it reads two factors between one unit off a fitted pilot, and says so in a message. It used to write a specification that left half the cells of a 2 × 2 pilot empty.Specification version 0.4 allocates several between factors on one unit jointly, so a 2 × 2 between-subjects design over 40 subjects has four cells of 10. The units fall into one block per combination of levels, with the first-listed factor varying slowest, and nothing is drawn. A specification using this must declare
"spec_version": "0.4", a version pilotr 0.3.x refuses to read. One that does not still gets the refusal introduced above, which names both remedies.build_spec()andspec_from_model()declare the lowest version a specification needs, so their output stays readable by earlier releases unless it uses a 0.4 feature. A 0.4 specification whosevary_withinomits a unit of the design is refused.spec_from_model()turned a counterbalanced pilot, each subject seeing each item under one condition, into a specification that crosses every subject with every item under every condition, without saying so. Planning a counterbalanced study that way doubles the observations per subject and overstates power. By the formulas of Judd, Westfall and Kenny (2017), thecrossed_mixed_rtexample, with 30 subjects, 24 items and an effect of 0.05 on the log scale, has a power of about 0.80 crossed and 0.55 counterbalanced. The specification it returns for such a pilot now setscounterbalanceon the factor, a field of specification version 0.4 (see New features).counterbalance = FALSEreturns the crossed specification, which earlier releases read, with a warning. The warning names the field, and for a two-level factor also the encoding that earlier releases read, a list factor between subjects and an item-set factor between items.power_mixed()andprecision_design()fit the model each family needs. They fitted a linear mixed model to every response, so forbernoulliandpoissonoutcomes the estimate was a difference on the response scale, compared with a true value on the link scale. Take 20 subjects crossed with 16 items and an effect of 0.5 on the logit scale. Over 30 replicates, the mean estimate was 0.090 and Type M 0.21, where a logistic mixed model of the same replicates gives 0.47 and 1.12, so real exaggeration read as underestimation. Against a region of practical equivalence of 0.1, the interval was 0.14 wide and fell outside the region in none of the replicates, against 0.71 wide and 17 of 30 on the logit scale. Counts on the same crossing, with an effect of 0.3 on the log scale, gave a mean estimate of 0.90 and Type M 3.01 over 20 replicates, against 0.31 and 1.02. Bernoulli and Poisson designs are now fitted withlme4::glmer()and tested with a Wald z, so Type M, mean estimates, interval widths and ROPE decisions change materially. Power barely moves: 22 of the 30 accuracy replicates are significant under either model. A Wald z is the test Bolker et al. (2009) recommend for such a model. With few subjects or items, it rejects more often than its nominal level (Li and Redden, 2015).A design without random effects, which failed every replicate with “No random effects terms specified in formula”, is fitted with
lm()orglm(), so every between-subjects example has a power path. That includes the shippedpoisson_counts_between,ordinal_likert_betweenandbeta_proportion, which had none in either language.For
ordinalandbetaoutcomes, which have no frequentist backend here yet, both functions warn, withhold the mean estimate, Type M and the ROPE decisions, and point togenerate_design_analysis(). A later release will refuse them or fit a proportional-odds and a Beta model. Type S, which compares signs, is kept for main effects and withheld for interactions. A bounded response compresses differences near the ends of its range, so an interaction that is positive on the logit scale can be negative on the response scale.Results name the fitter and the test.
power_mixed()returns them asfitterand prints them, andprecision_design()and the sweeps carry them as a column. A formula given by the user is fitted as written, bylmer(), or bylm()when it has no random terms, whatever the family.calibrate_response(tune = "all")holds the intercept and rescales only what carries variance, since the intercept is a location. Multiplying the intercept too raised every response of the lognormal families, less any shift, to the power of the scaling factor. The documented example turned a median reaction time of 600 ms into 16.6 hours. Forpoissonandbeta, the search for the factor began where every term, the intercept included, is zero.poisson_counts_between, whose own total variance is 0.27, was refused a target of 1 by a message that put the family’s residual alone at 1.64. A target of 2 returned an intercept of 11.4, too large to simulate. The example now calibrates to either target and simulates. Through a log or logit link, the output changes materially. Forgaussianandexgaussian, only the intercept of the output changes, and no power or precision result for a coefficient does.scale_intercept = TRUErestores the old behaviour.Each point of a sweep draws its own replicates. Every grid point reused the specification’s seed, so neighbouring points shared their random numbers, while
solve_curve()treats them as independent. Over 1,000 emulated curves a setting, its nominal 95% intervals covered the true sample size in 88% of solves on a tight grid, and the true effect in 70% on an effect-size axis. Grid point i ofsweep_spec()now takesreplicate_seeds(seed, length(values))[i], which brings the two to 96.5% and 95.7%. Every number a sweep produces changes, inpower_curve_mixed(),precision_curve(), the power curve of the no-code app, installed or in the browser, and the HPC precision-array job, whose tasks a localprecision_curve()over the same grid now reproduces.seeds = "shared"(.seedsinsweep_spec()) keeps the old behaviour. The coverage figures quoted forsolve_curve()come from a new calibration,tools/calibration/solve_curve_coverage.R, where the earlier 35 in 36 rested on 12 curves whose points shared their seeds. Rerun with independent points, those 12 curves no longer favour the probit link over the logit, and the documentation now says so.
New features
calibrate_response()gainsdenominator = "random", which calibrates the random components plus the residual. The coefficient of a contrast codedc(-0.5, 0.5)is then Cohen’s d in the sense of Westfall, Kenny and Judd (2014). The documentation no longer calls the total, which includes the fixed effects, the right denominator for an effect size, and?response_varianceshows how to turn a published d into a coefficient. Inreading_time_continuous, whose fixed effects carry 15% of the total,condcalibrates to 0.127 against the random components and the residual, and to 0.117 against the total.Every rate carries its Monte Carlo error.
power_design()gainspower_mcse,power_loandpower_hi, the standard error and 95% Wilson interval of power, whichpower_mixed()already reported, and it still returns a plain list. Both functions gaintype_m_mcse, the standard error of Type M, and a Wilson interval for Type S,type_s_loandtype_s_hi, since the standard error of Type S is zero whenever no sign is wrong.power_curve_mixed()andsweep_spec()return them as columns. Type S and Type M rest on the significant replicates alone, which are often a handful, yet they were reported without any uncertainty. Printing apower_mixed()result shows them in a second table, and the no-code app, installed or in the browser, shows each rate with its error. The Python twin computes these values with the same operations in the same order, and both test suites pin them bit for bit.power_mixed()andprecision_design()simulate a preregistered simplification rule. An analysis plan that drops the random correlations, then the slopes, whenever a fit is singular analyses each replicate with a model that depends on its data. Its power and Type I error rate are therefore not those of the maximal model, which pilotr fitted to every replicate.formulatakes a ladder of models, andfallback,"singular"or"warning", says when a replicate moves on to the next rung. Each replicate keeps the first fit the rule accepts, or else the last rung’s fit.model_formula()gainsstructure = c("maximal", "zcp", "intercepts"). A ladder written as those names, such asc("maximal", "zcp", "intercepts"), is fitted with the model the family needs, and a list of formulas is fitted as written.power_mixed()returns and prints the share of the replicates analysed at each rung asrung_shares, andprecision_design(),precision_curve()and the sweeps return it asrung_<name>columns.precision_curve()gainsfallbackas well. The power vignette runs the rule on its priming design, null condition included, since a rule that can end on random intercepts alone can reject a true null hypothesis too often (Barr et al., 2013; Matuschek et al., 2017). A single formula, the default included, gives the results it gave before.spec_from_model()reports which fixed effects it took from the pilot, and gainseffects = "safeguard", which replaces each effect by the bound of its 60% interval nearer zero (Perugini, Gallucci and Costantini, 2014), andsesoi, which sets named effects to a smallest effect size of interest. A pilot’s point estimates are noisy and, when a pilot is followed up because it looked promising, biased upwards, so a study powered for them tends to be underpowered (Albers and Lakens, 2018). An interval that includes zero leaves even the sign of the effect open, so the safeguard then stops with an error that points tosesoi. The result records the choice in itseffectsattribute. By default, the coefficients are the pilot’s estimates, as before.?spec_from_modelexplains when each choice fits, and that a namedfocalinpower_mixed()leaves the simulated effect as it is.counterbalanceon a within factor gives each subject one condition per item, rotating the lists across subjects and items, as in a Latin-square design. Planning such a study with a fully crossed specification doubled the observations per participant and overstated power. Subject s meets item t in condition ((s − 1) + (t − 1)) mod C, where C counts the conditions, or the combinations of levels when several factors are counterbalanced, and nothing is drawn. The field belongs to specification version 0.4, needs an item unit and is refused withper_subject. A warning says when the lists are unequal, because the subjects or the items are not a multiple of C.model_formula()andmodel_data()are unchanged, since with at least C subjects and C items every subject meets every condition across items and every item every condition across subjects.build_spec()gainscounterbalance = TRUE, and the no-code app, installed or in the browser, offers it as “Counterbalanced lists” for a within design with items.spec_from_model()sets the field when it reads a counterbalanced pilot, as the next entry describes. The two-list encoding of the specification suits two conditions only, and it stays documented for releases that read no version above 0.3.The simulated data of a counterbalanced specification name each subject’s list in a column
list, placed aftersubjectanditem. Subject s follows list ((s − 1) mod C) + 1, and nothing is drawn for it, so a specification without a counterbalanced factor simulates as before. Such a design keeps the namelistfor that column, andvalidate_spec()refuses a factor, predictor, grouping factor, contrast column or response of that name in it.spec_from_model()recognises a counterbalanced pilot and returns a specification-0.4 design withcounterbalanceset, and says so in a message.counterbalance = FALSEkeeps the earlier output, a crossed specification that releases reading 0.3 accept, with a warning. A pilot whose subjects each saw only some of the items keepsper_subjectand stays crossed, with the warning, since the field needs every subject to see every item. A pilot whose model has a term namedlist, as when the list is fitted beside the condition, stays crossed too, with a warning that asks for that term to be renamed.power_design(),power_mixed()andpower_curve_mixed()gainalternative, which plans the one-sided test of a directional prediction with"greater"or"less". Every earlier result was two-sided, and the default,"two.sided", leaves every number as it was. The one-sided p-value comes from the statistic behind the two-sided one, a t with its degrees of freedom fromt.test(),lm()or lmerTest, or a Wald z. Type S and Type M are taken over the replicates significant in the tested direction.power_design()records the test in a new element,alternative, and thefitterofpower_mixed()names the direction. On the same replicates, a one-sided test at alpha rejects exactly where the two-sided test at 2 alpha rejects with the estimate in the tested direction. A two-sided result at 2 alpha from an earlier version therefore gives the one-sided power at alpha, in the direction of the true effect, aspower * (1 - type_s).focalinpower_mixed()andpower_curve_mixed()accepts named sets of coefficients, each tested jointly, so the power of a multi-degree-of-freedom effect, such as a three-level factor or its interaction, can be planned.list(interaction = c("A1_B", "A2_B"))tests the two coefficients of a 3 × 2 interaction together. Each coefficient could only be tested by itself, and the power of the joint test differs from that of either coefficient. A linear mixed model has Satterthwaite’s F fromlmerTest::contest(), aglmer()orglm()fit a Wald chi-square and anlm()fit the F test of the model with and without the set, so a set of one coefficient reproduces the single-coefficient test. Each set’s power carries its Monte Carlo error and Wilson interval, andfitternames the joint test. A joint test has neither a single estimate nor a direction, so Type S and Type M areNAand a one-sidedalternativeis refused. The result records the sets, its printout lists their coefficients and a sweep adds acoefficientscolumn.precision_design()andprecision_curve()refuse sets, since a set has no single interval. The Python twin’spower_mixed()has no joint test, and its documentation says so.power_report()writes a sample-size justification in Markdown from a power result and, optionally, a solved curve. The documentation called the sample size fromtarget_n()the number a preregistration quotes, but the justification behind it had to be assembled by hand. The report gives the items Lakens (2022, Table 4) asks for. It states the primary analysis with its fitter, test, alpha and sidedness, and the desired power with the solved sample size, its interval, transform and heterogeneity factor. Each focal effect follows with the justification given for its size, andnull_resultgives the power when every focal effect is zero. The simulation is accounted for as Morris, White and Crowther (2019) recommend, with the replicates, the fit counts, the share analysed at each rung of a simplification rule and the Monte Carlo standard error and Wilson interval of every rate, computed in the report from the counts. The report ends with the versions of pilotr, R, lme4 and lmerTest, the specification’s seed and an MD5 checksum of the specification file’s bytes. For aprecision_design()result, each focal effect has its mean interval width and the probabilities of the two decisions about the region of practical equivalence, whichropestates. The Python twin’spower_report()writes the same text, byte for byte, from the same counts, whichtools/parity/report_cross.pychecks in CI. The power vignette writes one for its priming design.
Fixes
Printing a
power_mixed()result appliesdigitsto the interval bounds as well as to the rates. The bounds were written with three decimals whateverdigitssaid, and the documentation calleddigitssignificant digits, where it counts decimal places.The no-code app, installed or in the browser, sends a design its power tab cannot analyse to the analysis that fits it. It sent every such design to the
power_mixed()of both packages. The Python one is now offered only for one within factor crossed with items, in a family whose coefficients are on the scale it analyses, and ordinal and Beta designs are sent togenerate_design_analysis()andbrms_bridge().The no-code app, installed or in the browser, keeps the session open after an input its power tab cannot use, and says what is wrong. An N of 2, a cleared N subjects box or a pasted three-level design ended the session, and the design was lost with it. The Power tab now asks for at least 4 subjects and reports any other error as text. In the installed app, a pasted specification that
load_spec()refuses made the power buttons do nothing, and its refusal now appears there.In the no-code app, installed or in the browser, the Data tab, the summary, the plot and the data download show the last simulation, labelled with its own seed, and a note above the tabs says when the controls have changed since. The summary and the plot failed after a change of family or response name and lost the grouping after a change of factor name. The data download was simulated afresh from the controls, so it could differ from the table on screen, and the installed app’s Data tab printed the Seed box, which after a change or under a pasted specification was not the seed of the data. The download is now named after the simulated specification and its seed. The plot of a pasted design whose factor has more than two levels used to stop with “Insufficient values in manual scale” and now draws.
The no-code app’s power curve, installed or in the browser, extends its grid until it brackets the 0.80 target. The grid stopped at twice the current N, which for the default design is the target itself (127.5 subjects by
power.t.test()), so about half of all runs were refused and those that solved were biased low, at 122 subjects on average in an emulation of 3,000 runs. A point half as large again is now added above the grid while the top point’s power falls short of 0.80 by more than twice its Monte Carlo error, and a point two thirds the size is added below it while the bottom point’s power exceeds 0.80 by as much. The grid stops at 8 times N in the installed app and at 2,000 subjects in the browser, and holds 10 points at most. Each point keeps the seed it was first given. At the six seeds the app’s tests use, the default design now solves every time, to a mean of 129 subjects, where three of the six were refused. For a design without random effects or continuous predictors, the Power tab also gives the sample sizepower.t.test()computes, as a check on the curve’s.run_app()restores the caller’s future plan when the app stops, sets a plan only over a sequential one, with two workers in place of one for every core, and honoursasync = FALSE, which the app ignored. It used to end by setting a sequential plan, discarding any plan set before. Withasync, the power curve now runs in the background process as well as the power analysis. The documentation ofrun_app(), the README and the article on the no-code app say what this does: it frees the app’s R process for other sessions, while the session that started a run waits for its result. They used to say that the app does not block and that its simulations use all of your local cores.n_warningno longer counts boundary-singular fits, whichn_singularalready counts. lme4 records its singular-fit notice among the optimiser’s messages, so every singular fit was also counted as a fit with warnings, and a run could not show whether any fit had failed to converge.n_convergedandn_singularare unchanged.Every specification field is read by its exact name. R looked fields up with
$, which matches a name partially, so a design whose random entries weresubject_siteand nosubjectacquired by-subject random effects it never declared, and its data differed from the Python twin’s in every row. Withstrict = FALSE, a misspelt field such asroundingacted as the field it resembled, although the warning said it would be ignored.load_spec()andvalidate_spec()refuse a specification that repeats a key within one object. JSON leaves a repeated key undefined and the two readers disagree:{"grp": 0.3, "grp": 0.5}applied 0.3 twice in R and 0.5 once in Python. The refusal does not depend onvalidate.A seed must lie within ±(2^53 − 1), the range in which both twins read a JSON integer exactly. Larger seeds were read as different numbers by the two readers, which then drew different data.
build_spec()andspec_from_model()keep a seed of 2^31 or more, which they used to turn intoNA, and leave a non-whole seed forvalidate_spec()to refuse, where they used to truncate it. Seeds in the integer range are stored as before. The app accepts a larger seed for the same reason and prints it in full.A factor level, a
vary_withinentry or acorrelatedflag written asnullis refused, as the Python twin refuses it. R labelled those rowsNAand read the flag asfalse.A malformed
response,predictors,randomorspec_versionvalue is reported in the validator’s own words. Some shapes stopped with a base R error, and aspec_versiongiven as an array of two versions was read as the first.load_spec()reads a file that starts with a byte-order mark without warning about it. The specification format allows one, since some Windows editors add it.design_conditions()changes only the effects it names. Throughsweep_spec(), its conditions replaced the whole coefficient list, so every coefficient they did not name was dropped from data generation, frommodel_formula()and from the default focal set, contrary to the documentation. Onreading_time_continuous, a sweep ofcondsimulated and analysed a one-coefficient model. Conditions are now merged into the specification’s own coefficients, in its order, and a condition naming a coefficient the specification lacks is refused. They carry the classpilotr_conditions, which subsetting them with[and joining them withc()keep. Any other list still replaces the addressed field wholesale. With 0.3.x, pass.base = spec$fixed$coefficientstodesign_conditions()for the same result.The
sweep_spec()example no longer runs the null condition twice.pilotr::run_app()works in a session that has not attached pilotr. The app looked for the package’s functions among attached packages and then for source files, and in an installed package found neither, so the documented launcher stopped with “no pilotr engine sources”. The app now takes the functions from the package’s namespace, without attaching the package.validate_spec()refuses specifications whose names collide. Each of these validated and then moved an effect, rescaled a variance or overwrote a column without a word: two columns with one name (a factor calledsubject, a response named like a factor), a contrast column defined by two factors or named like a predictor, a unit, a grouping factor, another factor or the response, an interaction whose analysis columna_balready exists, a level listed twice, a correlation pairing a term with itself or giving one pair twice, a factor both between and within a unit, and arandom.itementry in a design without items, which was dropped. Each refusal reads the same in the Python twin. The rules are set out under Names in the specification.validate_spec()refuses a factor, predictor or extra grouping-factor name that is empty or made only of spaces, and a response name made only of spaces (an empty one was already refused). Such a name validated and then became a column with no visible name. An empty factor name stoppedsimulate_design()with base R’s “replacement has length zero”, which does not say which field is at fault, and a blankrandomkey was reported asrandom. must be an object.Two refusals read as the Python twin’s do. A
correlatedflag that is not a boolean must betrue or false, as the JSON file spells it, andresponse.thresholdsmust be “a number or a non-empty numeric array”.model_data()no longer fills a factor’s later contrast columns withNAwhen the first contrast column carries the factor’s own name. With three levels, everypower_mixed()replicate then failed to fit.power_design()reports a true effect of 0, with Type S and Type M undefined (NaN), when the specification has no coefficient for the factor’s contrast, as with"coefficients": {}. It stopped with “missing value where TRUE/FALSE needed”.precision_design()andprecision_curve()checkrope. A vector was recycled across replicates, and a negative value was used as given. On the design of theprecision_design()example, the range formc(-0.02, 0.02)reported ap_meaningfulof 0.200 over 30 replicates, where0.02gave 0.067.ropetakes a half-width or an increasing pairc(lower, upper), which also allows an asymmetric region, and anything else is refused. Results for a non-negative half-width are unchanged. The HPC precision-array job reads itsROPEvariable in either form, as0.05or-0.02,0.05, and checks it in the same way.A Poisson mean above about 708 is refused. The sampler’s starting value
exp(-mean)is subnormal there, and from a mean of about 738 the counts were biased or intermittently refused, while the specification promised service up to 746. Over 40,000 draws, a mean of 744 drew counts with a mean of 733.5 and a variance of 407, and a mean of 742 was refused for 4% of them. The refusal reads as the Python twin’s and asks for a mean of at most 708.spec_json()writes each number so that ‘jsonlite’ and ‘Python’ read back exactly the double R holds. Version 0.3.1 checked each candidate width withas.numeric(). On builds with long-double arithmetic, among them R on x86 Windows and Linux,as.numeric()rounds a decimal twice, so the check accepted some 15- and 16-digit forms that a correctly rounded reader puts one unit in the last place away, about one number in 5,000 to 7,000. A specification saved and reloaded then simulated different unrounded data, in R and in ‘Python’ alike. Candidates are now checked with ‘jsonlite’, the readerload_spec()uses.generate_r_script()andgenerate_design_analysis()embed the specification as the JSON textspec_json()writes, and the script reads it withload_spec(), so ‘jsonlite’ recovers every number exactly on every build of R. The specification used to be written as R code. A build without long-double arithmetic, such as R on Apple silicon, can parse a 17-digit decimal in R code one unit in the last place away, and likewise a 16-digit one whose digits exceed 2^53. A script carrying a fitted coefficient could then simulate different data there. In a design analysis, the decision rule, the gate and the true values stay plain decimals wherever every build reads them exactly, as it does any value typed with up to four decimal places. Any other value is read through ‘jsonlite’.Beta responses stay inside (0, 1). Near a ceiling,
x / (x + y)rounds to exactly 1 in double precision (161 of 4,000 rows at an intercept of 3 andphi = 2), and rounding sent low proportions to exactly 0 (17% of the rows at three decimals in one realistic design). brms’sBeta()refuses either value. An exact 1 or 0 now becomes the nearest double inside the interval, a rounded value at 0 or 1 becomes the nearest value inside it at that many decimals, andround = 0is refused for Beta. No other value changes, so the shipped examples simulate the same data as before.Extreme linear predictors are refused with the same message in both twins. R returned
Inffor a lognormal or shifted lognormal response whoseexp(eta + sigma * z)overflowed, and 0, 1 orNaNfor a Beta response whose shape parameter was zero or whose two Gamma variates both underflowed. Python raised a bareOverflowErrororZeroDivisionErrorin the same cases.power_curve_mixed()andprecision_curve()start one worker pool for the whole sweep, as their documentation said. Since 0.3.0 every grid point started and stopped its own, at a cost of 20 to 50 seconds a point on a loaded machine.power_mixed(),precision_design()andpower_design()gaincl, for a cluster the caller has made, which they run on and leave running. Passed throughsweep_spec(), as insweep_spec(spec, path, values, power_mixed, cl = cl), one cluster serves every point of an effect-size sweep as well.The no-code app’s Verify button runs the script that
generate_r_script()exports, in a clean R session, and reports reproduction only when the script’s data are identical, column by column, to the data the current specification produces. Otherwise, it names the first column and row that differ. It used to simulate from the JSON in that session without running the script, and compared the sums of the response column with a tolerance, so it certified a script it had never run. A specification the engine refuses is now reported beside the button, where a cleared N subjects box used to end the session.Exported scripts give the CRAN installation line, naming lme4 and lmerTest only where the suggested analysis needs them, and suggest the analysis that fits the design:
power_design()for a two-group Gaussian design with one row per subject,generate_design_analysis()for ordinal and Beta outcomes, andpower_mixed()for any other design. Every script used to suggestpower_mixed(), and its installation line said “once available”, although pilotr is on CRAN.generate_r_script()reads a specification given as the path of its file. It used to embed the path itself, so the script ran only while the file stayed at that path.The tests need testthat 3.1.5, which added
expect_no_error()andexpect_no_warning(), so DESCRIPTION now asks for it in place of 3.0.0. The continuous-integration job meant to test the declared floors had never installed them: on R 4.6, testthat 3.0.0 does not compile and ggplot2 3.4.0 does not load, and the job went on to pass on the current releases. It now runs on an older R, reads the floors from DESCRIPTION and fails unless each one is installed.The browser build of the no-code app gives the command that installs the Python package as the bare
pip install "pilotr[mixed]", with its explanation on the line before. The command ended with a#comment, which an interactivezsh, the default shell on macOS, passes on to pip, so a pasted line stopped with “Invalid requirement: ‘#’”. The R installation line keeps its trailing comment, which R ignores.power_design()refuses a design with fewer than two subjects at a level of the between factor, so fewer than 4 subjects in all, in the ‘Python’ twin’s words. With one subject at each level,stats::t.test()stopped with “not enough observations”, while the ‘Python’ twin returned a power of 0. With one subject in all, the two twins stopped with different errors, and with three, the pooled variance rested on a single degree of freedom. The no-code app already asked for 4 subjects.model_data()andmodel_formula()read a specification from its path. Both documentedspecas a path or a list, as the other entry points do, and stopped with “subscript out of bounds” when given a path. The ‘Python’ twin’s ports read one.model_formula()refuses a specification with neither a fixed coefficient nor a random effect, in the ‘Python’ twin’s words. It wrote.y ~with nothing after it, andas.formula()stopped on that with a parse error.tools/parity/formula_cross.pynow compares the two twins’ refusals as text.build_spec()assembles anexgaussianresponse. It wrote neithersigmanorbetafor the family, sovalidate_spec()refused the specification it returned, anddefault_response_name()named the responseoutcome, the name for an unknown family, where it now givesRT. The response is rounded to four decimals, as in the other continuous families but Beta.build_spec()leaves a number of subjects or items that is not a whole number forvalidate_spec()to refuse, as it already did for the seed. It truncated 10.5 subjects to 10 in silence. Whole numbers are stored as before.The installed no-code app names its specification, script and data downloads after the specification they hold, with
designfor an empty name, as the browser build does. The specification and script downloads took the Design name box, so a pasted specification was saved under the name in that box, and an emptied box named the files.json,.Rand_seed2024.csv, which are hidden on Unix.The installed no-code app labels its paste box “Design spec (JSON)”, as the browser build does. The label was empty, so a screen reader had only the placeholder to read out.
Deprecated
- A within factor whose
vary_withinomits a unit of the design now warns, in strict and lenient validation alike. pilotr has always crossed a within factor with every unit, so the list changed nothing. From spec version 0.4, such a list is refused.
Documentation
citation("pilotr")names the R package version alone. It read “R and Python package version 0.3.1” while the Python package stood at 0.3.0, and the two packages are released separately. It also gives the author’s ORCID iD now.citation("pilotr"), the About page and the repository’sREADME.mdname the R package’s publisher, in full: “Comprehensive R Archive Network (CRAN)” stands before the DOI in the reference, and the BibTeX entry gives it aspublisher. The reference named no publisher before. Every reference across the package family and the author’s website now names CRAN in the same words.citation("pilotr")and the website give the DOI that CRAN assigned to the package, https://doi.org/10.32614/CRAN.package.pilotr, in place of the Zenodo concept DOI. It names the R package across all its versions, and it is the DOI that the CRAN page and the citation R generates for a CRAN package already gave, so the R package no longer circulates under two.CITATION.cffdescribes both packages, so it keeps the Zenodo DOI, which archives every GitHub release of both alike, and now lists each package’s identifiers, marked with the package they name. The repository’sREADME.mdgives a reference for each package. The CRAN DOI and the Zenodo DOI are shown as full links on the website and in theREADME.mdfiles. The R package’sREADME.mdgains a badge for the DOI CRAN assigned, for GitHub and CRAN, which the home page leaves out of its sidebar, where the DOI is already given under Citation.The 0.3.0 and 0.3.1 entries below no longer give release dates. Both gave dates on which neither version reached CRAN, and the site already prints the CRAN date beside each entry.
The tree CRAN published as pilotr 0.3.0 is tagged
cran-0.3.0on GitHub, and 0.3.1 is taggedv0.3.1and archived on Zenodo. Thev0.3.0tag and its Zenodo record predate two changes that CRAN’s 0.3.0 carried, namely thatbrms_bridge()returns a visiblepilotr_bridgeobject and that this object has a print method.DESCRIPTION declares British English (
Language: en-GB), as the author’s other R packages do. A new spelling test checks the help pages, the vignettes,README.mdandNEWS.mdagainst that dictionary andinst/WORDLIST. It fails a check run withNOT_CRANset, as indevtools::check()and on GitHub Actions, at any word found in neither, and CRAN and a plainR CMD checkskip it. The word list holds the names, terms and abbreviations that the dictionary lacks, none of them a misspelling, and seven passages of the help and vignettes are reworded so that the checker can read them.sweep_spec()andsolve_curve()say that a sweep over lists,design_conditions()included, records a grid index in its leading column, and thatsolve_curve()solves for that index unlessxnames another column. Both pages show the sweep to use for a single effect,sweep_spec(spec, "fixed$coefficients$cond", values, power_mixed). It keeps the other coefficients, andsolve_curve()returns a minimum detectable effect from its curve.The specification states how units are allocated: the block rule for between factors and extra grouping factors, the nesting of a between factor within clusters that follows from it, and the full crossing of within factors. It gives worked encodings for a 2 × 2 between design, a two-list counterbalanced design and randomisation within clusters, each held bit-identical across the twins by the parity harness. The power vignette says that its priming example is fully crossed, and that simulated data are complete, so expected attrition is allowed for by recruiting N / (1 − p).
The precision vignette says that its priming example is fully crossed, and that a counterbalanced study has half the observations and is planned with
counterbalance = TRUEinbuild_spec()or with the two-list encoding.?spec_from_modelexplains the counterbalancing warning, the field and that encoding. It also shows aprepfunction forpower_mixed()andprecision_design()that drops the rows a counterbalanced study would not observe and runs in the worker processes ofworkers > 1.The cross-language guarantee is stated as it holds, in the specification, the README files,
DESCRIPTION, the vignettes, the apps’ guides and the landing page. R and Python produce bit-identical data wherever they share a maths library, as on Linux. On Windows they do not, and a value that passes throughexp(),log()orpow()can differ in its last bits. Measured there, about 1.4e-5 of normal draws differed, since the tail branch of AS 241 callslog(), so Gaussian data are not exempt. Lognormal and ex-Gaussian responses differed by one unit in the last place and unrounded Beta responses by up to 35, and a discrete outcome can in principle move by a whole category. The guarantee used to cover the Gaussian family everywhere and every family withroundset, althoughroundis refused for the discrete families. Rounding has removed every continuous difference observed, but it guarantees nothing. One Beta case can differ on Linux too. From R 4.6.0, R’s^computes a cube or a fourth power by repeated multiplication, where Python callspow(). A Beta response whose Gamma shape is exactly 1/3 or 1/4 can therefore differ in its last bits on any platform, and R’s own data for such a design changed with R 4.6.0. Within one language, the data are identical on one platform, and across operating systems on the same terms as across languages. The vignettes show determinism withidentical(), sinceall.equal()allows a relative difference of 1.5e-8.The specification documents every sampling rule a third implementation needs, in a section of pseudocode: the partial Fisher-Yates shuffle, the block rules, uniform predictors and reliability, the Cholesky factorisation, the two-branch inverse logit, each family’s response, the Poisson walk, the ordinal rule, the Marsaglia-Tsang Gamma sampler and the Beta ratio. It corrects the Gamma sampler’s draw consumption. A proposal draws a uniform only when its normal gives a positive
v, and a boosted variate draws its uniform after the Gamma(shape + 1) variate. The schema refuses an unknown top-level field and a response parameter its family does not use, requires each family’s parameters, and accepts the single-value forms ofvary_withinandthresholds, as strict validation does.?build_specsays why a Beta response is left unrounded: at four decimals, rounding put 9.8% of the rows of one design on 0.0001 or 0.9999, and such rows dominate a Beta likelihood.?replicate_seedsgives the right reason why consecutive seeds correlate. Seeding is affine in the seed, so the first draws of consecutive seeds step by a constant mod 1 and correlate 0.95, and a longer warm-up changes only the step (Matsumoto et al., 2007). It said that consecutive replicates began a few steps apart in one sequence.The power and precision vignettes show results produced by this version, with the denominator of power stated correctly (
n_returned), Wilson intervals taken from the returned columns and the model thatpower_mixed()fits. Their cached results predated 0.3.0. The precision vignette no longer says that every replicate converged, and it computes each figure it quotes from the cached sweep.tools/vignette-caches.Rin the source repository regenerates the caches, and a test fails when the seeds, the generative engine or a cached chunk change and the caches do not.?power_curve_mixed,?solve_curveand the power vignette say that when an effect varies from item to item, power at a fixed number of items levels off below 1, however many subjects are added (Westfall, Kenny and Judd, 2014; Judd, Westfall and Kenny, 2017). A curve over subjects that levels off near the target can then be solved far from the true crossing, or as though it crossed when it never does. The vignette shows how to compare designs that trade subjects for items, by sweeping the number of items at a few subject counts and solving each curve.The HPC job runs on any SLURM cluster and calls
precision_design(), so its output has the same columns and fit accounting asprecision_curve(). The job charged the author’s account, wrote under the author’s project paths and ran a copy of its runner kept outside the repository, with a bootstrap script the repository did not hold. Its runner repeated the replicate loop and counted every fit that returned an estimate as converged, the definition 0.3.0 replaced. The wrapper now marks the account and the project directory as the two values to fill in, and runs the runner saved beside it. The README gives the commands that install pilotr, lme4 and lmerTest into the project’s library, shows how the task files combine into a curve forsolve_curve(), and limits the claim of exact reproduction to the simulated data, since the model fits depend on the lme4 release and the platform.The power vignette’s table of fitters, the cross-language vignette and
README.mddescribe the Python twin’spower_mixed()as it now stands. It fits the model the specification implies, with each random effect as an independent variance component, from ports ofmodel_formula()andmodel_data().tools/parity/formula_cross.pyholds the ports to this package’s functions on every shipped example and parity case, the formulas character for character and the contrast and product columns bit for bit.Both apps’ guides say that crossed mixed-effects power runs in R through lme4, the reference backend, and in Python through statsmodels. They named lme4 for both. Before a run, the installed app’s power tab said that simulations were unlimited once you install the package, although the app is part of the installed package. It now gives the app’s cap, which
PILOTR_MAX_SIMSsets, and says to callpower_design()directly for more. The vignette on the no-code app names the tabs of both apps. It named only the tabs of the browser build.The website’s R pages link favicons that exist. pkgdown 2.1.2 changed the set of favicon files its pages link, and three of them,
favicon-96x96.png,favicon.svgandsite.webmanifest, returned 404 on every page.tools/favicons/build_favicons.Rin the source repository builds the set offline. In a browser tab, the browser build of the app is titled “pilotr: no-code app”, not “Shiny App”, and shows the pilotr icon. The theme toggle on the site’s landing and not-found pages works in a browser that blocks site storage, where its script used to stop before the toggle was wired up. The contributing guide names the checks intools/parity/that CI runs, in place of a comparison of CSV files to a tolerance of 1e-6. The note on advanced designs in the repository describes specification 0.3.?load_specand?simulate_designsay that avalidateother thanTRUEorFALSEvalidates leniently, withstrict = FALSE.?load_specsaid that such a value was passed tovalidate_spec()as itsstrictargument, and?simulate_designsaid nothing of it.The
?pilotroverview lists the ex-Gaussian among the response families, and the guide in the browser build of the no-code app says that the paste box links to example specifications and to the format. The guide sent the reader to example buttons, which the app does not have.
References
Albers, C. and Lakens, D. (2018). When power analyses based on pilot data are biased: Inaccurate effect size estimators and follow-up bias. Journal of Experimental Social Psychology, 74, 187-195. doi:10.1016/j.jesp.2017.09.004
Barr, D. J., Levy, R., Scheepers, C. and Tily, H. J. (2013). Random effects structure for confirmatory hypothesis testing: Keep it maximal. Journal of Memory and Language, 68(3), 255-278. doi:10.1016/j.jml.2012.11.001
Bolker, B. M., Brooks, M. E., Clark, C. J., Geange, S. W., Poulsen, J. R., Stevens, M. H. H. and White, J.-S. S. (2009). Generalized linear mixed models: A practical guide for ecology and evolution. Trends in Ecology & Evolution, 24(3), 127-135. doi:10.1016/j.tree.2008.10.008
Judd, C. M., Westfall, J. and Kenny, D. A. (2017). Experiments with more than one random factor: Designs, analytic models, and statistical power. Annual Review of Psychology, 68, 601-625. doi:10.1146/annurev-psych-122414-033702
Lakens, D. (2022). Sample size justification. Collabra: Psychology, 8(1), 33267. doi:10.1525/collabra.33267
Li, P. and Redden, D. T. (2015). Comparing denominator degrees of freedom approximations for the generalized linear mixed model in analyzing binary outcome in small sample cluster-randomized trials. BMC Medical Research Methodology, 15, 38. doi:10.1186/s12874-015-0026-x
Matsumoto, M., Wada, I., Kuramoto, A. and Ashihara, H. (2007). Common defects in initialization of pseudorandom number generators. ACM Transactions on Modeling and Computer Simulation, 17(4), 15. doi:10.1145/1276927.1276928
Matuschek, H., Kliegl, R., Vasishth, S., Baayen, H. and Bates, D. (2017). Balancing Type I error and power in linear mixed models. Journal of Memory and Language, 94, 305-315. doi:10.1016/j.jml.2017.01.001
Morris, T. P., White, I. R. and Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. Statistics in Medicine, 38(11), 2074-2102. doi:10.1002/sim.8086
Perugini, M., Gallucci, M. and Costantini, G. (2014). Safeguard power as a protection against imprecise power estimates. Perspectives on Psychological Science, 9(3), 319-332. doi:10.1177/1745691614528519
Westfall, J., Kenny, D. A. and Judd, C. M. (2014). Statistical power and optimal design in experiments in which samples of participants respond to samples of stimuli. Journal of Experimental Psychology: General, 143(5), 2020-2045. doi:10.1037/xge0000014
pilotr 0.3.1
CRAN release: 2026-09-20
Fixes
spec_json()no longer corrupts a coefficient on a build of R without long-double arithmetic. Numbers were written by askingjsonlitefor 17 significant digits and then shortening each one in the text, which meant reading the number back withas.numeric(). That read-back is not correct everywhere. Without long doublesas.numeric()accumulates the mantissa in a double before applying the decimal exponent, and 17 significant digits overflow the 53-bit mantissa on the way, so"0.33333333333333331"came back one unit in the last place below1/3. The shortened form then recorded that wrong value, and the saved specification no longer held the coefficient the user had set. Each number is now formatted from the double itself, beforejsonlitesees the document, so nothing is ever read back from the text it was just written to. A coefficient of1/3is written at 16 significant digits rather than 17, which is both the shorter form and the one such a build reads back exactly. Found by CRAN’s no-long-double check.generate_r_script()could writeInfin place of a finite coefficient on the same builds. It embeds each number through the helper that looks for the shortest decimal that reads back as the same double, and that helper returned nothing at all when none of the three widths it tried passed the check, a case the caller took to mean the value was infinite. Without long doubles the 17-digit width fails that check for the very values that need it. The helper now falls back to 17 significant digits, which is the widest form there is and so the one to use when nothing shorter will do, and returns nothing only for a value that really is not finite.
pilotr 0.3.0
CRAN release: 2026-09-12
Read this first
Two changes alter numbers that earlier versions produced. Both are deliberate corrections, and both are unavoidable if the results are to mean what the documentation says they mean. Install 0.2.1 to reproduce output from 0.2.1.
Replicate seeds changed. The power and precision loops previously seeded replicate
iwithseed + (i - 1). Consecutive seeds are not independent streams in this generator, and the measured consequence was severe. The first draw of replicateicorrelated 0.95 with the first draw of replicatei + 1. Seeds are now drawn from the shared generator, which brings that correlation to -0.02 and moves a Ljung-Box test over the replicate means from p below 0.0001 to p of 0.94. Every number the replicate loops produce is therefore different, and the first replicate no longer uses the specification’s own seed.replicate_seeds()is exported so that a hand-written loop or a cluster array task can use the same rule.An interaction random slope now reaches the data. A slope keyed on an interaction, such as
"z_cosine:z_ISI", was accepted, sized into the covariance, drawn from the random stream and then multiplied by a lookup that returned nothing, so it was silently discarded. Meanwhilemodel_formula()andbrms_bridge()both emitted that slope, so the model fitted was richer than the process that generated the data, which inflates power in the direction Barr et al. (2013) describe. Any design that used one was anti-conservative and its data have changed.
Cross-language reproducibility
The R and ‘Python’ implementations agreed on far less than the documentation claimed. Four separate defects made them diverge, none of them visible in the shipped examples, because six of the eight set response.round, which quantises exactly these differences away. With rounding removed, three of the eight examples differed, in up to 9.24% of cells.
- The accumulation order of the linear predictor is now fixed by the specification. R folded the terms one at a time while ‘Python’ summed them and added the total. Floating-point addition is not associative, so the two disagreed on 63.8% of rows in a matched reproduction over 200,000 rows.
- Neither language’s built-in summation is used any more. Base R’s
sum()accumulates in 80-bit long double, and CPython’ssum()has applied Neumaier compensation to floats since 3.12. Both are more accurate than a plain double fold, in different ways, so an inner product of three terms or more could land on different doubles. Every inner product is now an explicit fold. - Integer powers are written as repeated multiplication. R special-cases small integer exponents while ‘Python’ calls the library
pow(). Over 200,000 draws in the Gamma sampler’s range the two disagreed on a third of inputs by up to 6 ulp. Because that value decides a rejection step, it also changed how many draws were consumed. This alone accounted for every difference in thebeta_proportionexample. - The random-effect covariance is bracketed identically. R computed
(sd_i · sd_j) · r_ijand ‘Python’(sd_i · r_ij) · sd_j. Multiplication is commutative but not associative, and the difference propagated through the Cholesky factor into every random effect drawn.
All eight shipped examples are now bit-identical between the two languages as shipped, as are five adversarial specifications added to exercise what the shipped examples do not. With response.round removed, six of the eight remain bit-identical, and the two whose family applies exp() to the linear predictor, crossed_mixed_rt and reading_time_continuous, differ by at most one unit in the last place, on 0.09% and 0.05% of cells. That residue is the libm limit described immediately below, and tools/parity/tolerance.json records which cases carry an allowance for it, and why.
The scope of the guarantee is now stated honestly in
spec/SPEC.md. It is exact forgaussian, for any design applying no transcendental function to the linear predictor, and for any family withresponse.roundset. The families that applyexp()orlog()may differ in the last unit in the last place, because IEEE-754 does not require correct rounding for those functions and the two builds need not share a maths library. Measured rates are given, and the attribution is demonstrated: the same design switched togaussian, with an identical seed, structure and draw sequence, is bit-identical.Three small divergences between the two engines are closed. A non-whole seed truncates identically, a whole
spec_versionreads the same however it was written, and a non-object unit is now reported as such, where R used to crash with a base error. The message text of each is character-for-character identical across the twins.
Validation and versioning
validate_spec()checks a specification against the schema and against the cross-field rules the schema cannot express, andload_spec()now calls it by default. A strict draft-07 schema had shipped since 0.1 with no code path consulting it.Validation exists because several ways of getting a specification wrong produced plausible data and no error at all. A mistyped coefficient key resolved to no column and so silently set that effect to zero, which generates exactly the data of a null design and reports success. A response parameter left over from another family was ignored. Both are now refused.
spec_versionnegotiates the format. A specification with no such field is a 0.2 specification. One using a 0.3 feature must declare 0.3, because a 0.2 implementation reads it differently and generates different data without complaint. One declaring a version newer than the implementation understands is refused outright.simulate_design()gainsvalidate, defaulting toTRUE. The replicate loops validate once and then skip it, so a sweep pays the cost once.spec_from_model()gains test coverage: the recovered specification’s units,between/vary_withinplacement, interaction keys read back off product columns, and random-effect estimates are checked against the design that generated the pilot data, alongside the refusal paths for models the function cannot read.The package now declares a minimum R version,
Depends: R (>= 4.0.0). The cross-language claim assumes theround()that arrived in 4.0.0, which is where that floor comes from. A new CI job checks the declared Suggests floors, which the matrix (release, devel, oldrel-1) sat well above and so never exercised.The packaged example specifications are now tested against the repository’s own copies.
spec/examples/*.jsonis canonical and both packages carry a mirror so an installed copy can reach it, but nothing enforced the mirror. A load-and-simulate test cannot: a stale packaged copy still loads and simulates perfectly well, it simply describes a different design from the one the repository documents. The newtest-examples.Rcompares the bytes, and is twinned with the Python suite’stest_examples.py. It skips when the package is checked in isolation from the repository.
Fixes
n_convergedwas not the number of converged fits. The replicate loops wrapped each fit insuppressWarnings(), so only a hard error was visible and every boundary-singular fit counted as converged. In the package’s own documented example, 12 subjects by 12 items with a maximal model, 85 of 100 replicates were boundary-singular whilen_convergedreported 100. The loops now reportn_attempted,n_returned,n_converged,n_singularandn_warningseparately. Singular and warning fits are still used, since their fixed-effect estimates remain interpretable and dropping them would bias the result.A non-positive-definite random-effect covariance is now an error, naming the grouping factor and the column at which the factorisation failed. The failing pivot was previously clamped at zero and the result returned in silence, which produced random effects whose standard deviations were several times those requested. In one test a requested 0.200 came back as 0.805.
spec_json()lost precision on nearly every number. It wrote throughas.character(), so a coefficient of1/3round-tripped to0.33333333333333298. Over a sample of 214 doubles, 189 failed to round-trip. Since the JSON file is the portable artefact, the specification itself was a source of divergence. Numbers are now written at the shortest precision that round-trips exactly, so0.3still reads as0.3. The blanketauto_unboxis gone too, so a one-elementvary_withinor a single ordinal threshold stays an array.generate_r_script()embedded the specification throughdeparse(), which prints 15 significant digits and so does not round-trip. It now emits numbers at full precision, which matters because the point of the script is bit-for-bit reproduction.brms_bridge()wrote its model to the console instead of returning it. The code reached standard output on every call, whether or not anything was there to read it, and the one internal caller,generate_design_analysis(), had to divert that output around itself through a sink to a temporary file. It now returns apilotr_bridgeobject, visibly, carrying the sameformula,family,priorsandcodeas before, and the newprint()method writes the code. A bare call at the console still shows the model; an assignment is silent.model_formula()andbrms_bridge()emitted correlated random effects unconditionally, along with an LKJ prior, while the generative process only correlates them whencorrelationsis supplied. They now follow the newcorrelatedflag and emit a double bar otherwise. A group with no slopes keeps a single bar, sincelme4cannot parse(1 || g).model_data()did not create the product column an interaction random slope needs, so the emitted formula referred to a variable the modelling data lacked. It now covers the union of the fixed-coefficient and random-slope keys.A focal effect that never appears in any fit now warns. It used to return decision proportions of zero, which read as ‘this design can decide nothing’ when the cause was a name that did not match the model.
A replicate loop in which no fit succeeds now reports why, passing on the fitter’s own message. An unidentifiable random-effects structure previously produced a silent result of
NA.A correlation naming a random-effect term that does not exist now raises a clear error, where it used to fail at a subscript.
A Poisson mean beyond the sampler’s reach was returned as the iteration cap. The inverse-CDF walk starts from
exp(-mean), which underflows to exactly zero once the mean passes about 746 (a poisson intercept of 7 already implies a mean of exp(7), about 1097), after which every simulated count came back as the cap of 1000000 while reporting success. Both engines now refuse such a mean, namingexp(eta)and the offending value, with message text character-for-character identical across the twins. Feasible means are untouched and the parity dumps are unchanged.generate_design_analysis(file = )wrote the script through a text-mode connection. On Windows the file arrived with CRLF line endings, which turns the SLURM part’s first line into a#!/bin/bash\rshebang no cluster can execute, and on every platform it carried a doubled trailing newline. The script now reaches the disk byte for byte as returned, and the app’s script and specification downloads write through the same binary path.The emitted SLURM wrapper could only run for its author. It hard-coded the author’s cluster account and project paths, so any other user’s submission failed at the scheduler while the surrounding instructions told them to save the parts under their own names. The wrapper now carries two placeholders marked
EDIT, the--accountdirective and a writablePROJECT_DIR, and it invokes thedesign_analysis.Rthat sits beside it, wherever the user saved the pair.precision_design()documents all sixteen of its columns. The Monte Carlo standard errors and Wilson interval bounds were returned but missing from the reference page.A true effect of exactly zero returned
Inf, and the package recommends that input.design_conditions()deliberately produces a null condition so a run can show how often it declares something when there is nothing to find. Putting it throughpower_design()returnedtype_m = Inf, straight into the app’s display, while Type S silently degenerated to “the estimate is positive” becausetrue_effect > 0isFALSEat zero. The Python twin raisedZeroDivisionErroron the same input. Type S and Type M are nowNAwhen the true effect is zero or unknown, in both engines, which is the guardpower_mixed()already carried, applied to the other three sites.response_variance()reported a total that was not the sum of its parts. An undefined fixed component was laundered into zero while the result kept calling itself the sum. A single-row design now reports a fixed component of 0 and an honest total.The HPC precision-array job was broken and stale. It hand-listed the engine files and omitted
validate.R, so it died on its first real line, and it used the indexed seed rule abandoned at 0.3, which correlates consecutive replicates and understates the job’s own Monte Carlo error. It now sources the package wholesale, as the parity harness does, and uses the package’s own replicate seeds,qnorm(0.975)and error reporting.A test now holds
print.pilotr_power()to keeping its whole output on one stream. A header written withmessage()or through cli would land on the message stream while the table beneath it went to standard output, which knitr collects separately and renders as two boxes for one printed object. The method already usedcat()throughout, andtest-print.Rkeeps it there.Every vignette now turns console colour off and fixes the console width while it renders. pkgdown passes the calling terminal’s colour support into its build subprocess, so a coloured message or error would otherwise reach the reader as escape sequences in the middle of the text.
New in the generative core
varies_by = "observation"draws a predictor once per row, for a quantity that varies trial by trial.varies_byis also validated now, because anything other than"subject"was previously read as item-level, so a predictor declared to vary by"trial"was silently given one value per item, and anyone who wrote that got wrong results.dist = "uniform", withminandmax. A uniform costs the same single draw as a normal, so it does not move the stream.reliabilityon a predictor simulates imperfect measurement. The latent value drives the linear predictor and any slope keyed on it, while the contaminated observed value goes into the data. No comparable package models unreliable predictors, and cross-level interactions are where unreliability bites hardest. Population moments are used rather than sample ones, since R’smean()andsd()accumulate in long double and ’Python’s do not.The
exgaussianfamily, the registered model family for reaction-time work, in brms’s parameterisation so that a specification and the model fitted to it agree on what the intercept means. A shifted lognormal is not a substitute, becausemodel_data()logs the response back and leaves a symmetric residual on the analysis scale.-
response_variance()decomposes the linear predictor’s variance into the fixed part, each grouping factor’s part, and the residual. Each grouping factor’s component is exact for the realised design, averaging over the random-effect distribution analytically, because estimating it from the drawn effects of 30 subjects carries a sampling error of around a quarter of the component.A residual is reported for all eight families, not only the four carrying an explicit
sigma, so the components are a complete decomposition everywhere and their ratios read as the design’s intraclass correlations. For the link families it is the latent-scale distribution-specific variance (Nakagawa, Johnson and Schielzeth, 2017). Three of those four are derived from the process pilotr simulates and are exact for it: abernoullirow is drawn as1[u < invlogit(eta)], so its latent error is a standard logistic variate of variancepi^2 / 3,ordinalcompares the same uniform against cumulative thresholds and inherits it, and forbetathe identityVar(logit(Y)) = trigamma(a) + trigamma(b)is exact.poissonis the one approximation, since a count of zero has no logarithm. The trigamma form is used, and it is worth reading as an order of magnitude when counts are rare, where the published alternatives diverge sharply from it. calibrate_response()rescales a design to a target total variance, which is what lets a region of practical equivalence be stated in standard-deviation units and read the same way across designs. It now accounts for a residual it cannot move. Abernoulliorordinaldesign carries a latent residual of about 3.29, so calibrating one to a total variance of 1 is impossible, and is refused with that number in the message. Forpoissonandbetathe residual moves with the linear predictor, so the factor is solved numerically, and the whole search costs one extra simulation.
New in the design-analysis layer
generate_design_analysis()emits a Bayesian design analysis as a runnable script: abrm()call withsample_prior = "yes", a Savage-Dickey Bayes factor, a highest-density interval against a region of practical equivalence, a three-way supported/null/inconclusive verdict, and a convergence gate that withholds every verdict when R-hat or the divergence rate fails it. Optionally it also emits a SLURM array wrapper and an aggregator. Emitting a script is what keeps this reachable from the browser build, where Stan cannot run at all.power_mixed()is no longer restricted to one within-unit factor and a crossed design, and no longer fits a formula written into the source. It takesfocalandformula, derives the model from the specification throughmodel_formula()andmodel_data(), and runs the same replicate loop asprecision_design(). A design with two factors, with continuous predictors, or with a smaller random-effects structure than the maximal one was previously refused outright or analysed under a model it had not described.sweep_spec()runs an analysis over any addressed field of a specification.power_curve_mixed()andprecision_curve()are now thin wrappers over it for sample size, which was previously the only axis reachable without writing the loop by hand.design_conditions()builds the coefficient sets for an effect-size sweep, including a shared all-zero condition.Every reported rate now carries its Monte Carlo standard error and a Wilson interval. The curve functions default to 60 replicates, at which a rate near 0.5 has a standard error of 0.065, which cannot support a claim anyone would want to make. The Wilson interval is used because the plain standard error collapses to zero at rates of 0 and 1, reading as certainty precisely where there is least of it.
precision_design()andpower_mixed()defaultfocalto every coefficient in the specification.print()for a power result shows the fit accounting and each estimate beside its uncertainty.-
solve_curve()andtarget_n()solve a simulated design curve for the value that meets a target. The package computed the whole curve and then handed back the last and most consequential step, the number that goes into a preregistration, at replicate counts where neighbouring points are not significantly different. The plots drew a dashed line at 0.80 and left the reader to judge the crossing by eye.target_n()now takes the data framepower_curve_mixed()already returns and reports the sample size at which power reaches the target, with a confidence interval on it, rounded up to whole subjects.solve_curve()is the general form and readsprecision_curve()andsweep_spec()output through the same column names, on any axis the sweep varied.The fit is a binomial probit regression of the decision rate against the swept value, weighted by the replicates behind each point and inverted by the delta method that
MASS::dose.p()applies to a fittedglm. The probit follows from the design itself: under the normal approximation to a two-group comparison the probit of power is linear in the square root of the sample size. Checked againststats::power.t.test()over 36 combinations of effect size, target power and grid shape, at 400 replicates a point, the solved sample size sat within 2.9% of the analytic answer on average and 6.9% at worst, against 3.4% and 8.9% for a logit fitted the same way.tools/calibration/solve_curve_calibration.Rruns that comparison and writes every figure quoted for it to a file beside itself. Where the two-parameter model does not describe the curve, the interval is widened by the heterogeneity factor of probit analysis, reported asdispersion, since a curve the model fits badly should not report the narrow interval its replicate counts alone imply.Nothing extrapolates. A curve that does not reach the target within the sizes it swept is refused, with the range it did cover, and so is a fit that solves past the end of the sweep, a curve with no trend to invert, and a slope that cannot be told from zero. The app reports the solved size beside the curve it draws, and prints the refusal when the sweep does not settle the question.
The Bayesian design-analysis record carries the rule that produced it, namely the decision thresholds, the convergence gate, a specification fingerprint and the package version, and the aggregator refuses to pool replicates that disagree on any of them.
References
Barr, D. J., Levy, R., Scheepers, C., & Tily, H. J. (2013). Random effects structure for confirmatory hypothesis testing: Keep it maximal. Journal of Memory and Language, 68(3), 255-278. doi:10.1016/j.jml.2012.11.001
Nakagawa, S., Johnson, P. C. D. and Schielzeth, H. (2017). The coefficient of determination R2 and intra-class correlation coefficient from generalized linear mixed-effects models revisited and expanded. Journal of the Royal Society Interface, 14(134), 20170213. doi:10.1098/rsif.2017.0213
pilotr 0.2.1
Documentation
- A package-level help topic,
?pilotr, introduces the workflow and groups every export by the stage it belongs to. - The reference examples that cut the replicate count so they run quickly now say so, and each
n_simsargument documents the Monte Carlo error and a usable recommendation for real planning. - The
precision_curve()example passes a region of practical equivalence clearly inside the effect it declares. It previously fell back to a default equal to that effect, the configuration the vignette warns against.
Fixes
-
model_formula()binds the global environment, so a formula prints without the environment tag that varied between builds and leaked into the rendered reference pages.
pilotr 0.2.0
Design specifications
-
pilotr_example()lists the design specifications shipped with the package, one per design family, and returns the path to each forload_spec(). They are the same JSON files that drive the Python package and the no-code app, and the Python package gains the same function. - The eight per-family specifications now travel inside the package itself, so an installed copy can load them without a checkout of the repository.
pilotr 0.1.0
Initial release.
Design specifications and simulation
-
simulate_design()generates a data set from a portable JSON design specification shared with the Python package of the same name, withbuild_spec()composing a specification from a flat list of design inputs,load_spec()reading one authored elsewhere (such as one downloaded from the no-code app) andspec_json()serialising it back to JSON. - The response families are Gaussian, lognormal, shifted lognormal, Bernoulli, Poisson, ordinal and Beta.
- A shared cross-language random-number generator (
make_rng(), with the AS 241 inverse normal inas241()) makes the simulated data bit-identical to the Python package’s given the same specification and seed. - A
per_subjectvalue below 1 or above the number of items is rejected with a clear error. The Python package raises on the same inputs.
Power and precision analysis
- Simulation-based power and design analysis with
power_design(),power_mixed()andpower_curve_mixed(), reporting power alongside the Type S and Type M errors of Gelman and Carlin (2014).power_curve_mixed()returns ann_convergedcolumn alongsidepowerandtype_m, so the Monte Carlo standard error can be computed over the converged replicates. - Precision and region-of-practical-equivalence (ROPE) design analysis with
precision_design()andprecision_curve(). - Every analysis seeds the shared RNG as
seed + (replicate - 1), the same indexed-seed rule across all power and precision functions, the HPC array script and both languages. -
power_mixed()andpower_curve_mixed()reject a specification without an item unit with a clear error instead of returningNaNpower. - All simulation-based power and precision analyses take a
workersargument that spreads the Monte Carlo replicates across local cores with base R’sparallelpackage. Because every replicate seeds the shared RNG from its own index, any worker count returns results identical to a serial run, and the sweep functions create their worker pool once and reuse it across all sample sizes.
Modelling bridges and the no-code app
-
model_formula()andmodel_data()derive thelmerformula and modelling data frame implied by a specification, andbrms_bridge()derives abrmsformula, family and priors, mapping every response family to itsbrmscounterpart (includingBeta()). -
generate_r_script()writes a self-contained, reproducible R script from a specification. - A no-code Shiny application over the same specification, launched with
run_app().