assign_triggers() gains a conditions argument that fixes the order of the
condition codes, and export_experiments() gains one that defaults to the
order of the design's conditions. The codes used to follow the order in which
the conditions first appeared in the counterbalanced trials, so a different
seed could swap them, and low_frequency was 101 in a design that lists
high_frequency first. The pipeline now passes the design's order, followed by
any other condition in the order the item source lists it, so the first
condition the design lists is 101. The condition codes of 12 of the 21
demonstration designs change with it. assign_triggers() called without
conditions behaves as before.cohens_d_ci() uses the large-sample standard error of d,
sqrt(1/nx + 1/ny + d^2 / (2 * (nx + ny))) (Hedges & Olkin, 1985; Borenstein
et al., 2009). It left out the d^2 term, which treated the pooled SD as known
and made the interval on a manipulated dimension far too narrow. In the English
frequency contrast, d = 5.27 had the interval [5.01, 5.53] and now has
[4.72, 5.83]. A matched control, whose d is near zero, moves by at most 0.002
across the 21 demonstration designs, and no d, TOST p-value or verdict changes.
The comparisons, datasheets and run logs of the designs with a manipulated
dimension change with it. No stimulus moves.match_report() documents the direction of its signed statistics. cohens_d
and its interval are the reference (first) condition's mean minus the mean of
the condition in that row, and var_ratio is that condition's variance over
the reference's.lexsync_cache_clear() removes a fetched corpus from the cache, together
with any partial download of it, or with no argument removes the whole cache.
It returns the paths it removed, invisibly.lexsync_cache_dir() only reports the cache's path. It created the directory
on every call, so asking where the cache was left an empty directory behind.
fetch_corpus() now creates the cache when a download into it starts, once
the registry entry and its URL have been accepted, so a refused call writes
nothing. That download begins by deleting any .part sidecar in the cache
that has gone a day without a write. An interrupt or a process that died
mid-transfer leaves one behind, and no handler removes it. A younger sidecar
may belong to a download still running in another session, so it is left
alone. A dest of the caller's choosing behaves as before.citation("lexsync") gives the DOI that CRAN assigned to the package,
10.32614/CRAN.package.lexsync, with the CRAN page as its URL. The 0.1.0 build
on CRAN predates the DOI and gives the repository's address. The entry names
CRAN as its publisher, and its text version is the APA reference for software:
author, year, title, version, "[Computer software]", CRAN and the DOI.templates/
directory found the unrelated 'templates' package there and failed. A test that
compares the package with repository files now accepts a root only if it holds
lexsync's own DESCRIPTION under R_workflow/ and the Python package beside it,
and skips on CRAN.local_mocked_bindings() can replace an imported function, as the corpus
download tests do with download.file().CHANGELOG.md for the full, cross-language history and the
planned methodological roadmap.These notes follow the package through its development up to the release, so they describe each change against the state it replaced.
Depends: R (>= 4.0.0). tools::R_user_dir does not
exist before 4.0.0, and round()'s post-4.0 algorithm shapes artefact bytes, so an
older R would fail obscurely or write different bytes without complaint.describe_stimuli() was
the one reduction still on stats::median, which averages the two middle values
through mean()'s long-double accumulator while the Python engine reduced through
numpy. Both engines now sort and take the exact middle, with (a + b) / 2 in plain
double arithmetic for even n, and regenerating every design moved no byte.
Reproducibility and parity claimed that every reduction shared one
compensated-summation algorithm,
which the median never did, and now describes the sort-and-middle rule beside it.define_by selects identically in both engines. This engine
always fell back to the whole pool. The Python engine crashed with a bare KeyError on
the same design and now mirrors the fallback, pinned by twin tests.INTER_TRIGGER_S is substituted through %.17g like its neighbours in the
generated PsychoPy script. It used as.character(), whose rendering of a
non-integer quotient differs from Python's str(). The shipped default still renders
"0.01", so no committed experiment byte moved.hash_unit()
always has, so a latin1-marked condition read from a user's CSV can no longer rank by
different digests in the two engines.R_workflow/run_pipeline.R prefers an installed lexsync over the edited sources, so
edits silently did nothing until a reinstall. It now states the loaded copy and the
remedy on stderr at startup.design_sha256, which must not depend on the
operating system), and its parity caption now matches the keyed-hash guarantee: the
engines produce byte-identical stimuli and trial order, where the caption used to
promise only the stimuli. The
Python engine's twin fixes land in the same release: its writer refuses 16-digit
integer columns as this engine always has, its scalar rounder passes an overflowing
scale through, and run_pipeline(verbose = FALSE) keeps its console silent.n_per_condition is now an error. Every selector
used to clip the request to the available pool and said so at most
through a verbose message, while the datasheet and the generated Methods text kept
stating the requested n. The new matching: shortfall policy defaults to error,
and allow accepts the shrink. Its sibling matching: on_insufficient_tolerance (default
relax) can likewise turn the silent tolerance-window widening into a refusal, and a
relaxation that does happen is now recorded in the run log and the datasheet
(selection.window_relaxations), where before it was only narrated to the console. No
committed design experiences either condition.joint and optimal matching can no longer select the same word twice. With
overlapping condition windows a word could be paired with itself at zero cost, or
mirrored across two sets, appearing in both conditions. Both matchers now track used
words as the anchored matcher always has, and every matcher asserts its output holds
each word at most once. Every committed design has disjoint conditions, and those
select identically, byte for byte.condition
reached Python's hash-key guard as the string "nan" and passed the very check built
to refuse it, a blank item cell turned numeric identifiers into "1.0", and R failed
on the same table with a bare "missing value where TRUE/FALSE needed". Missingness is
now checked before any coercion, item is read as text, empties after trimming and
duplicated item-and-condition rows are refused, and both engines say the same thing.equivalent: false.
The sentence is now conditional on the stored verdicts, prints the signed worst
difference and the configurable bound, and the analysis note names the suggested
formula as lme4 syntax, where it used to imply that statsmodels accepts it. The
datasheet also gains the SHA-256 of the design and schema, the pairwise matchers'
candidate cap and whether it fired, the equivalence bound tested against, and the
yaml, stringi and operating-system entries the environment record omitted.continuous block now works over a supplied pool, and two silent table-mode gaps
are refusals. source: pool reaches the continuous selector (it previously fell
through to the conditions matcher and crashed). A continuous table without members
is an error now, as is pool_filters on a plain table design, where each used to do
nothing silently while the provenance recorded otherwise. Misspelt pool_filters or
define_by columns, duplicate condition names, reversed or non-finite ranges,
negative tolerances and non-integer n are likewise refusals now..round_dp and a
new vectorised numpy twin), replacing a pairing of R's round() with numpy's, two
rounders the project measured to disagree at boundaries. Regenerating all 21 designs
moved no stimulus, report or experiment byte. A custom norm named in match_on now
also reaches the descriptives, comparisons and realised-control record, which
previously listed only the built-in dimensions.language_tag is shape-checked before it is used. Escaping
them correctly would have taken three rules per engine.
.pyq() escapes newlines now as well: an .osexp is line-oriented, so a raw
newline closed the inline-script block and let the rest of the value start a new
top-level item. Pinned by test-injection.R. No generated artefact changed.
Adversarially attacking that fix then found four ways through it, all now closed and
pinned. The largest: a response event's keys were joined into OpenSesame's set allowed_responses "a;b" with no validation at all, and that is one line of a
line-oriented format, so a key holding a double quote closed the string, a newline
ended the line, and the rest of the value became new top-level items in the
experiment, including an inline_script whose body OpenSesame runs. Keys are
validated now. The shape guards were also anchored with $, which in both Python's
re and R's PCRE matches just before a final newline, so a port address ending in one
passed the check. They are anchored at end-of-string now. A scalar keys: space or
blocks: practice was iterated character by character in Python and kept whole in R,
so one design gave two different allowed-response lists and a block-restricted event
ran everywhere in one engine and nowhere in the other. And R's HTML escape did not
cover U+2028/U+2029, which end a line in JavaScript but are not ASCII controls: Python
escaped them, R did not, so the same design produced different bytes and R's
<script> was a syntax error before ES2019.write_csv_utf8() reproduced readr's format for small magnitudes and left the top end
alone, since nothing lexsync computes reaches it. Nothing lexsync computes does. A
joined norm table, a supplied pool or an item table carries whatever columns the user
has, and those go straight into the stimuli CSV, so the guarantee held for the shipped
designs and failed silently for the user's own data. readr's layout above 1e15 could not
be reproduced in Python, which writes 1.5e16 as 15e15, the largest double as
17976931348623157e292, and the double nearest 5e22 as 4.9999999999999996e+22. Both
engines now refuse such a value and name the column. One engine accepting it would
leave the two writing different bytes. A value with two equally short decimal forms is
refused for the same reason. Everything verified to render identically, across 465
values compared against readr's own output, writes as before.feedback event or a blocks: restriction
would not run. The emitter wrote unicode(...), a Python 2 builtin that
OpenSesame's Python 3 inline workspace does not provide, so the experiment died with
NameError on the first trial. It also passed None as a var.get() default, which
OpenSesame cannot distinguish from no default, so it raises where it should return.
Both are fixed, both spellings are now pinned by a test, and a feedback event with
no preceding response or question is refused at generation time. Left to run time,
that one design error would surface as three different failures, one per target.round() disagrees with
Python's builtin, Python's builtin disagrees with numpy's, and even R's
sprintf("%.3f") disagrees with Python's "%.3f" on 274 of them. Some values move by
one in the last published digit. No selection changes.condition cell, a routine data error neither
reader rejects, rendered "NA" in R and "nan" in Python, so the two engines
produced different trial orders from the same design, reproducibly and with nothing to
signal it. Booleans get a pinned spelling and are still accepted.joint and optimal matchers goes through the
compensated reduction. The cap does fire on shipped designs, so it decides which
candidates reach matching. It was verified to change no design's selection.R CMD check --as-cran is clean: 0 errors, 0 warnings, 1 note (the standard
new-submission note). Fixed on the way: three undocumented arguments on
select_continuous_stimuli(), an unqualified utils::head(), and the package's only
example, which called an unexported function from inside \dontrun{} and so could
never have worked if a user copied it. The package now has executable, offline
examples, and cran-comments.md no longer misstates the check result.practice: and fillers: item tables. Those trials are
presented but not analysed, so the stimuli file and the
reports are written from the main rows while the generated experiments run every
trial. Practice comes first. Fillers are interleaved with the main trials, because a
block of fillers at the end is not a filler but a second block a participant can tell
apart. A design declaring neither is unaffected, down to not gaining a block column.feedback trial event, and a blocks: key that restricts any event to named
blocks. Together these confine feedback to practice, which is the usual arrangement:
feedback teaches the mapping, and would contaminate the reaction times it is
measuring. Implemented for PsychoPy, OpenSesame and jsPsych alike. The
PsychoPy runner now returns the pressed key so it can be scored, and each runner pauses
at a block boundary.NaN in the generated jsPsych experiment where a
trial had no value for a field another block supplies (a main-block trial in a design
whose practice items carry an answer). That is not valid JSON, and the R engine
dropped the key instead, so the two engines' experiments differed. Both now drop it.1 and 1.0 are the same number. Thirteen of the 18 shipped designs differed
byte for byte while the gate stayed green. Three differences were serialisation (a
whole number written 1 and 1.0, a boolean written FALSE and False, a small
value written 9e-4 and 0.0009) and one was not: two reported means differed in the
last decimal published, because numpy sums pairwise and R's mean() does not. Every
reduction in the package now uses one compensated-summation algorithm written out in
both engines, whose agreement follows from IEEE-754 and no longer from measurement.
The writers agree on every value, and the artefacts are now compared as bytes. No R
golden moved, because R's two-pass mean was already the correctly-rounded one.f was silently turned into FALSE. readr reads a column
whose values are all f, t, T or F as logical, while pandas keeps the string, so
an item table using the commonest two-choice key pair had its correct answer corrupted
in one engine. Item tables now read the condition label and the paradigm's presented
fields as text in both engines.jsonlite's default of four digits was truncating the datasheet: a design declaring
tolerance_k: 0.1111111111111111 had it recorded as 0.1111, which does not reproduce
the run the record exists to describe. The JSON is now written at full display
precision, and the Python engine writes the same precision.categorisation: a category cue, then the word to judge against it, with
answer holding the correct response key so the data are scoreable. Counterbalanced by
Latin-square rotation, so a participant never sees the same target twice.counterbalance.optimise (off by default) assigns item sets to lists so the lists are
equated on the declared dimensions, where the old deal went by set rank. The search is
a deterministic integer descent with a keyed-hash tie-break, so it uses no random
number generator and both engines produce the same assignment. balance_lists() is
exported.pool: a supplied word list goes through the matcher without having
to masquerade as a corpus lexicon. load_pool() is exported. The neighbourhood
dimensions are computed against the lexicon's words, because a word's neighbours are
its neighbours in the language.norms: block. They are joined onto the
lexicon before the candidate pool is built, so a semantic dimension the corpus
does not carry (concreteness, age of acquisition, valence) can be filtered on,
matched on or spanned like any other. No norm data is bundled. Every joined table
is recorded in the materials datasheet with its checksum and its per-column
coverage, because a norm table can supply the very variable a design manipulates.materials_source$norms and, for a pair-keyed
design, a relational block naming the members, the pair count, the member
lexicon and its checksum, and the member-level dimensions separately from the
relational ones. selection$cross_engine no longer reports
"n/a (user-supplied items)" for a pair-keyed continuous design: that design does
perform a selection, and it is byte-identical across engines.merge_norms() returns the lexicon with the norm columns appended, in the
lexicon's own row and column order. It previously used merge(), which hoists
the join column to position 1 where pandas keeps the left frame's order, so the
two engines disagreed on column order whenever on was not already first. The
join key is now case- and whitespace-folded on the lexicon's side too, where only
the norm table's was folded before: a lexicon holding Dog used to match nothing and
leave an all-NA dimension. A norm column whose name already exists on the lexicon
is now an error. Renaming it to frequency.x / frequency.y, as merge() did, left
the matched dimension under a name nothing looks for.write_datasheet() and write_run_log() now write LF on every platform. They
used a text-mode connection, so on Windows the datasheet, its Markdown rendering
and the Markdown run log came out CRLF while the Python engine's twins were LF.
The datasheet is the provenance record. Its bytes no longer depend on the machine
that built it.add_pair_overlap() and resolve_trial_timing() are now exported in fact as well as
in the documentation. Both were marked for export and documented, but the NAMESPACE
had not been regenerated, so library(lexsync) did not make them available.items.generation.method: subsyllabic): whole onset/nucleus/coda constituents are swapped for attested
same-role, same-length alternatives, preserving syllabic structure and length.
Byte-identical across engines, and the default letter-substitution generator is
unchanged.continuous block and
select_continuous_stimuli() spans a predictor's range evenly while holding the
controls near-constant, with predictor-control correlations and a regression
suggested-model in the datasheet. Byte-identical across the R and Python engines.mahalanobis (a covariance-aware distance that
down-weights correlated dimensions) and optimal (a globally optimal assignment
for two-condition designs, using the suggested clue package). Unlike the
default methods, these use a covariance inverse and an assignment solver, so the
R and Python engines agree closely but not byte-for-byte on them.matching.method, and a candidate pool too small for the requested
n_per_condition, now raise an actionable error. Each used to fall back silently,
to a default method or to a short set. The Python engine raises the
same message, and code that relied on the old fallback will now stop.stringi is a new hard dependency (Imports). The canonical word key is
case-folded with ICU at the root locale, so the key no longer depends on the
machine's locale and matches the Python engine byte for byte. shiny, bslib,
DT and zip are new Suggests, for the Shiny app.PARADIGMS(), build_datasheet(), build_lexdec_stimuli(),
count_syllables(), generate_pseudowords(), make_pseudoword(),
methods_paragraph(), required_fields(), resolve_events() and
write_datasheet().tost_equiv() now defaults to bound_d = 0.5, the value the Python engine
already used. The R default was 0.4, so the equivalence bound is wider and a
comparison is easier to declare equivalent. Reported verdicts can change for the
same data.describe_stimuli() now orders rows by each group's first appearance, matching
pandas. This changes the row order of generated descriptives.seed|replicate|list|set|condition, a tuple that
identifies the trial uniquely under either counterbalancing recipe, so the
permutation is a pure function of the design: byte-identical from both engines
on any platform, and different for every seed. Previously the order was drawn
from sample(), which could never match numpy's generator for the same seed,
so the trial lists were the one engine-specific artefact. All 75 generated
experiment files across the 15 designs bundled at the time are now byte-identical across
the engines, and the parity gate now compares the trial column of the
stimuli CSVs. Stimulus selection, pairing and lists are unchanged, but every
design's trial order changes relative to the previous artefacts. The package
no longer uses R's random-number generator at all, so a seeded run cannot
perturb the calling script's random stream and there is no RNG state to save
or restore.merge_norms() preserves the lexicon's row order, and participant_table()
crosses factors in expand.grid() order in both engines.match_stimuli() raises where it used to re-pick an already-used row, when a
relaxed window re-admits candidates missing a matched dimension. Such a
candidate has no defined distance and is never assigned, yet it still counted
towards the pool-size guard. An NA-depleted pool could therefore pass the guard
and go on to emit the same word in several sets.match_stimuli() and select_continuous_stimuli() no longer select an all-NA
row when the tolerance window is NA (an anchor of a single item gives sd = NA).
An undecided comparison now resolves to FALSE, as it does in Python, so the
window is relaxed and a real word is selected.