Зачем я вообще полез в ДНК
Первое — это интерес, как двигатель узнать что-то новое. Это также напрямую связано с развитием ИИ, где я принимаю непосредственное участие.
И второе — если посмотреть, как реальная клетка читает мРНК и собирает белки, это похоже на исполнение байткода:
-
Рибосома движется по мРНК в одном направлении и читает её триплетами — по три нуклеотида за раз. Для программиста это похоже на последовательный разбор потока, где каждый следующий токен имеет фиксированную длину.
-
Старт-кодон в живой клетке рибосома читает мРНК, где стартовый кодон записывается как 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 |
три классических типа повреждения ДНК: замена одной буквы, вставка лишней, удаление существующей. У нас управляется параметром |
|
MMR / BER / NER / NHEJ / HR |
пять реальных систем репарации ДНК. MMR ловит ошибки полимеразы, BER чинит окисленные основания, NER лечит UV-повреждения, NHEJ и HR заделывают двунитевые разрывы по разным стратегиям |
|
Proofreading у polymerase |
у настоящей полимеразы есть |
|
Gene duplication |
при копировании генома изредка целый участок дублируется. Один из главных эволюционных механизмов появления новых генов: копия может мутировать без потери исходной функции |
|
Transposable elements |
«прыгающие гены»: участки ДНК с особым маркером, способные с малой вероятностью самокопироваться в случайное место генома. У человека ~45% всей ДНК — это потомки таких прыжков |
|
Horizontal gene transfer |
клетка может «забрать» фрагмент ДНК у живого соседа. У бактерий — главный путь распространения новых признаков, в том числе устойчивости к антибиотикам |
|
Гомологичная рекомбинация в реальном времени |
две клетки обмениваются гомологичными участками генома. Это аналог |
|
Кроссинговер по границам кодонов |
точка разреза при рекомбинации выровнена по 3 нуклеотидам, чтобы не сдвигалась рамка считывания |
Клеточный цикл и регуляция
|
что реализовано |
биологический смысл |
|---|---|
|
Фазы G1 / S / G2 / M / G0 |
стандартный клеточный цикл: подготовка → синтез ДНК → проверка → митоз → возможный покой |
|
Checkpoint G1/S (Restriction point) |
|
|
Checkpoint Intra-S |
|
|
Checkpoint G2/M |
|
|
SAC (Spindle Assembly Checkpoint) |
|
|
Quiescence (G0) |
состояние покоя: клетка жива, активно метаболизирует, но не делится. Большинство нейронов и кардиомиоцитов так и живут всю жизнь |
|
AMPK (энергетический сенсор) |
замечает падение уровня ATP. При энергетическом кризисе блокирует деление и запускает autophagy — расщепление собственных белков ради энергии |
|
mTOR (рост и синтез белка) |
сенсор питательных веществ. При нехватке аминокислот тормозит трансляцию, экономит ресурс на чёрный день |
|
HIF-1α (гипоксия) |
реагирует на низкий уровень кислорода. Переключает клетку с эффективного |
|
UPR (ER-стресс) |
при накоплении неправильно свёрнутых (misfolded) белков останавливает трансляцию. Если стресс хронический — запускает apoptosis |
|
Apoptosis (запрограммированная смерть) |
контролируемый сценарий смерти клетки. Срабатывает по возрасту, по превышению порога повреждений или по сигналу извне |
|
Hayflick limit + telomere clock |
теломеры — защитные «колпачки» на концах хромосом — укорачиваются при каждой репликации. Когда заканчиваются, клетка уходит в |
Многоклеточность
|
что реализовано |
роль в ткани |
|---|---|
|
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) |
клетка чувствует физическое давление соседей. При тесном контакте перестаёт делиться — это |
Ткань и среда
|
что реализовано |
как именно |
|---|---|
|
3D-пространство с непрерывными координатами |
каждая клетка имеет (x, y, z); поиск соседей через |
|
Метаболизм: пул нуклеотидов + 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. Я не претендую на открытие новых научных направлений. Сейчас моя основная цель — разобраться, как устроены живые организмы изнутри, и переложить это в виде программируемых моделей. Биология описана в учебниках формулами и схемами — а мне хочется уметь её запускать.
Автор: Qulisun
