Skip to content

rcicr 1.2.0

Choose a tag to compare

@rdotsch rdotsch released this 28 Jul 13:44
· 86 commits to main since this release
526ced0

Upgrading from the CRAN version? The last release on CRAN was 0.3.4.1, before the package
was archived in 2021. 1.0.1 and 1.1.0 were GitHub-only releases made in the meantime, so the
1.1.0 section below applies to you too — it is where the bulk of the bug fixes are.

Reproducibility impact

  • generateCI(zmap = TRUE) blurred z-maps with the wrong sigma, for stimulus sets
    generated with 1.1.0 only.
    generateCI() reads the stimulus set with load(), which
    assigns into the function's own frame, and 1.1.0 began storing the noise sigma there —
    the same name as the z-map blur argument. The saved value replaced the argument, so z-maps
    were blurred with 25 rather than the documented 3, and passing sigma did nothing.

    In practice this affects nobody. 1.1.0 was a GitHub tag that stood for about a day and
    was never on CRAN, so almost no stimulus set carries the field that triggers it. Files from
    1.0.1 and earlier have no sigma at all and were never affected. Only z-maps are involved
    — classification images, scaling, InfoVal and every saved number are untouched. It is
    recorded here because it did change a number, and because if you are the one person who
    generated stimuli that day, regenerating the z-map is a one-line rerun.

    Every argument is now kept across the load(), so a field added to the .Rdata later
    cannot quietly capture another one. Found by tools/compare-release-output.R, the release
    gate introduced in this version — the first bug it caught.

Behaviour change

  • plotZmap(mask = ...) now actually masks the z-map. The argument has been documented
    since 2016 — "if a cell evaluates to TRUE, the corresponding zmap pixel will be masked" —
    and until now it did nothing at all: the mask was read from its PNG or matrix, checked
    against the z-map's dimensions, validated as binary, converted to boolean, and then
    discarded before plotting. A correct mask produced an unmasked z-map, with no error and no
    warning. Masked cells are now dropped from the z-map exactly as sub-threshold cells are.

    Who is affected. Only direct calls to plotZmap(mask = ...). generateCI() does not
    pass mask to plotZmap() — it masks the classification image itself, via a separate and
    working code path — so z-maps produced through the normal pipeline are unchanged, and no
    stored numbers change anywhere. If you have been passing a mask and your z-maps looked
    unmasked, that is why; they will now come out masked, and the earlier images were wrong
    about which regions carry signal.

    A second bug is fixed alongside: the conversion to boolean set every cell to FALSE
    whatever you passed, so even once applied the mask would have masked nothing.

  • The mask convention is documented correctly for the first time, in both
    ?plotZmap and ?generateCI. Both said a matrix masks where the value is 1/TRUE
    while a PNG masks where it is black (0) — two opposite conventions in one sentence.
    generateCI()'s implementation has always masked where the value is 0, for a matrix and
    a PNG alike, so the matrix half of the documentation was simply wrong. The code is
    unchanged and the documentation now matches it
    , since existing masks were built against
    the behaviour, not the prose. plotZmap() follows the same single convention, so one mask
    can be passed to both functions — which is now asserted by a test rather than assumed.

    If you built a mask by reading ?generateCI rather than by looking at your output, check
    it: 0/black/FALSE is the region that gets masked away.

New features

  • generateReferenceDistribution2IFC() and computeInfoVal2IFC() gained a
    response_seed argument, so the null distribution InfoVal is scored against can be
    varied deliberately. Until now there was no way to draw a second, independent null from
    the same stimuli — which meant you could not check how much Monte Carlo error your choice
    of iter was leaving in your InfoVal. response_seed seeds the simulated responses only;
    the stimuli, and so the noise basis the null is built on, are untouched.

    Existing calls are unaffected. The default (NULL) issues no set.seed() call at
    all, so the reference distribution is byte-identical to what earlier versions produced.
    Verified against norms generated before the change, not merely assumed.

    In computeInfoVal2IFC(), passing response_seed forces the reference distribution to be
    regenerated even when the .Rdata file already holds one, and the result is deliberately
    not written back — a one-off check of the Monte Carlo error cannot silently become the
    number every later analysis of that stimulus set reports.

  • generateReferenceDistribution2IFC() gained save_rdata (default TRUE, i.e. unchanged)
    and now returns the reference distribution invisibly instead of returning nothing, so the
    norms are reachable when you ask it not to write them to the .Rdata file.

  • The .Rdata file gained a reference_norms_seed field recording the response_seed the
    stored reference_norms were generated with (NULL for the default). Purely additive;
    files written by earlier versions simply lack it. A stimulus set carrying a deliberately
    varied null is no longer indistinguishable from one carrying the default.

Bug fixes

  • autoscale() works on masked classification images. generateCI(mask = ...) sets masked
    pixels to NA by design, and autoscale() took a bare range() over them, so the scaling
    constant became NA and the call died with missing value where TRUE/FALSE needed. The
    scaling constant is now computed from the unmasked pixels, exactly as generateCI()'s own
    scaling has always done, and masked pixels stay masked in the result. A CI that is
    entirely NA now raises an error naming the CI instead of failing the same opaque way.

  • generateCI() accepts a pre-0.3.0 .Rdata file when computing a CI from a single
    trial. rcicr 0.3.0 stopped drawing four random contrasts per trial that no patch index
    ever referred to (4096 → 4092), and generateCI() has truncated older files ever since —
    but the single-trial branch tested for a length of 4092 and then truncated to 4092, a
    no-op that could never fire on the 4096-parameter input it existed for. Such a call failed
    with Stimulus generation aborted: number of parameters doesn't equal number of patches!.
    The multi-trial path was always correct and is unchanged.

  • computeInfoVal2IFC() and generateReferenceDistribution2IFC() work on .Rdata files
    written before noise_type was saved (#94). Such a file failed outright with
    object 'noise_type' not found, and the workaround on record was to load the file and
    assign the variable by hand. It now falls back to sinusoid with a loud warning, matching
    how nscales is handled — a warning rather than a silent default, because guessing wrong
    means the null is built on a different kind of noise than participants saw, and the
    resulting InfoVal would be wrong. Files written by 1.1.0 or later already store the field
    and are unaffected.

  • generateStimuli2IFC(return_as_dataframe = TRUE) shows its progress bar (#82). The
    return handing back each trial's noise exits the entire loop body, so it jumped past the
    progress-bar update and the bar sat at zero for the whole run — on the slowest path there
    is, since generateReferenceDistribution2IFC() takes it for every InfoVal.

  • The .Rdata file written by generateStimuli2IFC() now records the rcicr version that
    actually wrote it (#29). generator_version was a hardcoded '0.4.0' string from 2016
    onwards, so every file produced by 0.4.0 through 1.1.0 claims to come from 0.4.0 —
    useless for the provenance the field exists for, and it disagreed with
    p$generator_version, which held the real version all along.

    No result changes: nothing in the package has ever read this field. If your own code
    does, note two things. Existing files cannot be trusted to say what wrote them, so treat
    a stored '0.4.0' as "unknown, somewhere between 0.4.0 and 1.1.0" rather than as a
    version. And the field is now a package_version object rather than a character string,
    so compare with utils::packageVersion() or numeric_version(), never as text —
    '0.10.0' < '0.4.0' is TRUE when compared as strings.

Documentation

  • README.md now describes the package's architecture and, more usefully, the anatomy of
    the .Rdata file
    field by field. That file is the only link between stimulus generation
    and analysis — nothing about a stimulus set is recoverable without it — and until now its
    contents were documented nowhere a user would look.

  • A CONTRIBUTING.md sets out how to work on the package, leading with the constraint that
    makes it unusual: researchers re-run old analysis scripts years later and publish the
    results, so numeric output must not change silently.

  • ?generateStimuli2IFC documents a restriction on return_as_dataframe = TRUE: the frame
    holds one noise image per trial, so it is meaningful only under the default
    use_same_parameters = TRUE. With use_same_parameters = FALSE and more than one base
    image, only the first base image's noise comes back — the frame's shape cannot represent
    trial × base image. Behaviour is unchanged, and the files written to disk were never
    affected; the restriction simply was not stated.

  • ?generateReferenceDistribution2IFC now documents as a guarantee what was previously
    only true by accident: with the default response_seed, the reference distribution — and
    therefore InfoVal — is reproducible from the stimulus .Rdata file alone, independent of
    the calling session's random number state and of ncores. This held before, but nobody
    had chosen it: it is a consequence of generateStimuli2IFC()'s internal set.seed()
    landing before the simulation draws. That call now carries a comment saying what depends
    on it, so it is not moved casually.