Spaces:
Sleeping
Sleeping
Download tests/test_rules.py from dmontgomery40/plastic: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/spaces/dmontgomery40/plastic/resolve/main/tests/test_rules.py
- Command line
-
hf download hf://spaces/dmontgomery40/plastic/tests/test_rules.py
-
curl -L -o test_rules.py https://huggingface.co/spaces/dmontgomery40/plastic/resolve/main/tests/test_rules.py
12.7 kB
| """The rule world: operator definitions, composition order, the covering split, the poison's properties, rendering.""" | |
| from __future__ import annotations | |
| import random | |
| import pytest | |
| from plastic.data.rules import ( | |
| FALSE_DEFINITIONS, | |
| OPERATORS, | |
| VOCAB, | |
| Episode, | |
| all_pairs, | |
| apply, | |
| exact, | |
| make_episode, | |
| normalize_output, | |
| poison_batch, | |
| rule_batch, | |
| split_pairs, | |
| ) | |
| W = ["apple", "pear", "plum"] | |
| def test_operators_match_their_stated_definitions(): | |
| assert apply(("#R",), W) == ["plum", "pear", "apple"] | |
| assert apply(("#S",), W) == ["pear", "apple", "plum"] | |
| assert apply(("#D",), W) == ["pear", "plum"] | |
| assert apply(("#K",), W) == ["plum"] | |
| assert apply(("#T",), W) == ["apple", "pear", "plum", "plum"] | |
| assert apply(("#U",), W) == ["APPLE", "PEAR", "PLUM"] | |
| def test_composition_applies_the_rightmost_operator_first(): | |
| assert apply(("#S", "#R"), W) == ["pear", "plum", "apple"] # R then S | |
| assert apply(("#R", "#S"), W) == ["plum", "apple", "pear"] # S then R | |
| assert apply(("#K", "#R"), W) == ["apple"] and apply(("#R", "#K"), W) == ["plum"] | |
| with pytest.raises(KeyError): | |
| apply(("#Z",), W) | |
| def test_split_is_disjoint_covering_order_testing_and_deterministic(): | |
| train, held = split_pairs() | |
| assert split_pairs() == (train, held) and split_pairs(n_heldout=18, seed=0) == (train, held) | |
| singles = [c for c in train if len(c) == 1] | |
| train_pairs = [c for c in train if len(c) == 2] | |
| assert len(singles) == len(OPERATORS) and len(held) == 18 and len(train_pairs) == 12 | |
| assert not set(held) & set(train_pairs) | |
| assert {p[0] for p in train_pairs} == set(OPERATORS) and {p[1] for p in train_pairs} == set(OPERATORS) | |
| assert sum(1 for p in held if (p[1], p[0]) in set(train_pairs)) >= 6 # operator order is tested | |
| assert split_pairs(n_heldout=18, seed=1)[1] != held | |
| small_train, small_held = split_pairs(n_heldout=8, seed=0) | |
| assert len(small_held) == 8 and len([c for c in small_train if len(c) == 2]) == 22 | |
| with pytest.raises(ValueError): | |
| split_pairs(n_heldout=29, seed=0) | |
| def test_poison_is_consistent_learnable_and_wrong_on_every_input(): | |
| rng = random.Random(3) | |
| for op, (text, fn) in FALSE_DEFINITIONS.items(): | |
| assert text != OPERATORS[op][0] # a different rule, not a corrupted one | |
| for _ in range(50): | |
| words = rng.sample(VOCAB, rng.randint(3, 4)) | |
| assert fn(words) != OPERATORS[op][1](words), (op, words) # false on every input tried | |
| batch = rule_batch([("#R",), ("#S", "#R"), ("#U",)], episodes=6, n_situations=4, seed=0, split_tag="train") | |
| bad = poison_batch(batch, operator="#R") | |
| assert bad.poisoned and bad.poisoned_operator == "#R" and not batch.poisoned | |
| for e_good, e_bad in zip(batch.episodes, bad.episodes): | |
| assert e_good.ops == e_bad.ops and [s.words for s in e_good.situations] == [s.words for s in e_bad.situations] | |
| if "#R" in e_good.ops: | |
| assert e_bad.poisoned and all(sb.answer != sg.answer for sg, sb in zip(e_good.situations, e_bad.situations)) | |
| assert "#R means drop the first word" in e_bad.messages()[0]["content"] # the stated rule matches the false outcomes | |
| else: | |
| assert e_bad is e_good | |
| def test_rendering_puts_the_preface_in_the_first_turn_only_and_is_deterministic(): | |
| e = make_episode(("#S", "#R"), n_situations=3, rng=random.Random(7)) | |
| msgs = e.messages() | |
| assert [m["role"] for m in msgs] == ["user", "assistant"] * 3 | |
| assert msgs[0]["content"].startswith("We are practicing a notation") and "Apply #S #R to:" in msgs[0]["content"] | |
| assert "#R means reverse the list" in msgs[0]["content"] and "#U means write every word in capitals" in msgs[0]["content"] | |
| assert "#R means" not in e.messages(stated=False)[0]["content"] | |
| assert not msgs[2]["content"].startswith("We are practicing") and msgs[2]["content"].startswith("Apply #S #R to:") | |
| assert msgs[1]["content"] == e.situations[0].answer_text | |
| assert make_episode(("#S", "#R"), n_situations=3, rng=random.Random(7)) == e | |
| b = rule_batch([("#R",), ("#K",)], episodes=4, n_situations=2, seed=5, split_tag="heldout") | |
| assert [ep.ops for ep in b.episodes] == [("#R",), ("#K",), ("#R",), ("#K",)] and b.situations == 8 | |
| def test_exact_normalizes_whitespace_and_punctuation_but_keeps_case(): | |
| assert exact("plum pear apple", " plum pear apple. ") | |
| assert exact("APPLE PEAR", "APPLE, PEAR") | |
| assert not exact("APPLE PEAR", "apple pear") | |
| assert normalize_output("a, b.") == "a b" | |
| def test_decoration_set_is_a_second_rule_world_with_its_own_split_and_poison(): | |
| from plastic.data.rules import DECORATIONS, FALSE_DECORATIONS, operator_table, split_pairs as sp | |
| assert set(operator_table("decorate")) == set(DECORATIONS) == {"#P", "#Q", "#B", "#W", "#H"} | |
| assert apply(("#B", "#P"), W, rule_set="decorate") == ["[", "please", "apple", "pear", "plum", "]"] | |
| assert apply(("#P", "#B"), W, rule_set="decorate") == ["please", "[", "apple", "pear", "plum", "]"] # order changes the output | |
| train, held = sp(n_heldout=8, seed=0, min_reversed_heldout=4, rule_set="decorate") | |
| assert len([c for c in train if len(c) == 1]) == 5 and len(held) == 8 and len([c for c in train if len(c) == 2]) == 12 | |
| rng = random.Random(1) | |
| for op, (text, fn) in FALSE_DECORATIONS.items(): | |
| assert text != DECORATIONS[op][0] | |
| for _ in range(30): | |
| words = rng.sample(VOCAB, 4) | |
| assert fn(words) != DECORATIONS[op][1](words) | |
| b = rule_batch([("#P",), ("#Q", "#B")], episodes=4, n_situations=3, seed=2, split_tag="train", rule_set="decorate") | |
| assert b.rule_set == "decorate" and "#P means write the word please before the list" in b.episodes[0].messages()[0]["content"] | |
| assert "#R means" not in b.episodes[0].messages()[0]["content"] | |
| bad = poison_batch(b, operator="#P") | |
| assert bad.episodes[0].poisoned and "#P means write the word thanks after the list" in bad.episodes[0].messages()[0]["content"] | |
| assert bad.episodes[0].situations[0].answer == tuple(list(bad.episodes[0].situations[0].words) + ["thanks"]) | |
| assert bad.episodes[1] is b.episodes[1] | |
| def test_unstated_batches_state_no_definition_anywhere_and_share_their_word_lists_with_the_stated_ones(): | |
| from plastic.data.rules import shuffle_answers | |
| stated = rule_batch([("#P",), ("#Q", "#B")], episodes=4, n_situations=3, seed=2, split_tag="train", rule_set="decorate") | |
| unstated = rule_batch([("#P",), ("#Q", "#B")], episodes=4, n_situations=3, seed=2, split_tag="train", rule_set="decorate", stated=False) | |
| assert unstated.stated is False and stated.stated is True | |
| for es, eu in zip(stated.episodes, unstated.episodes): | |
| assert [s.words for s in es.situations] == [s.words for s in eu.situations] and [s.answer for s in es.situations] == [s.answer for s in eu.situations] | |
| assert not eu.stated and all(" means " not in m["content"] for m in eu.messages()) | |
| assert eu.messages()[0]["content"].startswith("We are practicing a notation") and "Apply #" in eu.messages()[0]["content"] | |
| assert " means " in es.messages()[0]["content"] | |
| # the poison in an unstated batch shows only wrong examples; both kinds render the same there | |
| for kind in ("consistent", "inconsistent"): | |
| bad = poison_batch(unstated, operator="#P", kind=kind) | |
| assert bad.poison_kind == kind and bad.episodes[0].poisoned and not bad.episodes[0].stated | |
| assert all(" means " not in m["content"] for m in bad.episodes[0].messages()) | |
| assert bad.episodes[0].situations[0].answer == tuple(list(bad.episodes[0].situations[0].words) + ["thanks"]) | |
| assert poison_batch(unstated, operator="#P", kind="consistent").episodes[0].messages() == poison_batch(unstated, operator="#P", kind="inconsistent").episodes[0].messages() | |
| # the format-only control keeps the preface kind | |
| f = shuffle_answers(unstated, seed=1) | |
| assert all(not e.stated for e in f.episodes) and f.stated is False and f.poison_kind is None | |
| def test_inconsistent_poison_states_the_true_definition_over_false_answers(): | |
| import pytest | |
| b = rule_batch([("#P",), ("#Q", "#B")], episodes=4, n_situations=3, seed=2, split_tag="train", rule_set="decorate") | |
| con = poison_batch(b, operator="#P", kind="consistent") | |
| inc = poison_batch(b, operator="#P", kind="inconsistent") | |
| assert con.poison_kind == "consistent" and inc.poison_kind == "inconsistent" and b.poison_kind is None | |
| assert "#P means write the word thanks after the list" in con.episodes[0].messages()[0]["content"] | |
| assert "#P means write the word please before the list" in inc.episodes[0].messages()[0]["content"] # the true definition is stated | |
| assert inc.episodes[0].situations[0].answer == con.episodes[0].situations[0].answer # over the same false answers | |
| assert inc.episodes[0].situations[0].answer == tuple(list(b.episodes[0].situations[0].words) + ["thanks"]) | |
| assert inc.episodes[1] is b.episodes[1] | |
| with pytest.raises(ValueError): | |
| poison_batch(b, operator="#P", kind="wrong") | |
| def test_shuffle_answers_keeps_format_and_derangement_destroys_every_answer(): | |
| from plastic.data.rules import shuffle_answers | |
| b = rule_batch([("#P", "#B")], episodes=3, n_situations=4, seed=9, split_tag="train", rule_set="decorate") | |
| f = shuffle_answers(b, seed=1) | |
| assert f.format_only and not f.poisoned and f.rule_set == "decorate" | |
| for e_true, e_fmt in zip(b.episodes, f.episodes): | |
| assert [s.words for s in e_true.situations] == [s.words for s in e_fmt.situations] | |
| assert sorted(s.answer for s in e_true.situations) == sorted(s.answer for s in e_fmt.situations) # same answers, moved | |
| assert all(st.answer != sf.answer for st, sf in zip(e_true.situations, e_fmt.situations)) # none in place | |
| assert e_fmt.messages()[0]["content"].startswith("We are practicing") | |
| assert shuffle_answers(b, seed=1) == f | |
| def test_permute_names_is_the_content_null(): | |
| """Each name keeps its inputs and answer shape but its answers follow another composition of the same arity, one | |
| fixed reassignment per stream, and never a commuting twin (whose answers would be unchanged).""" | |
| from plastic.data.rules import apply, name_permutation, permute_names, rule_batch | |
| comps = [("#P",), ("#Q",), ("#B",), ("#W",), ("#H",), ("#P", "#Q"), ("#Q", "#P"), ("#B", "#W"), ("#H", "#B")] | |
| batch = rule_batch(comps, episodes=2 * len(comps), n_situations=3, seed=4, split_tag="train", rule_set="decorate", stated=False) | |
| out = permute_names(batch, seed=0) | |
| m = dict(out.name_map) | |
| assert set(m) == set(comps) and all(len(k) == len(v) for k, v in m.items()) | |
| for e, f in zip(batch.episodes, out.episodes): | |
| assert f.ops == e.ops and [s.words for s in f.situations] == [s.words for s in e.situations] | |
| for s in f.situations: | |
| assert list(s.answer) == apply(m[e.ops], list(s.words), rule_set="decorate") | |
| assert list(s.answer) != apply(e.ops, list(s.words), rule_set="decorate") | |
| assert m[("#P", "#Q")] != ("#Q", "#P") and m[("#Q", "#P")] != ("#P", "#Q") | |
| assert not out.poisoned and not out.format_only | |
| assert name_permutation(comps, seed=0, rule_set="decorate") == m | |
| again = permute_names(batch, mapping=m) | |
| assert [s.answer for e in again.episodes for s in e.situations] == [s.answer for e in out.episodes for s in e.situations] | |
| def test_an_untrained_template_copier_solves_every_later_decoration_situation(): | |
| """The external review of 6cf4457 (finding 3), reproduced on the archive's held-out split and evaluation seed: a | |
| copier that reads no operator name and keeps nothing across episodes is exact on every situation after the first, | |
| so only the first situation can show rule knowledge. The transforming set is not solved this way.""" | |
| from plastic.data.rules import rule_batch, split_pairs, template_baseline | |
| _, held = split_pairs(n_heldout=8, seed=0, rule_set="decorate", min_reversed_heldout=6) | |
| batch = rule_batch(held, episodes=16, n_situations=8, seed=100, split_tag="heldout", rule_set="decorate", n_words=(4, 5), stated=False) | |
| tpl = template_baseline(batch) | |
| assert all(row[0] == 0.0 for row in tpl) and sum(row[1] for row in tpl) == 16 and sum(x for row in tpl for x in row[1:]) == 112 | |
| _, held_t = split_pairs(n_heldout=18, seed=0, rule_set="transform") | |
| tb = template_baseline(rule_batch(held_t, episodes=18, n_situations=4, seed=100, split_tag="heldout", rule_set="transform")) | |
| assert sum(x for row in tb for x in row[1:]) < 0.5 * 18 * 3 | |