InfoVal reference distributions are now computed through the stimulus Gram matrix by default, and differ from before by rounding. reference_method = "images" reproduces earlier references bit for bit. computeInfoVal2IFC() and generateReferenceDistribution2IFC() gain a reference_method argument. The default, "gram", no longer renders each noise image and multiplies the noise for every simulated draw. The norms follow from the trials-by-trials Gram matrix of the saved noise, which sums in a different order. That matrix is built from the noise rendered through the sparse basis for a small set, and from the basis's cross-product, kept sparse, otherwise, one block of image columns at a time. "images" is the calculation of rcicr 1.5.0 and earlier, unchanged. (#354)
The measured configurations are listed in analyses/gram-reference-accuracy.md: 64 to 512 pixels square, up to 770 stimuli, the reference BLAS. None gave a bit-identical reference. The largest relative difference in a norm was 3.3e-14, InfoVal moved by at most 4.8e-13, and no call at 1.96 changed among 2,800 classification images. For comparison, the standard deviation of InfoVal at 1.96 across 10,000-draw references, estimated for the one stimulus set it was measured on (512 pixels, 300 stimuli), was 0.031. Other BLAS libraries were not measured.
Only a reference computed anew changes: for a new stimulus file, with force_gen_ref_dist = TRUE, with a response_seed, or when rcicr automatically rebuilds an unmarked reference that an older version left in the file. A reference already stored in the file is reused as stored, whichever method is asked for, so its InfoVal does not change. To reproduce an earlier InfoVal exactly after a reference has been rebuilt, rebuild it with reference_method = "images". Each newly stored reference records its method, as reference_norms_method or as method in the per-base and per-subset entries.
Computing a reference with "gram" prints a message saying so, and naming "images" as the way to reproduce earlier references. The method is never switched for you.
generateStimuli2IFC() now leaves your random stream where it was. It seeds with its seed argument and used to leave the stream wherever stimulus generation ended. Now the next random number your script draws after the call is the one it would have drawn without it, and a session that had no .Random.seed before has none after. A script that draws random numbers after generating stimuli without calling set.seed() first therefore gets different draws. Nothing rcicr computes changes: stimuli, classification images, z-maps and InfoVal are all drawn from their own seeds. (#189)
InfoVal references for new stimulus files no longer depend on the scoring session's RNGkind(). generateStimuli2IFC() saves the session's RNGkind() in the .Rdata file as rng_kind, and reference responses, with or without a response_seed, are drawn under it. A file scored under the kind it was generated with gives the same reference as before. Scored under another kind, it used to give a different reference and now gives that same one. Stimuli are unchanged under every kind, and files written before this version still draw under the session's kind, with the message on an automatic refresh that says when values changed. (#315)
In that cross-kind case, an explicit regeneration (force_gen_ref_dist = TRUE, a response_seed, or generateReferenceDistribution2IFC()) now leaves your random stream untouched, kind and position, instead of advancing it; advancing it would leave your session on the file's generator. Under the session's own kind it still advances the stream as before.
Pixels above white are written white, not dark, in stimulus and classification-image PNGs. png::writePNG() does not clip values above 1; it wraps them, so a pixel at 1.004 was written black and one at 1.1 almost black, while values below 0 were clipped. Every PNG rcicr writes is now clipped to [0, 1] first, which changes the files only at pixels that were above 1. No returned matrix, .Rdata value, CI or InfoVal changes.
nscales = 1, 12.5% at 2, 2% at 3 and 0.5% at 4. None was found at the default nscales = 5, in 20 trials each of sinusoid and Gabor noise at 512 pixels. Only those pixels on a bright part of the base wrapped. Stimuli already shown cannot be corrected; if you used fewer than 5 scales on a base with white areas, regenerating the stimuli with the same seed shows which pixels were affected.scaling = "constant" with a constant too small for the CI, whose warning already said "clipping will occur", and under scaling = "none" with rating weights large enough to push the noise past the base. On a mostly white 32-pixel base with scaling_constant = 0.01, 311 of 1024 pixels were above 1 and 248 of them were written darker than 0.5. (#371)A masked classification image now gets an InfoVal instead of NA. generateCI(mask = ) stores NA in masked pixels, and computeInfoVal2IFC() and batchComputeInfoVal2IFC() returned NA for such a CI without saying why. The InfoVal is now computed over the unmasked pixels, against a reference built over the same pixels from the same stimuli and the same replayed response stream, so it is reproducible from the stimulus file. The mask is read from the CI's own NA pixels. Each mask's reference is simulated once and stored in the .Rdata file apart from the default one, and generateReferenceDistribution2IFC() gains a mask argument to store one in advance, for example with a response_seed for a file without a stimulus seed. An InfoVal over part of the image is not comparable with one over the whole image. Unmasked classification images are unaffected. (#374)
Logical, missing, infinite and non-numeric responses now stop generateCI() and the functions built on it. A script that passed logical responses got a classification image and now gets an error: TRUE/FALSE weighted every FALSE trial 0 rather than -1, so half the trials did not count. To keep exactly the old numbers, pass as.numeric(responses); for the CI a two-image choice means, pass ifelse(responses, 1, -1). The error says both. Missing values never gave a usable result: one NA response made every pixel NA (the PNG black), with only R's own "no non-missing arguments to min" warnings; factor and character responses did the same, and an infinite one made the CI NaN. The error names the trials. batchGenerateCI() and batchGenerateCI2IFC() check the whole response column first and name the groups affected. Finite numeric responses, including ratings, are unaffected. (#372)
Reference distributions are much faster and need less memory. At 10,000 iterations, generateReferenceDistribution2IFC() was about 19x faster at 128 pixels and 300 stimuli with the default ncores, and about 29x faster at 256 pixels with ncores = 2, measured by analyses/gram-reference-benchmark.R against the build before this change. At the 512-pixel, 770-stimulus default, the peak R memory of the session knitting analyses/gram-reference-accuracy.md was 1.7 GB while the Gram route ran, against 3.3 GB for the rendered route. ncores is now used only with reference_method = "images". (#354)
Rendering noise takes less time and each parallel worker less memory, with identical results. Every render now indexes the noise basis with integer patch indices; the .Rdata file keeps the basis as saved. With participants, each participant's rows go only to the worker computing that participant, and no loop starts more workers than it has participants or trials. On 512-pixel stimuli in analyses/worker-memory.md, generating stimuli, the t-test z-map and an "images" reference took 23% to 29% less time on one core and 13% to 24% less on four, and each worker took 0.8 to 1 GB instead of 0.9 to 1.2 GB. n_cores and ncores now document what a worker costs, and generateCI()'s that it also runs the per-participant CIs. (#405)
dplyr and yesno are no longer dependencies, and tibble is only suggested. All three served a table of precomputed InfoVal references that had been empty since 2018, so every lookup missed and every InfoVal was simulated; it is gone, and no InfoVal changes. The interactive prompt it could have shown never ran. (#392)
matlab and scales are no longer dependencies. Their calls in the noise basis and simulateNoiseIntensities() are base R now, written from the packages' own definitions. Every sinusoid and Gabor patch and every noise pattern is identical to before, including the 1-pixel patches of an img_size equal to 2^(nscales - 1). With dplyr and yesno gone too, and tibble only suggested, rcicr imports 10 packages where 1.5.0 imported 14, Matrix included. (#208)
Matrix, an R recommended package that rcicr already depended on through spatstat, is now a direct import.
Every classification image records how it was scaled, as a scaling attribute: the method applied and its constant, including the one 'independent' scaling computes from each CI, which was discarded before. With participants, it also records each participant's constant, so the individual CI images can be reproduced. autoscale(), and the batch functions with their default 'autoscale', record the shared constant they printed, and keep the earlier record for $combined, which they leave unchanged. No pixel changes; saveRDS() keeps the record with the result. (#9)
batchGenerateCI() and batchGenerateCI2IFC() gain a participants argument, the name of a column of participant IDs. Each unit's CI is then the average of its participants' CIs, as generateCI(participants = ) computes it, so with by naming a condition every participant counts equally however many trials they gave. Every row needs an ID; a missing one stops the call before anything is computed. Without the argument nothing changes: the two functions now share one implementation and return exactly what they did. (#87)
batchComputeInfoVal2IFC() computes the InfoVal of every classification image in a list, such as the result of batchGenerateCI2IFC(), and returns them as a named vector. The values are identical to those of computeInfoVal2IFC() called on each image in turn. reference_stimuli can be given per image, for example lapply(cis, function(ci) attr(ci, "trial_design")$stimuli). Each distinct reference is found or simulated once, and the stimulus file is read at most three times to find stored references, however many images there are, rather than twice per image. For 20 images on a 300-trial, 512-pixel stimulus file, that was about 13x faster than the loop with the reference already stored, and about 19x faster with a response_seed, where the loop simulates the reference again for every image (analyses/infoval-batch-cost.R). Trial-design messages are collected into one per kind, naming the images concerned. (#85)
computeInfoVal2IFC() and generateReferenceDistribution2IFC() can build the InfoVal reference over the stimuli a classification image was actually built from. Brinkman et al. (2019) require the reference to use the same stimuli as the CI, but the reference has always used every saved stimulus. A CI built from fewer, for example after dropping missed trials, therefore got an inflated InfoVal: at 512 pixels, pure-noise CIs had a median InfoVal of 0.09 to 0.11 with 1% of trials missing and 0.51 to 0.58 with 5%, in the stimulus sets measured in analyses/infoval-design-mismatch.md. Pass those stimuli as the new reference_stimuli argument, each once. The reference is reproducible from the stimulus file and stored in it, as reference_norms_by_stimuli, apart from the default reference. No default changes, so every existing call returns the same number. (#349)
generateCI() now records which stimuli it used, whether any was repeated, and how many participants it averaged, as a trial_design attribute. computeInfoVal2IFC() reads it and prints a message when a CI is scored against a reference over different stimuli, giving the call that matches them: reference_stimuli = attr(ci, "trial_design")$stimuli. For a CI that averages repeated presentations or several participants, the message says instead that no reference is defined for that design, and suggests an InfoVal for each participant's CI. The message never changes the number returned. A CI from an older version has no attribute and gets no message. The attribute leaves the list's fields unchanged.
checkStimulusPNGs2IFC() checks a stimulus .Rdata file against stimulus PNGs, trial by trial, without the base images. It returns the share of each trial's pixels whose difference between the original and inverted stimulus agrees with the file's noise, for every base label, and lists PNGs the file does not cover. Use it to confirm the settings of a file regenerated after the original was lost; vignette("recipes") shows how. In the configurations measured in analyses/stimulus-png-residuals.md, a nearby Gabor sigma agreed on up to 99.6% of a trial's pixels, so compare candidates rather than reading a share alone.
checkStimulusPNGs2IFC() recognizes the byte wrap in older stimulus PNGs. Before
1.6.0, an original or inverted stimulus above white was encoded by png::writePNG()
as an 8-bit byte that wrapped into the image, while the checker treated that decoded
pixel as an ordinary interior value. It could therefore report a low agreement share
for the correct saved noise. The check now accepts the direct difference or the one
256/255 difference made by that historical wrap, only when the decoded pair is
dark enough to permit it. Pixels decoded as exactly black or
white remain excluded, and a matching share is still evidence to assess across all
trials and against other candidate settings; the images participants saw cannot be
changed by this check. Current clamped PNGs and saved stimulus data are unchanged. (#419)
generateStimuli2IFC() never overwrites stimulus PNGs. PNG names carry no time, so a call in a later minute into the same folder, with the same label, base label and seed, overwrote the earlier PNGs. The earlier .Rdata survived and no longer described them, and a shorter rerun left old and new PNGs mixed. Now every PNG the call would write is reserved before anything is generated. If any exists, the call stops with nothing changed. It also stops when two base labels name the same file on this file system, such as face and Face where case is ignored, and while another same-seed call is writing PNGs into the folder. A call that fails part-way, or that you abort with Esc or Ctrl-C, first stops its parallel workers and then removes the files it created. Only if R itself is killed can empty placeholder PNGs remain; the next call's message says the files it found are empty, as such placeholders would be, so they can be checked and deleted. To regenerate a stimulus set on purpose, delete its PNGs and its .Rdata first, or use a different label or stimulus_path. (#350)
generateCI(zmap = TRUE) no longer stops with color intensity ... not in [0,1] under scaling = "none" or an out-of-range "constant". The z-map is drawn over the combined image, which then leaves [0, 1]: under "none", wherever the base is black and the CI negative, and contrast maximization always makes one base pixel black. The background is now clipped to [0, 1] where it is drawn, in plotZmap() too. (#373)
autoscale() scales every element of its list, whatever the names. It looked elements up by name, so with a name used twice, as c() of two batch results with the same participant IDs gives, only the first was read and scaled: the shared constant ignored the others, and they were returned without $scaled. An unnamed list failed with "dimensions must be a positive quantity". Lists with unique names get the same constant and output as before. With save_as_pngs = TRUE, where names become file names, a missing or repeated name now stops the call before anything is written. (#375)
An unrecognised noise_type now stops generateStimuli2IFC() and generateNoisePattern(). Anything but exactly "sinusoid" or "gabor" used to produce sinusoid noise and record the value as given, so noise_type = "Gabor" wrote sinusoid stimuli into a file that said Gabor. The check runs before anything is written. Every value still accepted produces the same noise as before. (#376)
Every InfoVal reference path makes the same checks. generateReferenceDistribution2IFC() checks iter before anything is loaded or simulated: on a file whose base images share their parameters, iter = 2.5 used to run 2 iterations, and 0 or NA failed inside a progress bar. A reference with a MAD of 0 now stops computeInfoVal2IFC() on every path, where only a reference_stimuli reference did before and the others returned an infinite or NaN InfoVal; a stored default reference holding NA or infinite values stops as stored per-base and subset references already did. Every finite InfoVal is unchanged. (#377)
generateStimuli2IFC() checks n_trials before writing anything. 0, negative and missing values failed with messages such as "dimensions must be a positive quantity", depending on save_as_png. A fractional one, such as 2.5, wrote 2 trials into a stimulus file whose n_trials every InfoVal then rejected. All now stop with "n_trials must be a positive whole number". (#385)
An unknown zmapmethod now says so. generateCI(zmap = TRUE) with a method other than "quick" or "t.test" stopped with "argument of length 0" from inside the plotting code; it now names the two methods. (#388)
A single generated trial or a single participant no longer stops with must have 'max' > 'min'. generateStimuli2IFC(n_trials = 1), and generateCI() with one participant ID, now run: the progress bars start at 0. So does one participant with one trial. Where each stimulus appears once, one participant's CI is the same as the pooled CI of their trials. With repeated presentations the two weight trials differently, as ?generateCI describes. A t.test z-map over fewer than two images, one participant or one distinct stimulus, cannot be computed. It now stops before any work with a message that says so and suggests zmapmethod = "quick", instead of failing inside t.test(). No result that was computed before changes. (#351)
vignette("recipes"), answers questions that come up after the basics. It covers noise-only stimuli from a uniform grey base image, which needs maximize_baseimage_contrast = FALSE; rating scales as response weights, and how to check their direction; and matching the InfoVal reference to the design: dropped trials (reference_stimuli), masked classification images, scoring a batch, and why a CI averaged over participants has no calibrated InfoVal. It runs at 64 pixels and builds in seconds. (#69, #6)vignette("stored-data"), lists every field of the stimulus .Rdata file and every element and attribute of the classification image generateCI() returns. It replaces the README's table of the file, which lacked reference_norms_method and the method and mask fields of reference entries. The vignette's build checks its tables against freshly generated files and classification images, so they always describe the installed version. ?generateCI and ?autoscale now point there for the fields of the trial_design and scaling attributes.vignette("recipes") has a recipe for reproducing a number from an earlier version: running that release from a library of its own, or recomputing with this one. It says which stored InfoVal references are reused and which are rebuilt, shows that reference_method = "images" rebuilds one with the calculation of 1.5.0 and earlier, and points to the individual-CI filename advisory for per-participant images from GitHub installs before 1.3.0.vignette("recipes") shows how to regenerate a lost .Rdata file from the seed and the generation settings, and how to check the result against the stimulus PNGs without the base images. The README, the walkthrough and vignette("stored-data") no longer say the file cannot be recovered.generateStimuli2IFC(), generateCI(), autoscale(), computeInfoVal2IFC() and the other functions the vignettes use link to the vignette sections that show them, and ?rcicr names all the vignettes.batchGenerateCI() and batchGenerateCI2IFC() now group a tibble correctly. Called on a tibble with more than one group in by, rcicr 1.1.0 through 1.4.1 returned a single classification image instead of one per group. That image was built from alternating trials of different groups and named after the first group. A data.frame, or a tibble with one group, was never affected, and its results do not change. (#336)
You were affected if the list these functions returned had one element although your data held several participants or conditions; data read with readr or processed with dplyr is a tibble. Recompute those CIs with this version, or with an older one after as.data.frame(). No stimuli need regenerating or responses recollecting. Of the affected versions, only 1.3.0 and 1.4.1 were released on CRAN; the others were GitHub releases. The 1.1.0 entry below saying these functions accept tibble columns held only for a single group.
The default InfoVal reference now needs the stimulus seed saved in the file. Without a response_seed, the reference replays that seed. A file without one (made with generateStimuli2IFC(seed = NULL), or with the seed field removed) now stops with an error before anything is simulated. The error gives the call that stores a reproducible reference, generateReferenceDistribution2IFC(rdata, response_seed = <n>), which later computeInfoVal2IFC() calls reuse, and, for a file that cannot be written, computeInfoVal2IFC(..., response_seed = <n>). Files with a seed are unaffected.
Such a file used to reseed from the clock, so its reference could not be reproduced. generateReferenceDistribution2IFC() and computeInfoVal2IFC(force_gen_ref_dist = TRUE) returned a different one on every call. The first automatic computeInfoVal2IFC() stored one that later calls reused, so the InfoVal was stable on that copy of the file and different on any other. This happened on every version back to 1.0.1 for files made with seed = NULL, and on 1.4.0 and 1.4.1 for files without the field; earlier versions stopped on those. A reference already stored by 1.4.0 or later is still used, with a message that it cannot be regenerated from the file. An older stored reference, which rcicr rebuilds, now stops like any other. Replacing either with a seeded reference makes the InfoVal reproducible; it will differ from the one reported before. (#334)
Saving a reference distribution no longer risks the stimulus .Rdata file. computeInfoVal2IFC() and generateReferenceDistribution2IFC() store the reference in that file, and a save that was interrupted (Esc, an error, R killed or crashing) could leave it unloadable, losing the only record of the stimuli. The file is now backed up beside itself for the duration of the save, as <file>.rcicr-backup, and restored automatically if the save fails. It keeps its owner, group and permissions. (#333)
If R is killed during the save, the backup remains, and any function that reads the damaged file says so. If the file still loads, the next save stops and asks you to delete the backup, since rcicr does not delete a file it cannot prove it made. If the file no longer loads, check that the backup holds this experiment (the error shows how), restore it with file.copy("<file>.rcicr-backup", "<file>", overwrite = TRUE, copy.mode = FALSE), check that the file loads again, and only then delete the backup. Leftover <file>.rcicr-staging-* files are incomplete copies and can be deleted.
Limits: a writable file in a read-only directory (Unix), and a file name over 228 bytes, are saved as before, without a backup, with a warning. On Windows, a writable file in a folder that refuses new files now stops with an error instead of being saved, as does any save where the backup cannot be made, such as on a full disk. On Windows the backup has its folder's permissions rather than being private to you. Power loss and operating-system crashes are not covered, and two R sessions saving the same file at once remain unsafe.
generateCI() stops when some, but not all, participants IDs are NA. It used to return a CI in which every pixel was NA, with only min/max warnings, and write it as a solid black PNG; every version since 1.0.1 did this. The error names the trials without an ID. Give them one, or remove them from stimuli, responses and participants alike. An all-NA participants still means no grouping, and no valid result changes. (#337)
generateStimuli2IFC() never overwrites a stimulus .Rdata file. Its file name records the minute, so two calls into one folder with the same label and seed in the same minute used to write one file, and the first set's PNGs lost the only record of their noise parameters. The name is now reserved before anything is generated, and the call stops, having written nothing, if that file exists or if another call into the same folder with the same seed, started in the same minute, is still running. The error says to use a different label or stimulus_path, or to delete the file if the set is being regenerated on purpose. The time in the name is now the start of the call rather than its end. (#338)
An img_size the noise scales cannot tile now gets an error that says so. The finest of nscales scales tiles the image with 2^(nscales - 1) patches per side, so img_size must be divisible by that. generateNoisePattern(), and through it generateStimuli2IFC() and simulateNoiseIntensities(), used to fail with "number of items to replace is not a multiple of replacement length". The error now names the constraint, the nearest valid sizes and the largest nscales that fits. Every size that worked still works. (#339)
generateStimuli2IFC(return_as_dataframe = TRUE) returns one row per pixel and one column per trial, not the reverse.generateCI() takes its scaling constant as scaling_constant, not constant.batchGenerateCI() also accepts scaling = "matched".plotZmap() takes a background image matrix, not an .Rdata file.generateNoisePattern() returns four elements, not two.?computeInfoVal2IFC cites the published InfoVal paper (Brinkman et al., 2019) instead of a manuscript in preparation.computeInfoVal2IFC() always simulates a reference distribution the .Rdata file does not hold; rcicr ships no precomputed ones.InfoVal references for independent base images now use the selected base's saved noise. Previously, with multiple bases and use_same_parameters = FALSE, bases after the first were scored against the first base's reference. Shared-parameter results and the default simulated-response stream are unchanged. This defect did not affect classification images, z-maps, stimuli or responses.
The first base's InfoVal is also unchanged for files written from rcicr 0.3.0 onward. For older independent-base files, even the first base can change: their stored noise cannot be reconstructed by the newer generator from the seed alone. Recompute InfoVal for all bases in those files.
When saved parameter matrices differ, both computeInfoVal2IFC() and generateReferenceDistribution2IFC() now require the baseimage label used for generateCI(); omitting it reports an error. References are cached separately by base, preserving but ignoring old unscoped norms. Recompute affected InfoVal values from the existing CI object and stimulus .Rdata, using the same base label (here, "second"):
computeInfoVal2IFC(ci, "rcic_..._.Rdata", baseimage = "second")
No stimuli need regenerating or responses recollecting. The synthetic analysis in #307 characterizes the reference mismatch through modeled shifts across 100 base-image pairs. It does not estimate affected-study rates or bound an individual study's change; recomputing its InfoVal gives that change.
InfoVal references now use the saved noise basis and parameters. Scoring no longer reopens base images or reconstructs the stimuli from their seed. Archived experiments with moved images and uniform bases made with maximize_baseimage_contrast = FALSE can therefore be scored. The obsolete nscales, noise_type and sigma fallback warnings are removed. (#301)
References can change for pre-1.1.0 files whose missing settings were previously replaced by defaults, and for pre-0.3.0 files whose parameters could not be reconstructed from the seed. References are otherwise reproduced bit-for-bit when the old reconstruction matched the saved noise and the same RNGkind() is used for the response draws. Changing the RNG kind changes those draws, not the saved noise basis. The CI itself is unaffected; no stimuli need regenerating or responses recollecting.
Existing default caches without matching provenance are refreshed once, for both shared and independent bases. Automatic refresh keeps the cached iteration count and preserves the caller's random state. Explicit regeneration still uses the requested count and advances the random stream. Read-only files use the norms in memory and identify the file and, for independent references, the base that could not be saved; they build them again on the next call. That applies to every reference computeInfoVal2IFC() builds, not only a refresh — see Bug fixes.
If the norms change, a message says that the returned InfoVal supersedes earlier values. This is a message under every warning setting, including options(warn = 2); it is not collected by warnings() and can be hidden by suppressMessages(). Unchanged norms produce no superseding notice. Provenance includes a full copy of the norms, compared exactly, so an older writer cannot leave a trusted marker on replaced values. Earlier development fingerprints refresh once; no new package dependency is needed.
Deliberately seeded caches are retained. Explicitly regenerate any seeded reference built from an incorrect reconstruction, including affected pre-1.1.0 files, before recomputing InfoVal.
generateStimuli2IFC() now ignores an image's alpha channel when reading a base face.
Greyscale and RGB images are unaffected. At the default contrast setting, an opaque PNG can
differ only in rounding in the stored base-face doubles; its participant-facing 8-bit stimuli
and rendered CI are unchanged. A base with varying transparency, or any alpha image with
contrast maximization disabled, could previously look visibly distorted in the stored base,
stimuli and rendered CI. The CI calculation remains correct for the stimuli shown. If a base
needs correcting, inspect and flatten or composite the source image as intended, regenerate
the stimuli and collect new responses; do not rerun generateCI() from the existing .Rdata.
Malformed CI trial inputs now report an error instead of silently changing trial assignments. generateCI() requires one participant ID per trial when grouping is enabled. It, generateCI2IFC() and computeCumulativeCICorrelation() reject empty, missing, nonfinite, nonpositive, fractional or out-of-range stimulus IDs, and factor, character or logical IDs. Direct generateCINoise() calls reject mismatched parameter rows and responses. Valid numeric inputs and the all-NA no-grouping convention are preserved. If an existing script now errors, recover the correct trial IDs and participant assignments from the experiment records and recompute affected CIs and downstream results; do not pad or recycle IDs to satisfy the check. (#294, #300)
A classification image with no range now renders as a uniform neutral image instead of
NaN. Three cases divided by a zero range. generateCI() under its default
scaling = "independent" did so for a CI whose pixels are all exactly zero — what exactly
cancelling responses produce — making every pixel of $scaled and $combined NaN. Under
scaling = "matched" the same happened for any CI with no variation at all, a uniform
non-zero one included. autoscale() did so when every CI in its list was zero, in $scaled
only: it leaves the caller's $combined untouched, as always.
All three now fill the unmasked pixels with a neutral value and warn: 0.5 for independent
and autoscale(), which is what constant scaling already returned for the same input, and
the midpoint of the base image's range for matched, which renders into that range. Masked
pixels keep their NA and the unscaled $ci is unchanged.
Every value that changes was NaN before. No finite number moves, no CI with any variation in it is affected, and a result that was NaN cannot have carried a published number. (#303)
generateStimuli2IFC() now writes stimuli for every base image when it also returns a data
frame. With more than one base image and return_as_dataframe = TRUE, only the first base
image's PNGs were written, although the call succeeded and the .Rdata file recorded every
base image's parameters. A stimulus set generated that way is incomplete: re-run the same call
with the same seed to write the missing files. Nothing already collected is invalidated. The
stimuli that were written are unchanged, as is the returned data frame, which still holds one
noise image per trial for the first base image. (#302)Parallel workers now start whatever the session's OutDec and scipen are. With both
options(OutDec = ",") and a scipen negative enough to force scientific notation — neither
alone does it — every call that spawns workers failed, after waiting out the whole connection
timeout and then naming the workers rather than the cause:
generateStimuli2IFC(), generateCI() and friends with n_cores > 1, and reference
generation. parallel renders a worker's port into a command line as text, where those
options wrote 11000 as 1,1e+04 for the child to read as NA. Both are now neutralised
across cluster construction and restored immediately afterwards. They govern rendering and
never arithmetic, so no numeric output changes and no existing result is affected. (#316)
Scoring a read-only archive no longer fails when it has no cached reference to reuse.
computeInfoVal2IFC() simulates the reference before storing it, then asked to store it
even where the file could not be written, so a call that had already computed its answer
ended in cannot open the connection. Three cases reached it: a stimulus file with no
cached reference, force_gen_ref_dist = TRUE, and — for saved base images with different
noise parameters — any archive predating per-base caches, whose unscoped reference_norms
older versions of rcicr scored from without writing anything. Only an unsolicited refresh
of a stale cache had the fallback. All of them now use the computed norms in memory, say
which file and base could not be saved, and leave the archive untouched. The InfoVal is the
value a writable run returns; nothing about how references are simulated or cached changed.
A direct generateReferenceDistribution2IFC(save_rdata = TRUE) still reports an error,
because there the save is what was asked for.
generateReferenceDistribution2IFC() no longer re-copies its stimulus data on every simulated response set (#306). Converting it once per call speeds up reference simulation, and so the computeInfoVal2IFC() calls that have to generate one. The gain is largest at small image sizes and narrows as the matrix-vector product itself becomes memory-bound. Reference norms and the random-number stream are unchanged.ChangeLog is frozen at 1.0.1 and is no longer updated. It keeps the record of
everything before 1.1.0, including the whole CRAN era up to the archived 0.3.4.1.
NEWS.md is the changelog from 1.1.0 onward and the only one R indexes, so
news(package = "rcicr") is unaffected. Entries for 1.1.0 to 1.3.0, which only
pointed here, were removed from it.A decorated z-map on a device too small for it now says so, and can be made to fit.
plotZmap(decoration = TRUE) needs room for margins, labels and the colour scale — about
160px at the default text size on Linux and macOS, and about 200px on Windows, whose
graphics device is 96 ppi where theirs is 72 — and below that base R stopped with figure margins too large, naming neither rcicr nor a way forward. It now stops with a message giving the
size, the minimum, and the three ways out. plotZmap() gains pointsize and
generateCI() gains zmappointsize: the decoration is measured in lines of text, so a
smaller size fits it onto a smaller image. This matters most through generateCI(), which
sizes the z-map to img_size, so a stimulus set below about 160px could not produce a
decorated z-map at all — a 128px set now can, with zmappointsize = 6.
Both arguments default to the graphics device's own 12, so every existing call renders
exactly as before; a 512px decorated z-map is byte-identical.
plotZmap() no longer depends on raster, and its ... arguments now go to
graphics::image() instead of the raster package's plot method. col behaves the same
way in both (and now actually works — see the bug fixes below); a call passing an argument
specific to that method will now be rejected as unused. Dropping the dependency also removes
terra, sp and Rcpp, and with them the GDAL/GEOS/PROJ system libraries that every
Linux CI job had to install before it could start.
The z-map itself renders identically — the undecorated figures generateCI() writes
are pixel-for-pixel the same as before, within colour quantisation, and a golden reference
rendered by the old raster code is committed as a test fixture to keep it that way. The
palettes are unchanged, including the quirk that a z-map drawn over a background image uses
the default palette rather than the viridis one.
A decoration = TRUE z-map is laid out slightly differently. The colour bar is now
drawn by hand rather than by raster, and the map is a few pixels wider at 512px. If you
regenerate a decorated z-map figure, it will not be pixel-identical to one saved with 1.2.3
— the same is already true of regenerating it on a different operating system, and for the
same reason: what a graphics device paints is not part of what this package computes. No
z-score, classification image, scaling result or informational value changes, and
generateCI()'s own z-map figures are undecorated and unaffected. See
?plotZmap, "Reproducibility across platforms".
generateCI(mask = matrix(NA, 1, 1)) now reports the malformed mask instead of silently
ignoring it. The internal test for "was a mask supplied?" asked only whether the argument
was a single NA — which a one-cell NA matrix, a one-element array(NA) and a list(NA)
all are. Such a mask was mistaken for the NA default, discarded without a word, and an
entirely unmasked classification image came back. A call that passes a mask and gets an
unmasked CI is the failure worth catching early; it now stops in the same mask validation
every other malformed mask reaches. The sentinel is now specifically an atomic scalar with no
dimensions, so mask = NA still means "no mask" — it is the default, and every unmasked call
relies on it — while matrix(1, 1, 1), larger matrices, PNG paths and NULL are unchanged.
generateStimuli2IFC() now checks base_face_files before it generates anything,
and names the entry it cannot use. Four inputs used to get past the old check and
fail from inside a parallel worker with attempt to select less than one element in get1index, which names neither the argument nor the file: a list with no names, a
list with some names missing, an empty list, and an element that is not a single file
name. They now stop immediately with a message saying which entry is wrong and why.
One of them could previously appear to work. A base_face_files with two entries
under the same name silently dropped all but the first — the loop looked each name
up by string, so list(face = 'a.png', face = 'b.png') produced one set of stimuli,
from a.png, and nothing at all from b.png. That is now an error naming the
duplicated name. If you have a script that relies on it, the stimuli it produced were
never what the call asked for; give each base image its own name.
The PNG-or-JPEG test now looks at the file extension rather than anywhere in the
path. It was grepl('png|PNG', filename) against the whole path, so a JPEG stored
under a directory called png was handed to png::readPNG() and died with file is not in PNG format, blaming the file for a choice the package had made. Files are now
recognised by a .png, .jpg or .jpeg extension, case-insensitively. A base image
whose extension does not say what it is is rejected up front, by name, instead of
reaching a reader that cannot parse it.
The .Rdata validation errors now name the version of rcicr that wrote the file.
generateCI() and computeCumulativeCICorrelation() already said which field a file was
missing; they now also say where the file came from, which is what turns "this file has no
stimuli_params" into "it predates the version that added it — regenerate the stimuli, or
install that version". The version is read tolerantly, because the field cannot be taken at
face value: p$generator_version is preferred over the top-level generator_version, which
every release from 0.4.0 through 1.1.0 recorded as a hardcoded 0.4.0, and a file that only
claims 0.4.0 is reported as unknown rather than as 0.4.0. A file with no version field at
all reports the absence and stops there — an absent field is equally a file older than 0.4.0,
a truncated one, or one rcicr never wrote.
generateReferenceDistribution2IFC()'s two warnings about a missing nscales or
noise_type were reworded for the same reason. They said the file "was written by a version
of rcicr that did not save" the field, which an absence does not establish; they now report
what is missing, and keep the version as context rather than as a conclusion. The advice is
unchanged, and was always right either way: regenerate the stimulus set with this version.
A stimulus file with gabor noise and no saved sigma now says so.
generateReferenceDistribution2IFC() assumes the historical default of 25 when a file
predates 1.1.0 and lacks the field, which it has always done silently — unlike the loud
warnings for a missing nscales or noise_type. For gabor noise that silence hid the same
hazard those warnings exist for: sigma is what shapes the Gaussian mask, so guessing it
wrong rebuilds the null on a different noise basis than participants saw, and the resulting
InfoVal is wrong. On a 1.0.1 gabor stimulus set the reference norms move from
0.681/0.689/0.680 at sigma = 25 to 0.615/0.620/0.626 at sigma = 10.
Sinusoidal files are unaffected and stay silent, which is the point of doing this by
noise type rather than by field: sigma reaches the basis through generateGabor() alone,
so for sinusoidal noise the norms are identical whatever it is, and a warning would be pure
noise on the far more common legacy file. Nothing warns that did not previously produce a
wrong answer, and no numeric output changes.
plotZmap(mask = ...) now accepts a mask with an alpha channel. It previously required
every colour channel of a multi-channel PNG mask to match exactly, so a greyscale-plus-alpha
or RGBA mask whose alpha plane happened to differ from its colour planes was rejected — alpha
carries no colour information and is now always ignored, matching generateCI(mask = ...).
A rectangular zmap/mask pair continues to work as before. plotZmap(mask = ...) also now
rejects a mask that is neither a string nor a matrix with a clear error, instead of failing
later inside png::readPNG() with an unrelated message.
plotZmap(mask = NA) now means "no mask", as it already does in generateCI(). The two
functions detected a supplied mask differently — plotZmap() asked only whether the argument
was non-NULL — so the same sentinel meant opposite things: generateCI() read NA as "no
mask" while plotZmap() passed it on and stopped with The mask argument is neither a string nor a matrix!. NaN behaved the same way. Both now render an unmasked z-map. No call that
worked before changes: the inputs affected all raised that error.
generateStimuli2IFC() no longer saves trial in the .Rdata file. It was the loop
counter left over from stimulus generation — always equal to n_trials, which is already
saved. Nothing in the package or the documented contract reads it. Existing .Rdata files
that contain trial continue to work; the field is simply ignored on load.
Individual-CI PNGs written by generateCI(save_individual_cis = TRUE) carried the wrong
participant's name, and are now named correctly. The per-participant loop selects each
participant's trials by sorted order and took the output filename from order of
appearance. At each position where those two orders differ, the file in
<targetpath>/individual_cis was given another participant's ID. The pixels were always
right; only the names were wrong.
Affected: a direct call to
generateCI(participants = ..., save_individual_cis = TRUE) where the participants
vector is not already in sorted order. Such a call produced correct images under incorrect
filenames, so a figure published as participant p2 may be someone else's classification
image.
Do not assume tidy data was safe — the common case is affected. Sorting is lexical,
so text labels like "p10" sort before "p2". A study whose participants appear in
collection order p1, p2, ..., p10 is therefore affected from the tenth participant
onward, even though nothing about that data is out of order in any ordinary sense:
| participants, in collection order | affected |
|---|---|
| "p1" … "p9" | no |
| "p1" … "p10" or beyond | yes |
| "p01" … "p12" (zero-padded) | no |
| "1" … "12" (character) | yes |
| 1 … 12 (numeric) | no |
Unpadded text IDs and ten or more participants is the ordinary shape of a reverse correlation study, so if that describes yours and you used the call above, check rather than assume.
Not affected — and this is the documented route, so most analyses are in this group:
batchGenerateCI(), batchGenerateCI2IFC() and generateCI2IFC(). None of them
exposes participants or save_individual_cis at all; they split the data themselves
and name each image from the grouping value directly, never reaching the code that was
wrong. Per-participant classification images computed the way the package documentation
has recommended since the CRAN era — batchGenerateCI2IFC(), and batchGenerateCI() in
the current vignette — were correctly labelled throughout.generateCI() returns. The group classification image is a mean across
participants and so does not depend on their order; the per-participant stack, and every
z-map built from it, were internally consistent throughout. No returned value, and no
number in any .Rdata file, ever changed.save_individual_cis at its default FALSE. No individual-CI
file was written, so there was nothing to mislabel.save_individual_cis option
entirely. install.packages('rcicr') never produced a mislabelled file at any point.If you have published from individual-CI images, check your analysis scripts. Two steps, neither of which requires re-running anything. The individual-CI filename advisory carries the fuller version of this, including what the mislabelling does to a second-stage analysis and how much of an effect survives it; where the two differ, that page is kept current and this section is not.
Step 1 — did you use the relevant call at all? Look for an individual_cis/ directory in your output. Nothing else in the package creates it, so finding it means the direct call ran. For output made before 1.3.0, continue to step 2. If your per-participant images came from batchGenerateCI() or batchGenerateCI2IFC(), those cannot reach the defect.
Check the output rather than the script. save_individual_cis is the sixth formal of
generateCI(), so a call can set it positionally —
generateCI(stim, resp, "face", rdata, pids, TRUE, ...) — and write those files without
the argument name appearing anywhere; do.call() hides it the same way. Searching your
scripts and finding nothing does not clear an analysis.
Not finding the directory clears it only if the output still sits where the run left it. Renamed, archived or moved, it will not answer to that name, and the individual files are then recognisable but not conclusive: ci_<participant>.png is what the affected call writes, and also what generateCI(save_as_png = TRUE, filename = "<participant>") writes for a group CI one directory up.
Step 2 — if the directory is there, was your participants vector in sorted order?
Note "sorted" means lexically sorted for text labels, per the table above — sorting your
data frame by a "p1"-style column does not make it safe once you reach ten participants.
Where you still have the vector, one expression settles it:
identical(unique(participants), sort(unique(participants))) # TRUE = names were correct
Two conditions on that TRUE, both of which change what sort() returns: use the original participants object rather than a character copy, since a factor carries its own level order and sort() follows it; and run it under the collation the original analysis ran under, since identifiers mixing case, accents or punctuation sort differently by locale. Numeric identifiers are not exposed to the second, numeric sorting being locale-independent, and p1/p12 are not realistically exposed either. Lowercase ASCII is no guarantee in general, though, since some locales collate letter pairs as units (Czech ch, Danish aa).
If the output is long gone and only the script survives, search it for individual rather
than the full argument name, and read by hand any generateCI() call passing six or more
arguments positionally.
Recovering existing output is a rename, not a re-run. The mapping is exact: the file
named unique(participants)[i] holds the classification image of
sort(unique(participants))[i].
old <- unique(participants) # the names the files were given
new <- sort(unique(participants)) # the participants they actually hold
Every release from 1.0.1 (January 2023) through 1.2.3 (August 2026) carries the defect, as does every install from the development branch from 15 August 2017 onward — the date it was introduced, in the commit that first made individual-CI saving work at all. There were no releases between 2017 and 2023, so a copy obtained in those years came from the branch and is affected too.
Getting it from GitHub took a deliberate step, until December 2021. The default branch
was master, which carried an older repository layout with no DESCRIPTION at its root, so
install_github('rdotsch/rcicr') did not install anything at all — it failed. The affected
code was on the development branch, reached only by asking for it:
install_github('rdotsch/rcicr', ref = 'development'), which the README of the time
labelled "AT YOUR OWN RISK" and specifically advised against for analyses meant for
publication. development became the default branch on 2021-12-28, and from then a plain
install returns affected code.
If you need to work out which version a stored analysis actually used, the version number
alone will not tell you: 0.4.0 sat on the development branch for five years, with the
defect entering partway through. A note listing every release, its date, whether it came
from CRAN or GitHub, and what "the latest version" gave you in each window is at
https://github.com/rdotsch/rcicr/blob/main/notes/individual-ci-mislabelling.md.
computeCumulativeCICorrelation() with a masked targetci now returns numeric
correlations where it previously returned all-NA. No existing analysis could have used
the old result — it carried no information — but code that checked for NA on the returned
curve will see a different answer. Unmasked targets are bit-identical.
generateCI(save_individual_cis = TRUE) names each PNG for the participant it was
actually computed from. The loop selects participants by sorted level order and the
filename was taken from order of appearance, so the two disagreed for any data not already
sorted by participant. See "Reproducibility impact" above for how to tell whether files you
already have are affected, and how to correct them without recomputing anything.
computeCumulativeCICorrelation() now returns real correlations when targetci was
generated with a mask. generateCI() stores NA in every pixel a mask excludes,
and the correlation was taken over all pixels (use = "everything"), so a single masked
pixel made every point on the curve NA however strong the signal. Correlations are now
computed over the unmasked pixels only. A fully masked target (every pixel NA) still
returns an all-NA curve — there are no complete pairs to correlate.
plotZmap(col = ...) works. Supplying a palette is how ?plotZmap has always told
you to change the colours, and doing it stopped the call with formal argument "col" matched by multiple actual arguments before anything was drawn — the function passed its
own col alongside yours. This affected every released version; the argument is now taken
as an override, and is used for the colour bar as well as the map.
A base image with no contrast no longer becomes an all-NaN base image. Under
maximize_baseimage_contrast = TRUE, the default, generateStimuli2IFC() rescales
with (img - min(img)) / (max(img) - min(img)) — which is 0/0 when every pixel is the
same value. The resulting NaN base face was written into the .Rdata with no error
and no warning, every classification image computed from that stimulus set inherited
it, and the stimuli themselves came out uniformly black, png::writePNG() clamping
NaN to zero. It now stops with an error naming the file.
A photograph is never uniform, so this bit synthetic and accidentally-blank base
images — but it failed silently, and the symptom appeared a long way from the cause.
The error fires only under maximize_baseimage_contrast = TRUE: a flat base image is
perfectly usable with the rescale switched off, and the message says so.
computeCumulativeCICorrelation() reads pre-0.3.0 stimulus files again. Files written
before rcicr 0.3.0 (January 2015) store 4096 contrast parameters per trial where only 4092
patches exist, and generateCI() has truncated the four unused columns for years.
computeCumulativeCICorrelation() did not, so on such a file the extra columns reached
generateNoiseImage() as a length mismatch and it aborted with "number of parameters
doesn't equal number of patches" — the cumulative-correlation curve could not be computed at
all. It now applies the same truncation. Files from 0.3.0 onward already have 4092 parameters
and are unaffected. The same fix keeps a single presented stimulus two-dimensional, so a
one-stimulus call — which aborted with "incorrect number of dimensions" on any file — now
returns its (single-point) curve.
generateCI() with participants works again on more than one core. Supplying
participants without targetpath — the normal thing to do when you are not saving
individual CIs — stopped with argument "targetpath" is missing, with no default as soon as
n_cores was greater than 1, which is the default on any multi-core machine. foreach
inspects the loop body and fetches every variable it mentions, including one used only in
the branch that saves individual CIs, and that branch is precisely the one that cannot run
when you have not asked for those files. Single-core calls were unaffected, as was saving
individual CIs with a targetpath.
The progress bar moves again during parallel runs. generateStimuli2IFC() and
generateCI() build their progress bar in your session but were advancing it from inside
the parallel loop, where each worker holds a private copy — so under the default
(ncores/n_cores greater than 1) the bar sat at 0% for the whole job and jumped to 100%
at the end. On a 770-trial 512px set that is a long time with nothing to watch. Affects
stimulus generation, per-participant CIs, and t.test z-maps. Serial runs were never
affected and are unchanged.
This required swapping the foreach backend from doParallel to doSNOW, which is the
one that can report task completions back to the parent session. doParallel is no longer
a dependency; doSNOW and snow take its place, for slightly less installed than it took.
Nothing about how the loops compute changes, and no numeric output changes.
The base_face_files type check raises an error you can actually read. It wrote
its explanation to stderr() and then called stop() with no arguments, so the
condition it raised carried an empty message: conditionMessage() returned "", and
anything wrapping the call — a Shiny app, a batch script, knitr — caught an error it
could not report. Under sink() or capture.output(type = "message") the explanation
could be separated from the failure entirely. The text now travels with the condition.
A base image that cannot be read is reported with the file it came from. The
reader's own complaint (file is not in PNG format, and the like) is kept and
prefixed with the entry's name and path, and a file that does not exist is now
reported as missing rather than as unopenable.
Base images are validated before the noise basis is built and before the output directory is created. A mistyped path used to cost the full noise-pattern computation — appreciable at the default 512px — and left an empty stimulus directory behind before reporting the problem.
No numeric output changes. Every check here either replaces an error with a clearer error, or rejects input that could not have produced correct stimuli; a call that succeeds today produces exactly what it did before.
A multi-channel PNG mask one pixel wide or tall is now rejected instead of silently
masking everything. Dropping the colour channel from such a mask also dropped its
singleton spatial dimension, leaving a plain vector whose absent dimensions made the
size check pass vacuously; the mask was then recycled by linear indexing. A 1-by-8 RGB
mask against an 8-by-8 stimulus set returned an entirely NA classification image with
no error or warning. It now fails the size check and says so. Masks of any other shape
are unaffected.
generateCI(mask = ...) no longer crashes on a greyscale-plus-alpha PNG mask. The
internal mask importer hardcoded three colour channels, so any 2-channel PNG (the form
png::readPNG() produces for 8-bit greyscale-plus-alpha) failed with subscript out of bounds rather than a clean error — plotZmap(mask = ...)'s separate, now-removed mask code
already accepted the same file. Mask import is now shared between the two functions (#185),
so both accept a 2-channel mask, and a 4-channel (RGBA) mask continues to work exactly as
before. The mask-size-mismatch message now names what the mask is being checked against
("stimuli" from generateCI(), "z-map" from plotZmap()) instead of always saying
"stimuli", and reports the mask's dimensions in the same row-by-column order as the
target's rather than transposed.
?computeCumulativeCICorrelation explains what its curve ending at 1 does and does not
mean. With no targetci, the final CI it compares against is built from the same
un-aggregated trials as the curve, so wherever the evaluated trials reach the last one the curve
ends at 1 — self-consistency, not convergence. That is every call at the default step = 1,
though a larger step can stop short of the final trial and end below 1, and responses that
cancel exactly give a constant CI and an all-NA curve. That final CI is
identical to generateCI()'s where every stimulus was
presented the same number of times, and differs where repeat counts vary, because this
function weights each trial equally while generateCI() weights each unique stimulus equally.
Measured on an 8-trial set with counts 4/2/1/1, the two correlate at 0.77. Pass
targetci = generateCI(...) when you want the curve to describe the CI you will report.
Nothing changed in what the function computes.
?generateCI documents the weighting of repeated stimulus presentations. When
participants is NA, repeated presentations are collapsed before building the CI: each
unique stimulus gets equal weight, regardless of how many times it was presented. With equal
repeat counts this changes nothing; with unequal counts it is a different estimand from
weighting each trial equally, and the difference can be substantial. Previously this was
described only as a performance optimisation in a code comment.
There is now a documentation website: https://rdotsch.github.io/rcicr/. The
function reference, both vignettes and this changelog are readable without installing
the package first. It is generated from the same sources — README.md is the home page,
so "How it works" and "Anatomy of the .Rdata file" are not maintained twice — and
rebuilt by GitHub Actions on every push, so it cannot drift from the code.
The repository now carries a CITATION.cff, so GitHub offers a "Cite this repository"
button. citation("rcicr") is unchanged and still reads inst/CITATION, which remains the
citation of record; the new file is generated from it and DESCRIPTION by cffr, and
carries it as preferred-citation. The two cannot drift apart: CI regenerates the file,
compares every field, and validates the result against the CFF schema.
The install instructions now install a release rather than the development version.
remotes::install_github('rdotsch/rcicr') takes the tip of main, which carries unreleased
changes; the README.md and walkthrough-vignette instructions now lead with
@*release, and show @v1.2.3 for installing one specific version — worth recording in an
analysis script, since a classification image is only reproducible against the version that
computed it.
?generateGabor now documents generateGabor(). Its example called
generateSinusoid(), so the help page and the reference site demonstrated a call that never
reached the function being documented; @return described a sinusoid rather than a gabor
patch, and @param sigma was an unfinished sentence that did not say what the value means.
sigma is the standard deviation of the Gaussian mask in pixels — measured, not
inferred: the mask falls to exp(-1/2) at 24.5 px from centre for sigma = 25. The
function itself is unchanged.
The pre-0.3.0 0-indexed noise path is now pinned against an independent oracle. Reading a
.Rdata file whose patch indices start at 0 takes a backward-compatibility branch in
generateNoiseImage() that drops the 0-indexed cells and recycles a too-short weight vector —
which looks like it could misalign the whole image. It does not: the last patch layer is
all-zero, so the recycled values land only where the patch is zero and are multiplied away.
A test confirms the output equals the honest "one patch not shown" result exactly, so no
change to the indexing can silently reintroduce a misalignment. No behaviour changes.
Stimulus sets generated by 1.0.1 and 1.1.0 are now covered by tests. The suite carries
.Rdata files those releases actually wrote — generated by installing each tag, not
reconstructed — and checks that the current generateCI() still reads them and produces a
classification image. A 1.0.1 file predates nscales, so
generateReferenceDistribution2IFC() has to assume it: the test asserts the null it builds is
bit-identical to one from a file that states nscales outright, because a null built on the
wrong noise basis finishes just as happily and is wrong. (The parallel sigma fallback only
affects Gabor noise, which these sinusoidal fixtures do not exercise — tracked separately.)
Every other
fixture is built by the current generator, so nothing before this could catch a field this
version expects and an older one never saved. It runs on every platform with no network: the
fixtures are 205 KB (1.0.1) and 45 KB (1.1.0).
Documentation only. Nothing this package computes has changed — no function, argument, return value or number differs from 1.2.2, and no analysis script needs revisiting.
The release exists because the package-level help page, ?rcicr, had gone stale enough
to contradict the release before it. It was a hand-maintained .Rd file that roxygen
never touched, so the sweeps behind 1.2.2 — which all worked from R/ — went straight
past it. Every code snippet on it was wrong:
generateStimuli2IFC(base_face_files, n_trials = 770)
and generateCI2IFC(stimuli, responses, baseimage, rdata), would now error: they
omit the write paths that 1.2.2 made required.autoscale(cis, saveasjpegs = TRUE), named an argument that has never
existed under that spelling. It is save_as_pngs.\examples section was a single commented-out line, #simple examples will be added soon., left over from 2016.Version: 0.4.0 and Date: 2017-07-25, five releases and
nine years out of date.The page is now generated by roxygen from R/rcicr-package.R, so its title,
description, author and URLs come from DESCRIPTION and cannot drift from it again. The
version and date table is gone rather than corrected — the way to keep a fact current is
to stop writing it down twice. What remains is a short pointer to
vignette("reverse-correlation-walkthrough") and to the three functions a new user
starts with; the walkthrough itself lives in the vignette and README.md, which are
tested on every build.
The page also now lists all three key references — Dotsch & Todorov (2012), Brinkman, Todorov & Dotsch (2017), and Dotsch, Wigboldus, Langner & Van Knippenberg (2008) — each with a DOI. The 2008 paper had been on the old page and was the only one not carried anywhere else.
?rcicr and package?rcicr both still work.
This release exists to answer the changes CRAN asked for when reviewing the previous submission. Nothing it changes affects a number this package computes: classification images, scaling, z-maps and informational value are identical to 1.2.1, and the release gate confirms that against both 1.2.1 and 1.0.1.
Functions that write files now require you to say where. stimulus_path
(generateStimuli2IFC()), targetpath (generateCI(), generateCI2IFC(),
batchGenerateCI(), batchGenerateCI2IFC(), autoscale(), plotZmap()) and
zmaptargetpath (generateCI()) have lost their defaults. They used to be ./stimuli,
./cis and ./zmaps, which meant a default call created directories in whatever your
working directory happened to be — writing to your filespace without being asked, which
CRAN policy does not permit.
What to change in your scripts. If you relied on the old defaults, name them:
# before
generateCI(stimuli, responses, "face", rdata)
# after
generateCI(stimuli, responses, "face", rdata, targetpath = "./cis")
You will not silently get files somewhere new — a call that would have written to a
default path now stops with an error naming the argument to supply. If you do not want
files at all, save_as_png = FALSE (or save_as_pngs = FALSE for autoscale()) needs
no path.
batchGenerateCI() no longer produces a spurious CI for rows with no group. Rows
whose by column was NA were kept and collapsed into an extra group named after NA,
so a data frame with any missing grouping value returned one more classification image
than it had groups — computed from whatever rows happened to be missing that value.
batchGenerateCI2IFC() has always dropped those rows; the two now agree.
generateCI(mask = ) accepts a logical matrix. The matrix branch tested
typeof(mask) == 'double', so a mask built the obvious way — as TRUE/FALSE rather
than 1/0 — fell through to The mask argument is neither a string nor a matrix!,
despite the documentation describing exactly that form. It is now tested with
is.matrix().
generateCI() and computeCumulativeCICorrelation() no longer print the entire base
image when they cannot find stimulus parameters. The "No parameters found for base image"
error named the base image matrix where its label was meant. Because paste0() is
vectorized, this did not paste one matrix into one message — it built one complete message
per pixel, so the error came back as 1,024 concatenated copies at a 32x32 base image (8,190
characters) and roughly 7 MB at the 512x512 size researchers actually use, with the reason
for the failure buried inside it. The message now reads, in full, No parameters found for base image: <label>.
Only the text of an error changed. No function's return value, arguments or numeric output are affected, and the condition that triggers the error is unchanged — if your analysis script runs today, it behaves identically.
generateReferenceDistribution2IFC() no longer leaves a stray stimuli directory
behind. It re-derives the noise basis by calling generateStimuli2IFC() with both save
options off, purely to work in memory — but the directory was created before either
option was consulted, so every call to it, and to computeInfoVal2IFC() when no
reference distribution was cached, created an empty ./stimuli wherever you happened to
be working.
plotZmap() restores the graphics parameters it changes. The undecorated branch set
par(mar = ...) and left it set. It also now closes its PNG device through on.exit(),
so a failure part-way through plotting can no longer leak the device or leave a
half-written file.
The DESCRIPTION description no longer opens with the redundant "Functions to", and
cites the two method references: Dotsch and Todorov (2012)
doi:10.1177/1948550611430272 and Brinkman, Todorov and Dotsch (2017)
doi:10.1080/10463283.2017.1381469.
Every example runs. The \donttest{} wrappers are gone from all eight examples that
carried them, simulateNoiseIntensities()'s \dontrun{} is gone, and
generateNoiseImage()'s example is real code rather than three commented-out lines that
would not have worked (p was never defined, and params was the wrong length for the
pattern). The whole example set now runs in about nine seconds.
simulateNoiseIntensities()'s note claiming the function always errors is removed. It
described two bugs that were fixed in 1.1.0; the note was left behind.
Bare T and F are replaced by TRUE and FALSE throughout R/. Two of these were
public API defaults visible in the documentation (generateCI(zmap =, zmapdecoration =)
and plotZmap(decoration =)); the values are unchanged.
The guard that keeps a function's arguments across load() is now a shared helper,
captureArgs(). It skips required arguments that were not supplied — necessary once
paths became required, since mget() forces the promise and a wrapper forwarding its own
missing argument would abort there. Defaulted arguments are still captured: missing()
reports those missing too, and their default is exactly as vulnerable to being replaced
by a field in the .Rdata file as a value passed explicitly.
The failure paths are tested. The suite had 9 assertions covering 33 stop() and
warning() calls, so most of the package's error messages had never been run. They now
are: the stimuli/responses length mismatch, every "this .Rdata file did not contain X"
guard in generateCI() and computeCumulativeCICorrelation(), all four mask-import
failures, and base images that are unreadable or not square. No behaviour changed — this
is coverage of messages that were already there. It matters because an unexercised guard
is indistinguishable from one that works, which is how three separate bugs in this package
stayed live for years, the most recent being the one fixed just above.
Every function that reads a stimulus set now keeps its arguments across the
load(). load() assigns straight into the calling function's frame, so an object
stored in an .Rdata file silently replaces an argument of the same name. generateCI()
and generateReferenceDistribution2IFC() already guarded against this; computeInfoVal2IFC()
guarded three of its five arguments, and computeCumulativeCICorrelation() none.
No file this package has ever written triggers the problem, so no result changes and
no analysis needs revisiting — the guard is preventive. It is worth having because the one
collision that did occur (the z-map sigma, fixed in 1.2.0) was created by adding a field
to the file, not by adding an argument, so an argument that is safe today stops being safe
without anything in the function changing. The case now closed in computeInfoVal2IFC() is
the one that would have mattered most: target_ci is read at the very end to compute the
CI norm, and after a second load(), so a file carrying that name would have scored a
different classification image and returned a plausible number rather than an error.
No user-facing changes. Nothing this package computes differs from 1.2.0 — no function, argument, return value or number has changed, and no analysis script needs revisiting.
The release exists because the 1.2.0 source tree does not pass R CMD check on macOS. The
fault was in the package's own test suite, not in the package: a test asserted properties of
a rendered PNG that belong to the graphics device rather than to what was drawn, and those
properties differ between macOS and Linux. No released function was ever affected. A package
still has to pass its own checks on the platforms CRAN builds for, which is what this
release restores.
?plotZmap and the README now describe what is and is not reproducible across operating
systems. Classification images, scaling, informational value and the z-scores themselves
are ordinary R arithmetic and do not depend on your platform — as of this release that is
verified on Linux, macOS and Windows on every change, rather than assumed. The PNG written
by plotZmap() is the one exception: it is drawn through a graphics device, and devices
differ by platform in colour management and in whether they write an alpha channel, so the
same z-map produces visibly identical figures whose files are not byte-identical.
The practical advice, now stated in both places: compare numbers, not rendered figures, when checking that an analysis reproduces. A z-map image that differs pixel-for-pixel on a colleague's machine is not a different result. Every other PNG the package writes — stimuli, classification images, autoscaled classification images — is written directly from the pixel array and carries no such dependence.
R CMD check now runs on macOS and Windows as well as Linux, on every change. It
previously varied only the R version against a single platform, which is how the macOS
failure above went unnoticed.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.
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.
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.
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.
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.
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.
First release since 1.0.1, and the version submitted to CRAN to reinstate the package after its 2021 archival. The minor bump rather than 1.0.2 is deliberate: some of the changes below alter behaviour rather than only repairing it. The public API is unchanged — no function, argument or argument meaning was removed or redefined — so a 2.0.0 is not warranted. Anyone re-running an old analysis script should read the "Reproducibility impact" section below.
generateCI(zmap = TRUE, zmapdecoration = FALSE) works at all. The undecorated
branch of plotZmap() tested its background image with if (bgimage != ''), a
condition of length img_size^2, which R >= 4.2 treats as an error rather than
silently taking the first element. generateCI() always supplies a background
image, so this path could never run. Same root cause as the mask bug above; the
decorated branch already used identical().plotZmap(decoration = FALSE) works on small images. plot.new() was called
before the margins were reset to zero, and it rejects a device too small to hold
the default margins, so any z-map below roughly 100 pixels failed with
figure margins too large. Rendered output at usual sizes is unchanged.generateCI(zmaptargetpath = ...) is honoured. The argument was documented and
accepted but never forwarded to plotZmap(), so z-maps were always written to
./zmaps relative to the working directory regardless of what was requested.generateStimuli2IFC() now validates that the base image matches img_size, with an
error naming the file and both sizes, instead of failing inside a parallel worker with
non-conformable arrays (#124).mask argument of generateCI() works again. It was unusable on R >= 4.2, was
restricted to 512px stimuli by a hardcoded size check, and rejected greyscale-as-RGB
PNG masks even when it had successfully converted them.generateCI(), batchGenerateCI() and computeCumulativeCICorrelation() accept
tibble columns, not only data.frame columns (#70, #123). generateCI() also checks
that stimuli and responses are the same length up front.generateStimuli2IFC() saves nscales and sigma to the .Rdata file, and
generateReferenceDistribution2IFC() uses them (#81). See below.computeInfoVal2IFC(force_gen_ref_dist = TRUE) regenerates the reference distribution
instead of silently reusing the cached one (#113).generateNoiseImage() accepts pre-0.3.3 .Rdata noise patterns using the
sinusoids/sinIdx layout. This backward-compatibility path had never worked.simulateNoiseIntensities() runs at all, and honours its img_size argument.generateReferenceDistribution2IFC() gained an ncores argument instead of always
using detectCores() - 1.computeInfoVal2IFC() no longer creates a stray file called data in your working
directory. One write() call omitted its destination, and base R's write() defaults
to file = "data" — so the "reference distribution has been saved" message was written
to that file instead of the console.generateCI() accepts a greyscale-encoded RGB PNG as a mask. The conversion worked,
but the error for genuinely non-greyscale images was raised unconditionally afterwards,
so every such mask was rejected anyway.generateReferenceDistribution2IFC() no longer writes its own arguments into the
.Rdata file it updates. load() assigns straight into the calling function's frame,
so a stored rdata (or, newly, ncores) object overwrote the argument of the same name
on the next call — meaning a second call ignored the ncores you passed and wrote back
to the path recorded during the first call. computeInfoVal2IFC() additionally guards
against this when reading .Rdata files written by older versions, which still contain
the stale path.generateStimuli2IFC() no longer preallocates the full stimulus array before starting
its parallel workers. At the default 512px / 770 trials that array was ~1.5 GB, and
foreach copied it to every worker, which each wrote one slice into and discarded.
Addresses issue #12.on.exit(), so workers are released even when an
error interrupts the loop. Fixes the "closing unused connections" warnings (issue #50).generateNoiseImage() is about 6x faster — 1.66s to 0.28s per call at the default
512px with nscales = 5 — and allocates about 30% less memory. The per-pixel average
across patch layers is now computed with rowMeans(..., dims = 2) instead of
apply(..., 1:2, mean); that step alone is ~31x faster, and what remains is building
the weighted patch array, which is unavoidable. Because this function is called for
every trial during stimulus generation and again for every CI and z-map, the saving
compounds. Thanks to @hvalev, who diagnosed this and
benchmarked it in #122. See "Reproducibility impact" below — the result is not
bit-identical to the old one.ncores = 1 no longer builds a cluster. The parallel loops in
generateStimuli2IFC() and generateCI() previously started a one-worker cluster even
when asked for a single core — a second R process, loading the package, receiving each
iteration and sending it back. They now run in the current process. The test suite went
from 140s to 4s, and R CMD check from [8s/126s] on tests (eight seconds of CPU
against 126 elapsed, over 22 cluster spawns) to a fraction of that. Results are
unaffected and this is verified rather than asserted: ncores = 1 and ncores = 2
produce bit-identical stimulus parameters, noise bases and classification images, now
pinned by tests/testthat/test-parallel-equivalence.R. Neither loop draws random
numbers — parameters are drawn under set.seed() before the loop — so there is no
worker RNG stream to diverge. Addresses the remainder of issue #50's neighbourhood.Imports shrank from 27 packages to 15; none of the removed ones were used.dplyr::progress_estimated(), purrr::rbernoulli(), and
citEntry()/personList() in inst/CITATION. The rbernoulli() replacement was
chosen to preserve the random number stream exactly — see below.This release fixes several long-standing bugs. Most of them turn a crash into working behaviour, and therefore cannot change any result you already have — if the old code errored, there was no number to change.
Only two fixes can change a value you may already have reported. Both are listed first, with the exact conditions under which they apply.
A golden-master test (tests/testthat/test-regression-baseline.R) pins the
numeric output of the default pipeline — noise basis, classification image,
scaling, and infoVal — to the values produced before these fixes. It passes,
which is the evidence for the claim that default-configuration results are
unchanged.
1. infoVal, if you used a non-default nscales (issue #81)
generateStimuli2IFC() did not save nscales or sigma into the .Rdata file.
generateReferenceDistribution2IFC() re-generates the stimulus set from that
file to build the infoVal null distribution, so with nscales missing it
silently fell back to the default nscales = 5 — building the reference
distribution on a different noise basis than the stimuli participants actually
saw.
generateStimuli2IFC(..., nscales = <not 5>)
and then reported an infoVal (or sigma with noise_type = "gabor").nscales = 5, where the fallback happened to
coincide with the real value. This is the common case..Rdata files do
not contain nscales, so the fix warns rather than guessing — regenerate the
stimulus set, or pass the value explicitly.2. infoVal, if you passed force_gen_ref_dist = TRUE (issue #113)
The flag was silently ignored whenever a reference_norms vector already existed
in the .Rdata file, so the reference distribution was reused rather than
recomputed.
force_gen_ref_dist = TRUE expecting a fresh
simulation. You received the cached distribution instead.force_gen_ref_dist = FALSE, which was always
meant to reuse the cache.These fixes only affect code paths that previously errored out, so no previously obtained result changes:
generateCI() (issues #70, #123) — previously failed with
arguments must have same length.mask argument — previously failed on R >= 4.2 with
the condition has length > 1, and was restricted to 512px stimuli by a
hardcoded size check.img_size (issue #124) — previously failed inside a
parallel worker with non-conformable arrays..Rdata files using the sinusoids/sinIdx layout — the
backward-compatibility path was unreachable and always errored.simulateNoiseIntensities() — errored on every call.load() — only reachable if you renamed or moved an
.Rdata file between calls, in which case the old code either errored with
cannot open the connection or wrote the reference distribution back to the file's
former path. The ncores half affects speed only. No infoVal changes.generateNoiseImage() now averages patch layers with rowMeans(..., dims = 2)
rather than apply(..., 1:2, mean). These compute the same quantity but sum in a
different order, so they are not bit-identical: they differ by about one unit
in the last place, ~1e-19 in absolute terms on pixel values of order 0.01.
This is a different class of thing from the rbernoulli case below. That one would
have changed the random stream — a large, systematic divergence. This is
floating-point summation order, and it was checked against an independent oracle
(the average written as an explicit triple loop, using neither apply() nor
rowMeans()) across noise types, spatial scales and seeds: both the old and new
forms sit ~5.6e-17 from that oracle. Neither is "more correct" than the other.
tests/testthat/test-generateNoiseImage.R now pins the new implementation against
the original apply() form across 12 configurations, so it cannot drift further.purrr::rbernoulli(n, p) (deprecated) is internally runif(n) > (1 - p). The obvious
modern replacement, rbinom(n, 1, p), draws from the stream differently — verified across
150 seed/probability combinations that the two diverge. Swapping it in would have silently
changed every reference distribution, and therefore every infoVal, computed from a given
seed. The runif form is used instead, verified bit-identical to the old behaviour.
mad() (constant = 1.4826). This was fixed years
ago; it is now covered by a regression test so it cannot silently drift.generateNoisePattern()), the stimulus parameter draw, and
all four scaling methods, for the default configuration.Changes before 1.1.0 are recorded in ChangeLog, which is frozen at 1.0.1 and covers
the package back to 2014, including its CRAN era.