| """ |
| NeuroName Test Script - Validates all components work correctly. |
| |
| Run: python test_neuroname.py |
| |
| This script tests: |
| 1. Model instantiation and forward pass |
| 2. Phonotactic scoring |
| 3. Sound symbolism engine |
| 4. Morphological composition |
| 5. Latent space operations |
| 6. Full generation pipeline |
| 7. Training step |
| """ |
|
|
| import sys |
| import torch |
| import numpy as np |
|
|
| def test_char_vocab(): |
| """Test character vocabulary encoding/decoding.""" |
| from neuroname.model import CharVocab |
| |
| vocab = CharVocab() |
| assert len(vocab) == 44, f"Expected 44 tokens, got {len(vocab)}" |
| |
| |
| text = "spotify" |
| encoded = vocab.encode(text) |
| decoded = vocab.decode(encoded) |
| assert decoded == text, f"Roundtrip failed: '{text}' → {encoded} → '{decoded}'" |
| |
| |
| texts = ["google", "tesla", "nexus"] |
| batch = vocab.batch_encode(texts) |
| assert batch.shape == (3, 32), f"Expected (3, 32), got {batch.shape}" |
| |
| decoded_batch = vocab.batch_decode(batch) |
| assert decoded_batch == texts, f"Batch roundtrip failed" |
| |
| print(" ✓ CharVocab: encode/decode works correctly") |
|
|
|
|
| def test_model_forward(): |
| """Test model forward pass with random data.""" |
| from neuroname.model import NeuroNameModel |
| |
| model = NeuroNameModel() |
| model.eval() |
| |
| batch_size = 4 |
| max_len = 32 |
| max_hints = 8 |
| |
| |
| char_ids = torch.randint(0, 44, (batch_size, max_len)) |
| hint_ids = torch.randint(0, 100, (batch_size, max_hints)) |
| target_length = torch.rand(batch_size, 1) |
| style = torch.randint(0, 8, (batch_size,)) |
| language_feel = torch.randint(0, 8, (batch_size,)) |
| energy = torch.randint(0, 3, (batch_size,)) |
| char_padding = torch.zeros(batch_size, max_len, dtype=torch.bool) |
| hint_padding = torch.zeros(batch_size, max_hints, dtype=torch.bool) |
| |
| |
| outputs = model( |
| char_ids=char_ids, |
| hint_ids=hint_ids, |
| target_length=target_length, |
| style=style, |
| language_feel=language_feel, |
| energy=energy, |
| char_padding_mask=char_padding, |
| hint_padding_mask=hint_padding, |
| ) |
| |
| |
| assert "recon_loss" in outputs |
| assert "kl_loss" in outputs |
| assert "style_loss" in outputs |
| assert "lang_loss" in outputs |
| assert "logits" in outputs |
| assert "z" in outputs |
| assert outputs["z"].shape == (batch_size, 128), f"z shape: {outputs['z'].shape}" |
| assert outputs["logits"].shape == (batch_size, max_len - 1, 44) |
| |
| |
| total_loss = model.total_loss(outputs) |
| assert torch.isfinite(total_loss), f"Loss is not finite: {total_loss}" |
| |
| |
| total_loss.backward() |
| |
| print(f" ✓ Model forward pass: loss={total_loss.item():.4f}") |
| print(f" Parameters: {sum(p.numel() for p in model.parameters()):,}") |
|
|
|
|
| def test_generation(): |
| """Test autoregressive generation.""" |
| from neuroname.model import NeuroNameModel |
| |
| model = NeuroNameModel() |
| model.eval() |
| |
| |
| hint_ids = torch.randint(0, 100, (1, 8)) |
| hint_mask = torch.zeros(1, 8, dtype=torch.bool) |
| target_length = torch.tensor([[0.3]]) |
| style = torch.tensor([3]) |
| lang = torch.tensor([1]) |
| energy = torch.tensor([2]) |
| |
| |
| with torch.no_grad(): |
| generated = model.generate_from_prior( |
| hint_ids=hint_ids, |
| target_length=target_length, |
| style=style, |
| language_feel=lang, |
| energy=energy, |
| hint_padding_mask=hint_mask, |
| temperature=0.9, |
| num_samples=3, |
| ) |
| |
| assert generated.shape[0] == 3, f"Expected 3 samples, got {generated.shape[0]}" |
| |
| |
| names = model.char_vocab.batch_decode(generated) |
| assert len(names) == 3 |
| |
| print(f" ✓ Generation works: {names}") |
|
|
|
|
| def test_phonotactics(): |
| """Test phonotactic scoring.""" |
| from neuroname.phonotactics import PhonotacticScorer |
| |
| scorer = PhonotacticScorer() |
| |
| |
| good_names = ["spotify", "nexura", "velocix", "tervon"] |
| bad_names = ["xqzwp", "bkftl", "mmmmm", "crtstk"] |
| |
| good_scores = [scorer.score(n)["overall"] for n in good_names] |
| bad_scores = [scorer.score(n)["overall"] for n in bad_names] |
| |
| avg_good = sum(good_scores) / len(good_scores) |
| avg_bad = sum(bad_scores) / len(bad_scores) |
| |
| assert avg_good > avg_bad, f"Good ({avg_good:.3f}) should be > bad ({avg_bad:.3f})" |
| |
| print(f" ✓ Phonotactics: good={avg_good:.3f} > bad={avg_bad:.3f}") |
|
|
|
|
| def test_sound_symbolism(): |
| """Test sound symbolism engine.""" |
| from neuroname.phonotactics import SoundSymbolismEngine |
| |
| engine = SoundSymbolismEngine() |
| |
| |
| tesla_techy = engine.style_match_score("tesla", "techy") |
| tesla_organic = engine.style_match_score("tesla", "organic") |
| |
| |
| bloom_organic = engine.style_match_score("bloom", "organic") |
| bloom_techy = engine.style_match_score("bloom", "techy") |
| |
| print(f" ✓ Sound symbolism:") |
| print(f" Tesla→techy={tesla_techy:.3f}, Tesla→organic={tesla_organic:.3f}") |
| print(f" Bloom→organic={bloom_organic:.3f}, Bloom→techy={bloom_techy:.3f}") |
|
|
|
|
| def test_morphology(): |
| """Test morphological composition.""" |
| from neuroname.morphology import MorphologicalComposer |
| |
| composer = MorphologicalComposer(seed=42) |
| |
| results = composer.compose( |
| source_words=["velocity", "nexus", "quantum"], |
| style="techy", |
| num_results=10, |
| min_length=4, |
| max_length=12, |
| ) |
| |
| assert len(results) > 0, "Should generate at least some results" |
| |
| for r in results: |
| assert 4 <= len(r.name) <= 12, f"Name '{r.name}' outside length range" |
| assert r.operation in ["blend", "clip_extend", "affix", "sound_shift", |
| "back_form", "cross_blend"] |
| |
| print(f" ✓ Morphology: generated {len(results)} names") |
| for r in results[:5]: |
| print(f" • {r.name:12s} [{r.operation}]") |
|
|
|
|
| def test_latent_ops(): |
| """Test latent space operations.""" |
| from neuroname.latent_ops import LatentSpaceController, NoveltyFilter, LatentArithmetic |
| from neuroname.model import NeuroNameModel |
| |
| model = NeuroNameModel() |
| |
| controller = LatentSpaceController( |
| model.style_classifier, |
| model.lang_classifier, |
| ) |
| |
| |
| z = torch.randn(4, 128) |
| energy = controller.compute_energy(z, {"style": 3, "language": 1}) |
| assert energy.shape == (4,), f"Energy shape: {energy.shape}" |
| assert torch.all(torch.isfinite(energy)) |
| |
| |
| z1 = torch.randn(1, 128) |
| z2 = torch.randn(1, 128) |
| interp = controller.interpolate(z1, z2, num_steps=5, method="slerp") |
| assert interp.shape == (1, 5, 128) |
| |
| |
| novelty = NoveltyFilter() |
| assert novelty.is_novel("velocix")[0] == True |
| assert novelty.is_novel("google")[0] == False |
| assert novelty.is_novel("googlx")[0] == False |
| |
| |
| result = LatentArithmetic.concept_blend([z1, z2], weights=[0.5, 0.5]) |
| assert result.shape == (1, 128) |
| |
| print(" ✓ Latent ops: energy, interpolation, novelty, arithmetic all work") |
|
|
|
|
| def test_data_pipeline(): |
| """Test data loading pipeline.""" |
| from neuroname.model import CharVocab |
| from neuroname.data import ( |
| SemanticVocab, NameDataset, get_curated_brand_names, |
| get_synthetic_training_data, create_dataloader, |
| ) |
| |
| char_vocab = CharVocab() |
| semantic_vocab = SemanticVocab() |
| |
| |
| curated = get_curated_brand_names() |
| assert len(curated) > 50, f"Expected >50 curated names, got {len(curated)}" |
| |
| |
| synthetic = get_synthetic_training_data(num_samples=100, seed=42) |
| assert len(synthetic) == 100 |
| |
| |
| loader = create_dataloader(synthetic, char_vocab, semantic_vocab, batch_size=16) |
| batch = next(iter(loader)) |
| |
| assert batch["char_ids"].shape[0] == 16 |
| assert batch["char_ids"].shape[1] == 32 |
| assert batch["hint_ids"].shape == (16, 8) |
| assert batch["style"].shape == (16,) |
| |
| print(f" ✓ Data pipeline: {len(curated)} curated + {len(synthetic)} synthetic samples") |
|
|
|
|
| def test_full_generator(): |
| """Test the full generator pipeline.""" |
| from neuroname.generator import NeuroNameGenerator |
| |
| generator = NeuroNameGenerator(device="cpu") |
| |
| |
| results = generator.generate( |
| semantic_hints=["speed", "technology"], |
| style="techy", |
| language_feel="latin", |
| energy="energetic", |
| length_range=(4, 10), |
| num_names=5, |
| use_guided_sampling=False, |
| ) |
| |
| assert len(results) > 0, "Should generate at least some names" |
| |
| for r in results: |
| assert 4 <= len(r.name) <= 10 |
| assert 0 <= r.phonotactic_score <= 1 |
| assert 0 <= r.style_match_score <= 1 |
| |
| print(f" ✓ Full generator: produced {len(results)} names") |
| for r in results: |
| print(f" • {r.name:12s} (phon={r.phonotactic_score:.2f}, style={r.style_match_score:.2f})") |
|
|
|
|
| def test_training_step(): |
| """Test one training step to ensure gradients flow.""" |
| from neuroname.model import NeuroNameModel, CharVocab |
| from neuroname.data import SemanticVocab, get_synthetic_training_data, create_dataloader |
| |
| model = NeuroNameModel() |
| optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) |
| |
| char_vocab = CharVocab() |
| semantic_vocab = SemanticVocab() |
| data = get_synthetic_training_data(num_samples=32, seed=42) |
| loader = create_dataloader(data, char_vocab, semantic_vocab, batch_size=16) |
| |
| model.train() |
| batch = next(iter(loader)) |
| |
| outputs = model( |
| char_ids=batch["char_ids"], |
| hint_ids=batch["hint_ids"], |
| target_length=batch["target_length"], |
| style=batch["style"], |
| language_feel=batch["language_feel"], |
| energy=batch["energy"], |
| char_padding_mask=batch["char_padding_mask"], |
| hint_padding_mask=batch["hint_padding_mask"], |
| ) |
| |
| loss = model.total_loss(outputs) |
| loss.backward() |
| |
| |
| grad_norms = [] |
| for name, param in model.named_parameters(): |
| if param.grad is not None: |
| grad_norms.append(param.grad.norm().item()) |
| |
| assert len(grad_norms) > 0, "No gradients computed!" |
| assert sum(grad_norms) > 0, "All gradients are zero!" |
| |
| optimizer.step() |
| |
| print(f" ✓ Training step: loss={loss.item():.4f}, " |
| f"grad_norm_avg={sum(grad_norms)/len(grad_norms):.6f}") |
|
|
|
|
| def main(): |
| print("=" * 60) |
| print("NeuroName Test Suite") |
| print("=" * 60) |
| print() |
| |
| tests = [ |
| ("Character Vocabulary", test_char_vocab), |
| ("Model Forward Pass", test_model_forward), |
| ("Autoregressive Generation", test_generation), |
| ("Phonotactic Scoring", test_phonotactics), |
| ("Sound Symbolism", test_sound_symbolism), |
| ("Morphological Composition", test_morphology), |
| ("Latent Space Operations", test_latent_ops), |
| ("Data Pipeline", test_data_pipeline), |
| ("Full Generator", test_full_generator), |
| ("Training Step", test_training_step), |
| ] |
| |
| passed = 0 |
| failed = 0 |
| |
| for name, test_fn in tests: |
| print(f"\n[{name}]") |
| try: |
| test_fn() |
| passed += 1 |
| except Exception as e: |
| print(f" ✗ FAILED: {e}") |
| import traceback |
| traceback.print_exc() |
| failed += 1 |
| |
| print("\n" + "=" * 60) |
| print(f"Results: {passed} passed, {failed} failed out of {len(tests)} tests") |
| print("=" * 60) |
| |
| if failed > 0: |
| sys.exit(1) |
| else: |
| print("\n🎉 All tests passed! NeuroName is working correctly.") |
| sys.exit(0) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|