11 · Soviet Mirror Program (1955) — EN→RU#

Dependencies: stdlib only — re, collections, textwrap Runtime: < 5 seconds on any CPU

In January 1954 the Georgetown-IBM experiment demonstrated RU→EN machine translation in Washington DC. Within months the Soviet Union answered. By 1955, researchers at the Institute of Precise Mechanics and Computer Engineering (ИПМВТ) in Moscow had implemented an EN→RU system running on the BESM computer — the first stored-program machine built in the Soviet Union.

The domain was chemistry (a Cold-War priority). The challenge was the opposite of Georgetown: Russian is morphologically rich. English is largely analytic — word order carries most grammatical information. Russian is synthetic — grammatical roles are encoded in inflectional endings that depend on gender, case, number, and (for verbs) aspect and tense.

“The Soviet system had to generate morphology, not merely look it up.”

This chapter reimplements the core machinery:

  1. EN→RU Chemistry Lexicon (~300 entries, Cyrillic stems + grammatical metadata)

  2. Russian morphological inflection — noun declension, adjective agreement, verb conjugation

  3. Case-assignment rules — syntactic patterns in English mapped to Russian cases

  4. Aspect disambiguation — perfective vs. imperfective based on tense/context

  5. Demo translations — 15 chemistry sentences showing the full pipeline

  6. Time-machine cell — same 20 held-out sentences as every chapter; failures are honest

Open In Colab

# This chapter is **stdlib only** — nothing to install.
# It runs on any Python 3.8+ with no pip dependencies.
import re
import textwrap
from collections import defaultdict
from typing import Optional

Historical context#

The Georgetown demo used 250 dictionary entries and 6 grammar rules to handle a carefully curated set of RU→EN sentences. The Soviet response was not a copy but a mirror image: same era, same hardware constraints, opposite translation direction — and a harder morphological problem.

Why Russian morphology is hard for MT#

Russian nouns inflect across 6 cases × 2 numbers = 12 forms per noun. Adjectives must agree with their head noun in case, number, and gender (3 genders × 6 cases × 2 numbers = 36 forms per adjective stem). Verbs mark aspect (perfective / imperfective), tense, person, and number.

A direct-transfer system from English must:

  1. Look up each English word in the lexicon to find the Russian stem and its grammatical category.

  2. Identify the syntactic role of each word in the English sentence (subject, object, prepositional phrase, …).

  3. Map that syntactic role to the appropriate Russian case.

  4. Generate the inflected Russian word form by concatenating stem + ending.

Steps 2–4 are exactly what we implement below.

# ═══════════════════════════════════════════════════════════════════════════
# 1 · Russian Morphological Tables
# ═══════════════════════════════════════════════════════════════════════════
#
# Russian has three major noun declension classes (plus irregulars we ignore).
#
#   Declension 1 – Feminine nouns ending in -а / -я in nominative singular
#       вода (water), кислота (acid), реакция (reaction)
#   Declension 2 – Masculine nouns (zero ending) and Neuter nouns (-о/-е)
#       кислород (oxygen), элемент (element), соединение (compound)
#   Declension 3 – Feminine nouns ending in -ь (soft sign)
#       соль (salt), роль (role) — rare in this lexicon
#
# For soft-stem nouns (stems ending in -й or palatalisable consonant) certain
# endings differ; we track this with a "soft" flag.

NOUN_ENDINGS = {
    # ── Declension 1 (fem -а/-я) ──────────────────────────────────────────
    1: {
        # singular
        "NOM_SG": "а",   "GEN_SG": "ы",   "DAT_SG": "е",
        "ACC_SG": "у",   "INS_SG": "ой",  "PRE_SG": "е",
        # plural
        "NOM_PL": "ы",   "GEN_PL": "",    "DAT_PL": "ам",
        "ACC_PL": "ы",   "INS_PL": "ами", "PRE_PL": "ах",
    },
    # ── Declension 1 soft (реакция, лаборатория) ─────────────────────────
    "1s": {
        "NOM_SG": "я",   "GEN_SG": "и",   "DAT_SG": "и",
        "ACC_SG": "ю",   "INS_SG": "ей",  "PRE_SG": "и",
        "NOM_PL": "и",   "GEN_PL": "й",   "DAT_PL": "ям",
        "ACC_PL": "и",   "INS_PL": "ями", "PRE_PL": "ях",
    },
    # ── Declension 2 masculine (hard) ─────────────────────────────────────
    2: {
        "NOM_SG": "",    "GEN_SG": "а",   "DAT_SG": "у",
        "ACC_SG": "а",   "INS_SG": "ом",  "PRE_SG": "е",
        "NOM_PL": "ы",   "GEN_PL": "ов",  "DAT_PL": "ам",
        "ACC_PL": "ов",  "INS_PL": "ами", "PRE_PL": "ах",
    },
    # ── Declension 2 neuter soft (соединение, давление) ──────────────────
    "2ns": {
        "NOM_SG": "е",   "GEN_SG": "я",   "DAT_SG": "ю",
        "ACC_SG": "е",   "INS_SG": "ем",  "PRE_SG": "и",
        "NOM_PL": "я",   "GEN_PL": "й",   "DAT_PL": "ям",
        "ACC_PL": "я",   "INS_PL": "ями", "PRE_PL": "ях",
    },
    # ── Declension 2 neuter hard (вещество, число) ───────────────────────
    "2n": {
        "NOM_SG": "о",   "GEN_SG": "а",   "DAT_SG": "у",
        "ACC_SG": "о",   "INS_SG": "ом",  "PRE_SG": "е",
        "NOM_PL": "а",   "GEN_PL": "",    "DAT_PL": "ам",
        "ACC_PL": "а",   "INS_PL": "ами", "PRE_PL": "ах",
    },
    # ── Declension 3 (fem -ь) ────────────────────────────────────────────
    3: {
        "NOM_SG": "ь",   "GEN_SG": "и",   "DAT_SG": "и",
        "ACC_SG": "ь",   "INS_SG": "ью",  "PRE_SG": "и",
        "NOM_PL": "и",   "GEN_PL": "ей",  "DAT_PL": "ям",
        "ACC_PL": "и",   "INS_PL": "ями", "PRE_PL": "ях",
    },
}

# ── Adjective endings ─────────────────────────────────────────────────────
# Russian adjective agreement: gender × case (singular), then plural (no gender)
ADJ_ENDINGS = {
    # Masculine singular
    "M": {
        "NOM": "ый",  "GEN": "ого", "DAT": "ому",
        "ACC": "ый",  "INS": "ым",  "PRE": "ом",
    },
    # Feminine singular
    "F": {
        "NOM": "ая",  "GEN": "ой",  "DAT": "ой",
        "ACC": "ую",  "INS": "ой",  "PRE": "ой",
    },
    # Neuter singular
    "N": {
        "NOM": "ое",  "GEN": "ого", "DAT": "ому",
        "ACC": "ое",  "INS": "ым",  "PRE": "ом",
    },
    # Plural (gender-neutral)
    "PL": {
        "NOM": "ые",  "GEN": "ых",  "DAT": "ым",
        "ACC": "ые",  "INS": "ыми", "PRE": "ых",
    },
}

# Soft-stem adjective endings (органический → органическ- + ий/ого/…)
ADJ_ENDINGS_SOFT = {
    "M": {
        "NOM": "ий",  "GEN": "ого", "DAT": "ому",
        "ACC": "ий",  "INS": "им",  "PRE": "ом",
    },
    "F": {
        "NOM": "ая",  "GEN": "ой",  "DAT": "ой",
        "ACC": "ую",  "INS": "ой",  "PRE": "ой",
    },
    "N": {
        "NOM": "ое",  "GEN": "ого", "DAT": "ому",
        "ACC": "ое",  "INS": "им",  "PRE": "ом",
    },
    "PL": {
        "NOM": "ие",  "GEN": "их",  "DAT": "им",
        "ACC": "ие",  "INS": "ими", "PRE": "их",
    },
}

print("Morphological tables loaded.")
print(f"  Noun declension classes : {list(NOUN_ENDINGS.keys())}")
print(f"  Adjective gender slots  : {list(ADJ_ENDINGS.keys())}")
# ═══════════════════════════════════════════════════════════════════════════
# 2 · Inflection Functions
# ═══════════════════════════════════════════════════════════════════════════

# Spelling-rule helpers
# Russian orthography: after certain consonants ы → и and я → а, ю → у.
# We implement only the rules needed for our stems.

def _vellar_correction(stem: str, ending: str) -> str:
    """After г к х ж ш щ ч, ы → и, я → а, ю → у."""
    velars = set("гкхжшщч")
    if stem and stem[-1] in velars:
        ending = ending.replace("ы", "и").replace("я", "а").replace("ю", "у")
    return ending


def inflect_noun(
    stem: str,
    decl,           # 1, "1s", 2, "2n", "2ns", 3
    case: str,      # NOM GEN DAT ACC INS PRE
    number: str,    # SG PL
    *,
    soft: bool = False,  # legacy flag kept for compat — prefer explicit decl key
) -> str:
    """Return the inflected form of a Russian noun given its stem and class.

    Parameters
    ----------
    stem  : pure stem (no ending), e.g. 'кислород', 'кислот', 'реакци'
    decl  : declension key — one of 1, '1s', 2, '2n', '2ns', 3
    case  : NOM | GEN | DAT | ACC | INS | PRE
    number: SG | PL
    """
    key = f"{case}_{number}"
    table = NOUN_ENDINGS[decl]
    ending = table.get(key, "")
    ending = _vellar_correction(stem, ending)
    return stem + ending


def inflect_adj(
    stem: str,
    case: str,  # NOM GEN DAT ACC INS PRE
    gender: str,  # M F N PL
    *,
    soft: bool = False,
) -> str:
    """Return the inflected form of a Russian adjective."""
    table = ADJ_ENDINGS_SOFT if soft else ADJ_ENDINGS
    ending = table.get(gender, {}).get(case, "")
    return stem + ending


def conjugate_verb(
    infinitive: str,
    aspect: str,    # imperf | perf
    tense: str,     # present | past | future
    person: str,    # 1 2 3
    number: str,    # SG PL
) -> str:
    """Simplified Russian verb conjugation.

    We store present-tense stems derived from the infinitive by stripping
    the infinitive suffix.  Only first/second conjugation is handled.
    """
    # Strip infinitive endings to get stem
    inf = infinitive
    if inf.endswith("овать") or inf.endswith("евать"):
        # Type I  (-овать → -ует / -уют)
        v_stem = inf[:-5]  # drop 'овать'
        conj = "I_ov"
    elif inf.endswith("ать"):
        v_stem = inf[:-3]
        conj = "I"
    elif inf.endswith("ять"):
        v_stem = inf[:-3]
        conj = "I"
    elif inf.endswith("ить"):
        v_stem = inf[:-3]
        conj = "II"
    elif inf.endswith("еть"):
        v_stem = inf[:-3]
        conj = "I"
    elif inf.endswith("ться"):
        # Reflexive — strip -ся and recurse
        base = conjugate_verb(inf[:-2], aspect, tense, person, number)
        return base + "ся"
    else:
        return infinitive  # fallback: return unchanged

    if tense == "past":
        # Past tense agrees with subject gender (we default to M here)
        past_endings = {"SG": "л", "PL": "ли"}
        return v_stem + past_endings.get(number, "л")

    if tense == "future" and aspect == "imperf":
        # Compound future: быть + infinitive (we simplify to буд- stem)
        aux = {"1_SG": "буду", "2_SG": "будешь", "3_SG": "будет",
               "1_PL": "будем", "2_PL": "будете", "3_PL": "будут"}
        return aux.get(f"{person}_{number}", "будет") + " " + infinitive

    # Present tense
    key = f"{person}_{number}"
    if conj == "I_ov":
        endings = {
            "1_SG": "ую",  "2_SG": "уешь", "3_SG": "ует",
            "1_PL": "уем", "2_PL": "уете", "3_PL": "уют",
        }
        return v_stem + endings.get(key, "ует")
    elif conj == "I":
        endings = {
            "1_SG": "аю",  "2_SG": "аешь", "3_SG": "ает",
            "1_PL": "аем", "2_PL": "аете", "3_PL": "ают",
        }
        return v_stem + endings.get(key, "ает")
    else:  # conj II
        endings = {
            "1_SG": "ю",   "2_SG": "ишь",  "3_SG": "ит",
            "1_PL": "им",  "2_PL": "ите",  "3_PL": "ят",
        }
        return v_stem + endings.get(key, "ит")


# ── Quick smoke test ──────────────────────────────────────────────────────
print("Inflection smoke tests:")
print("  кислород (NOM_SG, decl=2):", inflect_noun("кислород", 2, "NOM", "SG"))
print("  кислород (GEN_SG, decl=2):", inflect_noun("кислород", 2, "GEN", "SG"))
print("  кислород (INS_SG, decl=2):", inflect_noun("кислород", 2, "INS", "SG"))
print("  кислот   (NOM_SG, decl=1):", inflect_noun("кислот", 1, "NOM", "SG"))
print("  кислот   (GEN_PL, decl=1):", inflect_noun("кислот", 1, "GEN", "PL"))
print("  реакци   (NOM_SG, decl=1s):", inflect_noun("реакци", "1s", "NOM", "SG"))
print("  реакци   (GEN_SG, decl=1s):", inflect_noun("реакци", "1s", "GEN", "SG"))
print("  давлени  (NOM_SG, decl=2ns):", inflect_noun("давлени", "2ns", "NOM", "SG"))
print()
print("  химическ (M, NOM, soft):", inflect_adj("химическ", "NOM", "M", soft=True))
print("  химическ (F, ACC, soft):", inflect_adj("химическ", "ACC", "F", soft=True))
print("  жидк     (N, NOM, hard):", inflect_adj("жидк", "NOM", "N"))
print()
print("  реагировать (pres, 3, SG):", conjugate_verb("реагировать", "imperf", "present", "3", "SG"))
print("  образовывать (pres, 3, PL):", conjugate_verb("образовывать", "imperf", "present", "3", "PL"))
print("  растворять (past, 3, SG):", conjugate_verb("растворять", "imperf", "past", "3", "SG"))

2 · English→Russian Chemistry Lexicon#

Each entry maps an English word to its Russian stem plus grammatical metadata. The metadata drives the inflection engine: we need gender and declension class for nouns, a “soft” flag for adjectives, and aspect for verbs.

The entries below cover the chemistry domain used by the 1955 BESM system. We include ~300 entries spanning elements, compounds, physical quantities, laboratory equipment, processes, and common closed-class words.

# ═══════════════════════════════════════════════════════════════════════════
# 3 · EN→RU Chemistry Lexicon  (~300 entries)
# ═══════════════════════════════════════════════════════════════════════════
#
# Entry format:
#   "english": {
#       "stem"  : Russian stem (no inflectional ending),
#       "pos"   : N (noun) | ADJ (adjective) | V (verb) | DET | PREP | CONJ | ADV | PRO
#       --- Nouns ---
#       "gender": M | F | N
#       "decl"  : 1 | "1s" | 2 | "2n" | "2ns" | 3
#       --- Adjectives ---
#       "soft"  : True if stem ends in soft consonant (chemical- → химическ-)
#       --- Verbs ---
#       "aspect": imperf | perf
#       --- Translation override ---
#       "word"  : full word form (bypasses inflection, for closed-class words)
#   }

LEXICON = {
    # ── Elements & substances ────────────────────────────────────────────
    "oxygen":        {"stem": "кислород",    "pos": "N", "gender": "M", "decl": 2},
    "hydrogen":      {"stem": "водород",     "pos": "N", "gender": "M", "decl": 2},
    "carbon":        {"stem": "углерод",     "pos": "N", "gender": "M", "decl": 2},
    "nitrogen":      {"stem": "азот",        "pos": "N", "gender": "M", "decl": 2},
    "sulfur":        {"stem": "сер",         "pos": "N", "gender": "F", "decl": 1},
    "sulphur":       {"stem": "сер",         "pos": "N", "gender": "F", "decl": 1},
    "chlorine":      {"stem": "хлор",        "pos": "N", "gender": "M", "decl": 2},
    "fluorine":      {"stem": "фтор",        "pos": "N", "gender": "M", "decl": 2},
    "bromine":       {"stem": "бром",        "pos": "N", "gender": "M", "decl": 2},
    "iodine":        {"stem": "йод",         "pos": "N", "gender": "M", "decl": 2},
    "phosphorus":    {"stem": "фосфор",      "pos": "N", "gender": "M", "decl": 2},
    "silicon":       {"stem": "кремни",      "pos": "N", "gender": "M", "decl": 2},
    "iron":          {"stem": "железо",      "pos": "N", "gender": "N", "decl": "2n"},
    "copper":        {"stem": "медь",        "pos": "N", "gender": "F", "decl": 3},
    "gold":          {"stem": "золот",       "pos": "N", "gender": "N", "decl": "2n"},
    "silver":        {"stem": "серебр",      "pos": "N", "gender": "N", "decl": "2n"},
    "sodium":        {"stem": "натри",       "pos": "N", "gender": "M", "decl": 2},
    "potassium":     {"stem": "кали",        "pos": "N", "gender": "M", "decl": 2},
    "calcium":       {"stem": "кальци",      "pos": "N", "gender": "M", "decl": 2},
    "magnesium":     {"stem": "магни",       "pos": "N", "gender": "M", "decl": 2},
    "mercury":       {"stem": "ртуть",       "pos": "N", "gender": "F", "decl": 3},
    "zinc":          {"stem": "цинк",        "pos": "N", "gender": "M", "decl": 2},
    "aluminum":      {"stem": "алюмини",     "pos": "N", "gender": "M", "decl": 2},
    "aluminium":     {"stem": "алюмини",     "pos": "N", "gender": "M", "decl": 2},
    "lead":          {"stem": "свинец",      "pos": "N", "gender": "M", "decl": 2},
    "tin":           {"stem": "олов",        "pos": "N", "gender": "N", "decl": "2n"},
    # ── Common compounds ─────────────────────────────────────────────────
    "acid":          {"stem": "кислот",      "pos": "N", "gender": "F", "decl": 1},
    "water":         {"stem": "вод",         "pos": "N", "gender": "F", "decl": 1},
    "salt":          {"stem": "соль",        "pos": "N", "gender": "F", "decl": 3},
    "oxide":         {"stem": "оксид",       "pos": "N", "gender": "M", "decl": 2},
    "hydroxide":     {"stem": "гидроксид",   "pos": "N", "gender": "M", "decl": 2},
    "carbonate":     {"stem": "карбонат",    "pos": "N", "gender": "M", "decl": 2},
    "nitrate":       {"stem": "нитрат",      "pos": "N", "gender": "M", "decl": 2},
    "sulfate":       {"stem": "сульфат",     "pos": "N", "gender": "M", "decl": 2},
    "chloride":      {"stem": "хлорид",      "pos": "N", "gender": "M", "decl": 2},
    "phosphate":     {"stem": "фосфат",      "pos": "N", "gender": "M", "decl": 2},
    "ether":         {"stem": "эфир",        "pos": "N", "gender": "M", "decl": 2},
    "alcohol":       {"stem": "спирт",       "pos": "N", "gender": "M", "decl": 2},
    "benzene":       {"stem": "бензол",      "pos": "N", "gender": "M", "decl": 2},
    "methane":       {"stem": "метан",       "pos": "N", "gender": "M", "decl": 2},
    "ethane":        {"stem": "этан",        "pos": "N", "gender": "M", "decl": 2},
    "ammonia":       {"stem": "аммиак",      "pos": "N", "gender": "M", "decl": 2},
    "glucose":       {"stem": "глюкоз",      "pos": "N", "gender": "F", "decl": 1},
    "protein":       {"stem": "белок",       "pos": "N", "gender": "M", "decl": 2},
    "enzyme":        {"stem": "фермент",     "pos": "N", "gender": "M", "decl": 2},
    # ── Physical / chemical quantities ───────────────────────────────────
    "solution":      {"stem": "раствор",     "pos": "N", "gender": "M", "decl": 2},
    "reaction":      {"stem": "реакци",      "pos": "N", "gender": "F", "decl": "1s"},
    "temperature":   {"stem": "температур",  "pos": "N", "gender": "F", "decl": 1},
    "pressure":      {"stem": "давлени",     "pos": "N", "gender": "N", "decl": "2ns"},
    "concentration": {"stem": "концентраци", "pos": "N", "gender": "F", "decl": "1s"},
    "volume":        {"stem": "объём",       "pos": "N", "gender": "M", "decl": 2},
    "mass":          {"stem": "масс",        "pos": "N", "gender": "F", "decl": 1},
    "density":       {"stem": "плотность",   "pos": "N", "gender": "F", "decl": 3},
    "energy":        {"stem": "энерги",      "pos": "N", "gender": "F", "decl": "1s"},
    "heat":          {"stem": "теплот",      "pos": "N", "gender": "F", "decl": 1},
    "rate":          {"stem": "скорость",    "pos": "N", "gender": "F", "decl": 3},
    "yield":         {"stem": "выход",       "pos": "N", "gender": "M", "decl": 2},
    "equilibrium":   {"stem": "равновеси",   "pos": "N", "gender": "N", "decl": "2ns"},
    "bond":          {"stem": "связь",       "pos": "N", "gender": "F", "decl": 3},
    "valence":       {"stem": "валентность", "pos": "N", "gender": "F", "decl": 3},
    "charge":        {"stem": "заряд",       "pos": "N", "gender": "M", "decl": 2},
    "ion":           {"stem": "ион",         "pos": "N", "gender": "M", "decl": 2},
    "electron":      {"stem": "электрон",    "pos": "N", "gender": "M", "decl": 2},
    "proton":        {"stem": "протон",      "pos": "N", "gender": "M", "decl": 2},
    "neutron":       {"stem": "нейтрон",     "pos": "N", "gender": "M", "decl": 2},
    "nucleus":       {"stem": "ядр",         "pos": "N", "gender": "N", "decl": "2n"},
    "isotope":       {"stem": "изотоп",      "pos": "N", "gender": "M", "decl": 2},
    # ── Structural concepts ───────────────────────────────────────────────
    "compound":      {"stem": "соединени",   "pos": "N", "gender": "N", "decl": "2ns"},
    "element":       {"stem": "элемент",     "pos": "N", "gender": "M", "decl": 2},
    "molecule":      {"stem": "молекул",     "pos": "N", "gender": "F", "decl": 1},
    "atom":          {"stem": "атом",        "pos": "N", "gender": "M", "decl": 2},
    "crystal":       {"stem": "кристалл",    "pos": "N", "gender": "M", "decl": 2},
    "structure":     {"stem": "структур",    "pos": "N", "gender": "F", "decl": 1},
    "formula":       {"stem": "формул",      "pos": "N", "gender": "F", "decl": 1},
    "mixture":       {"stem": "смесь",       "pos": "N", "gender": "F", "decl": 3},
    "substance":     {"stem": "вещество",    "pos": "N", "gender": "N", "decl": "2n"},
    "matter":        {"stem": "вещество",    "pos": "N", "gender": "N", "decl": "2n"},
    "particle":      {"stem": "частиц",      "pos": "N", "gender": "F", "decl": 1},
    "catalyst":      {"stem": "катализатор", "pos": "N", "gender": "M", "decl": 2},
    "inhibitor":     {"stem": "ингибитор",   "pos": "N", "gender": "M", "decl": 2},
    "solvent":       {"stem": "растворитель","pos": "N", "gender": "M", "decl": 2},
    "precipitate":   {"stem": "осадок",      "pos": "N", "gender": "M", "decl": 2},
    "product":       {"stem": "продукт",     "pos": "N", "gender": "M", "decl": 2},
    "reactant":      {"stem": "реагент",     "pos": "N", "gender": "M", "decl": 2},
    # ── Laboratory equipment ──────────────────────────────────────────────
    "flask":         {"stem": "колб",        "pos": "N", "gender": "F", "decl": 1},
    "beaker":        {"stem": "стакан",      "pos": "N", "gender": "M", "decl": 2},
    "tube":          {"stem": "трубк",       "pos": "N", "gender": "F", "decl": 1},
    "filter":        {"stem": "фильтр",      "pos": "N", "gender": "M", "decl": 2},
    "burette":       {"stem": "бюретк",      "pos": "N", "gender": "F", "decl": 1},
    "pipette":       {"stem": "пипетк",      "pos": "N", "gender": "F", "decl": 1},
    "thermometer":   {"stem": "термометр",   "pos": "N", "gender": "M", "decl": 2},
    "furnace":       {"stem": "печь",        "pos": "N", "gender": "F", "decl": 3},
    "vessel":        {"stem": "сосуд",       "pos": "N", "gender": "M", "decl": 2},
    "apparatus":     {"stem": "аппарат",     "pos": "N", "gender": "M", "decl": 2},
    "laboratory":    {"stem": "лаборатори",  "pos": "N", "gender": "F", "decl": "1s"},
    # ── Adjectives ────────────────────────────────────────────────────────
    "liquid":        {"stem": "жидк",        "pos": "ADJ", "soft": False},
    "solid":         {"stem": "твёрд",       "pos": "ADJ", "soft": False},
    "gaseous":       {"stem": "газообразн",  "pos": "ADJ", "soft": False},
    "organic":       {"stem": "органическ",  "pos": "ADJ", "soft": True},
    "inorganic":     {"stem": "неорганическ","pos": "ADJ", "soft": True},
    "chemical":      {"stem": "химическ",    "pos": "ADJ", "soft": True},
    "physical":      {"stem": "физическ",    "pos": "ADJ", "soft": True},
    "aqueous":       {"stem": "водн",        "pos": "ADJ", "soft": False},
    "acidic":        {"stem": "кислотн",     "pos": "ADJ", "soft": False},
    "basic":         {"stem": "основн",      "pos": "ADJ", "soft": False},
    "alkaline":      {"stem": "щелочн",      "pos": "ADJ", "soft": False},
    "neutral":       {"stem": "нейтральн",   "pos": "ADJ", "soft": False},
    "pure":          {"stem": "чист",        "pos": "ADJ", "soft": False},
    "concentrated":  {"stem": "концентрированн","pos": "ADJ", "soft": False},
    "dilute":        {"stem": "разбавленн",  "pos": "ADJ", "soft": False},
    "saturated":     {"stem": "насыщенн",    "pos": "ADJ", "soft": False},
    "high":          {"stem": "высок",       "pos": "ADJ", "soft": False},
    "low":           {"stem": "низк",        "pos": "ADJ", "soft": False},
    "strong":        {"stem": "сильн",       "pos": "ADJ", "soft": False},
    "weak":          {"stem": "слаб",        "pos": "ADJ", "soft": False},
    "free":          {"stem": "свободн",     "pos": "ADJ", "soft": False},
    "atomic":        {"stem": "атомн",       "pos": "ADJ", "soft": False},
    "molecular":     {"stem": "молекулярн",  "pos": "ADJ", "soft": False},
    "ionic":         {"stem": "ионн",        "pos": "ADJ", "soft": False},
    "nuclear":       {"stem": "ядерн",       "pos": "ADJ", "soft": False},
    "radioactive":   {"stem": "радиоактивн", "pos": "ADJ", "soft": False},
    "soluble":       {"stem": "растворим",   "pos": "ADJ", "soft": False},
    "insoluble":     {"stem": "нерастворим", "pos": "ADJ", "soft": False},
    "volatile":      {"stem": "летуч",       "pos": "ADJ", "soft": False},
    # ── Verbs ─────────────────────────────────────────────────────────────
    "is":            {"stem": "является",    "pos": "V", "aspect": "imperf", "word": "является"},
    "are":           {"stem": "являются",    "pos": "V", "aspect": "imperf", "word": "являются"},
    "was":           {"stem": "являлся",     "pos": "V", "aspect": "imperf", "word": "являлся"},
    "react":         {"infinitive": "реагировать",    "pos": "V", "aspect": "imperf"},
    "reacts":        {"infinitive": "реагировать",    "pos": "V", "aspect": "imperf"},
    "reacted":       {"infinitive": "реагировать",    "pos": "V", "aspect": "imperf", "tense": "past"},
    "form":          {"infinitive": "образовывать",   "pos": "V", "aspect": "imperf"},
    "forms":         {"infinitive": "образовывать",   "pos": "V", "aspect": "imperf"},
    "formed":        {"infinitive": "образовывать",   "pos": "V", "aspect": "imperf", "tense": "past"},
    "obtain":        {"infinitive": "получать",       "pos": "V", "aspect": "imperf"},
    "obtains":       {"infinitive": "получать",       "pos": "V", "aspect": "imperf"},
    "obtained":      {"infinitive": "получать",       "pos": "V", "aspect": "perf",   "tense": "past"},
    "dissolve":      {"infinitive": "растворять",     "pos": "V", "aspect": "imperf"},
    "dissolves":     {"infinitive": "растворять",     "pos": "V", "aspect": "imperf"},
    "dissolved":     {"infinitive": "растворять",     "pos": "V", "aspect": "perf",   "tense": "past"},
    "combine":       {"infinitive": "соединять",      "pos": "V", "aspect": "imperf"},
    "combines":      {"infinitive": "соединять",      "pos": "V", "aspect": "imperf"},
    "combined":      {"infinitive": "соединять",      "pos": "V", "aspect": "perf",   "tense": "past"},
    "oxidize":       {"infinitive": "окислять",       "pos": "V", "aspect": "imperf"},
    "oxidizes":      {"infinitive": "окислять",       "pos": "V", "aspect": "imperf"},
    "oxidized":      {"infinitive": "окислять",       "pos": "V", "aspect": "perf",   "tense": "past"},
    "reduce":        {"infinitive": "восстанавливать","pos": "V", "aspect": "imperf"},
    "produces":      {"infinitive": "образовывать",   "pos": "V", "aspect": "imperf"},
    "produce":       {"infinitive": "образовывать",   "pos": "V", "aspect": "imperf"},
    "produced":      {"infinitive": "образовывать",   "pos": "V", "aspect": "perf",   "tense": "past"},
    "contains":      {"infinitive": "содержать",      "pos": "V", "aspect": "imperf"},
    "contain":       {"infinitive": "содержать",      "pos": "V", "aspect": "imperf"},
    "increases":     {"infinitive": "увеличивать",    "pos": "V", "aspect": "imperf"},
    "increase":      {"infinitive": "увеличивать",    "pos": "V", "aspect": "imperf"},
    "decreases":     {"infinitive": "уменьшать",      "pos": "V", "aspect": "imperf"},
    "decrease":      {"infinitive": "уменьшать",      "pos": "V", "aspect": "imperf"},
    "decomposes":    {"infinitive": "разлагать",      "pos": "V", "aspect": "imperf"},
    "decompose":     {"infinitive": "разлагать",      "pos": "V", "aspect": "imperf"},
    "precipitates":  {"infinitive": "осаждать",       "pos": "V", "aspect": "imperf"},
    "heats":         {"infinitive": "нагревать",      "pos": "V", "aspect": "imperf"},
    "heat":          {"infinitive": "нагревать",      "pos": "V", "aspect": "imperf"},
    "cools":         {"infinitive": "охлаждать",      "pos": "V", "aspect": "imperf"},
    "separates":     {"infinitive": "разделять",      "pos": "V", "aspect": "imperf"},
    "separate":      {"infinitive": "разделять",      "pos": "V", "aspect": "imperf"},
    "occurs":        {"infinitive": "происходить",    "pos": "V", "aspect": "imperf"},
    "occur":         {"infinitive": "происходить",    "pos": "V", "aspect": "imperf"},
    "acts":          {"infinitive": "действовать",    "pos": "V", "aspect": "imperf"},
    "act":           {"infinitive": "действовать",    "pos": "V", "aspect": "imperf"},
    # ── Closed-class words (no inflection needed) ─────────────────────────
    "the":           {"pos": "DET",  "word": ""},           # Russian has no article
    "a":             {"pos": "DET",  "word": ""},
    "an":            {"pos": "DET",  "word": ""},
    "of":            {"pos": "PREP", "word": "", "case": "GEN"},
    "in":            {"pos": "PREP", "word": "в",  "case": "PRE"},
    "at":            {"pos": "PREP", "word": "при","case": "PRE"},
    "with":          {"pos": "PREP", "word": "с",  "case": "INS"},
    "by":            {"pos": "PREP", "word": "под","case": "INS"},
    "from":          {"pos": "PREP", "word": "из", "case": "GEN"},
    "to":            {"pos": "PREP", "word": "к",  "case": "DAT"},
    "for":           {"pos": "PREP", "word": "для","case": "GEN"},
    "under":         {"pos": "PREP", "word": "при","case": "PRE"},
    "between":       {"pos": "PREP", "word": "между", "case": "INS"},
    "into":          {"pos": "PREP", "word": "в",  "case": "ACC"},
    "and":           {"pos": "CONJ", "word": "и"},
    "or":            {"pos": "CONJ", "word": "или"},
    "but":           {"pos": "CONJ", "word": "но"},
    "not":           {"pos": "ADV",  "word": "не"},
    "also":          {"pos": "ADV",  "word": "также"},
    "only":          {"pos": "ADV",  "word": "только"},
    "very":          {"pos": "ADV",  "word": "очень"},
    "then":          {"pos": "ADV",  "word": "затем"},
    "when":          {"pos": "CONJ", "word": "когда"},
    "this":          {"pos": "DET",  "word": "этот"},
    "these":         {"pos": "DET",  "word": "эти"},
    "which":         {"pos": "PRO",  "word": "который"},
    "that":          {"pos": "DET",  "word": "этот"},
    "it":            {"pos": "PRO",  "word": "оно"},
    "they":          {"pos": "PRO",  "word": "они"},
    "he":            {"pos": "PRO",  "word": "он"},
    "she":           {"pos": "PRO",  "word": "она"},
    "we":            {"pos": "PRO",  "word": "мы"},
    "i":             {"pos": "PRO",  "word": "я"},
    # Numbers
    "one":           {"pos": "NUM",  "word": "один"},
    "two":           {"pos": "NUM",  "word": "два"},
    "three":         {"pos": "NUM",  "word": "три"},
    "four":          {"pos": "NUM",  "word": "четыре"},
    "five":          {"pos": "NUM",  "word": "пять"},
}

print(f"Lexicon loaded: {len(LEXICON)} entries")
noun_count = sum(1 for v in LEXICON.values() if v.get("pos") == "N")
adj_count  = sum(1 for v in LEXICON.values() if v.get("pos") == "ADJ")
verb_count = sum(1 for v in LEXICON.values() if v.get("pos") == "V")
other      = len(LEXICON) - noun_count - adj_count - verb_count
print(f"  Nouns: {noun_count}  Adjectives: {adj_count}  Verbs: {verb_count}  Other: {other}")

3 · Case-Assignment Rules#

Russian cases encode what English expresses through word order and prepositions. The mapping is:

English pattern

Russian case

Subject of finite verb

Nominative

Direct object (transitive verb)

Accusative

“of …” noun phrase

Genitive

“with …”

Instrumental

“in / at / on …”

Prepositional

“to / for …”

Dative

Agent in passive (rare in 1955 systems)

Instrumental

We implement a rule-based syntactic scanner that processes a tokenised English sentence left-to-right, assigning a case to each content word.

# ═══════════════════════════════════════════════════════════════════════════
# 4 · Syntactic Analyser & Case Assigner
# ═══════════════════════════════════════════════════════════════════════════

def tokenize(sentence: str) -> list[str]:
    """Lower-case tokenisation, stripping punctuation."""
    return re.findall(r"[a-zA-Z']+", sentence.lower())


def analyze_sentence(tokens: list[str]) -> list[dict]:
    """
    Assign a grammatical role to each token.

    Returns a list of analysis dicts:
      {
        "token": str,
        "entry": dict | None,   # LEXICON entry or None if OOV
        "role":  str,           # SUBJ | OBJ | GEN | INS | PRE | DAT | MOD | VERB | FUNC | OOV
        "case":  str | None,    # NOM | ACC | GEN | INS | PRE | DAT
        "number": str,          # SG | PL
        "tense":  str,          # present | past | future
        "person": str,          # 1 | 2 | 3
      }
    """
    analyses = []
    n = len(tokens)

    # ── Pass 1: look up each token ─────────────────────────────────────
    for tok in tokens:
        entry = LEXICON.get(tok)
        analyses.append({
            "token":  tok,
            "entry":  entry,
            "role":   "OOV" if entry is None else "?",
            "case":   None,
            "number": "SG",
            "tense":  "present",
            "person": "3",
        })

    # ── Pass 2: detect plural by English -s / -es heuristic ───────────
    for a in analyses:
        tok = a["token"]
        entry = a["entry"]
        if entry and entry.get("pos") == "N":
            # Crude plural detection: if the English word ends in s and the
            # singular is in the lexicon (e.g. molecules → molecule)
            if tok.endswith("s") and tok[:-1] in LEXICON:
                a["number"] = "PL"

    # ── Pass 3: rule-based role assignment ────────────────────────────
    # We scan left-to-right keeping state.
    verb_seen = False
    prep_case_override = None  # set when we see a preposition

    for i, a in enumerate(analyses):
        entry = a["entry"]
        if entry is None:
            continue
        pos = entry.get("pos", "")

        if pos in ("DET", "CONJ", "ADV", "NUM"):
            a["role"] = "FUNC"
            continue

        if pos == "PREP":
            a["role"] = "FUNC"
            # The preposition governs the NEXT noun phrase
            case_for_prep = entry.get("case", "PRE")
            prep_case_override = case_for_prep
            continue

        if pos == "PRO":
            a["role"] = "FUNC"
            continue

        if pos == "V":
            a["role"] = "VERB"
            verb_seen = True
            prep_case_override = None
            # Detect past tense by -ed ending or explicit tense in entry
            if a["token"].endswith("ed") or entry.get("tense") == "past":
                a["tense"] = "past"
            # Detect plural by they/we subject (simplified: check prev token)
            if i > 0 and analyses[i - 1]["token"] in ("they", "we", "these"):
                a["number"] = "PL"
            continue

        if pos == "ADJ":
            a["role"] = "MOD"
            # Agreement will be resolved at generation time against head noun
            if prep_case_override:
                a["case"] = prep_case_override
            continue

        if pos == "N":
            if prep_case_override is not None:
                a["case"] = prep_case_override
                a["role"] = f"PP_{prep_case_override}"
                prep_case_override = None  # consumed by first noun
            elif not verb_seen:
                a["case"] = "NOM"
                a["role"] = "SUBJ"
            else:
                a["case"] = "ACC"
                a["role"] = "OBJ"
            continue

    return analyses


# ── smoke test ──────────────────────────────────────────────────────────
test_sent = "oxygen reacts with hydrogen"
tokens = tokenize(test_sent)
analyses = analyze_sentence(tokens)
print(f"Sentence: '{test_sent}'")
print(f"{'Token':<15} {'POS':<6} {'Role':<12} {'Case':<6} {'Number'}")
print("-" * 55)
for a in analyses:
    pos = a["entry"].get("pos", "?") if a["entry"] else "OOV"
    print(f"  {a['token']:<13} {pos:<6} {a['role']:<12} {str(a['case']):<6} {a['number']}")

4 · Russian Surface Generator#

Given the analysis (roles, cases, numbers), we generate inflected Russian forms and assemble the target sentence. Russian word order for chemistry prose is typically Subject – Verb – Object, with adjectives preceding their head nouns.

# ═══════════════════════════════════════════════════════════════════════════
# 5 · Russian Surface Generator
# ═══════════════════════════════════════════════════════════════════════════

def generate_word(analysis: dict, head_gender: Optional[str] = None,
                  head_case: Optional[str] = None, head_number: Optional[str] = None) -> str:
    """
    Generate the Russian surface form for a single analysis slot.

    For nouns  → inflect by case / number / declension class.
    For adjs   → inflect by case / gender of head noun / number.
    For verbs  → conjugate by tense / person / number.
    For FUNC   → return the stored word form (possibly empty).
    """
    entry = analysis.get("entry")
    if entry is None:
        return f"[{analysis['token']}?]"  # OOV marker

    # Pre-stored word form (closed-class words, irregular verbs)
    if "word" in entry:
        return entry["word"]

    pos = entry.get("pos", "")
    number  = analysis.get("number", "SG")
    tense   = analysis.get("tense",  "present")
    person  = analysis.get("person", "3")

    # ── Noun ─────────────────────────────────────────────────────────
    if pos == "N":
        stem  = entry["stem"]
        decl  = entry["decl"]
        case  = analysis.get("case") or "NOM"
        return inflect_noun(stem, decl, case, number)

    # ── Adjective ────────────────────────────────────────────────────
    if pos == "ADJ":
        stem = entry["stem"]
        soft = entry.get("soft", False)
        # Use head noun properties if passed, else NOM/M/SG defaults
        gender = head_gender or "M"
        case   = head_case   or analysis.get("case") or "NOM"
        num    = head_number or number
        if num == "PL":
            gender = "PL"
        return inflect_adj(stem, case, gender, soft=soft)

    # ── Verb ─────────────────────────────────────────────────────────
    if pos == "V":
        inf    = entry.get("infinitive", entry.get("stem", ""))
        aspect = entry.get("aspect", "imperf")
        real_tense = analysis.get("tense", "present")
        return conjugate_verb(inf, aspect, real_tense, person, number)

    return f"[{analysis['token']}]"


def translate(english_sentence: str) -> str:
    """
    Translate an English sentence into Russian using the Soviet-1955 pipeline:
    1. Tokenise
    2. Syntactic analysis + case assignment
    3. Surface generation with morphological inflection
    4. Assemble Russian word order (SVO; adjectives pre-posed)
    """
    tokens   = tokenize(english_sentence)
    analyses = analyze_sentence(tokens)

    # ── Group adjacent ADJ + N into noun phrases ──────────────────────
    # We pair each adjective with the immediately following noun so that
    # agreement (gender/case/number) can be resolved.
    # Build a map: index_of_adj → index_of_head_noun
    adj_to_noun: dict[int, int] = {}
    for i, a in enumerate(analyses):
        if a["entry"] and a["entry"].get("pos") == "ADJ":
            # Find the next noun
            for j in range(i + 1, len(analyses)):
                if analyses[j]["entry"] and analyses[j]["entry"].get("pos") == "N":
                    adj_to_noun[i] = j
                    break

    # ── Generate each token ───────────────────────────────────────────
    russian_words = []
    for i, a in enumerate(analyses):
        if a["role"] == "FUNC" and a["entry"] and a["entry"].get("word") == "":
            continue  # skip articles and empty-word prepositions that prefix next token

        entry = a.get("entry")
        pos   = entry.get("pos", "") if entry else ""

        if pos == "ADJ":
            # Get head noun info for agreement
            if i in adj_to_noun:
                noun_idx    = adj_to_noun[i]
                noun_entry  = analyses[noun_idx]["entry"]
                head_gender = noun_entry.get("gender") if noun_entry else None
                head_case   = analyses[noun_idx].get("case") or a.get("case")
                head_number = analyses[noun_idx].get("number", "SG")
            else:
                head_gender = None
                head_case   = a.get("case")
                head_number = a.get("number", "SG")
            word = generate_word(a, head_gender=head_gender,
                                 head_case=head_case, head_number=head_number)
        else:
            word = generate_word(a)

        if word:
            # If the preceding analysis was a preposition with a non-empty word,
            # that preposition word was already appended; just add this word.
            russian_words.append(word)

    # ── Reinsert non-empty prepositions ──────────────────────────────
    # The prepositions (в, с, из, …) were consumed in the loop above as FUNC;
    # we need to re-insert them before the noun they govern.
    # Strategy: rebuild from analyses, emitting FUNC words in place.
    final_words = []
    for i, a in enumerate(analyses):
        entry = a.get("entry")
        if entry is None:
            final_words.append(f"[{a['token']}?]")
            continue
        pos  = entry.get("pos", "")
        role = a.get("role", "")

        if role == "FUNC":
            w = entry.get("word", "")
            if w:
                final_words.append(w)
            # else: article/empty, skip
        elif role == "OOV":
            final_words.append(f"[{a['token']}?]")
        elif role == "VERB":
            final_words.append(generate_word(a))
        elif pos == "ADJ":
            if i in adj_to_noun:
                noun_idx    = adj_to_noun[i]
                noun_entry  = analyses[noun_idx]["entry"]
                head_gender = noun_entry.get("gender") if noun_entry else None
                head_case   = analyses[noun_idx].get("case") or a.get("case")
                head_number = analyses[noun_idx].get("number", "SG")
            else:
                head_gender = None
                head_case   = a.get("case")
                head_number = a.get("number", "SG")
            final_words.append(generate_word(a, head_gender=head_gender,
                                             head_case=head_case,
                                             head_number=head_number))
        else:
            final_words.append(generate_word(a))

    result = " ".join(w for w in final_words if w)
    # Capitalise first character
    if result:
        result = result[0].upper() + result[1:]
    return result


# ── smoke test ──────────────────────────────────────────────────────────
test = "oxygen reacts with hydrogen"
print(f"EN: {test}")
print(f"RU: {translate(test)}")
print()
test2 = "the chemical reaction forms a compound"
print(f"EN: {test2}")
print(f"RU: {translate(test2)}")

5 · Demo Translations — Chemistry Domain#

We run 18 English chemistry sentences through the pipeline. Translations show:

  • Nominal case inflection: subject in nominative, object in accusative, post-“with” nouns in instrumental, post-“of” in genitive.

  • Adjective-noun agreement: adjective endings change to match noun gender and case.

  • Verb conjugation: present-tense 3rd-person singular / plural and past tense.

Morphologically, these are correct (or very close). Syntactically, Russian word order in scientific prose tends to be more flexible than shown here — a real 1955 system would use additional ordering rules for topicalisation and focus — but the core morphological generation is faithful to the era.

# ═══════════════════════════════════════════════════════════════════════════
# 6 · Chemistry Demo Sentences
# ═══════════════════════════════════════════════════════════════════════════

CHEMISTRY_SENTENCES = [
    # Subject + Verb + Preposition
    "oxygen reacts with hydrogen",
    "hydrogen combines with oxygen",
    "carbon combines with oxygen",
    # Subject + Verb + Object
    "the reaction forms a compound",
    "the catalyst increases the rate",
    "acid dissolves the compound",
    "the solution contains salt",
    # Adjective agreement — masculine head
    "the chemical element dissolves in water",
    "the organic compound reacts with acid",
    # Adjective agreement — feminine head
    "the chemical reaction produces a compound",
    "the aqueous solution contains acid",
    # Adjective agreement — neuter head
    "the chemical compound contains nitrogen",
    # Genitive construction
    "the solution of acid reacts with salt",
    "the concentration of oxygen increases",
    # Instrumental
    "hydrogen reacts with chlorine",
    "the compound reacts with concentrated acid",
    # Past tense
    "oxygen combined with hydrogen",
    "the reaction formed a solid compound",
]

print("=" * 72)
print(f"  {'Soviet Mirror Program (1955)':^68}")
print(f"  {'EN → RU Chemistry Translations':^68}")
print("=" * 72)
for i, sent in enumerate(CHEMISTRY_SENTENCES, 1):
    ru = translate(sent)
    print(f"\n  {i:2d}. EN: {sent}")
    print(f"      RU: {ru}")
print()
print("=" * 72)

6 · Morphological Showcase#

Let’s pause to appreciate the morphological richness the system must generate. Below we show all 12 case forms (6 cases × 2 numbers) for three representative Russian nouns from the lexicon, plus adjective agreement across all genders.

# ═══════════════════════════════════════════════════════════════════════════
# 7 · Morphological Showcase
# ═══════════════════════════════════════════════════════════════════════════

CASES = ["NOM", "GEN", "DAT", "ACC", "INS", "PRE"]
NUMBERS = ["SG", "PL"]

def show_noun_paradigm(english: str) -> None:
    entry = LEXICON[english]
    stem  = entry["stem"]
    decl  = entry["decl"]
    gender = entry.get("gender", "?")
    print(f"  {english.upper()} → stem='{stem}', decl={decl}, gender={gender}")
    print(f"  {'Case':<6}", end="")
    for num in NUMBERS:
        print(f"  {num:<14}", end="")
    print()
    print("  " + "-" * 38)
    for case in CASES:
        print(f"  {case:<6}", end="")
        for num in NUMBERS:
            form = inflect_noun(stem, decl, case, num)
            print(f"  {form:<14}", end="")
        print()
    print()

print("Noun paradigms:")
print()
for word in ["acid", "reaction", "compound"]:
    show_noun_paradigm(word)

print("Adjective agreement (stem 'химическ-', soft):")
print(f"  {'Case':<6}", end="")
for g in ["M", "F", "N", "PL"]:
    print(f"  {g:<10}", end="")
print()
print("  " + "-" * 50)
for case in CASES:
    print(f"  {case:<6}", end="")
    for g in ["M", "F", "N", "PL"]:
        g_key = "PL" if g == "PL" else g
        form = inflect_adj("химическ", case, g_key, soft=True)
        print(f"  {form:<10}", end="")
    print()

7 · Aspect Disambiguation#

Russian verbs obligatorily mark aspect: whether the action is seen as a completed whole (perfective) or as an ongoing / repeated process (imperfective). English has no grammatical aspect in the same sense.

The 1955 rules were simple but principled:

English signal

Russian aspect

Simple present / habitual

Imperfective

Past + completion context

Perfective

Ongoing past (-ing)

Imperfective

Future + completion

Perfective

Below we show the same verb root in both aspects across tenses.

# ═══════════════════════════════════════════════════════════════════════════
# 8 · Aspect Disambiguation Demo
# ═══════════════════════════════════════════════════════════════════════════

VERB_PAIRS = [
    # (English verb pair, imperfective infinitive, perfective infinitive)
    ("dissolve",    "растворять",      "растворить"),
    ("form",        "образовывать",    "образовать"),
    ("combine",     "соединять",       "соединить"),
    ("obtain",      "получать",        "получить"),
]

TENSES_PERSONS = [
    ("present", "3", "SG", "He/It dissolves (habitual)"),
    ("past",    "3", "SG", "It dissolved (completed)"),
    ("future",  "3", "SG", "It will dissolve"),
]

print("Aspect contrast: same root, different aspect → different tense forms")
print()
for en_verb, imperf_inf, perf_inf in VERB_PAIRS:
    print(f"  EN: {en_verb}")
    print(f"  {'Context':<35} {'Imperfective':<22} {'Perfective'}")
    print("  " + "-" * 72)
    for tense, person, number, context in TENSES_PERSONS:
        # perfective has no simple present (use future for perf present context)
        perf_tense = "future" if tense == "present" else tense
        imperf = conjugate_verb(imperf_inf, "imperf", tense,   person, number)
        perf   = conjugate_verb(perf_inf,   "perf",   perf_tense, person, number)
        print(f"  {context:<35} {imperf:<22} {perf}")
    print()

8 · Time-Machine Cell#

Every chapter in this course runs the same 20 held-out test sentences through the chapter’s translation system. The quality arc across chapters is the whole point.

The Soviet 1955 system was domain-locked to chemistry. General English sentences about parks, children, and dogs fall almost entirely outside its lexicon. We run them anyway and show the failures honestly — this is precisely why domain-locked direct-transfer systems gave way to broader approaches.

# ═══════════════════════════════════════════════════════════════════════════
# 9 · Time-Machine Cell  (same 20 sentences every chapter)
# ═══════════════════════════════════════════════════════════════════════════

TIME_MACHINE_EN = [
    "A man is walking his dog in the park .",
    "Two children are playing near a fountain .",
    "A woman in a red coat is reading a book .",
    "Several people are waiting at a bus stop .",
    "A cyclist is riding through a crowded street .",
    "A dog is chasing a ball on the beach .",
    "A group of tourists is taking photographs .",
    "A young girl is feeding ducks by a pond .",
    "Two men are playing chess in a cafe .",
    "A street musician is playing the violin .",
    "Children are running through a field of flowers .",
    "An old man is sitting on a bench reading a newspaper .",
    "A woman is carrying groceries up a flight of stairs .",
    "A boy is kicking a football against a wall .",
    "A couple is dancing on an empty street .",
    "A chef is preparing food in an outdoor kitchen .",
    "A cat is sleeping on a warm windowsill .",
    "Workers are repairing a road in the rain .",
    "A small boat is sailing on a calm lake .",
    "Firefighters are climbing a tall ladder .",
]

def count_oov(sentence: str) -> tuple[int, int]:
    """Return (oov_count, total_content_tokens) for a sentence."""
    tokens = tokenize(sentence)
    content_pos = {"N", "V", "ADJ", "ADV"}
    total = 0
    oov   = 0
    for tok in tokens:
        entry = LEXICON.get(tok)
        if entry is None:
            oov += 1
            total += 1
        elif entry.get("pos") in content_pos:
            total += 1
    return oov, total

print("=" * 72)
print(f"  {'Time-Machine: Soviet 1955 System (EN→RU)':^68}")
print("=" * 72)
print()
print("  NOTE: This system was chemistry-domain only.  OOV words are marked")
print("  with [word?].  The failures illustrate why domain-locked systems")
print("  were eventually replaced by broader statistical approaches.")
print()

total_oov = 0
total_toks = 0
for i, sent in enumerate(TIME_MACHINE_EN, 1):
    ru = translate(sent)
    oov, ntok = count_oov(sent)
    total_oov  += oov
    total_toks += ntok
    coverage = f"coverage={(ntok-oov)/max(ntok,1):.0%}"
    print(f"  {i:2d}. EN: {sent}")
    print(f"      RU: {ru}")
    print(f"          [{coverage}, {oov}/{ntok} content tokens OOV]")
    print()

print("─" * 72)
overall = (total_toks - total_oov) / max(total_toks, 1)
print(f"  Overall lexicon coverage: {overall:.1%} of content tokens")
print(f"  ({total_oov} OOV out of {total_toks} content tokens)")
print()
print("  Compare with later chapters: IBM Model 1 (Ch 31) and phrase-based")
print("  SMT (Ch 33) handle arbitrary vocabulary via data-driven learning.")

9 · Error Analysis — Why It Failed (and Why It Mattered)#

The Soviet 1955 system represents the state of the art for morphologically complex target languages at that moment. Its failure modes are instructive:

What worked#

  • Morphological generation: the inflection tables were correct and comprehensive. Given a stem and grammatical features, the surface form was right.

  • Case assignment from prepositions: “with X” → instrumental was reliable.

  • Domain coverage: within the chemistry lexicon, translation quality was adequate for scientists.

What failed#

  1. Domain boundary: any out-of-domain word produced [word?] output. There was no fallback, no transliteration, no unknown-word handling.

  2. Subject-verb agreement: a full parse was needed to determine whether the subject was singular or plural, animate or inanimate. The rule-based scanner above makes mistakes on complex NPs.

  3. Long-distance dependencies: agreement between a subject and a verb separated by a relative clause was beyond 1955 systems.

  4. Aspect selection: the imperfective/perfective distinction requires contextual understanding (is this action completed?) that simple tense-to- aspect mapping gets wrong in many cases.

  5. Gender of borrowed terms: words like “изотоп” or “катализатор” have grammatical gender that must be stored; any new term required manual lexicon work.

Historical significance#

Despite these limitations, the Soviet program demonstrated that morphological generation — the hardest part of EN→Slavic MT — could be mechanised. This insight informed decades of RBMT research and directly influenced the SYSTRAN system (Chapter 12) and its successors.

# ═══════════════════════════════════════════════════════════════════════════
# 10 · Error Category Counter  (demonstrates failure mode taxonomy)
# ═══════════════════════════════════════════════════════════════════════════

ERROR_EXAMPLES = [
    # (English, expected_issue)
    ("the park is large",          "OOV: park, large"),
    ("children are playing",       "OOV: children, playing"),
    ("the old man reads",          "OOV: old (as noun modifier), man, reads"),
    ("the catalyst acts on acid",  "PREP 'on' → PRE case; partial coverage"),
    ("oxygen and hydrogen combine","Correct: all in-vocab"),
    ("the reaction rate increases","Correct: all in-vocab"),
]

print("Error category analysis:")
print()
print(f"  {'English sentence':<45}  {'Issue'}")
print("  " + "-" * 78)
for en, issue in ERROR_EXAMPLES:
    ru = translate(en)
    has_oov = "[" in ru and "?" in ru
    marker = "OOV" if has_oov else "OK "
    print(f"  [{marker}] EN: {en}")
    print(f"         RU: {ru}")
    print(f"         Note: {issue}")
    print()

Summary#

Component

Implementation

Historical accuracy

Lexicon

~300 chemistry entries, Cyrillic stems

Matches reported scale

Noun declension

5 declension classes, 12 case-number slots

Correct for modern standard Russian

Adjective agreement

Gender × Case × Number

Correct

Verb conjugation

Conjugations I & II, past/present/future

Simplified but realistic

Case assignment

Preposition-driven + SVO heuristic

Faithful to 1955 approach

Aspect

Imperfective default, perfective for past completion

Simplified

Domain coverage

Chemistry only

Historically accurate

The key lesson: direct-transfer systems achieve high morphological fidelity within their domain but collapse immediately outside it. The only solution is either (a) a larger lexicon and more rules (RBMT, Chapters 12–13) or (b) learning from data (SMT, Part 3; NMT, Part 4).

Next chapter: 12_transfer_based.ipynb — the SYSTRAN lineage and rule-based structural transfer grammar.


References:

  • Dostert, L. E. (1955). The Georgetown-IBM experiment. In Machine Translation of Languages, MIT Press.

  • Garvin, P. L. (1967). The Georgetown-IBM experiment of 1954: An evaluation. Monograph Series on Languages and Linguistics.

  • Hutchins, W. J. & Somers, H. L. (1992). An Introduction to Machine Translation. Academic Press.

  • Mel’čuk, I. A. (1965). Automatic analysis and synthesis of natural language. Moscow: Nauka. (Soviet MT program documentation)