Зачем я вообще полез в ДНК

Первое — это интерес, как двигатель узнать что-то новое. Это также напрямую связано с развитием ИИ, где я принимаю непосредственное участие.

И второе — если посмотреть, как реальная клетка читает мРНК и собирает белки, это похоже на исполнение байткода:

  • Рибосома движется по мРНК в одном направлении и читает её триплетами — по три нуклеотида за раз. Для программиста это похоже на последовательный разбор потока, где каждый следующий токен имеет фиксированную длину.

  • Старт-кодон в живой клетке рибосома читает мРНК, где стартовый кодон записывается как AUG. Я же дальше буду работать с ДНК-записью и алфавитом A/C/G/T, поэтому в статье старт будет записан как ATG…

  • Стоп-кодоны TAA / TAG / TGA — три варианта возврата.

  • Кодоны между ними — основная программа. В живой клетке каждый такой триплет обычно означает: какую аминокислоту нужно добавить к растущей белковой цепочке.

Подменим один-единственный шаг: пусть рибосома вместо аминокислоты выполняет инструкцию виртуальной машины. Этого уже достаточно, чтобы получить маленький, но полноценный интерпретатор. С тем же четырёхбуквенным алфавитом и 64 возможными триплетами, которые природа уже использует для генетического кода.

Меня соблазнила минимальность. На обычном байткоде типа JVM писать квайн — несложно, но скучно. А вот написать его на ДНК-байткоде, чтобы каждый нуклеотид нёс смысл и при этом всё это выглядело как настоящий ген — это уже интересная задача про экономию (задача уместить максимум смысла в минимум нуклеотидов).

Оговорка для биологов: нет, я не буду упрощать клетку до «интерпретатора байткода и больше ничего». В проекте за этой статьёй есть полноценный клеточный цикл с G1/S/G2/M и чекпоинтами p53/ATR/ATM, метаболика с AMPK/mTOR/HIF, апоптоз, теломеры по Хейфлику, стволовая иерархия с Notch-Delta, внеклеточный матрикс с anoikis, paracrine-сигнализация, репарация ДНК (MMR/BER/NER/NHEJ/HR), транспозоны, горизонтальный перенос генов, сплайсинг интронов, и трёхмерная морфогенетика с диффундирующим морфогеном. Полный список — ближе к концу статьи. В первой части мы аккуратно начнём с базы — рибосомы и квайна — а всю остальную биологию я разверну в следующих статьях серии.

ДНК как четверичный байткод

Начнём с базы. Каждый нуклеотид — это одна цифра в системе с основанием 4:

нуклеотид

значение

A

0

C

1

G

2

T

3

Кодон — это три нуклеотида подряд, то есть число от 0 до 63:

codon_value(a, b, c) = a * 16 + b * 4 + c

Маленький бонус: кодировка A=0, T=3 и C=1, G=2 даёт удобное правило комплементарности. Сумма пары всегда равна 3, то есть комплемент любого нуклеотида — это 3 - n. Очень чистая арифметика для двойной спирали.

В коде это выглядит так:

from enum import IntEnum

class Nucleotide(IntEnum):
    A = 0
    C = 1
    G = 2
    T = 3

    @property
    def complement(self):
        return Nucleotide(3 - self.value)

Теперь ДНК-цепь — это просто список этих чисел. А кодон — целое значение от 0 до 63. У нас будет 64 слота под опкоды нашей виртуальной машины. Используем мы из них около 20 — остальные пока пустые, но мы вспомним о них в следующих статьях, когда подключим эволюцию.

Виртуальная рибосома: стек-машина в 64 опкода

Дальше нам нужна машина, которая по этому байткоду умеет ходить и что-то делать. Я выбрал самую простую архитектуру: стек-машина с парой регистров. Состояние такое:

  • Стек целых чисел — для арифметики и аргументов

  • Словарь переменных (адресуются одним кодоном-литералом, итого 64 слота памяти)

  • Указатель PC — какой кодон сейчас читаем

  • Выходной буфер — сюда пишутся новые нуклеотиды (понадобится для квайна)

И собственно набор инструкций. Я сделал так, чтобы биологические старт- и стоп-кодоны работали по своему прямому назначению:

опкод   | кодон  | действие
─────────┼────────┼────────────────────────────────────
NOP      | AAA    | ничего
PUSH     | AAC    | следующий кодон в стек как литерал
POP      | AAG    | выкинуть верхний
DUP      | AAT    | продублировать верхний
SWAP     | ACA    | поменять местами два верхних
ADD      | ACC    | a + b
SUB      | ACG    | a − b
MUL      | ACT    | a · b
EQ       | AGA    | a == b это 0 или 1
LT       | AGC    | a < b это 0 или 1
JMP      | AGG    | pc = next_codon
JZ       | AGT    | если top == 0, то pc = next_codon
LOAD     | ATA    | стек - vars[next]
STORE    | ATC    | vars[next] - pop
START    | ATG    | вход (работает как NOP)
GENLEN   | ATT    | длина генома (в нт) - стек
READAT   | CAA    | стек - genome[pop()] 
WRITE    | CAC    | output - Nucleotide(pop())
PRINT    | CAG    | напечатать pop() (для отладки)
STOP     | TAA    | остановка (плюс TAG/TGA)

Шаг рибосомы выглядит так: считываем кодон по pc, увеличиваем pc на 1, выполняем соответствующее действие. Если действие требует литерал — считываем ещё один кодон и снова увеличиваем pc.

В Python это получается компактно. Вот скелет — без отделочных деталей:

class Ribosome:
    def __init__(self, genome):
        self.genome = genome     # список Nucleotide
        self.stack  = []
        self.vars   = {}
        self.pc     = 0          # индекс текущего кодона
        self.output = []         # дочерняя цепь
        self.halted = False

    def codon_at(self, idx):
        # склеить три нт в число 0..63
        base = idx * 3
        s = self.genome
        return int(s[base]) * 16 + int(s[base + 1]) * 4 + int(s[base + 2])

    def fetch(self):
        c = self.codon_at(self.pc)
        self.pc += 1
        return c

    def step(self):
        code = self.fetch()
        if code in STOP_CODONS:
            self.halted = True
        elif code == PUSH:
            self.stack.append(self.fetch())
        elif code == ADD:
            b = self.stack.pop()
            self.stack[-1] += b
        elif code == LT:
            b = self.stack.pop()
            self.stack[-1] = 1 if self.stack[-1] < b else 0
        elif code == JZ:
            target = self.fetch()
            if self.stack.pop() == 0:
                self.pc = target
        elif code == LOAD:
            slot = self.fetch()
            self.stack.append(self.vars.get(slot, 0))
        elif code == STORE:
            slot = self.fetch()
            self.vars[slot] = self.stack.pop()
        elif code == GENLEN:
            self.stack.append(len(self.genome))
        elif code == READAT:
            i = self.stack.pop()
            self.stack.append(int(self.genome[i]))
        elif code == WRITE:
            v = self.stack.pop()
            self.output.append(Nucleotide(v % 4))
        # ... остальное аналогично

Запускать просто: вызывать step() в цикле, пока halted не станет True.

Удобно ещё и то, что у природы уже есть служебные кодоны — то, что в ассемблере мы пишем как «начало процедуры» и «возврат». Это ATG (старт-кодон, с него любой ген начинает читаться) и тройка TAA / TAG / TGA (стоп-кодоны). Представьте, что природа за вас уже выбрала номера для int 21h — нечестное преимущество.

Если интересно, какие кодоны эти служебные занимают: ATG = 14, TAA = 48, TAG = 50, TGA = 56. Я их зарезервировал и обыкновенные опкоды разместил в свободных номерах 0…18, чтобы не конфликтовать с биологией.

Вот в принципе и весь интерпретатор. На рабочем варианте получается около 80 строк — даже короче, чем эта секция статьи. Самое интересное начинается дальше.

Квайн: программа знает, как себя копировать

Теперь главное. Квайн в обычном программировании — это программа, которая печатает свой собственный исходный код. Классический пример на C занимает страницу и выглядит как смесь экранирования и магии.

Наш квайн делает то же, только в ДНК-форме. Печатает не символами через printf, а нуклеотидами через WRITE. И не текст исходника, а собственный геном — букву за буквой, прямо из памяти.

В живой клетке аналог этого процесса — репликация ДНК. ДНК-полимераза проходит по матричной цепи и нуклеотид за нуклеотидом строит дочернюю. Главное, что ей нужно — доступ к собственной матрице, к самой себе. Без рефлексии (доступа программы к своему коду) самовоспроизведение в общем виде невозможно — это известный результат теоретической информатики, см. теорему о неподвижной точке Клини.

У нас рефлексия есть — она называется READAT. Эта инструкция берёт со стека индекс и кладёт обратно нуклеотид на этой позиции. То есть программа может читать свой собственный геном. С этим уже всё получается.

Псевдоассемблер

Вот что должна делать наша квайн-программа на псевдоассемблере:

START                        # точка входа

PUSH 0                       # положить 0 на стек
STORE 0                      # сохранить в vars[0] — это наш счётчик i

loop:
  LOAD 0                     # положить i на стек
  GENLEN                     # положить длину генома
  LT                         # 1 если i < len, иначе 0
  JZ end                     # если 0 — выходим из цикла

  LOAD 0                     # i
  READAT                     # genome[i]  — это нуклеотид как число 0..3
  WRITE                      # вывести в output

  LOAD 0
  PUSH 1
  ADD                        # i + 1
  STORE 0                    # сохранить обратно

  JMP loop

end:
  STOP

Если эту программу скомпилировать в кодоны, получится 25 кодонов, то есть 75 нуклеотидов.

Эта же программа в нуклеотидах

После сборки получается такая ДНК-последовательность:

ATGAACAAAATCAAAATAAAAATTAGCAGTCGAATAAAACAACACATAAAAAACAACACCATCAAAAGGACCTAA

Дизассемблер квайна. 25 кодонов = 75 нуклеотидов = вся программа целиком. ATG в начале — точка входа, TAA в конце — выход. Между ними — обычный цикл со счётчиком и инструкцией READAT, через которую программа читает свою же ДНК.

Это полный исходный код. Двадцать пять кодонов:

Покодонная разбивка с мнемониками — для любителей всё проверять
codon | нуклеотиды | мнемоника
─────────┼─────────────┼──────────────
   0    | ATG         | START
   1    | AAC         | PUSH
   2    | AAA         | (литерал 0)
   3    | ATC         | STORE
   4    | AAA         | (литерал 0)
   5    | ATA         | LOAD            ← начало loop
   6    | AAA         | (литерал 0)
   7    | ATT         | GENLEN
   8    | AGC         | LT
   9    | AGT         | JZ
  10    | CGA         | (литерал 24, адрес end)
  11    | ATA         | LOAD
  12    | AAA         | (литерал 0)
  13    | CAA         | READAT
  14    | CAC         | WRITE
  15    | ATA         | LOAD
  16    | AAA         | (литерал 0)
  17    | AAC         | PUSH
  18    | AAC         | (литерал 1)
  19    | ACC         | ADD
  20    | ATC         | STORE
  21    | AAA         | (литерал 0)
  22    | AGG         | JMP
  23    | ACC         | (литерал 5, адрес loop)
  24    | TAA         | STOP

Что важно: этот геном не «перезаписан вручную». В нашем коде есть простой ассемблер, который собирает программу из инструкций и возвращает строку нуклеотидов. То есть это настоящий код, скомпилированный для четверичной VM. VM в контексте нашего проекта это программа, которая исполняет другие программы. Конкретно у нас VM — это Рибосома.

Запуск: смотрим, как указатель (рибосома) шагает по цепи

Самое наглядное в работе с такой VM — это запустить её и увидеть, как точка-указатель ползёт по цепи нуклеотидов, шаг за шагом.

Вот как это выглядит в терминале (тут урезанная трассировка маленькой программы 2 + 3; PRINT):

старт — рибосома села на ATG
    5'-ATGAACAAGAACAATACCCAGTAA-3'
       ^^^                     
    pc=0  стек=[]  вывод=∅

исполнено: AAC (PUSH)
    5'-ATGAACAAGAACAATACCCAGTAA-3'
                ^^^            
    pc=3  стек=[2]  вывод=∅

исполнено: AAC (PUSH)
    5'-ATGAACAAGAACAATACCCAGTAA-3'
                      ^^^      
    pc=5  стек=[2, 3]  вывод=∅

исполнено: ACC (ADD)
    5'-ATGAACAAGAACAATACCCAGTAA-3'
                         ^^^   
    pc=6  стек=[5]  вывод=∅

5    ← это PRINT вывел
исполнено: CAG (PRINT)
    5'-ATGAACAAGAACAATACCCAGTAA-3'
                            ^^^
    pc=7  стек=[]  вывод=∅

стоп-кодон TAA — рибосома отделяется

С квайн таким же образом, только цикл проходит 75 раз и в вывод накапливается копия исходной цепи. После завершения проверяем:

mother   = build_quine()           # исходный геном
ribosome = Ribosome(mother)
daughter = ribosome.run()           # запустить до STOP

print(mother)
print(daughter)
print(str(mother) == str(daughter))

Вывод:

ATGAACAAAATCAAAATAAAAATTAGCAGTCGAATAAAACAACACATAAAAAACAACACCATCAAAAGGACCTAA
ATGAACAAAATCAAAATAAAAATTAGCAGTCGAATAAAACAACACATAAAAAACAACACCATCAAAAGGACCTAA
True

Цепи побитово идентичны. И это работает не потому, что мы захардкодили вывод — мы реально читаем нуклеотид за нуклеотидом из собственного генома через READAT и пишем в выходной буфер через WRITE. На каждом тике программа может сделать что-то ещё (мутировать саму себя, например) — мы просто не делаем.

Если запустить ту же дочернюю цепь как новый вход, получится точно такая же копия. Я погонял через три поколения — все три побитово равны:

поколение 1:  ATGAACAAAATCAAAATAAAAATTAGCAGTCGAATAAAACAACACATAAAAAACAACACCATCAAAAGGACCTAA
поколение 2:  ATGAACAAAATCAAAATAAAAATTAGCAGTCGAATAAAACAACACATAAAAAACAACACCATCAAAAGGACCTAA
поколение 3:  ATGAACAAAATCAAAATAAAAATTAGCAGTCGAATAAAACAACACATAAAAAACAACACCATCAAAAGGACCTAA

Наследственность работает. На таком крошечном примере уже видно главное свойство живого: способность производить точную копию себя, опираясь на инструкции, которые сами в себе и записаны.

То есть квайн из этой статьи — это первый кирпичик. На нём всё остальное стоит. Но без него и всё остальное было бы не нужно.

А что если поменять одну букву?

Раз уж мы получили работающую самокопирующуюся программу — самое естественное, что хочется сделать дальше, это её сломать. И посмотреть, насколько она вообще терпима к ошибкам.

В реальной ДНК такие ошибки происходят постоянно. Случайная замена одной буквы (точечная мутация) — это базовое событие, на котором стоит вся эволюция. Каждая клетка тела человека получает примерно 10 000 повреждений ДНК в день — большинство тут же чинится репарационными системами, но не всё.

Я написал маленький эксперимент: берём наш 75-нуклеотидный квайн и пробуем все возможные одиночные замены. Каждый из 75 нуклеотидов можно заменить на 3 другие буквы — итого 225 вариантов. Каждый вариант запускаем и смотрим, что получится.

Вот, например, что происходит при мутации финального стоп-кодона TAA в позиции 73:

кодон #24: TAA (STOP) → TCA (?)

мутированный геном:
  5'-ATGAACAAAATCAAAATAAAAATTAGCAGTCGAATAAAACAACACATAAAAAACAACACCATCAAAAGGACCTCA-3'

✗ результат: loop (превышен лимит шагов)

Рибосома доходит до конца генома, но TCA не определён как стоп-кодон — она просто его пропускает и продолжает крутиться в цикле, пока не упрётся в искусственный лимит шагов. У живой рибосомы такого «лимита шагов» нет — она будет крутиться, пока хватает ресурсов. Гипотетически до тепловой смерти Вселенной.

Запуск всех 225 вариантов даёт довольно драматичную статистику:

Ноль идеально нейтральных — потому что в нашем квайне нет ни одного «лишнего» нуклеотида. Все 75 что-то делают: опкоды, литералы, адреса переходов. Каждая буква работает.

Но самое интересное — посмотреть на «повреждённые» поближе. Из этих 72 случаев 35 имеют сходство с оригиналом 90% и выше. То есть квайн всё-таки скопировался — но скопировался вместе со своей мутацией. Дочь получила ту же мутированную ДНК. Это и есть наследственная мутация — копирование с ошибкой, которая передаётся следующему поколению.

В наших 35 мутантах нет ни одного, который улучшил бы исходную программу — все они либо нейтральны, либо чуть портят картину. Но если запустить эту схему миллион раз и добавить отбор (выживают только те, кто хорошо копируется в текущих условиях) — получится дарвиновская эволюция в чистом виде. Об этом будет третья статья серии.

А пока — короткая мораль из этих 153 «летальных» случаев. Один случайный байт не там — и линия мертва. Если бы реальная ДНК работала так же, любой космический луч или химическая ошибка убивали бы клетку. Но клетка как-то живёт. Дело в том, что у неё есть несколько систем репарации ДНК, которые ловят и исправляют большую часть повреждений ещё до репликации. Пять разных систем для разных типов поломок — MMR, BER, NER, NHEJ, HR. Без них наш квайн не дожил бы до второй итерации цикла. Об этих системах — в следующих частях серии. А прямо сейчас — короткая карта всего, что у меня уже реализовано в проекте.

Карта проекта

Прежде чем перейти к финалу, надо пояснить суть статьи. Иначе создаётся впечатление, что у нас простой квайн плюс пара мутаций — а на самом деле там более 15000 строк Python и 25+ биологических подсистем, каждая со своим научным основанием. Я их буду раскручивать в следующих статьях, но раз вы дочитали до сюда — вот честная карта проекта.

Исходные коды проекта опубликую на публичный гит, после завершения цикла статей.

Приближенная карта проекта

Ниже — таблицы того, что за этой статьёй сейчас работает. Это тизер для двух следующих частей, не выводы из этой. Если что-то из терминов непонятно — большая часть имеет короткое пояснение справа.

Молекулярный уровень

что реализовано

биологический аналог

Стек-машина рибосомы (то, что мы сегодня собрали)

translation: настоящая рибосома точно так же читает mRNA по 3 нуклеотида за раз и собирает из этого белок

Двойная спираль с антипараллельным комплементом

реальная ДНК: две цепи закручены в обратных направлениях, A пара T, C пара G

RNA polymerase как отдельный автомат

transcription у pol II: специальный фермент идёт по ДНК и собирает по ней mRNA-копию

Сплайсинг интронов перед трансляцией

spliceosome у эукариот: вырезает из mRNA не-кодирующие участки (intron), оставляя только то, что пойдёт в белок (exon)

Ассемблер кодонов с rRNA-регионом

tRNA + аминоацил-tRNA-синтетазы: набор адаптеров, переводящих 3-буквенный кодон в нужную аминокислоту

Эволюция и наследственность

что реализовано

что это значит

Точечные мутации, insertions, deletions

три классических типа повреждения ДНК: замена одной буквы, вставка лишней, удаление существующей. У нас управляется параметром mutation_rate

MMR / BER / NER / NHEJ / HR

пять реальных систем репарации ДНК. MMR ловит ошибки полимеразы, BER чинит окисленные основания, NER лечит UV-повреждения, NHEJ и HR заделывают двунитевые разрывы по разным стратегиям

Proofreading у polymerase

у настоящей полимеразы есть exo-домен, который умеет вырезать только что вставленную ошибочную букву прямо в процессе репликации, как опечатку в текстовом редакторе

Gene duplication

при копировании генома изредка целый участок дублируется. Один из главных эволюционных механизмов появления новых генов: копия может мутировать без потери исходной функции

Transposable elements

«прыгающие гены»: участки ДНК с особым маркером, способные с малой вероятностью самокопироваться в случайное место генома. У человека ~45% всей ДНК — это потомки таких прыжков

Horizontal gene transfer

клетка может «забрать» фрагмент ДНК у живого соседа. У бактерий — главный путь распространения новых признаков, в том числе устойчивости к антибиотикам

Гомологичная рекомбинация в реальном времени

две клетки обмениваются гомологичными участками генома. Это аналог crossing-over в мейозе у животных

Кроссинговер по границам кодонов

точка разреза при рекомбинации выровнена по 3 нуклеотидам, чтобы не сдвигалась рамка считывания

Клеточный цикл и регуляция

что реализовано

биологический смысл

Фазы G1 / S / G2 / M / G0

стандартный клеточный цикл: подготовка → синтез ДНК → проверка → митоз → возможный покой

Checkpoint G1/S (Restriction point)

Rb / E2F / p21: точка принятия решения «можно ли вообще начинать делиться?». Проверяется размер клетки, наличие питания, отсутствие повреждений ДНК

Checkpoint Intra-S

ATR / CHK1: следит за самим процессом репликации, останавливает при нехватке нуклеотидов или замеченных повреждениях

Checkpoint G2/M

ATM / CHK2 / p53: повторная проверка перед митозом — «вся ли ДНК скопирована, нет ли двунитевых разрывов?». Главный страж генома

SAC (Spindle Assembly Checkpoint)

Mad2 / BubR1: не разрешает митозу завершиться, пока все хромосомы не закреплены за веретеном деления

Quiescence (G0)

состояние покоя: клетка жива, активно метаболизирует, но не делится. Большинство нейронов и кардиомиоцитов так и живут всю жизнь

AMPK (энергетический сенсор)

замечает падение уровня ATP. При энергетическом кризисе блокирует деление и запускает autophagy — расщепление собственных белков ради энергии

mTOR (рост и синтез белка)

сенсор питательных веществ. При нехватке аминокислот тормозит трансляцию, экономит ресурс на чёрный день

HIF-1α (гипоксия)

реагирует на низкий уровень кислорода. Переключает клетку с эффективного OXPHOS на менее эффективный glycolysis, но способный работать без O₂

UPR (ER-стресс)

при накоплении неправильно свёрнутых (misfolded) белков останавливает трансляцию. Если стресс хронический — запускает apoptosis

Apoptosis (запрограммированная смерть)

контролируемый сценарий смерти клетки. Срабатывает по возрасту, по превышению порога повреждений или по сигналу извне

Hayflick limit + telomere clock

теломеры — защитные «колпачки» на концах хромосом — укорачиваются при каждой репликации. Когда заканчиваются, клетка уходит в senescence (старение без деления)

Многоклеточность

что реализовано

роль в ткани

Stem cell hierarchy (15 уровней commitment)

лестница от тотипотентной стволовой клетки до terminally differentiated (окончательно специализированной). Каждый шаг — потеря возможностей в обмен на специализацию

Asymmetric division

одно деление стволовой клетки даёт одну новую stem (для поддержания резерва) и одну progenitor (которая дальше специализируется)

Niche-зависимость stemness

стволовая клетка сохраняет статус, только пока чувствует поддерживающие сигналы от ниши. Уйдёт из ниши — теряет stemness и становится обычной

Notch–Delta lateral inhibition

контактный механизм: stem-клетка через мембранные лиганды «сообщает» соседям что она stem, чтобы они не претендовали на эту роль. Предотвращает кластеры stem-клеток

Эпигенетическая память

состояние специальных ячеек памяти переживает деление и наследуется потомкам без изменения самой ДНК. Биологический аналог — паттерны метилирования

Дифференциация по морфогену

один и тот же геном даёт разное поведение в зависимости от концентрации сигнальной молекулы в данной точке пространства. Так формируются ткани и органы у эмбриона

Extracellular matrix (ECM)

белковое поле вокруг клеток (collagen, laminin, fibronectin) — то, за что клетки физически цепляются и что держит ткань вместе

Integrins + anoikis

интегрины — белки-якоря на мембране клетки, цепляющиеся за ECM. Если клетка не прикреплена — запускается apoptosis. Защита организма от метастазирования

Paracrine signals (mitogen, morphogen, death)

растворимые молекулы, диффундирующие от клетки-источника. Концентрация падает по закону 1/d² от расстояния

Mechanotransduction (YAP/TAZ-like)

клетка чувствует физическое давление соседей. При тесном контакте перестаёт делиться — это contact inhibition

Ткань и среда

что реализовано

как именно

3D-пространство с непрерывными координатами

каждая клетка имеет (x, y, z); поиск соседей через spatial hash за O(1) среднее

Метаболизм: пул нуклеотидов + ATP

конечные ресурсы внутри клетки. Клетки конкурируют за них; голод останавливает рибосому

Глобальный пул пищи

среда раздаёт еду равномерно на всех живых. Чем больше популяция, тем меньше каждой клетке. Естественный пресс на размножение

Гипоксия в плотном ядре

концентрация O₂ падает экспоненциально с плотностью соседей: O₂ = exp(−density × k). В реальной ткани без сосудов всё работает примерно так

Пульсирующий морфоген + дрейф центра

сигнальное поле меняется во времени. Заставляет клетки перестраиваться, не давая ткани застыть в одной форме

Apoptosis-волны

когда большая когорта клеток одного возраста доходит до Hayflick limit, они умирают почти одновременно. После такой волны популяция резко обновляется

Что мы только что увидели

Если смотреть на наш квайн с расстояния — это просто 75 букв в строке. Если приглядеться, эти 75 букв содержат в себе цикл со счётчиком, проверку условия, обращение к собственной памяти и аккуратный выход. Любой студент-первокурсник напишет такое на питоне за пять минут — здесь интересно не что написано, а где написано. Те же самые буквы (A, T, G, C), которые вы найдёте в учебнике по молекулярной биологии, в нашей системе работают как настоящий байткод. И программа на нём — настоящая, исполняемая, копирующая саму себя.

Главный фокус прячется в одной инструкции — READAT. Программа читает свой собственный код, нуклеотид за нуклеотидом, и сама же его выводит наружу. Это и есть та самая рефлексия, без которой самокопирование в общем виде математически невозможно (см. теорему Клини о неподвижной точке). В живой клетке роль READAT играет ДНК-полимераза, которая шагает по матричной цепи и считывает её букву за буквой. Один и тот же принцип на двух уровнях абстракции: что виртуальная машина в питоне, что белковая машина в клетке — обе делают по сути одно и то же: читают свой шаблон и копируют его наружу.

Что мы получили в итоге — 908 шагов виртуальной рибосомы, 75 скопированных нуклеотидов, побитовое совпадение материнской и дочерней цепи. Через три поколения — то же самое: ровно те же 75 букв в той же последовательности. Это не “жизнь” в биологическом смысле, а минимальная вычислительная игрушка, показывающая один из центральных мотивов живых систем: наследуемое самокопирование через чтение собственного шаблона. До настоящей клетки отсюда огромная дистанция — метаболизм, мембрана, регуляция, ошибки копирования, отбор и среда. Но как первый вычислительный кирпичик это уже интересно.

А вот дальше — самое интересное. Стоит добавить к процессу копирования крошечную ошибку (мутацию), и появится эволюция: одни линии будут вымирать, другие случайно получат преимущество. Дайте клетке метаболизм и пространство вокруг — и она начнёт делиться, образуя трёхмерную ткань с дифференциацией.

Об этом — следующие части серии, где мы дойдем до того, что научим нашу молекулярную колонию выполнять математические операции и может даже отвечать текстом на наши вопросы. Также, расскажу о том как уперся в вычислительные мощности. На Mac mini M4 Pro симуляция бежала отлично, пока я не дошёл до колоний на ±20 000 клеток. CPU+GPU стал узким местом — каждый тик на M4 при такой популяции занимал секунды, а потом минуты, но биология требует тысячи тиков для интересной динамики. На таких масштабах сидеть и смотреть на UI становилось утомительно. Пока намекну на ключевые слова: ROCm + GPU-kernels.

P.S. Я не претендую на открытие новых научных направлений. Сейчас моя основная цель — разобраться, как устроены живые организмы изнутри, и переложить это в виде программируемых моделей. Биология описана в учебниках формулами и схемами — а мне хочется уметь её запускать.

Комментарии (3)


  1. ARad
    26.04.2026 16:51

    Пока это обычная стек машина, связи с ДНК или рибосомой не наблюдаю.


    1. Qulisun Автор
      26.04.2026 16:51

      Спасибо!

      Формально, да, это стек-машина с 4 буквенным алфавитом. Я намеренно не пихал в первую часть всю биологию — иначе пост бы вышел на много текста. Но связь с ДНК все же есть: 

      • алфавит из 4 букв и триплетный кодон не выдуманы, они физически зафиксированы в реальной ДНК (A/T две водородные связи, G/C три)

      • START/STOP — реальные биологические кодоны. READAT как физическое сканирование матрицы рибосомой.

      • квайн моделирует ту самую задачу, которую решает живая клетка, т.е. самокопирование.

      В следующей статье, я постараюсь уже углубиться в апоптоз, теломеры, метаболизм, паракринность, эволюцию в популяции и другое, там связь будет точно видна :)


      1. Akina
        26.04.2026 16:51

        квайн моделирует ту самую задачу, которую решает живая клетка, т.е. самокопирование.

        Вот уж тут точно аналогии нет. Вообще. Это не "напишите программу, которая сама себя распечатает", где в принципе упущено существование интерпретатора. В живой клетке проблему создания копии генетического материала решает совершенно иная система - та, которая воссоздаёт вторую цепь ДНК по имеющейся одиночной, и, как финальный эффект, дублирует хромосомы. То есть в геноме есть информация о создании комплекса, выполняющего эту функцию, но комплекс (почти) универсален, и после создания ему формально пофиг, что именно копировать, лишь бы это была одиночная цепь ДНК (конечно, с определёнными особенностями - должно быть то, что будет опознано как место для "схватить и начать обрабатывать").

        А рибосомы синтезируют не ДНК, несущую генетическую информацию, а белки. Но в том числе и белки, необходимые как для формирования вышеупомянутого комплекса, так и для создания иных веществ (небелковой природы), необходимых для/в процессе этого формирования и/или последующей его работы.

        Т.е. в вашей модели один ген должен воссоздавать вашу стек-машину, второй - набор опкодов и действий по ним, третий - обеспечивать сборку их в один рабочий комплекс, и т.п.