API reference¶
Every public name in lexsync is documented here, grouped along the path a study takes: reach a
corpus, derive the dimensions, build a pool and match it, describe a trial, counterbalance it,
report what the matching achieved, generate the experiment, and write down the provenance. The
groups are the same ones the R package's reference index
uses, so a name can be found in the same place on either site.
Everything listed under a group heading is importable straight from lexsync, with a handful of
exceptions that are noted where they appear and are reached through their own module. Guides with
worked examples are linked from the home page, and the published work that these entries
cite is listed in full on the references page.
Corpora and lexica¶
Languages are supplied through a corpus registry, so reaching a new one takes a registry entry and no code. These functions find a corpus, download it into a per-user cache that can be cleared again, turn a corpus file in its publisher's layout into a lexicon, and read a derived lexicon or a prepared item table into the frame everything else expects.
lexsync.list_corpora(registry_path=None)
¶
Source code in src/lexsync/corpora.py
47 48 49 50 51 52 53 54 55 56 57 58 | |
lexsync.fetch_corpus(name, registry_path=None, n_words=10000, *, strict=False, exclude_latin=False)
¶
Fetch a registered corpus into the cache.
name must be a single corpus name: one holding '/', '\' or ':' is refused
before the registry is read, since the name becomes the cached file's name.
If name is a language code supported by the wordfreq connector, a lexicon
is built with wordfreq from up to n_words of its most frequent forms that
are spelt with letters. A form may hold combining marks, such as the vowel
signs of Hindi and Bengali, and the zero-width non-joiner of Persian. It may
also hold an apostrophe inside a Ukrainian word or a middle dot between two
l's in a Catalan one, but no digit and no other punctuation mark. A lexicon
left with fewer forms is reported on standard output, or refused with
strict, and exclude_latin drops every form holding a Latin letter. These
three arguments apply to the wordfreq connector only, and
lexsync.corpora.build_wordfreq_lexicon gives the rule in full.
For any other name the registry entry's file is downloaded, once its URL's scheme
has been checked. An entry's derived file, a lexicon already normalised, is
fetched when it has one and cached as it arrives. Otherwise its openlexicon
file is fetched and converted by :func:normalise_lexicon with the entry's
columns and duplicates, so fetch_corpus("subtlex_us") caches a lexicon
that :func:lexsync.load_lexicon reads. The transfer lands in a sidecar file that
is renamed into the cache only after the size cap, the markup sniff, any registered
sha256 and the normalisation have all passed. Every refusal names the registry
file it read. A registry_path that does not exist is refused.
The file lands in :func:cache_dir. The cache directory is created only once
the registry entry and its URL have been accepted, or the wordfreq lexicon
built, so a refused call writes nothing. Each fetch into the cache begins by
deleting any .part sidecar that an interrupted transfer left there more
than a day earlier. The cache persists between sessions, and one corpus may
reach the 200 MB download cap, so several of them add up. :func:cache_clear
removes one corpus or the whole cache, and the next call fetches the corpus
again.
Source code in src/lexsync/corpora.py
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 | |
lexsync.normalise_lexicon(path, columns=None, entry=None, delimiter=None, out=None)
¶
Turn a corpus file in its publisher's layout into a lexicon load_lexicon reads.
Reads a tab- or comma-separated file. delimiter names the separator, "," or
"\t". Left as None, it is taken from the extension (.tsv and .tab are tab, .csv is
comma) and otherwise from the header line, which is tab-separated when it holds a
tab. A tab-separated file is read with no quoting.
columns maps lexsync's column names to the file's, as a registry entry's
columns does: word, one of freq_zipf and freq_permillion, and
optionally pos. entry is a registry entry, or the name of one in the
registry lexsync finds by default (see :func:list_corpora), and supplies the map
when columns is None and the duplicates rule. Without a map the file's own
word, freq_zipf or freq_permillion, and pos columns are read.
Words are trimmed, lower-cased and put in Unicode NFC, as load_lexicon keys them,
and rows missing a word or a frequency are dropped. A form that then appears more
than once is resolved by duplicates: first (the default) keeps the first
row in file order, and sum adds its rows' frequencies per million, as Lexique
needs, since it splits a homograph's frequency between its rows. Frequencies per
million are converted with Zipf = log10(per million) + 3, rounded to three places
by lexsync's shared rounder. A value of 0 or below has no Zipf value and is
refused. Zipf values read as they are must lie between 0 and 9.
The lexicon has the columns word, freq_zipf, then freq_permillion when
Zipf was derived from it, and pos when the file has one, in the UTF-8 byte
order of word. With out it is also written there, comma-separated with LF
line endings. A line on standard output gives the number of forms, and for derived
Zipf values the number that lay within 1e-9 of a rounding boundary after scaling.
That is the only step at which the two engines' log10 could round a value apart,
and the count is expected to be 0. attrs["normalisation"] holds {"n_read",
"n_forms", "n_merged", "duplicates", "zipf_derived", "near_boundary"}, and a run
over the lexicon records the boundary count in its run log and datasheet.
Mirrors normalise_lexicon in R_workflow/R/corpora.R, which writes the same bytes.
Source code in src/lexsync/corpora.py
672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 | |
lexsync.corpora.build_wordfreq_lexicon(language, n_words=10000, min_len=2, max_len=15, *, strict=False, exclude_latin=False)
¶
Build a (word, freq_zipf) lexicon for any wordfreq language.
Takes wordfreq's most frequent forms for language, lower-cased and in Unicode
NFC, until n_words of them pass the letter filter and are min_len to
max_len characters long. It looks no further than the first 3 * n_words
forms. A form passes when it opens with a letter and every other character is a
letter, a combining mark or a zero-width non-joiner or joiner. Hindi है and
Bengali না, spelt with combining vowel signs, therefore pass, as do the many
Persian forms spelt with a non-joiner. A Ukrainian or Belarusian form may also
hold an apostrophe between two letters (п'ять), and a Catalan one a middle dot
between two l's (pel·lícula). Digits never pass, and nor do underscores or any
punctuation mark besides those joiners, so English contractions and French
elisions stay out. Length counts code points, as the lexicon readers count it,
so vowel signs, viramas and zero-width characters count. A form that wordfreq
gives no frequency is dropped.
When the lexicon holds fewer than n_words forms, a line on standard output says
so, and with strict the shortfall raises ValueError. exclude_latin drops every
form holding a Latin letter, such as the English tokens in the Hindi list. It is
not a one-script rule, so a Japanese lexicon keeps its kana and kanji together,
and it leaves nothing of a language written in the Latin alphabet.
Raises ModuleNotFoundError if the optional [corpora] extra is not installed.
Source code in src/lexsync/corpora.py
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | |
lexsync.corpora.cache_dir()
¶
Per-user cache directory for fetched corpora.
Where :func:fetch_corpus puts what it fetches, and the only place the
package writes to without being handed a path. This function only reports
the path. The directory is created by the first fetch into it, so until then
it does not exist.
The cache persists between sessions. A registered corpus is a delimited word
list, and a download is refused above 200 MB, so a cache holding several
large corpora can reach a few hundred megabytes. Each fetch into the cache
first deletes any partial download that an interrupted transfer left there
more than a day earlier, and fetching a corpus again replaces the earlier
copy. Fetched corpora otherwise stay until :func:cache_clear removes them,
one corpus at a time or all together. Nothing kept there is irreplaceable, so
the next call downloads afresh. The R twin documents the same contract in
lexsync_cache_dir.Rd; only the location differs, since R uses
tools::R_user_dir.
Source code in src/lexsync/corpora.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
lexsync.corpora.cache_clear(name=None)
¶
Remove fetched corpora from the cache.
Deletes what :func:fetch_corpus put in :func:cache_dir. Given a name,
that is the corpus's <name>.csv, any <name>.csv.part that an
interrupted download left behind and the <name>_wordfreq.csv lexicon the
wordfreq connector builds. With name left as None it is the whole cache
directory, and the ~/.lexsync directory above it once nothing else is left
there. A corpus needed again is fetched afresh by the next
:func:fetch_corpus call. Mirrors lexsync_cache_clear() in
R_workflow/R/corpora.R, whose engine has no wordfreq connector and so no
_wordfreq.csv to remove.
Returns the paths of the files removed, empty when there was nothing to remove. Raises OSError, naming them, when files survive the attempt (one held open on Windows, say).
Source code in src/lexsync/corpora.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
lexsync.load_lexicon(path, schema, language=None)
¶
Load a derived lexicon and give it the inexpensive dimensions.
word is trimmed, lower-cased and normalised to Unicode NFC, and duplicates are
then removed, keeping the first row in file order. When normalisation changed or
merged any word, attrs["input_normalisation"] holds {"changed",
"collapsed"}: the words it rewrote and the duplicate rows it removed.
A freq_zipf value outside 0 to 9 is refused: it is almost always a frequency per
million in the wrong column (convert it with normalise_lexicon). A log count such
as SUBTLEX-US's Lg10WF, log10(count + 1) and at most 6.33, passes the check and
must still be converted, since it reads about 1.3 below Zipf. Mirrors load_lexicon
in R_workflow/R/querying.R.
Source code in src/lexsync/querying.py
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | |
lexsync.load_items(path, required_fields)
¶
Load a paradigm item table (prime-target pairs, sentences, …).
The table must carry an item identifier, a condition label and the
paradigm's presented fields. Field values are validated (no control
characters; bounded length) so a crafted item cannot corrupt the generated
loop table or scripts. Items are mapped to a deterministic integer set id
(byte order) so counterbalancing matches the corpus path and the two engines.
The item id, the condition and the presented fields are normalised to Unicode
NFC. When that changed any cell, attrs["input_normalisation"] holds
{"changed", "collapsed"}, with collapsed always 0 since no row is removed.
Source code in src/lexsync/querying.py
802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 | |
lexsync.load_pool(path, schema, lexicon=None, language=None)
¶
Load a supplied candidate pool of words and give it the matcher's dimensions.
A researcher who already has a curated word list (from a previous study, a norming session, a colleague) should not have to dress it up as a corpus lexicon to get lexsync's matching, validation and datasheet. This reads such a list and returns something the matcher accepts.
The list needs only a word column. Length and the syllable estimate are derived
from the form. Everything else is either supplied on the list itself or looked up:
with lexicon given, the corpus dimensions (frequency above all) are joined for
those words, and a word the lexicon does not have is a hard error rather than a
NaN, because the tolerance windows drop missing rows silently and the pool would
then be smaller than the user believes it is.
The returned reference matters as much as the pool. n_density and old20
are properties of a word in its language, not among the handful of words a study
happens to use, so computing them against a 200-word supplied list would give
numbers that mean nothing. When a lexicon is given, the reference is the lexicon's
words; only without one does it fall back to the pool itself. A
reference_member column on whichever of the two supplies the reference keeps
only the rows it marks 1.
Returns {"pool": DataFrame, "reference": list}. Words are trimmed, lower-cased
and normalised to Unicode NFC as in load_lexicon, and the pool carries the same
attrs["input_normalisation"] record when that changed or merged any word of
the pool or the lexicon. Mirrors load_pool in R_workflow/R/querying.R.
Source code in src/lexsync/querying.py
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 | |
lexsync.merge_norms(lexicon, norms, on='word', columns=None)
¶
Left-join a norm table (e.g. concreteness, age of acquisition, valence).
norms is a data frame or the path to a CSV with a word column and one or
more norm columns. This is the connector for semantic dimensions: the norm data
themselves are fetched separately (licensing varies), then merged here so the
matcher can equate on them.
The result is the lexicon itself with the norm columns appended, and the key is
looked up positionally rather than through merge. That is what makes the two
engines agree by construction, with nothing to repair afterwards, because merge
and R's merge() were measured to diverge in three ways, each of them silent:
R hoists the by column to position 1 while pandas keeps the left frame's
order, so the column order differed whenever on was not already first; R
disambiguates a colliding column name with .x/.y and pandas with
_x/_y, and either way a dimension the design matches on disappears under
a name nothing looks for; and R's merge(sort = FALSE) leaves the row order
unspecified. A positional lookup has none of those degrees of freedom. A
colliding name is now an error instead.
The key is trimmed, case-folded and normalised to NFC on both sides. Only the
norm table's side was trimmed and case-folded before, so a lexicon holding
Dog matched nothing and the design carried on with an all-NaN dimension.
Because both engines agreed on that wrong answer, no parity test could have
caught it. The lexicon's
own spelling is preserved rather than folded in place: word is the
byte-order tie-break behind every selection, so the join must not rewrite it.
Mirrors merge_norms in R_workflow/R/querying.R.
Source code in src/lexsync/querying.py
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 | |
Lexical dimensions¶
Two dimensions arrive with the lexicon and the rest are derived from the orthographic forms. Derive them before matching, and compute the neighbourhood measures against the full lexicon rather than the pool, since a word's neighbours do not stop existing because a design excluded them.
lexsync.add_neighbourhood(df, reference=None, n_old=20)
¶
Coltheart's N (same-length, single substitution) and OLD20 for each word.
The Levenshtein distances to the reference come from one rapidfuzz distance matrix per block of rows, which took 0.9 s for 500 words against the 30,000-word English lexicon where the word-by-word loop it replaces took 19 s. Between two forms of the same length a distance of 1 is a single substitution, so N counts the same-length reference words at distance 1, as the Hamming distance did. The distances are integers, so N and OLD20 are the values the loop gave and the R engine gives.
Source code in src/lexsync/querying.py
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 | |
lexsync.add_bigram_frequency(df, reference=None)
¶
Mean bigram probability (type-based, non-positional), a phonotactic-probability proxy.
For each word, the mean over its adjacent letter bigrams of the corpus bigram probability (count divided by the total bigram count). Computed from integer counts and rounded, so it is identical in the R and Python engines.
Source code in src/lexsync/querying.py
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 | |
lexsync.add_pair_overlap(df, prime='prime', target='target')
¶
Orthographic overlap between the two members of each pair.
Adds two columns. pair.lev is the Levenshtein distance between the pair's
two orthographic forms, and pair.overlap is 1 - lev / max(len), the
proportion of the longer form the two share. Overlap is the standard confound
control in a priming design: a related pair that also shares letters confounds
semantic relatedness with orthographic similarity.
Both engines return identical values, and the reasons are worth stating because
they are the constraints on any future relational dimension. The core is an
integer edit distance, and rapidfuzz's Levenshtein.distance and
stringdist(method = "lv") agree exactly, including on decomposed Unicode and
CJK, which is the same cross-library agreement add_neighbourhood already
stakes old20 on. Length is counted in code points, len() and R's
nchar() default, never in bytes. The arithmetic uses only - and /,
which IEEE-754 mandates be correctly rounded, and the result is rounded to nine
decimal places, the constant used everywhere else in the package. A degenerate
pair of two empty forms returns 0 rather than 0/0, because a NaN would be
sorted and compared and would then drop the row from one engine's control window
but not the other's.
Source code in src/lexsync/querying.py
766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 | |
lexsync.count_syllables(word)
¶
An orthographic syllable estimate: the number of maximal vowel runs.
Counts the maximal runs of Latin vowel letters after decomposing the word and
setting its diacritics aside, so a vowel counts whatever its accent. The vowels
of Polish, Czech, Hungarian, Romanian, Turkish, Danish, Norwegian and Vietnamese
count as those of English, Spanish, French and German do. Hiatus (idea
counts 2) and syllabic consonants such as the Czech l in vlk and r in krk
stay undercounted.
Returns None for a missing value and for a word holding a character outside
the Latin script (Chinese, Cyrillic or Greek, for example), where the estimate is
undefined. load_lexicon and load_pool then leave n_syllables NaN.
Mirrors count_syllables in R_workflow/R/querying.R.
Source code in src/lexsync/querying.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | |
Pools and matching¶
The pool is the set of candidates a design will consider at all, and the matcher works only on what
it is given. match_stimuli never reads a design's pool_filters, so build_pool is a required
step.
lexsync.build_pool(lexicon, filters=None)
¶
Build an experimental candidate pool by filtering a lexicon.
filters maps a column to either a two-element numeric range or a list of
permitted values. A row missing the filtered column is dropped under either
kind, and a range with a reversed or non-finite bound is an error rather than an
empty pool.
Two numbers make a range, and a boolean is not a number, so YAML's
[true, false] is a list of permitted values. A list of numbers is compared
with a numeric column as numbers, and any other list as text, the cells and the
values written as R's as.character() writes them (see _r_text). A filter of 1
therefore keeps the 1.0 cells of a 0/1 column that a missing value has made
float. As in the R engine, [true, false] keeps both values of a logical
column and no row of a 0/1 column.
A filter naming a column the frame does not have is silently skipped, because the
same function filters lexica, supplied pools and pair tables, and those carry
different columns. The cost is that a misspelt key silently widens a
selection, so every caller that takes its filters from a design checks the names
against the frame first: run_pipeline for pool_filters,
match_stimuli for a condition's define_by, and
select_continuous_pairs for both. Mirrors build_pool in
R_workflow/R/querying.R.
Source code in src/lexsync/querying.py
899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 | |
lexsync.match_stimuli(pool, design, schema, verbose=False)
¶
Match stimuli across conditions on the match_on dimensions.
Two policies govern degraded selections, each read from the design's
matching block with the schema as fallback: shortfall ("error", the
default, refuses to return fewer sets than requested; "allow" accepts the
shrink) and on_insufficient_tolerance ("relax", the default, widens an
undersupplied tolerance window to the full condition subpool and records the
relaxation in out.attrs["audit"]; "error" refuses instead).
match_on must name at least one dimension. The number of items per
condition is n_per_condition, else n_per_cell, else 20, and must be a
whole number of at least 1. A null tolerance is read as R's modifyList()
reads it (see the schema's tolerance_k comment).
A candidate missing any match_on dimension cannot be scored, so every
method excludes it before selecting, and the number excluded per condition is
recorded in out.attrs["audit"]["incomplete_dropped"]. The shortfall policy
then applies to what remains, and a condition left with no complete candidate
is refused.
tie_break, read in the same way, decides between candidates at exactly the same
rounded distance, which integer dimensions make common: "hash", the default,
prefers the smaller SHA-256 of TIE_SALT, a bar and the word, which favours no
part of the alphabet, and "bytes" prefers the spelling that sorts first and
reproduces lexsync 0.1. The rule also orders the tied pairs of joint and
optimal and the tied rows at their candidate cap. The anchor's even spread
keeps its byte-order tie-break under both rules.
Source code in src/lexsync/matching.py
459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 | |
lexsync.resample_stimuli(pool, design, schema, n_sets, verbose=False)
¶
Produce n_sets disjoint matched item sets (a replicate column).
Each replicate is an independent, fully matched set drawn from the pool with the items of earlier replicates removed, so no item is reused. This lets a study treat its items as a random factor (running different item samples across participant groups, or showing an effect holds across samples) instead of treating them as a fixed set (Clark, 1973; Yarkoni, 2022). Deterministic: the matcher is deterministic and the used-item set evolves identically across engines.
The first replicate draws on the whole pool, so its count of the candidates
excluded for a missing matched value is the one recorded in
attrs["audit"]["incomplete_dropped"], as :func:match_stimuli records it.
A relaxed tolerance window inside a replicate is not recorded: it reaches the
console under verbose but not the run log or the datasheet.
n_sets must be a whole number of at least 1, as resample.n_sets must.
Source code in src/lexsync/matching.py
861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 | |
Continuous designs¶
Dichotomising a continuous predictor costs power and can introduce selection artefacts. A design may instead spread its items over the predictor's ranks, following the pool's distribution, while tolerance windows trim the tails of its controls, and be analysed by regression or a mixed model.
lexsync.select_continuous_stimuli(pool, design, schema, verbose=False, key='word', label='continuous', renumber_sets=True)
¶
Select a set spread over a continuous predictor, with windows on the controls.
Instead of dichotomising the predictor into conditions and matching, items are
spread evenly over the predictor's ranks in the pool, so the selection follows
the pool's distribution of the predictor and does not cover its range evenly.
The control dimensions are held within a tolerance band, which trims their
tails without holding them constant, so read each control's correlation with
the predictor in match_report_continuous before relying on it. Tightening
tolerance_k does not reliably lower those correlations. The set is analysed
by regression / mixed models rather than by between-condition contrasts, which
avoids the loss of power and the selection artefacts of matched dichotomies
(Kuperman, 2015; Liben-Nowell et al., 2019).
Two deterministic passes reuse the matcher's even-spread primitive, so the R and Python engines select byte-identical stimuli: an even spread over the predictor defines a tolerance window on each control; the pool is filtered to that window; a second even spread over the filtered pool is the selection. There is no per-item matching and no random number generator.
The design is checked before anything is selected, so a design that cannot be
honoured raises ValueError outright. continuous.controls must be non-empty
and must not name the predictor, match_on must name exactly the same dimensions
as continuous.controls, every dimension named and the key column must be
present in the pool, no tolerance_k may be negative, and the pool must not be
empty.
A row missing the predictor or a control is excluded before the first pass, and
the number excluded is recorded in attrs["audit"]["incomplete_dropped"] under
the condition continuous. A pool with no complete row is refused.
attrs["audit"]["control_windows"] records, for each control, the window the
first pass set (low, high) and the SD of the complete pool (pool_sd),
which :func:match_report_continuous sets the realised SD against. A relaxed
window is recorded as computed, and the relaxation in window_relaxations.
Returns the selected stimuli. Unless label is None the condition
column is set to it, continuous by default, and unless renumber_sets is
False the set column is renumbered 1..n.
Source code in src/lexsync/matching.py
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 | |
lexsync.match_report_continuous(stimuli, predictor, controls, schema, audit=None)
¶
Realised-control report for a continuous design.
Returns the same {descriptives, comparisons} shape as :func:match_report,
so the pipeline and datasheet stay uniform, but the comparisons describe a
continuous predictor instead of a between-condition contrast: the predictor's
realised span and, for each control, its Pearson correlation with the predictor
(near zero when the control is held constant). The set is meant for regression /
mixed-model analysis, not equivalence tests.
sd is each dimension's realised SD. For the predictor, sd_ratio sets it
against the SD of as many values spaced evenly over the same range,
(max - min) * sqrt(n * (n + 1) / 12) / (n - 1) with the n - 1 that sd
uses, so an even spread has a ratio of 1 whatever its size and a selection that
follows a skewed pool a ratio below 1. That benchmark tends to
(max - min) / sqrt(12), the SD of a uniform distribution over the range, as n
grows. For a control, sd_ratio sets the realised SD against the SD of the pool
the selector drew from, and window_low and window_high give the window the
selector banded it to, before any relaxation. Both come from audit, the
selector's record, which is read from stimuli.attrs["audit"] when not given.
Without one, a control's ratio and window are missing.
Source code in src/lexsync/validation.py
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 | |
Pseudoword generation¶
Non-words are generated deterministically, with no sampling anywhere, preserving length exactly and keeping every letter bigram attested in the corpus. Both methods select byte-identical stimuli in the R and Python engines.
lexsync.generate_pseudowords(base_words, reference_words, exclude=None)
¶
A length-matched pseudoword for each base word.
Base words are processed in byte order so the used set evolves identically
across engines. The letters substituted in are those the reference words attest.
exclude (optional) lists further word forms no pseudoword may equal, such as
a list of low-frequency words the reference lacks. A base word in a logographic
script is refused. Returns a frame with base_word and pseudoword.
Source code in src/lexsync/generation.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | |
lexsync.make_pseudoword(word, bigrams, lexicon, used, alphabet=None)
¶
The most bigram-plausible legal non-word at the smallest edit distance.
Searches single-letter substitutions first, then two-letter substitutions;
candidates are ranked by summed bigram frequency with a byte-order tie-break,
so the choice is deterministic and identical across engines. alphabet lists
the letters substituted in, in UTF-8 byte order. It defaults to the letters of
the bigram table, which hold every letter a legal candidate of two letters or
more can use. The generators pass the letters of the whole reference.
Source code in src/lexsync/generation.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | |
lexsync.build_lexdec_stimuli(pool, n, reference_words=None, method='letter_substitution', exclude=None)
¶
Assemble a word-vs-pseudoword lexical-decision set from a candidate pool.
Real words are drawn by an even spread across the byte-ordered pool (the same
deterministic device as the matcher's anchor), then a length-matched
pseudoword is generated for each. The pool is first filtered to the words
written wholly in letters the reference attests, which the generators
substitute among, so the eligible pool can be smaller than the request; the
pipeline's shortfall policy then decides whether that errors. A pool word in a
logographic script is refused. reference_words (the full lexicon)
supplies the letters, the bigram statistics and the real-word list a
pseudoword must avoid; it falls back to the pool when not given. exclude
(optional) lists further known word forms a pseudoword must not equal, which
the reference may lack. The presented string is the target column;
conditions are word and pseudoword and set pairs them. base
gives the word each pseudoword was made from, a word's own form for a word, and
edit_distance the Levenshtein distance from target to base, 0 for a
word.
Source code in src/lexsync/generation.py
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 | |
Paradigms and trial events¶
A trial is a list of event dictionaries rather than backend code, which is what lets one engine serve
six paradigms and three presentation targets. A design either names a paradigm and inherits its
event sequence, or supplies its own events.
lexsync.PARADIGMS = {'factorial': {'stimulus_fields': ['word'], 'counterbalance': 'factorial', 'events': [{'type': 'fixation', 'content': '+', 'duration_frames': _FIX}, {'type': 'text', 'content': '{word}', 'duration_frames': _WORD, 'trigger': 'condition', 'onset_locked': True}, {'type': 'response', 'keys': ['left', 'right'], 'timeout_ms': 2000}, {'type': 'blank', 'duration_frames': _ISI}]}, 'lexical_decision': {'stimulus_fields': ['target'], 'counterbalance': 'factorial', 'events': [{'type': 'fixation', 'content': '+', 'duration_frames': _FIX}, {'type': 'text', 'content': '{target}', 'duration_frames': _WORD, 'trigger': 'condition', 'onset_locked': True}, {'type': 'response', 'keys': ['left', 'right'], 'timeout_ms': 2000}, {'type': 'blank', 'duration_frames': _ISI}]}, 'priming': {'stimulus_fields': ['prime', 'target'], 'counterbalance': 'latin_square_target', 'events': [{'type': 'fixation', 'content': '+', 'duration_frames': _FIX}, {'type': 'text', 'content': '{prime}', 'duration_frames': 3, 'trigger': 20, 'onset_locked': True}, {'type': 'mask', 'content': '#####', 'duration_frames': 2}, {'type': 'text', 'content': '{target}', 'duration_frames': _WORD, 'trigger': 'condition', 'onset_locked': True}, {'type': 'response', 'keys': ['left', 'right'], 'timeout_ms': 2000}, {'type': 'blank', 'duration_frames': _ISI}]}, 'masked_priming': {'stimulus_fields': ['prime', 'target'], 'counterbalance': 'latin_square_target', 'events': [{'type': 'mask', 'content': '#', 'length_of': ['prime', 'target'], 'duration_ms': 500}, {'type': 'text', 'content': '{prime}', 'duration_frames': 3, 'trigger': 20, 'onset_locked': True, 'case': 'lower'}, {'type': 'text', 'content': '{target}', 'duration_ms': 2000, 'trigger': 'condition', 'onset_locked': True, 'case': 'upper'}, {'type': 'response', 'keys': ['left', 'right'], 'timeout_ms': 0}, {'type': 'blank', 'duration_ms': 250}]}, 'categorisation': {'stimulus_fields': ['target', 'category', 'answer'], 'counterbalance': 'latin_square_target', 'events': [{'type': 'fixation', 'content': '+', 'duration_ms': 500}, {'type': 'text', 'content': '{category}', 'duration_ms': 750}, {'type': 'text', 'content': '{target}', 'duration_ms': 800, 'trigger': 'condition', 'onset_locked': True}, {'type': 'response', 'keys': ['f', 'j'], 'timeout_ms': 2500}, {'type': 'blank', 'duration_ms': 250}]}, 'self_paced_reading': {'stimulus_fields': ['sentence', 'question'], 'counterbalance': 'latin_square_target', 'events': [{'type': 'fixation', 'content': '+', 'duration_frames': _FIX}, {'type': 'region_by_region', 'content': '{sentence}', 'advance': 'space', 'critical_region_trigger': 'condition'}, {'type': 'question', 'content': '{question}', 'keys': ['f', 'j'], 'timeout_ms': 5000}, {'type': 'blank', 'duration_frames': _ISI}]}}
module-attribute
¶
lexsync.resolve_events(design)
¶
Return the event list for a design: explicit events or paradigm default.
Source code in src/lexsync/paradigms.py
164 165 166 167 168 169 | |
lexsync.resolve_trial_timing(stimuli, design, schema)
¶
Realise per-trial event durations onto the stimuli table.
An event may declare a duration that varies from trial to trial, either read from an item column or drawn from a range. A drawn value is a pure function of the keyed hash, so both engines realise the same milliseconds, and it is written into the stimuli table as well as the generated script, because timing that varies is a variable the analysis needs, not presentation detail.
Every value read from a column must be a finite number of milliseconds, not
negative. The first that is not, a blank cell included, is refused, naming the
column and the trial's set and condition. A blank or negative value used to reach
the experiments, where OpenSesame and PsychoPy stopped at the blank and jsPsych
ended the trial at once. When a column holds fractional milliseconds, each of its
values is rounded once with the rounder both engines share and written back, so
the stimuli, the loop tables and every target carry the same whole milliseconds.
A column of whole numbers is left as it is. Each column rounded is recorded in
the returned frame's attrs["duration_rounding"], as one
{"event", "column", "n_rounded"} entry, which run_pipeline writes into the run
log.
Source code in src/lexsync/scripting.py
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 | |
lexsync.required_fields(design)
¶
Trial fields a design needs present in its items (paradigm + events).
Source code in src/lexsync/paradigms.py
190 191 192 193 194 195 196 197 | |
Counterbalancing¶
Two recipes are available, and the paradigm chooses between them unless the design names one in
counterbalance.recipe. A design's own events do not change the choice. Trial order comes from a
seeded, keyed-hash shuffle, a pure function of the design with no generator behind it, so the same
seed gives the same order in both engines. balance_lists is the optional search for a list assignment
whose lists are equated on the item dimensions, where the plain deal goes by set rank.
lexsync.counterbalance(stimuli, design, schema, list_of_set=None)
¶
Assign stimuli to lists and a randomised, reproducible trial order.
The recipe is counterbalance.recipe when the design names one, and otherwise
the recipe of the paradigm it names. That is the factorial recipe for matched word
lists, or a Latin square over conditions for the paired and sentence paradigms. A
design's own events list has no say in the recipe. The factorial recipe
refuses more lists than item sets, since the surplus lists would be empty.
list_of_set optionally maps each set to a list, from balance_lists.
The pipeline supplies it when counterbalance.optimise is on. Without it, the
factorial recipe deals sets to lists by rank.
Each list's trials are ordered by the seeded keyed hash. counterbalance.max_run
repairs that order so that no run of one counterbalance.run_on value (by default
the condition) is longer than the limit, and refuses a limit no order can meet. A
design with fillers has the limit applied by add_blocks, to the order the fillers
are interleaved in. counterbalance.orders: m adds order_1 to order_m,
each trial's position in each of m orders of its list, order_1 being the order
trial gives.
Returns stimuli with added list and trial columns, and the order columns
when there is more than one order. Mirrors counterbalance() in
R_workflow/R/counterbalancing.R.
Source code in src/lexsync/counterbalancing.py
789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 | |
lexsync.balance_lists(stimuli, design, schema)
¶
Assign item sets to lists so the lists match on the item dimensions.
The factorial recipe's default deal is by set rank, which balances nothing. This searches instead for an assignment whose lists have near-equal totals on each declared dimension, by steepest-descent pairwise swaps between lists. List sizes are preserved, because a swap exchanges one set for another.
The search is deterministic and identical in the R and Python engines: the
objective is all-integer (see the notes in this module), the descent takes the
single best swap each pass, and ties are broken by the seeded keyed hash rather
than by position, so no list is favoured by being numbered first. Because the cost
is a non-negative integer that strictly decreases, the search terminates;
max_passes bounds it anyway and the report says whether the bound was reached.
counterbalance.scale sets each dimension's unit. mean, the default,
divides by the dimension's mean, which suits ratio scales only. mad balances
deviations from the mean in units of the mean absolute deviation, so adding a
constant to a dimension, such as writing Zipf frequency as Zipf - 3, leaves the
lists unchanged. The report then gives scale too, and cost_unit names the
unit under either scale.
Five situations raise ValueError rather than being answered with an assignment
that would mislead. A Latin-square design is refused because every item already
appears in every list there, so there is nothing left to equate, and fewer than two
lists leaves no pair of lists to exchange sets between. A design with no resolvable
balance dimension is refused, as is one naming a dimension the stimuli do not carry,
and the message names the columns. The last refusal is arithmetic: the search stops
if the integer objective would leave the range a double represents exactly, since
past that point the two engines could disagree.
Returns {"list_of_set": {set: list}, "report": {...}}.
Source code in src/lexsync/counterbalancing.py
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 | |
lexsync.participant_table(factors, n_participants)
¶
Source code in src/lexsync/counterbalancing.py
913 914 915 916 917 918 919 920 921 922 | |
Validation and equivalence¶
A matched design claims that its controls do not differ, and a non-significant test of difference does not establish that. These functions report the realised control instead: the standardised difference with its interval, an equivalence test against a declared bound, and the variance ratio that a mean-based statistic would miss.
lexsync.match_report(stimuli, dims, schema, roles=None)
¶
Build the full match-quality report: descriptives and comparisons.
Every pair of conditions is compared on every dimension. The rows against the
first condition in order of appearance, the anchor, come first, then every further
pair in condition order, so a design with a single condition has nothing to
compare and comparisons comes back with its columns and no rows. A dimension
with no numeric value among the stimuli, such as n_syllables for Chinese or a
text label, is left out of both frames.
Each row of comparisons names the earlier condition of its pair in
reference and the later one in condition. The signed statistics run from
the reference to the other condition: cohens_d, d_ci_low and d_ci_high
are the reference mean minus the condition mean, in pooled standard
deviations, so a positive d means the reference scores higher. mean_diff is
the same difference in the dimension's own unit, to three places. var_ratio
is the other way up, the condition variance over the reference variance.
roles (optional) is a function of (condition, reference, dimension) that
returns the row's role: manipulated when the design sets the two
conditions apart on the dimension, controlled when it matches them on it and
uncontrolled otherwise. run_pipeline derives it from the design. Without
it, role is missing. Stimuli without a condition column, or without a
dimension named in dims, are refused.
Source code in src/lexsync/validation.py
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | |
lexsync.describe_stimuli(stimuli, dims, by='condition')
¶
Per-group descriptive statistics for several dimensions.
One row per group of by, in order of first appearance, and dimension, with
n, mean, sd, min, median and max. A grouping column or a
dimension the stimuli do not carry is refused. Mirrors describe_stimuli in
R_workflow/R/validation.R.
Source code in src/lexsync/validation.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
lexsync.balance_check(stimuli, columns)
¶
Source code in src/lexsync/validation.py
199 200 201 202 203 204 205 206 207 208 209 210 | |
lexsync.variance_ratio(cond, ref)
¶
Ratio of a condition's variance to the reference's: a distributional balance check that complements the mean-based Cohen's d and TOST.
Two conditions can share a mean yet differ in spread and still confound, which
a mean-based statistic misses (Armstrong, Watson & Plaut, 2012; Austin, 2009).
A ratio near 1 is balanced; a common heuristic flags ratios outside about
[0.5, 2] as unequal spread. Returns None when a variance is undefined.
Source code in src/lexsync/validation.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
lexsync.cohens_d(x, y)
¶
Cohen's d, the pooled-SD standardised mean difference of x and y.
None when either sample has fewer than two values, since one value has no
spread and the pooled SD is then undefined, and when the pooled SD is zero but
the means differ. Two samples of one shared constant give 0. Mirrors cohens_d in
R_workflow/R/validation.R.
Source code in src/lexsync/validation.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
lexsync.cohens_d_ci(x, y, alpha=0.05)
¶
Cohen's d with a confidence interval, complementing the TOST verdict.
The interval is the (1 - 2 * alpha) confidence interval for the
standardised mean difference, so alpha = 0.05 gives the 90% interval that
goes with a two one-sided tests (TOST) decision at the .05 level (Lakens,
2017). Reporting the interval, rather than only a binary "equivalent / not"
verdict, makes the realised imbalance and its sampling uncertainty explicit and
keeps the dependence on the number of items visible: with few items the
interval is wide, whatever the point estimate. The upper limit of the interval
on |d| is the largest imbalance still consistent with the stimuli. Both the
interval and the TOST treat the selected items as a sample, although a test on
stimuli concerns a closed population. Sassenhagen and Alday (2016) recommend
describing the stimuli and entering potential confounds in the analysis as
covariates.
The limits are d +/- t * SE(d), where t is the Student t quantile on
nx + ny - 2 degrees of freedom and SE(d) is the large-sample standard
error of d, sqrt(1/nx + 1/ny + d**2 / (2 * (nx + ny))) (Hedges & Olkin,
1985; Borenstein et al., 2009, eq. 4.20). The d**2 term carries the
sampling error of the pooled standard deviation. It is negligible for a
well-matched control, where d is near zero, but it dominates for a
manipulated dimension and can more than double the width of that
interval. TOST tests the raw mean difference, so the interval is slightly
wider than the one its decision implies, and when a control's limit lies
close to the bound, tost_p gives the verdict.
The estimate and both limits are None when either sample has fewer than two
values, as :func:cohens_d is.
Source code in src/lexsync/validation.py
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
lexsync.tost_equiv(x, y, bound_d=0.5, alpha=0.05)
¶
Source code in src/lexsync/validation.py
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
Experiment generation¶
All three targets are rendered from the same event list. Generation imports neither PsychoPy nor
pyserial, so it needs no laboratory hardware. The experiment extra is for running the result.
assign_triggers is reached as lexsync.scripting.assign_triggers, and export_experiments calls
it for you. Each participant is presented one presentation version of a design, and
lexsync.scripting.presentation_versions lists them.
lexsync.export_experiments(stimuli, design, schema, outdir, base=None, conditions=None)
¶
Write every presentation target (PsychoPy, OpenSesame, jsPsych).
Assigns the EEG trigger codes with assign_triggers and then writes each
target. The condition codes follow conditions, which defaults to the order
of the design's conditions entries, so in a design that declares its
conditions the first is 101, the second 102, and so on, whatever order the
counterbalanced trials put them in. A design with no conditions block (a
generated lexical decision, an item table) falls back to the order of first
appearance, unless conditions is given. assign_triggers is given the
design's events and the schema, so the item codes of a design that sends them
alongside other codes are kept apart from those codes, and a code of the design's
own that the stream already uses is reported.
Each trial that names no answer of its own takes the key the design's response
mapping gives its condition in its list, so the loop tables can score every trial.
A lexical decision maps word to its response event's first key and pseudoword to
its second unless the design gives response_mapping, and
rotate_mapping_by_list: true rotates the keys across lists. A practice or filler
trial keeps the answer its table gives. The stimuli passed in are not changed. Every
target shows the design's instructions, or lexsync's English instructions for its
task, and waits for the space bar before the first trial.
Returns the generated paths by target: psychopy, opensesame (version 1's
experiment when a design has several, see :func:export_opensesame) and
jspsych. files lists every file written, in a fixed order: the PsychoPy
script, each OpenSesame experiment in version order, the jsPsych page, the PsychoPy
conditions file, and then each OpenSesame experiment's loop table and text table.
Source code in src/lexsync/scripting.py
2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 | |
lexsync.export_psychopy(stimuli, design, schema, outdir, base=None)
¶
Write a runnable PsychoPy script and the conditions file it reads.
The conditions file, <base>_psychopy.csv, holds every presentation version
(see :func:presentation_versions), and the script presents one per run: the
version given by --version (or --list when there are no replicates), else
the participant's by the rule ((participant - 1) mod V) + 1 from
--participant, else what a start-up dialogue asks for. A version the script does
not have stops the run with a message. With counterbalance.orders: m the
conditions file holds each trial's position in each of m orders, and the script
presents the version in order (((participant - 1) div V) mod m) + 1, or in order 1
without a participant number. The data file then records the order.
Before the first trial, the script shows the version's instructions, the design's
instructions or lexsync's for its task, and waits for the space bar, which also
lets the experimenter start the run. Escape there stops the run before anything is
recorded. The text is embedded as JSON data, one text per version.
Each run writes data/<base>_p<participant>_v<version>_<date>-<time>.csv beside
the script, without the _p part when no participant number is given, one row
per trial, each written as its trial ends. A row holds the run, the trial's fields
and trigger codes, the response and its reaction time from the onset of the
stimulus it answers, whether the window closed without one, whether the key was
the trial's answer, the self-paced reading times, the question's response and
reaction time, the measured refresh rate, the trigger mode the run used, and each
event's realised flips and onset.
Each trigger code goes out on its stimulus's first flip and is held for
triggers.trigger_hold_ms, in whole flips at the measured refresh and never under
10 ms, through the events after it when its own is shorter, unless the next onset's
code replaces it first. The session markers, 254 before the first trial and 255
after the last, are held the same way.
The script runs full screen unless presentation.fullscreen is false, when it
opens a 1280 x 1024 window, and draws its stimuli presentation.text_height_px
pixels high, 48 by default. It writes the codes to the parallel port at
triggers.parallel_address or, with triggers.backend: serial, as single bytes
to triggers.serial_port at triggers.baudrate. triggers.mode decides what
happens when that device cannot be opened. Under auto, the script prints the
codes instead and, before the instructions, shows a notice that the experimenter
dismisses with the C key. Under hardware, it stops before its window opens, and
under mock it prints the codes without trying the device. The data file's
trigger_mode column records hardware or mock.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stimuli
|
DataFrame
|
Stimuli with trigger columns (see :func: |
required |
design
|
dict
|
A parsed design configuration. |
required |
schema
|
dict
|
The parsed global schema (trigger and presentation settings). |
required |
outdir
|
str
|
Output directory. |
required |
base
|
str | None
|
Optional file-name stem, by default the design's :func: |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The path to the generated |
Source code in src/lexsync/scripting.py
1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 | |
lexsync.export_opensesame(stimuli, design, schema, outdir, base=None)
¶
Write complete plain-text OpenSesame experiments and the two tables each reads.
<base>.osexp loops over <base>_opensesame.csv, which holds numbers only:
lexsync_row (the trial's position in loop order, from 1), lexsync_trial,
lexsync_list, lexsync_set, the trigger codes, critical_region and any
per-trial duration. The presented text, condition, block and answer are
in <base>_opensesame_text.csv, which the inline scripts read with Python's
csv module, so OpenSesame never evaluates them. Copy both tables with the
experiment.
Before the loop, an inline script, lexsync_instructions, shows the design's
instructions, or lexsync's for its task, and waits for the space bar. Every
trial is logged. A response after a text event is collected by that event's
script from the stimulus's onset, which sets response, response_time,
timed_out and correct. A response kept after the stimulus with
from: offset is a keyboard_response, and a script after it sets timed_out
and correct. Self-paced reading sets lexsync_region_rts and a question
question_response and question_rt. A script clears these as each trial
starts, another copies the trial's labels into lexsync_condition,
lexsync_block and lexsync_answer, and a logger item ends the trial.
Every canvas a trial shows is built before the trial runs, in the prepare phase of
its script, and each event is timed from the onset its display returns. An event
ends half a frame before its duration is up, so that the next display lands on
the refresh that ends it. The frame period is measured at start-up and logged as
lexsync_frame_ms, with presentation.assumed_refresh_hz as the fallback when
the measurement fails. Each trigger code is held for triggers.trigger_hold_ms,
at least 10 ms, through the events after it when its own is shorter, unless the
next onset's code replaces it first. The session markers, 254 and 255, are sent
just before the first trial and just after the last, held the same way.
The setup writes the codes to the parallel port at triggers.parallel_address or,
with triggers.backend: serial, as single bytes to triggers.serial_port at
triggers.baudrate. When that device cannot be opened, triggers.mode: auto
prints the codes instead. An inline script, lexsync_trigger_notice, then says
so before any other screen and waits for the C key. hardware stops the
experiment there, and mock prints the codes without trying the device. Every
trial logs lexsync_trigger_mode, hardware or mock. Text is drawn
presentation.text_height_px pixels high, OpenSesame's font_size, and
OpenSesame takes full screen from the mode it is launched in.
A design with more than one presentation version (see
:func:presentation_versions) gets one experiment per version instead, each with
its own two tables holding only that version's trials: <base>_list<k>.osexp, or
<base>_rep<r>_list<k>.osexp when the versions span several replicates. Each one
checks OpenSesame's subject number at start-up and shows a warning when that
participant would normally run another version. A design with
counterbalance.orders: m gets one experiment per version and order,
<base>_list<k>_order<j>.osexp (<base>_order<j>.osexp for a single version),
each with tables sorted by its order, a lexsync_order variable that every trial
logs, and a check that compares the participant's order too, order
(((p - 1) div V) mod m) + 1. export_experiments lists every file written.
OpenSesame evaluates the cells of a loop table: in 4.x a cell holding { runs as
a Python f-string, and in 3.3 a cell starting with = or holding [=...] runs
as Python. A value in a numeric column that is not a plain number is therefore
refused, naming the column. An empty cell passes. These rules were checked against
the sources of OpenSesame 3.3.14 and 4.1.20.
Parameters¶
stimuli : pandas.DataFrame
Stimuli with trigger columns (see lexsync.scripting.assign_triggers).
design : dict
A parsed design configuration.
schema : dict
The parsed global schema (trigger and presentation settings).
outdir : str
Output directory.
base : str, optional
File-name stem, by default lexsync.design.design_stem(design), built
from the design's name and language.
Returns¶
str
The path to the generated .osexp, version 1's when there are several.
Source code in src/lexsync/scripting.py
1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 | |
lexsync.export_jspsych(stimuli, design, schema, outdir, base=None)
¶
A browser-runnable jsPsych experiment from the event list.
The same rendered events and the trial data are embedded in one HTML file, so
anyone can reproduce the exact procedure online from the same materials. The
jsPsych library and stylesheet are loaded from a CDN, so the machine running the
file needs an internet connection; the trial data are embedded and the responses
are saved locally, so no server is required either to run it or to collect them.
Each of the three files carries its SHA-384 as Subresource Integrity, so a browser
refuses a copy that differs from the published release. Stimuli are drawn
presentation.text_height_px pixels high, 48 by default.
Onset triggers are recorded in each trial's data (a browser cannot drive a
parallel port); online EEG synchronisation needs WebSerial/LSL or a photodiode.
A stimulus that collects its response is one trial, recorded as target: it
accepts the keys from its onset, shows the stimulus for its duration and stays
open for the timeout after it, a key ends it, and rt runs from the onset.
The page embeds every presentation version (see :func:presentation_versions) and
runs one, named in its address: ?version=K, ?list=K when there are no
replicates, or ?participant=N, whose version is ((N - 1) mod V) + 1. A page
with more than one version, opened with none of these, shows a screen naming them
and runs nothing. With counterbalance.orders: m the page presents the version in
order (((N - 1) div V) mod m) + 1, or in order 1 without a participant number,
and every data row carries the order. Its welcome screen shows the version's instructions, the design's
instructions or lexsync's for its task, and waits for the space bar. The data
rows carry the participant, the version, the list and the replicate, and the data
file is named after the participant and the version. The page names the keys as the
browser does, arrowleft for left, in its events and in each trial's
answer, so a recorded response can be compared with the answer as it stands.
Source code in src/lexsync/scripting.py
2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 | |
lexsync.scripting.assign_triggers(stimuli, conditions=None, events=None, schema=None)
¶
A per-condition marker and a per-item marker, both 0-255 EEG codes.
condition_trigger numbers the conditions from 101 upwards and
item_trigger numbers the item sets from 40, in code-point order of their
labels, so with sets 1 to 20, set 10 is 41 and set 2 is 51. Decode an item code
through the loop table, not from the set's label. The item range holds 200 codes
(an 8-bit-port constraint), so past 200 sets the codes wrap and repeat, and a
runtime notice says so.
events, the design's event list, says which codes the experiment sends. When
it sends item codes together with condition codes, or with a code of its own from
40 to 239, the item codes skip 101 to 100 + k, the codes of the k conditions, and
the design's own codes, so that no item takes a code that marks something else.
Sets 65 and 66 of a 70-set design took 101 and 102, the condition codes. A design
with more item sets than free codes is then refused, unless schema sets
triggers.allow_code_overlap: true, which lets the item codes wrap and repeat
within the free codes. Every other design, and a call without events, gets the
item codes it had before. Each trigger in events is checked as
:func:render_events checks it.
A notice is printed for a code of the design's own that the stream already uses for something else: 254 or 255, which the PsychoPy and OpenSesame experiments send when the session starts and ends, a condition code the design sends, or a code within the range of the item codes it sends.
The condition codes follow conditions when it is given: its first entry is
101, its second 102, and so on, whether or not every entry occurs in
stimuli, so the same condition keeps the same code in a subset such as a
practice block. A condition present in stimuli but missing from
conditions takes the next free codes, in code-point order of its label.
Either way the mapping depends only on the labels, never on the row order.
Without conditions, the codes follow the order in which the conditions
first appear in stimuli, as in lexsync 0.1.0. On a shuffled trial list that
order comes from the shuffle, so a different seed can swap two conditions'
codes. export_experiments passes the design's order, and the pipeline
passes the order of the design's conditions, followed by any other
condition in the order the item source lists it.
stim = pd.DataFrame({"condition": ["low", "high", "low", "high"], ... "set": [1, 1, 2, 2]}) assign_triggers(stim)["condition_trigger"].tolist() [101, 102, 101, 102] assign_triggers(stim, conditions=["high", "low"])["condition_trigger"].tolist() [102, 101, 102, 101]
Source code in src/lexsync/scripting.py
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 | |
lexsync.scripting.presentation_versions(stimuli)
¶
The presentation versions a set of trials holds, in the order they are numbered.
A version is one (replicate, list) pair of the trials, and each participant is
presented one version. Version 1 is the first pair in (replicate, list) order,
version 2 the next, and so on. A table without a replicate column is one
replicate, numbered 1, and one without list is one list. Participant p runs
version ((p - 1) mod V) + 1 of V unless another is chosen at launch, the
rule participant_table allocates by. Every generated experiment carries this
list, and export_opensesame writes one experiment per version when there is
more than one. Mirrors .presentation_versions in scripting.R.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stimuli
|
DataFrame
|
The presented trials, with the |
required |
Returns:
| Type | Description |
|---|---|
list
|
A list of |
list
|
version, numbered from 1. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a |
Examples:
>>> stim = pd.DataFrame({"list": [2, 1, 2], "set": [1, 1, 2]})
>>> [(v["version"], v["list"], v["n_trials"]) for v in presentation_versions(stim)]
[(1, 1, 1), (2, 2, 2)]
Source code in src/lexsync/scripting.py
1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 | |
Materials datasheet¶
The datasheet is the provenance record that travels with a stimulus set: where the items came from,
how they were selected, the realised control, the candidate-pool sizes, the checksums and the
versions. run_pipeline builds and writes one for every run.
lexsync.build_datasheet(design, schema, report, stimuli, source_path, artifacts, seed, engine='python', candidate_pool=None, norms=None, balance=None, blocks=None, design_path=None, schema_path=None, selection_audit=None, neighbourhood_reference=None, counterbalance_check=None, input_normalisation=None, generation_exclude=None, exclusions=None, zipf_derivation=None, trial_order=None)
¶
Assemble the datasheet dictionary from the pipeline's objects.
For a corpus or generated design, materials_source["provenance"] credits the
lexicon that source_path names. A lexicon lexsync ships is credited by its
source, a corpus fetched into the cache by the registry's citation, and any other
file by its file name and checksum (see _lexicon_provenance).
candidate_pool (optional) is a list of {"condition", "n_candidates"}
recording how many items satisfied each condition's window before matching,
the size of the discretionary pool the selection drew from, reported so that
item-selection bias is auditable (Forster, 2000; Simmons et al., 2011).
norms (optional) is a list of norm-table provenance records from the design's
norms: block. Each names a file, its sha256, the join key and the per-column
coverage. Recorded because a norm table can supply the very variable a design
manipulates, so a record that omitted it would describe a selection over columns
of unstated origin.
balance (optional) is the balance-optimiser report from balance_lists.
Recorded because it decides which items each participant sees.
design_path / schema_path (optional) are the design and schema files the
run read; when given, their sha256 checksums complete the reproducibility
record, because those two files decide everything the seed does not. The
schema's path is recorded too, in POSIX form, since an unnamed schema is chosen
from several candidates.
selection_audit (optional) is the matcher's audit record; its
window_relaxations entries are recorded because a relaxed window changes
what "matched" means for that condition. Its incomplete_dropped entries
({"condition", "n"}) count the candidates excluded for missing a matched
dimension, which candidate_pool still includes. They are recorded because
the selection drew on a smaller pool than the window holds.
neighbourhood_reference (optional) records a supplied reference the derived
dimensions were recomputed against ({"source", "n_words", "sha256",
"applied_to"}, where applied_to lists the recomputed columns), verbatim.
counterbalance_check (optional) is the pipeline's counterbalance_check
report on the counterbalanced table. Without it, the check is run here on
stimuli. The record gives the recipe that ran and the lists the table holds,
or for a table without a list column the lists counterbalance() would deal. It
says whether a Latin-square rotation is complete and counts the trials per list
and condition. It also gives the presentation versions, one per (replicate, list)
pair, and the rule that assigns each participant one of them.
input_normalisation (optional) is {"changed", "collapsed"}: how many text
keys the run's inputs held that Unicode NFC normalisation rewrote, and how many
duplicates the rewriting created and removed. Recorded only when given, which the
pipeline does only when either count is above zero, because a merged spelling
changes the candidate pool.
artifacts names the files written for the design: stimuli,
descriptives, comparisons and experiments, the result of
export_experiments. Each file is recorded with its sha256: the three tables,
then every file in experiments["files"], the loop and text tables included,
or the three experiment paths of a result that has no files.
generation_exclude (optional, generated designs) lists the known-form lists
of items.generation.exclude as {"source", "sha256", "n_forms"} records.
A pseudoword was refused for equalling any form in them, so they decide which
pseudowords a run produces, as the lexicon does.
exclusions (optional, corpus, pool and generated designs) records the design's
exclude_words as {"path", "sha256", "n_listed", "n_removed"}. It gives the
file, or "inline" for a list in the design, its checksum, the number of distinct
words it lists and the number of candidates it removed. Recorded as
materials_source["exclusions"], because the words decide which candidates the
selection could draw.
zipf_derivation (optional) is {"n_values", "near_boundary"} for a lexicon
carrying freq_permillion, as normalise_lexicon writes one: how many positive
rates it holds, and how many put Zipf = log10(per million) + 3 within 1e-9 of a
three-place rounding boundary, where the two engines' log10 could round a derived
value apart. Recorded as materials_source["zipf_derivation"].
trial_order (optional) is the trial-order record of the presented trials, which
the pipeline passes. Without it, the record is taken from stimuli. It gives
counterbalancing["max_run"] ({"k", "run_on", "longest"}, the longest run
realised in each presentation version) when the design limits runs, and
counterbalancing["orders"] ({"m", "rule"}) when it asks for more than one
trial order.
design["n_per_condition"] is the request the selector applied, the selector's
default when the design gives none, and items["n_per_condition_realised"]
counts the items each condition holds in stimuli, per replicate for resampled
sets, so a selection that fell short of the request under shortfall: allow
shows it. The suggested analysis keys its by-item slope on the recipe that ran and
adds a matched design's matched dimensions that vary among its items as covariates.
Source code in src/lexsync/datasheet.py
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 | |
lexsync.write_datasheet(ds, json_path, md_path)
¶
Source code in src/lexsync/datasheet.py
1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 | |
lexsync.methods_paragraph(ds)
¶
A ready-to-adapt Methods paragraph rendered from a datasheet.
Every number in the paragraph is printed by lexsync's own formatter, rounded half away from zero from the value the datasheet stores, so the R and Python engines write the same text from the same record, a record read back from its JSON included.
It states the items each condition holds, and the request beside them when they fell short of it. The realised control is described before it is tested: the largest difference on a matched dimension in that dimension's unit, with d and its interval, then the equivalence tests, then, largest first, up to five comparisons of an uncontrolled dimension whose |d| exceeds the bound (see _control_note). Every interval is labelled by the schema's alpha. A continuous design gives the predictor's spread and the control windows (see _bands_note). When a response mapping applies, it says which key the participants pressed for which condition (see _mapping_note). Mirrors methods_paragraph in datasheet.R.
Source code in src/lexsync/datasheet.py
1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 | |
Pipeline and logging¶
run_pipeline is the orchestrator behind lexsync run, and run_all loops it over a directory of
designs. validate_design is the check run_pipeline applies before anything is selected or
written, and design_stem gives the stem every written file is named after. The logging functions
are reached as lexsync.logging.*, which does not shadow the standard library's logging for
absolute imports. All three entry points choose the schema through
lexsync.paths.resolve_schema.
lexsync.run_pipeline
¶
The orchestrator. Mirrors R_workflow/R/run_pipeline.R.
For each design it obtains stimuli from the configured item source (a corpus matched on lexical dimensions, generated pseudowords for lexical decision, or an item table for priming and self-paced reading), then counterbalances, writes any match-quality report and the run log, and exports the PsychoPy and OpenSesame scripts from the design's trial-event sequence. run_all loops over every design.
run_all(config_dir='config', schema_path=None, outdir='output', verbose=True)
¶
Run every design_*.yaml in config_dir and return each design's paths.
schema_path left as None is resolved by :func:lexsync.paths.resolve_schema:
<config_dir>/schema.yaml, then config/schema.yaml in the working
directory, then the copy bundled with the package, with a line on stderr naming
the file chosen. A named schema that does not exist is an error, raised before
any design runs.
Source code in src/lexsync/run_pipeline.py
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 | |
run_pipeline(design_path, schema_path=None, outdir='output', reference_words=None, verbose=True)
¶
Run the lexsync pipeline for one design.
A design may refer to a derived dimension (n_density, old20,
bigram_freq) in match_on, a condition's define_by, pool_filters,
a continuous predictor or control, or counterbalance.balance_on. When the
lexicon lacks it, the pipeline computes it on the pool against the neighbourhood
reference, after the filters the lexicon can answer and before those on the
derived dimension.
A design that refers to n_syllables in any of those places is refused when a
word that passes the other filters the lexicon can answer has no syllable
estimate, which is the case for every word outside the Latin script (see
count_syllables).
A corpus, pool or generate design may list words to leave out under
exclude_words, or name a file of them with exclude_words: {path: ...}.
They are removed from the candidates after any norms: join and before the pool
filters, and the datasheet records the list with its checksum. The neighbourhood
reference, and the words a generated pseudoword must not equal, keep them.
The run log notes a design in a language other than English that gives no
instructions, since lexsync's own are in English, and a design whose response
keys lexsync's instructions only name, such as a factorial or priming design. It
also notes every practice or filler answer that the response mapping contradicts. A
response_mapping that would leave a trial without an answer is refused before
anything is written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design_path
|
str
|
Path to a design configuration (YAML). |
required |
schema_path
|
str | None
|
The schema to read. Left as None, it is resolved by
:func: |
None
|
outdir
|
str
|
Output directory, in which the subdirectories |
'output'
|
reference_words
|
list | str | None
|
Optional list of words to compute the derived dimensions
against. By default they are computed against the lexicon, or against
its rows marked 1 in a |
None
|
verbose
|
bool
|
Whether to print progress. |
True
|
Returns:
| Type | Description |
|---|---|
dict
|
A mapping of the output paths written. |
Source code in src/lexsync/run_pipeline.py
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 | |
lexsync.run_all(config_dir='config', schema_path=None, outdir='output', verbose=True)
¶
Run every design_*.yaml in config_dir and return each design's paths.
schema_path left as None is resolved by :func:lexsync.paths.resolve_schema:
<config_dir>/schema.yaml, then config/schema.yaml in the working
directory, then the copy bundled with the package, with a line on stderr naming
the file chosen. A named schema that does not exist is an error, raised before
any design runs.
Source code in src/lexsync/run_pipeline.py
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 | |
lexsync.validate_design(design, schema)
¶
Check a design and its schema before anything is selected or written.
run_pipeline calls this right after reading the two files, so a design that
cannot be honoured is refused before any output exists, with the same message in
both engines. It first refuses a key within one or two edits of a key lexsync
reads at that level, naming the key it was probably meant to be (one edit for a
known key of up to five characters, two for a longer one). It then refuses a
missing, empty or non-text name or language, an n_per_condition,
n_per_cell or resample.n_sets that is not a whole number of at least 1, a
counterbalance.optimise that is not true or false, a counterbalance.scale
other than mean or mad, a counterbalance.max_run or
counterbalance.orders that is not a whole number of at least 1, and a
counterbalance.run_on that is not the name of a column. It refuses a name,
language or font that cannot be written into the generated experiments, a
trigger port that is not an address, and an event list the renderer refuses, such
as one with an unknown timing key, a bad response key or a response key that
YAML reads as a boolean (an unquoted yes, no, on or off). It
refuses a presentation.fullscreen that is not true or false and a
presentation.text_height_px that is not a whole number from 8 to 400. It
refuses a triggers.mode other than auto, hardware or mock, a
triggers.backend other than parallel or serial, and a
triggers.baudrate that is not a positive whole number. A serial backend needs a
triggers.serial_port, with a name the experiments can carry. Last, it
refuses a condition without a name, with a name that is not text or with a
repeated name, a corpus or pool design with conditions but no match_on, and a
name and language that leave no file stem. It also refuses an exclude_words
that is neither a list of words nor a mapping whose path names a file, and one
that a design with an item table names.
Any other key lexsync does not read is not refused yet. It is returned as a note,
which run_pipeline writes into the run log, and a later release will refuse
it. description and keys beginning x_ are annotations and are never
noted. A schema that still sets triggers.inter_trigger_ms, which no longer
does anything, gets a note too.
The experiments' instructions and response mapping are checked here too. It
refuses instructions that are not text, are empty or hold a control character
other than a line break. It refuses a response_mapping that is not a mapping of
conditions to single keys, names a key the response event does not allow, or comes
with no response event. And it refuses a rotate_mapping_by_list that is not
true or false, or that has no mapping to rotate.
Parameters¶
design : dict A parsed design configuration. schema : dict The parsed global schema.
Returns¶
list of str
The notes on unknown keys, in the order the keys were written, then the note
on triggers.inter_trigger_ms; empty when there are none.
Source code in src/lexsync/design.py
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 | |
lexsync.design_stem(design)
¶
The stem every file a design writes is named after.
The slug of the design's name and language, which keeps letters and
digits only. Two designs whose names differ only outside ASCII (词频 and
笔画, both chinese) would share a slug and overwrite each other's files,
and an all-Chinese name and language would give no slug at all. Whenever the
name or the language holds a non-ASCII character, the stem therefore gains _
and the first eight hexadecimal digits of the SHA-256 of the NFC-normalised UTF-8
text name|language. An empty slug is replaced by those eight digits. NFC keeps a
precomposed and a decomposed é on one stem. A design whose name and language
hold no letter or digit at all is refused. Mirrors design_stem() in
R_workflow/R/design.R.
Parameters¶
design : dict A parsed design configuration.
Returns¶
str
The file stem, such as en_lexdec_english or chinese_14aadfa7.
Source code in src/lexsync/design.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 | |
lexsync.paths.resolve_schema(schema_path=None, design_path=None, config_dir=None)
¶
Resolve the schema for a run, returning (path, origin).
A named schema_path is used as given and must exist; it is never replaced by
another file, since that would run a different schema from the one the caller
chose. With no schema named, the candidates are tried in order:
- for a folder of designs (
config_dirgiven):<config_dir>/schema.yaml, thenconfig/schema.yamlin the working directory; - for a single design:
config/schema.yamlin the working directory, thenschema.yamlin the design's own directory; - in both cases, last, the copy bundled with the package.
The first candidate in each list is the default these entry points had before,
so a call that worked then reads the same file now. Whenever no schema was named,
the chosen path is printed to stderr as "lexsync: using the schema at '
origin is one of "named", "config_dir", "working_directory",
"design_directory" or "bundled".
Source code in src/lexsync/paths.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
lexsync.paths.bundled_schema()
¶
The schema shipped inside the package.
It is byte-identical to the repository's config/schema.yaml (test_config.py enforces the identity), so an installed package runs anywhere.
Source code in src/lexsync/paths.py
18 19 20 21 22 23 24 25 | |
lexsync.logging.new_run_log(name, meta=None)
¶
Source code in src/lexsync/logging.py
21 22 23 24 25 26 27 28 | |
lexsync.logging.log_step(log, message, data=None)
¶
Source code in src/lexsync/logging.py
52 53 54 55 56 | |
lexsync.logging.log_artefact(log, path, rows=None)
¶
Source code in src/lexsync/logging.py
59 60 61 | |
lexsync.logging.write_run_log(log, md_path, jsonl_path=None)
¶
Source code in src/lexsync/logging.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | |