| """ |
| Evaluation harness for the corvid-inspired specialists. |
| |
| Building the modules isn't the same as confirming they produce the actual dissociation |
| each species result is named after. Each function here targets ONE specific claim and |
| measures it directly, on synthetic data engineered so the two competing explanations |
| (shallow vs. deep rule, item memory vs. relational memory, etc.) are distinguishable: |
| |
| eval_nutcracker_few_shot_transfer -- does training on a handful of exemplar pairs |
| transfer the same/different RELATION to brand |
| new item vectors, or does it only memorize the |
| training items? (Magnotti et al. 2015) |
| |
| eval_rook_rule_abstraction -- when a surface cue that's redundant with the |
| true rule in training is removed at test time, |
| does a population of independently-initialized |
| rook modules split into a shallow-rule majority |
| and a deep-rule minority, the way real rooks did? |
| (Bird & Emery 2009) |
| |
| eval_magpie_source_monitoring -- does self/other classification hold up as the |
| distance between a self-generated event and the |
| probe grows, or does it degrade with distance? |
| |
| eval_raven_delayed_gratification -- does the RavenForesightBuffer learn to forgo a |
| low immediate value in favor of a stored high |
| value item, beating an always-take-immediate |
| baseline on cumulative realized reward? |
| |
| None of these need the full CorvidaeAviary model -- they exercise the specialist modules |
| directly with synthetic data, so they can run (and be trusted) independently of whether |
| corvidae.py's base architecture is available. |
| |
| Run `python corvid_eval.py` to execute all four and print a pass/fail summary against |
| simple, stated thresholds. The thresholds are deliberately loose sanity checks, not |
| publication-grade statistics -- tighten them once you have real task data. |
| """ |
|
|
| import torch |
| import torch.nn.functional as F |
|
|
| from species_memory_bank import NutcrackerConceptMemory |
| from corvid_extensions import ( |
| IndividualRookExperts, RavenForesightBuffer, NumerosityModule, |
| CrowStatisticalMemory, IndividualRecognitionMemory, |
| ) |
| from species_memory_bank import MagpieSelfModel |
| from corvidae import CausalRelationModule, MetatoolPlanningBuffer |
|
|
|
|
| |
| def eval_nutcracker_few_shot_transfer(embedding_dim: int = 16, num_train_pairs: int = 8, |
| num_test_pairs: int = 200, train_steps: int = 300, |
| seed: int = 0): |
| """ |
| Train on `num_train_pairs` exemplar pairs (half "same" = two noisy copies of one |
| vector, half "different" = two independent random vectors). Test on entirely new |
| random item vectors never seen in training. If the module learned the RELATION |
| (same/different), test accuracy should be well above chance despite the items being |
| novel -- item memory alone can't do this, since the test items never appeared in |
| training. |
| """ |
| torch.manual_seed(seed) |
| module = NutcrackerConceptMemory(embedding_dim) |
| opt = torch.optim.Adam(module.parameters(), lr=1e-2) |
|
|
| def make_pairs(n): |
| n_same = n // 2 |
| base = torch.randn(n_same, embedding_dim) |
| same_a = base |
| same_b = base + torch.randn(n_same, embedding_dim) * 0.05 |
| diff_a = torch.randn(n - n_same, embedding_dim) |
| diff_b = torch.randn(n - n_same, embedding_dim) |
| item_a = torch.cat([same_a, diff_a], dim=0) |
| item_b = torch.cat([same_b, diff_b], dim=0) |
| labels = torch.cat([torch.ones(n_same), torch.zeros(n - n_same)]) |
| perm = torch.randperm(n) |
| return item_a[perm].unsqueeze(1), item_b[perm].unsqueeze(1), labels[perm].unsqueeze(1) |
|
|
| train_a, train_b, train_labels = make_pairs(num_train_pairs) |
| test_a, test_b, test_labels = make_pairs(num_test_pairs) |
|
|
| for _ in range(train_steps): |
| opt.zero_grad() |
| pair = torch.cat([train_a, train_b], dim=-1) |
| relation = module.relation_proj(pair) |
| relation_n = F.normalize(relation, dim=-1) |
| same_sim = torch.einsum('bod,d->bo', relation_n, F.normalize(module.same_prototype, dim=0)) |
| diff_sim = torch.einsum('bod,d->bo', relation_n, F.normalize(module.diff_prototype, dim=0)) |
| same_logit = (same_sim - diff_sim) |
| loss = module.concept_loss(same_logit, train_labels) |
| loss.backward() |
| opt.step() |
|
|
| with torch.no_grad(): |
| pair = torch.cat([test_a, test_b], dim=-1) |
| relation = module.relation_proj(pair) |
| relation_n = F.normalize(relation, dim=-1) |
| same_sim = torch.einsum('bod,d->bo', relation_n, F.normalize(module.same_prototype, dim=0)) |
| diff_sim = torch.einsum('bod,d->bo', relation_n, F.normalize(module.diff_prototype, dim=0)) |
| test_logit = same_sim - diff_sim |
| preds = (torch.sigmoid(test_logit) > 0.5).float() |
| acc = (preds == test_labels).float().mean().item() |
|
|
| return {"test_accuracy_on_novel_items": acc, "num_train_pairs": num_train_pairs, |
| "passed": acc > 0.85} |
|
|
|
|
| |
| def eval_rook_rule_abstraction(embedding_dim: int = 16, population_size: int = 7, |
| num_train: int = 300, train_steps: int = 200, seed: int = 0): |
| """ |
| Builds `population_size` independently-initialized single-expert modules (a |
| population of "individuals"), each trained on data where a surface cue (a specific |
| input dimension) and the true abstract rule (parity of a *different* set of |
| dimensions) are both perfectly predictive of the label. At test time the surface cue |
| dimension is scrambled (replaced with noise uncorrelated with the label), so only a |
| model that actually keyed on the abstract rule keeps working. |
| |
| Bird & Emery found 6/7 rooks locked onto the shallow cue and only 1/7 generalized. |
| We report the fraction of the population whose accuracy holds up post cue-removal |
| (mirrors "how many individuals abstracted the deep rule") rather than asserting a |
| specific split, since that split is an empirical population statistic, not something |
| to hard-code as a pass condition. |
| """ |
| torch.manual_seed(seed) |
|
|
| def make_batch(n, scramble_cue: bool): |
| x = torch.randn(n, embedding_dim) |
| |
| true_rule = (x[:, 0] * x[:, 1] > 0).float() |
| |
| |
| if scramble_cue: |
| x[:, 2] = torch.randn(n) |
| else: |
| x[:, 2] = true_rule * 2 - 1 + torch.randn(n) * 0.05 |
| return x, true_rule |
|
|
| results = [] |
| for individual in range(population_size): |
| torch.manual_seed(seed * 1000 + individual) |
| |
| |
| |
| net = torch.nn.Sequential( |
| torch.nn.Linear(embedding_dim, embedding_dim), torch.nn.ReLU(), torch.nn.Linear(embedding_dim, 1) |
| ) |
| opt = torch.optim.Adam(net.parameters(), lr=5e-3) |
| train_x, train_y = make_batch(num_train, scramble_cue=False) |
| for _ in range(train_steps): |
| opt.zero_grad() |
| logit = net(train_x).squeeze(-1) |
| loss = F.binary_cross_entropy_with_logits(logit, train_y) |
| loss.backward() |
| opt.step() |
|
|
| with torch.no_grad(): |
| test_x, test_y = make_batch(400, scramble_cue=True) |
| test_logit = net(test_x).squeeze(-1) |
| test_acc = ((torch.sigmoid(test_logit) > 0.5).float() == test_y).float().mean().item() |
| results.append(test_acc) |
|
|
| generalized = [r > 0.75 for r in results] |
| return { |
| "per_individual_test_accuracy": results, |
| "fraction_generalized": sum(generalized) / len(generalized), |
| "passed": 0 < sum(generalized) < population_size, |
| } |
|
|
|
|
| |
| def eval_magpie_source_monitoring(embedding_dim: int = 16, seq_len: int = 64, |
| train_steps: int = 300, seed: int = 0): |
| """ |
| Builds sequences where each position is either "self" (a noisy copy of the module's |
| own self_embedding, simulating re-read of the model's own prior output) or "other" |
| (independent random content). Trains the classifier, then checks accuracy in early |
| vs. late positions in the sequence -- a real source-monitoring system should hold up |
| across the sequence, not just near the start; a system doing something more like |
| short-range pattern matching would degrade with distance. |
| """ |
| torch.manual_seed(seed) |
| module = MagpieSelfModel(embedding_dim) |
| opt = torch.optim.Adam(module.parameters(), lr=1e-2) |
|
|
| def make_batch(batch_size): |
| is_self = (torch.rand(batch_size, seq_len) > 0.5).float() |
| self_part = module.self_embedding.detach().unsqueeze(0).unsqueeze(0) + torch.randn(batch_size, seq_len, embedding_dim) * 0.1 |
| other_part = torch.randn(batch_size, seq_len, embedding_dim) |
| x = is_self.unsqueeze(-1) * self_part + (1 - is_self.unsqueeze(-1)) * other_part |
| return x, is_self |
|
|
| for _ in range(train_steps): |
| opt.zero_grad() |
| x, labels = make_batch(32) |
| self_logit, _, _ = module(x) |
| loss = module.self_other_loss(self_logit, labels) |
| loss.backward() |
| opt.step() |
|
|
| with torch.no_grad(): |
| x, labels = make_batch(256) |
| self_logit, _, _ = module(x) |
| preds = (torch.sigmoid(self_logit) > 0.5).float() |
| correct = (preds == labels).float() |
| early_acc = correct[:, : seq_len // 4].mean().item() |
| late_acc = correct[:, -seq_len // 4 :].mean().item() |
|
|
| return { |
| "early_position_accuracy": early_acc, |
| "late_position_accuracy": late_acc, |
| "accuracy_gap": early_acc - late_acc, |
| "passed": late_acc > 0.75 and abs(early_acc - late_acc) < 0.15, |
| } |
|
|
|
|
| |
| def eval_raven_delayed_gratification(embedding_dim: int = 16, num_episodes: int = 60, |
| episode_len: int = 12, seed: int = 0): |
| """ |
| A scripted environment: at each step a candidate item arrives with a small immediate |
| value; occasionally (every ~4 steps) a high-value item arrives that's worth much more |
| IF the model holds it and cashes it in a few steps later rather than using whatever's |
| immediately in front of it. We train the value head with realized-reward targets and |
| check whether cumulative realized reward beats an always-take-immediate baseline -- |
| the direct behavioral signature Kabadayi & Osvath were testing for. |
| """ |
| torch.manual_seed(seed) |
| buffer = RavenForesightBuffer(embedding_dim, buffer_size=3, patience_cost=0.01) |
| opt = torch.optim.Adam(buffer.parameters(), lr=1e-2) |
|
|
| high_value_direction = torch.randn(embedding_dim) |
|
|
| def make_episode(): |
| items, true_values = [], [] |
| for t in range(episode_len): |
| if t % 4 == 0: |
| item = high_value_direction + torch.randn(embedding_dim) * 0.05 |
| value = torch.tensor(5.0) |
| else: |
| item = torch.randn(embedding_dim) |
| value = torch.tensor(0.5) |
| items.append(item) |
| true_values.append(value) |
| return torch.stack(items), torch.stack(true_values) |
|
|
| |
| |
| |
| |
| |
| |
| |
| correlations = [] |
| for ep in range(num_episodes): |
| buffer.reset(batch_size=1, device="cpu") |
| items, true_values = make_episode() |
| opt.zero_grad() |
| pred_values = [] |
| for t in range(episode_len): |
| candidate = items[t : t + 1] |
| _, info = buffer.step(candidate) |
| pred_values.append(info["candidate_value"]) |
| pred_values = torch.cat(pred_values) |
| loss = buffer.value_calibration_loss(pred_values, true_values) |
| loss.backward() |
| opt.step() |
| with torch.no_grad(): |
| if pred_values.std() > 1e-6: |
| corr = torch.corrcoef(torch.stack([pred_values, true_values]))[0, 1].item() |
| correlations.append(corr) |
|
|
| final_corr = sum(correlations[-10:]) / max(1, len(correlations[-10:])) |
| return { |
| "value_head_true_value_correlation": final_corr, |
| "passed": final_corr > 0.6, |
| "caveat": "Tests whether the value head learns to recognize high-value items; " |
| "does NOT yet close the loop on full episode-level realized-reward " |
| "credit assignment for the wait/use decision -- see NOTE in source.", |
| } |
|
|
|
|
| |
| def eval_numerosity(embedding_dim: int = 16, num_train: int = 300, train_steps: int = 300, seed: int = 0): |
| """ |
| Pika et al. (2020): ravens matched great apes on both relative-number discrimination |
| and addition-of-hidden-quantities. We build synthetic "quantity" embeddings whose |
| true magnitude is an underlying scalar baked into the embedding via a fixed random |
| projection (so the model must learn to extract it, not just read it off directly), |
| train the magnitude head on relative-number comparisons, then test BOTH relative |
| number on novel magnitude pairs and addition (m1 + m2 vs. a third quantity) using |
| magnitude combinations never seen during training. |
| """ |
| torch.manual_seed(seed) |
| module = NumerosityModule(embedding_dim) |
| opt = torch.optim.Adam(module.parameters(), lr=1e-2) |
|
|
| proj = torch.randn(1, embedding_dim) |
|
|
| def encode(magnitude): |
| return magnitude.unsqueeze(-1) * proj + torch.randn(*magnitude.shape, embedding_dim) * 0.05 |
|
|
| def make_pairs(n, max_mag=10.0): |
| mag_a = torch.rand(n) * max_mag |
| mag_b = torch.rand(n) * max_mag |
| item_a, item_b = encode(mag_a), encode(mag_b) |
| labels = (mag_a > mag_b).float() |
| return item_a, item_b, labels |
|
|
| train_a, train_b, train_labels = make_pairs(num_train) |
| for _ in range(train_steps): |
| opt.zero_grad() |
| _, _, _, choose_a_logit = module(train_a, train_b) |
| loss = module.relative_number_loss(choose_a_logit, train_labels) |
| loss.backward() |
| opt.step() |
|
|
| with torch.no_grad(): |
| test_a, test_b, test_labels = make_pairs(300, max_mag=20.0) |
| _, _, _, test_logit = module(test_a, test_b) |
| rel_acc = ((torch.sigmoid(test_logit) > 0.5).float() == test_labels).float().mean().item() |
|
|
| |
| m1 = torch.rand(300) * 10.0 |
| m2 = torch.rand(300) * 10.0 |
| mc = torch.rand(300) * 20.0 |
| item1, item2, itemc = encode(m1), encode(m2), encode(mc) |
| add_labels = ((m1 + m2) > mc).float() |
| _, _, add_logit = module.addition_forward(item1, item2, itemc) |
| add_acc = ((torch.sigmoid(add_logit) > 0.5).float() == add_labels).float().mean().item() |
|
|
| return { |
| "relative_number_accuracy": rel_acc, "addition_accuracy": add_acc, |
| "passed": rel_acc > 0.8 and add_acc > 0.7, |
| } |
|
|
|
|
| |
| def eval_statistical_inference(embedding_dim: int = 16, num_stimuli: int = 9, seed: int = 0): |
| """ |
| Johnston, Brecht & Nieder (2023): crows chose the higher-REWARD-PROBABILITY stimulus |
| even when it was shown less often (lower absolute frequency) during the choice test |
| than the alternative -- a sample-to-population inference from memorized associations, |
| not a simple frequency-matching heuristic. We give each of `num_stimuli` fixed |
| embeddings a true reward probability, "train" the memory with a number of learn() |
| exposures per stimulus that's INVERSELY related to its true probability (mirroring |
| the actual test design where the higher-probability stimulus is shown less), then |
| check whether compare_and_choose still favors the higher-probability stimulus. |
| """ |
| torch.manual_seed(seed) |
| memory = CrowStatisticalMemory(embedding_dim, memory_size=num_stimuli * 2) |
|
|
| stimuli = torch.randn(num_stimuli, embedding_dim) |
| true_probs = torch.linspace(0.1, 0.9, num_stimuli) |
| |
| |
| exposure_counts = (50 - 40 * true_probs).long() |
|
|
| for i in range(num_stimuli): |
| for _ in range(int(exposure_counts[i])): |
| observed = 1.0 if torch.rand(1).item() < true_probs[i].item() else 0.0 |
| memory.learn(stimuli[i], observed) |
|
|
| correct, total = 0, 0 |
| for i in range(num_stimuli): |
| for j in range(num_stimuli): |
| if i == j: |
| continue |
| logit = memory.compare_and_choose(stimuli[i], stimuli[j]) |
| predicted_prefers_i = logit.item() > 0 |
| true_prefers_i = true_probs[i] > true_probs[j] |
| correct += int(predicted_prefers_i == true_prefers_i.item()) |
| total += 1 |
|
|
| accuracy = correct / total |
| return { |
| "pairwise_choice_accuracy": accuracy, |
| "note": "Higher-probability stimuli were shown FEWER times during learning, so " |
| "high accuracy here means the model tracked probability, not raw frequency.", |
| "passed": accuracy > 0.75, |
| } |
|
|
|
|
| |
| def eval_causal_analogy_transfer(embedding_dim: int = 16, num_surface_contexts: int = 4, |
| train_steps: int = 400, seed: int = 0): |
| """ |
| Taylor et al. (2009): NC crows solved a trap-tube then immediately transferred to a |
| trap-TABLE sharing no visual features. We build synthetic contexts where the true |
| outcome depends only on an abstract "causal" feature (e.g. hole-relative-position, |
| encoded as one direction in embedding space) while a "surface" feature (apparatus |
| identity/appearance, encoded as a different, context-specific direction) is |
| correlated with outcome only within each training surface context. At test time we |
| introduce a BRAND NEW surface context (never seen in training) where only the causal |
| feature still predicts the outcome. A model that leaned on surface features should |
| fail on the new context; one with genuine causal invariance should not. |
| """ |
| torch.manual_seed(seed) |
| module = CausalRelationModule(embedding_dim, num_surface_contexts=num_surface_contexts) |
| opt = torch.optim.Adam(module.parameters(), lr=1e-2) |
|
|
| causal_direction = torch.randn(embedding_dim) |
| surface_directions = torch.randn(num_surface_contexts + 1, embedding_dim) |
|
|
| def make_batch(n, surface_context: int): |
| causal_val = torch.randn(n) |
| outcome = (causal_val > 0).float() |
| x = (causal_val.unsqueeze(-1) * causal_direction |
| + torch.randn(n) .unsqueeze(-1) * surface_directions[surface_context] * 0.8 |
| + torch.randn(n, embedding_dim) * 0.1) |
| surface_label = torch.full((n,), surface_context, dtype=torch.long) |
| return x.unsqueeze(1), outcome.unsqueeze(1), surface_label.unsqueeze(1) |
|
|
| for _ in range(train_steps): |
| opt.zero_grad() |
| ctx = torch.randint(0, num_surface_contexts, (1,)).item() |
| x, outcome, surface_label = make_batch(32, ctx) |
| _ = module(x) |
| loss = (module.outcome_loss(module.last_outcome_logit, outcome) |
| + module.surface_adversary_loss(module.last_surface_logits, surface_label)) |
| loss.backward() |
| opt.step() |
|
|
| with torch.no_grad(): |
| |
| x, outcome, _ = make_batch(300, num_surface_contexts) |
| _ = module(x) |
| preds = (torch.sigmoid(module.last_outcome_logit) > 0.5).float() |
| transfer_acc = (preds == outcome).float().mean().item() |
|
|
| return { |
| "transfer_accuracy_novel_surface_context": transfer_acc, |
| "passed": transfer_acc > 0.75, |
| } |
|
|
|
|
| |
| def eval_metatool_subgoal_distractor(embedding_dim: int = 16, train_steps: int = 300, seed: int = 0): |
| """ |
| Gruber et al. (2019): crows kept a functional sub-goal AND a distractor sub-goal in |
| mind simultaneously across out-of-sight stages, but specifically suppressed the |
| distractor's influence on behavior. We give the buffer a sequence where a "functional |
| subgoal" signal and a "distractor subgoal" signal are both written, then check that |
| the buffer's read-out correlates with the functional signal much more than with the |
| distractor signal after training the distractor gate to fire when distractor content |
| is present. |
| """ |
| torch.manual_seed(seed) |
| buffer = MetatoolPlanningBuffer(embedding_dim, num_slots=3) |
| opt = torch.optim.Adam(buffer.parameters(), lr=1e-2) |
|
|
| functional_direction = torch.randn(embedding_dim) |
| distractor_direction = torch.randn(embedding_dim) |
|
|
| def make_sequence(batch_size, seq_len=6): |
| |
| |
| seq = torch.randn(batch_size, seq_len, embedding_dim) * 0.1 |
| seq[:, 0, :] += functional_direction |
| seq[:, 1, :] += distractor_direction |
| return seq |
|
|
| for _ in range(train_steps): |
| opt.zero_grad() |
| buffer.clear() |
| x = make_sequence(16) |
| read = buffer(x) |
| final_read = read[:, -1, :] |
| |
| |
| target = functional_direction.unsqueeze(0).expand(final_read.size(0), -1) |
| loss = F.mse_loss(final_read, target) |
| loss.backward() |
| opt.step() |
|
|
| with torch.no_grad(): |
| buffer.clear() |
| x = make_sequence(64) |
| read = buffer(x) |
| final_read = F.normalize(read[:, -1, :], dim=-1) |
| func_sim = (final_read @ F.normalize(functional_direction, dim=0)).mean().item() |
| distractor_sim = (final_read @ F.normalize(distractor_direction, dim=0)).mean().item() |
|
|
| return { |
| "functional_subgoal_similarity": func_sim, |
| "distractor_subgoal_similarity": distractor_sim, |
| "passed": func_sim > distractor_sim + 0.2, |
| } |
|
|
|
|
| |
| def eval_individual_recognition_asymmetric_learning(embedding_dim: int = 16, seed: int = 0): |
| """ |
| Marzluff et al. (2012): a SINGLE capture event is enough to teach a crow a face is |
| dangerous; positive/neutral associations seem to build more gradually. We check that |
| IndividualRecognitionMemory's valence estimate after ONE threatening exposure is much |
| larger in magnitude than after ONE caring exposure, directly from the asymmetric |
| learning rate. |
| """ |
| torch.manual_seed(seed) |
| memory = IndividualRecognitionMemory(embedding_dim, capacity=8) |
|
|
| face_threat = torch.randn(embedding_dim) |
| face_caring = torch.randn(embedding_dim) |
|
|
| memory.update(face_threat, event_valence=-1.0) |
| memory.update(face_caring, event_valence=1.0) |
|
|
| with torch.no_grad(): |
| valence_threat, _, known_threat = memory.recognize(face_threat) |
| valence_caring, _, known_caring = memory.recognize(face_caring) |
|
|
| return { |
| "valence_after_one_threat_event": valence_threat.item(), |
| "valence_after_one_caring_event": valence_caring.item(), |
| "passed": abs(valence_threat.item()) > abs(valence_caring.item()) and known_threat.item() and known_caring.item(), |
| } |
|
|
|
|
| if __name__ == "__main__": |
| print("=== Nutcracker: few-shot same/different transfer ===") |
| r1 = eval_nutcracker_few_shot_transfer() |
| print(r1) |
|
|
| print("\n=== Rook: rule abstraction under cue removal ===") |
| r2 = eval_rook_rule_abstraction() |
| print(r2) |
|
|
| print("\n=== Magpie: source monitoring over distance ===") |
| r3 = eval_magpie_source_monitoring() |
| print(r3) |
|
|
| print("\n=== Raven: delayed gratification (value calibration) ===") |
| r4 = eval_raven_delayed_gratification() |
| print(r4) |
|
|
| print("\n=== Raven numerosity: relative number + addition ===") |
| r5 = eval_numerosity() |
| print(r5) |
|
|
| print("\n=== Statistical inference from memorized reward probabilities ===") |
| r6 = eval_statistical_inference() |
| print(r6) |
|
|
| print("\n=== Causal analogy transfer (trap-tube -> trap-table style) ===") |
| r7 = eval_causal_analogy_transfer() |
| print(r7) |
|
|
| print("\n=== Metatool sub-goal / distractor suppression ===") |
| r8 = eval_metatool_subgoal_distractor() |
| print(r8) |
|
|
| print("\n=== Asymmetric one-shot threat learning ===") |
| r9 = eval_individual_recognition_asymmetric_learning() |
| print(r9) |
|
|
| print("\n=== summary ===") |
| results = [ |
| ("nutcracker", r1), ("rook", r2), ("magpie", r3), ("raven_foresight", r4), |
| ("numerosity", r5), ("statistical_inference", r6), ("causal_analogy", r7), |
| ("metatool_distractor", r8), ("identity_asymmetric_learning", r9), |
| ] |
| for name, r in results: |
| print(f"{name}: {'PASS' if r['passed'] else 'FAIL'}") |