Предыстория

Очень давно не брал в руки шашек и почти ничего не писал на хабре. В основном активность в последние годы здесь ограничивается простановкой плюсов и минусов под статьями и комментариями. Поэтому коротко о себе. По образу жизни и работы я — физик‑теоретик, доцент кафедры теоретической же физики в Пермском классическом университете, и немного эфирно‑паяльный радиолюбитель. Поскольку физик‑теоретик — понятие очень абстрактное, размытое и растяжимое, заниматься по научной тематике доводилось различными задачами. Гидродинамика, нелинейные процессы, спектральный и вейвлет‑анализ, немного биомедицинских работ. Это всё немного пылится в прошлом, а сейчас основной темой стали спиновая динамика и всяческие волны намагниченности в твёрдых телах.

Давняя история магнетизма, ЯМР/ЭПР и спиновой динамики у нас здесь богатая (современность вот, увы, не очень). О ней напоминают, в частности, сборники «Радиоспектроскопия», выходившие в университете чуть реже, чем раз в год, начиная с 1962. И вот, при пролистывании одного из этих сборников, а именно за 1976 год, попалась на глаза статья наших уважаемых профессоров — Евгения Карловича Хеннера и его наставника, Ивана Григорьевича Шапошникова, — с неярким, но ёмким названием «Об одном численном методе статистической теории магнитного резонанса в твердых телах». Казалось бы, обычная заметка на 8 страниц об идее и особенностях реализации и тестирования расчётного метода. Но почти половину её объёма составил полный листинг для одной из реализаций языка АЛГОЛ-60, который в Перми, скорее всего, крутился на М-220 в нашем вычислительном центре (да простят меня мехматовцы, если в эту строку вкралась фактическая ошибка). Как это выглядит на бумаге, показано на фотографии. Для экономии места ликвидировано всё возможное форматирование, но, хвала точкам с запятыми и коротким комментариям, код вполне читаем и более‑менее структурирован.

Свитки программистов прошлого. Кусочек листинга как он есть. Карандашные пометки поверх уже мои.
Свитки программистов прошлого. Кусочек листинга как он есть. Карандашные пометки поверх уже мои.

Немножко квантовой магии

Коротко, что же этот «один численный метод» делает. Иван Григорьевич в 1994 г. собственноручно составил список из ста, на тот момент, начиная с 1936 г., своих публикаций, к каждой из которых дал краткую и ёмкую аннотацию. Оригинал списка хранится у нас на кафедре среди других артефактов. Да, было время, когда не количеством публикаций и цитируемостью мерялись учёные. Конкретно к описываемой здесь статье аннотация такова:

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

Что ж, давайте пройдёмся по пунктам.

Моделирование магнитного резонанса в магниторазведëнном кристалле. Или, как принято говорить в современных текстах, в магниторазбавленном. Это обширный пласт материалов, хотя в большинстве статей упоминается только арсенид галлия, допированный марганцем, Mnx: Ga(1-x) As, где доля Mn x — процентов 5–8. Это вроде и полупроводник, но очень хорошо чувствующий магнитное поле. Именно это и интересно. Или, прости господи, графен с сидящими на нём атомами чего‑то более магнитного, чем углерод. Тут получается примерно то же самое, что и с арсенидом галлия, с поправкой на необычное поведение электронов.

Примерно так может выглядеть сигнал в эксперименте. Только колебания происходят намного быстрее - десятки и сотни МГц и даже выше. Типичное время спада - десятки мкс
Примерно так может выглядеть сигнал в эксперименте. Только колебания происходят намного быстрее - десятки и сотни МГц и даже выше. Типичное время спада - десятки мкс

Как нам залезть в этот материал и узнать его структуру и энергетический спектр? Много способов. Один из них — это магнитный резонанс. Помещаем образец в сильное постоянное поле, и каждый магнитный центр начинает крутиться со своей собственной частотой, определяемой его окружением. «Стукаем» импульсом перпендикулярного поля или их серией, магнитные моменты частично «падают набок» и начинают возвращаться к исходной прецессии. Тут‑то мы их и ловим — записывается сигнал намагниченности во времени, откуда находится спектр магнитного резонанса и сопутствующие характеристики. Выглядят они примерно как на картинке. Вообще, спектр можно и напрямую измерить, просто плавно меняя параметры полей и регистрируя поглощение энергии образцом. Исторически это был первый метод, но он, как минимум, менее точный и более долгий.

И всё бы ничего, и даже теорию возмущений для свойств материала можно написать, но случайное распределение магнитных центров — в данном случае примеси марганца — всё портит. Где‑то его много, где‑то поменьше получилось при синтезе, а от окружения плывёт и спектр энергии, и возможные состояния. Вместо упорядоченной структуры у нас месиво кластеров из 2, 3, 4... и т. д. магнитных центров, и все кластеры имеют случайный размер и форму. В результате уровни энергии всего кристалла размазаны в почти что энергетические зоны, а резонансы совершенно не острые. Именно эти данные и генерирует алгоритм, моделируя спектр сигнала от разбавленного материала.

Как это происходит? Создаётся случайный кластер атомов, рассчитываются его свойства, записываются. Затем ещё кластер, ещё, ещё и пока не наберётся приемлемая статистика, и наконец вычисляется осреднëнный спектр магнитного резонанса образца, составленного из этих самых кластеров в предположении, что друг друга они не замечают. Для низких концентраций примеси это отлично работает. Весь расчёт выполняется из первых принципов — честно строится оператор (матрица) гамильтониана, итерационно, методом вращений, вычисляются её собственные значения — уровни энергии кластера, и вероятности переходов между ними под действием поперечного поля. Это весьма ресурсозатратно — если частицы имеют спин 1/2, и кластер содержит N частиц, то приходится строить и диагонализовать матрицу размерами 2N×2N, которая вдобавок ещё и не сильно разрежена. Но в результате программа отрабатывает крайне быстро, 100 случайных конфигураций с 8 частицами от силы за полминуты.

От древнего языка... к древнему языку

Шёл август 2018 года. Время было тихое. Очередная заявка на грант была подана и ждала подведения итогов, статья в ЖЭТФ принята, учебный год ещё не начался, отпуск не закончился, а другие глобальные задачи не очень-то клеились. Чем ещё заниматься учёному-преподавателю при таком раскладе, если не пить? Мгновенно возникло настойчивое желание вдохнуть жизнь в код, напечатанный на тот момент 42 (о, эта магия чисел) года назад. И программа медленно, оператор за оператором, начала воплощаться в окне Visual Studio.

Наверное, спросите, на каком же языке. Яблоко от яблони недалеко падает, а яблоня от яблока — тем более. На тот момент я уверенно работал только с FORTRAN-90, которому у нас долгое время учили на курсе численных методов, и, чего греха таить, научили! Игорь Геннадьевич Семакин гонял всех нещадно, особенно доставалось как раз теоретикам. Думаю, что не зря.

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

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

Вот оно, что получилось — под спойлером.

.f90
program NMR_rarefied
implicit none
real(8), dimension(:,:,:), allocatable :: dip, a1 ! dipolar interaction and additional
real(8), dimension(:,:), allocatable :: sx, sy, sz, sI, sp, sm, o, u, v, w, & ! spin matrices and additional
                                        x, y, z, r, & ! r = {x, y, z}, distances between magnetic ions (MI)
                                        J0, & ! exchange interactions
                                        ReH, ImH, b, q, q2, & ! Hamiltonian parts and additional
                                        d0, d1 ! for Kronecker product procedure
real(8), dimension(:), allocatable :: x1, y1, z1, & ! coordinates of MI
                                      RT2, RT3, RT4, RT1, sum_sqr, & ! for rotation method
                                      histp, hist ! partial and averaged histogram
logical, dimension(:), allocatable :: mask1
real(8), dimension(1:3) :: e1, e2, e3, d ! lattice basis
real(8), dimension(1) :: i11, k11
real(8) S, eps, & ! spin, precision
        alpha, beta, pd, & ! parameters of exchange (A*exp(-B*r)) and dipolar interaction (pd = (hbar*gamma)^2)
        res, & ! histogram resolution
        s1, s2, c1, c2, & ! rotation parameters
        m, e, c
integer N, N1, N2, N3, NN, Nres, & ! total number of MI and their concentration
        i, j, k, i1, j1, k1, t, m3, ms, l, r1, ires, & ! counters
        f, fn, & ! 2S+1, (2S+1)^N
        nh ! number of histogram bars
logical noconv
        
!----------------------------------------------------
! read and set the initial parameters
!----------------------------------------------------
open(1, file="init.txt", form="formatted", action="read")
read(1,*) eps, S, N, N1, N2, N3, nh, alpha, beta, pd, &
          e1, e2, e3, res, Nres
close(1)

f = 2*S + 1; fn = f**N
call RANDOM_SEED()

!----------------------------------------------------
! memory alloc
!----------------------------------------------------
allocate(sx(1:f,1:f)); allocate(sy(1:f,1:f)); allocate(sz(1:f,1:f))
allocate(sI(1:f,1:f)); allocate(sp(1:f,1:f)); allocate(sm(1:f,1:f))
allocate(o(1:f,1:f)); allocate(v(1:f,1:f)); allocate(w(1:f,1:f))
allocate(dip(1:6,1:N,1:N)); allocate(J0(1:N,1:N)); allocate(u(1:N,1:N))
allocate(x(1:N,1:N)); allocate(y(1:N,1:N)); allocate(z(1:N,1:N))
allocate(r(1:N,1:N))
allocate(x1(1:N)); allocate(y1(1:N)); allocate(z1(1:N))
allocate(ReH(1:fn,1:fn)); allocate(ImH(1:fn,1:fn))
allocate(b(1:fn,1:fn)); allocate(q(1:fn,1:fn)); allocate(q2(1:fn,1:fn))
allocate(mask1(1:fn))
allocate(a1(1:N,1:f,1:f))
allocate(RT1(1:fn)); allocate(RT2(1:fn))
allocate(RT3(1:fn)); allocate(RT4(1:fn))
allocate(sum_sqr(1:fn))
allocate(histp(1:nh)); allocate(hist(1:nh))

hist = 0.0d0

open(1, file="MI_coords.txt", form="formatted", action="write")
open(2,file="E.txt", form="formatted", action="write")

!----------------------------------------------------
! spin matrices initialization
!----------------------------------------------------
sx = 0.0d0; sy = 0.0d0; sz = 0.0d0; sp = 0.0d0; sm = 0.0d0; sI = 0.0d0
do i = 1, f, 1
  sI(i,i) = 1.0d0
  sz(i,i) = S - (i-1)
  if(i <= f-1) sp(i,i+1) = dsqrt(dabs(S*(S+1.0d0) - real(i*(i+1))))
enddo
sm = transpose(sp)
sx = 0.5d0*(sp + sm)
sy = 0.5d0*(sm - sp)

!----------------------------------------------------
ires = 1
REALIZATION: do while(ires <= Nres)
!----------------------------------------------------
! generation of MI distribution in lattice
!----------------------------------------------------
  x = 0.0d0; y = 0.0d0; z = 0.0d0; r = 0.0d0
  COORDS: do
  ! MI coordinates
    do i = 1, N, 1
      call RANDOM_NUMBER(d)
      d(1) = floor(d(1)*N1); d(2) = floor(d(2)*N2); d(3) = floor(d(3)*N3)
      x1(i) = d(1)*e1(1) + d(2)*e2(1) + d(3)*e3(1)
      y1(i) = d(1)*e1(2) + d(2)*e2(2) + d(3)*e3(2)
      z1(i) = d(1)*e1(3) + d(2)*e2(3) + d(3)*e3(3)
    enddo
  
  ! MI distances
    do i = 1, N, 1
      do j = 1, N, 1
        if(i /= j)then
          x(i,j) = x1(i) - x1(j)
          y(i,j) = y1(i) - y1(j)
          z(i,j) = z1(i) - z1(j)
          ! full interaction
          r(i,j) = dsqrt( x(i,j)**2 + y(i,j)**2 + z(i,j)**2 )
        endif
      enddo
      r(i,i) = 1.0d0
    enddo

    if(minval(r) > 0.5d0)exit
  enddo COORDS

  ! write MI coordinated
  write(1,'(A,I8)') "Realization ", ires
  do i = 1, N, 1
    write(1,'(I6, 3E20.6E3)') i, x1(i), y1(i), z1(i)
  enddo
  write(1,*)
  
!----------------------------------------------------
! pair interaction evaluation
!----------------------------------------------------
  ! isotropic part
  J0 = alpha*dexp(-beta*r)
  dip(1,:,:) = J0 + pd * (1.0d0 - 3.0d0*x**2 / r**2) / r**3
  dip(2,:,:) = J0 + pd * (1.0d0 - 3.0d0*y**2 / r**2) / r**3
  dip(3,:,:) = J0 + pd * (1.0d0 - 3.0d0*z**2 / r**2) / r**3
  ! anisotropic part
  dip(4,:,:) = -3.0d0*pd*x*y / r**5
  dip(5,:,:) = -3.0d0*pd*x*z / r**5
  dip(6,:,:) = -3.0d0*pd*y*z / r**5

!----------------------------------------------------
! real part of the Hamiltonian matrix
!----------------------------------------------------
  o = sz; call p78()
  ReH = b
  u = dip(1,:,:); v = sx; w = sx; call p56()
  u = dip(2,:,:); v = sy; w = -sy; call p56()
  u = dip(3,:,:); v = sz; w = sz; call p56()
  u = dip(5,:,:); v = sx; w = sz; call p56()
  q = ReH; ReH = 0.0d0

!----------------------------------------------------
! imaginary part of the Hamiltonian matrix
!----------------------------------------------------
  u = dip(4,:,:); v = sx; w = sy; call p56()
  u = dip(6,:,:); v = sy; w = sz; call p56()
  ImH = ReH; ReH = q

!----------------------------------------------------
! Diagonalization of the Hamiltonian matrix
!----------------------------------------------------
  t = 0; q = 0.0d0; q2 = 0.0d0
  do i = 1, fn, 1
    q(i,i) = 1.0d0
  enddo

  ! sum of squares by strings
  sum_sqr = 0.0d0
  do i = 1, fn, 1
    do j = 1, fn, 1
      if(i /= j) sum_sqr(i) = sum_sqr(i) + (ReH(i,j)**2 + ImH(i,j)**2)
    enddo
  enddo
  noconv = .false.
  
  ! rotation iterations
  ROTATION: do
    i11 = maxloc(sum_sqr); i1 = i11(1)
    mask1 = .true.; mask1(i1) = .false.
    k11 = maxloc(ReH(i1,:)**2 + ImH(i1,:)**2, mask=mask1); k1 = k11(1)
    m = maxval(ReH(i1,:)**2 + ImH(i1,:)**2, mask=mask1)
  
    if(m < eps)exit 
    if(t == 100000)then
      noconv = .true.
      exit
    endif

    mask1(k1) = .false.
    sum_sqr = sum_sqr - sum(ReH(:,i1)**2 + ImH(:,i1)**2, mask=mask1)
    
    i = i1; k = k1
  
    ! rotation parameters
    e = dabs(ReH(i,i) - ReH(k,k)) / dsqrt( (ReH(i,i) - ReH(k,k))**2 + 4.0d0*(ReH(i,k)**2 + ImH(i,k)**2) )
    s1 = dsqrt( 0.5d0*(1.0d0 - e) ); c1 = dsqrt( 0.5d0*(1.0d0 + e) )
    if(ReH(i,i) /= ReH(k,k)) s1 = sign(s1, ReH(i,i) - ReH(k,k))
    if(ReH(i,k) /= 0.0d0) s1 = sign(s1, ReH(i,k))
  
    s2 = ImH(i,k) / dsqrt(ReH(i,k)**2 + ImH(i,k)**2)
    if(ReH(i,k) /= 0.0d0) s2 = sign(s2, ReH(i,k))
  
    c2 = s1 * dabs(ReH(i,k)) / dsqrt(ReH(i,k)**2 + ImH(i,k)**2)
    s1 = s1*s2
  
    ! make the rotations
    RT1 = c1*q(:,i) + c2*q(:,k) + s1*q2(:,k)
    RT2 = -c2*q(:,i) + c1*q(:,k) + s1*q2(:,i)
    RT3 = -s1*q(:,k) + c1*q2(:,i) + c2*q2(:,k)
    RT4 = -s1*q(:,i) - c2*q2(:,i) + c1*q2(:,k)
    
    q(:,i) = RT1; q(:,k) = RT2; q2(:,i) = RT3; q2(:,k) = RT4

    RT1 = c1*ReH(:,i) + c2*ReH(:,k) + s1*ImH(:,k)
    RT2 = -c2*ReH(:,i) + c1*ReH(:,k) + s1*ImH(:,i)
    RT3 = -s1*ReH(:,k) + c1*ImH(:,i) + c2*ImH(:,k)
    RT4 = -s1*ReH(:,i) - c2*ImH(:,i) + c1*ImH(:,k)

    ReH(:,i) = RT1; ReH(:,k) = RT2; ImH(:,i) = RT3; ImH(:,k) = RT4

    RT1 = c1*ReH(i,:) + c2*ReH(k,:) - s1*ImH(k,:)
    RT2 = -c2*ReH(i,:) + c1*ReH(k,:) - s1*ImH(i,:)
    RT3 = c1*ImH(i,:) + c2*ImH(k,:) + s1*ReH(k,:)
    RT4 = -c2*ImH(i,:) + c1*ImH(k,:) + s1*ReH(i,:)

    ReH(i,:) = RT1; ReH(k,:) = RT2; ImH(i,:) = RT3; ImH(k,:) = RT4
    
    t = t+1
  
    mask1 = .true.; mask1(i1) = .false.; mask1(k1) = .false.
    sum_sqr = sum_sqr + sum(ReH(:,i1)**2 + ImH(:,i1)**2, mask = mask1) &
              + sum(ReH(:,k1)**2 + ImH(:,k1)**2, mask = mask1)
   
    mask1 = .true.; mask1(i1) = .false.
    sum_sqr(i1) = sum(ReH(i1,:)**2 + ImH(i1,:)**2, mask = mask1)
  
    mask1 = .true.; mask1(k1) = .false.
    sum_sqr(k1) = sum(ReH(k1,:)**2 + ImH(k1,:)**2, mask = mask1)
  enddo ROTATION

!----------------------------------------------------
  if(noconv)then
    print *, "Realization does not converge"
  else
    print *,ires, ", Rotation finished at ", t, " iterations"
!----------------------------------------------------
! eigenvalues
!----------------------------------------------------
    do i = 1, fn, 1
      RT1(i) = ReH(i,i)
    enddo

    ! write energy
    write(2,'(A,I8)') "Realization ", ires
    do i = 1, fn, 1
      write(2,'(I6,E20.6E3)') i, RT1(i)
    enddo
    write(2,*)

!----------------------------------------------------
! evaluate probabilities and histogram amplitudes
!----------------------------------------------------
    histp = 0.0d0
    o = sx

    call p78()

    ReH = matmul(b, q); ImH = matmul(b, q2)
    d1 = transpose(q); b = transpose(q2)

    q = matmul(d1, ReH) + matmul(b, ImH)
    q2 = matmul(d1, ImH) - matmul(b, ReH)

    do i = 1, fn, 1
      do j = 1, fn, 1
        c = q(i,j)**2 + q2(i,j)**2
        k = floor(1.0d0 + dabs(RT1(i) - RT1(j)) / res)
        if(k <= nh) histp(k) = histp(k) + c
      enddo
    enddo

    hist = hist + histp
    ires = ires + 1  
  endif
enddo REALIZATION
!----------------------------------------------------

open(3,file="hist.txt", form="formatted", action="write")
do i = 1, nh, 1
  write(3,'(2E20.6E3)') i*res, hist(i) / Nres
enddo
close(1); close(2); close(3)

print *,"Histogram is written at hist.txt"

!----------------------------------------------------
! memory clear
!----------------------------------------------------
deallocate(sx); deallocate(sy); deallocate(sz); deallocate(sI)
deallocate(o); deallocate(v); deallocate(w)
deallocate(ReH); deallocate(ImH); deallocate(b); deallocate(q); deallocate(q2)
deallocate(mask1); deallocate(a1)
deallocate(RT1); deallocate(RT2); deallocate(RT3); deallocate(RT4); deallocate(sum_sqr)
deallocate(histp); deallocate(hist)
deallocate(dip); deallocate(J0); deallocate(sp); deallocate(sm)
deallocate(x1); deallocate(y1); deallocate(z1)
deallocate(x); deallocate(y); deallocate(z); deallocate(r); deallocate(d1)

contains
!----------------------------------------------------
! Kronecker product of one spin and N-1 identity
!----------------------------------------------------
subroutine p78()
  b = 0.0d0

  do k = 1, N, 1
    a1(k,:,:) = sI
  enddo
  
  do k = 1, N, 1
    a1(k,:,:) = o
    call p34()
    a1(k,:,:) = sI
    b = b + d1
  enddo
end subroutine p78

!----------------------------------------------------
! Kronecker product of two spin and N-2 identity
!----------------------------------------------------
subroutine p56()
  do k = 1, N, 1
    do t = 1, N, 1
      if(k /= t)then
        a1(k,:,:) = v; a1(t,:,:) = w
      endif
      
      call p34()
            
      a1(k,:,:) = sI; a1(t,:,:) = sI
      ReH = ReH + u(k,t)*d1
    enddo
  enddo
end subroutine p56

!----------------------------------------------------
! Kronecker product of N matrices
!----------------------------------------------------
subroutine p34()
  if(allocated(d0)) deallocate(d0)
  if(allocated(d1)) deallocate(d1)
  
  allocate(d0(1:f,1:f)); d0 = 0.0d0
  allocate(d1(1:f**2,1:f**2)); d1 = 0.0d0
  
  d0 = a1(1,:,:)
  
  do m3 = 2, N, 1
    NN = f**(m3-1)
    do i = 1, NN, 1
      do j = 1, NN, 1
        do ms = 1, f, 1
          do l = 1, f, 1
            r1 = (i-1)*f + ms; i1 = (j-1)*f + l
            d1(r1,i1) = d0(i,j)*a1(m3,ms,l)
          enddo
        enddo
      enddo
    enddo
    
    if(m3 < N)then
      deallocate(d0); allocate(d0(1:f**m3,1:f**m3)); d0 = 0.0d0
      d0 = d1
      deallocate(d1); allocate(d1(1:f**(m3+1),1:f**(m3+1))); d1 = 0.0d0
    endif
    
  enddo
  deallocate(d0)
end subroutine p34

end program NMR_rarefied

Простите, без подсветки, хабраредактор всё-таки современен

Пройдёт ещё лет десять, и уже этот фортрановский файл можно будет тоже записывать в археологию. А может, уже пора. Но тем не менее, компилируем, запускаем:

Оно живое!
Оно живое!

И строим из выведенной таблички вполне приличный модельный спектр:

Резонансный пик вблизи единичной безразмерной частоты с мелкой рябью вокруг
Резонансный пик вблизи единичной безразмерной частоты с мелкой рябью вокруг

Итоги

Не надо конечно думать, что эта именно программа активно используется. Хотя и правда, мы поразбирались с её результатами и попытались адаптировать для конкретных задач, не без помощи вышеупомянутого автора Е.К. Хеннера. Программа жива, работоспособна и действительно позволяет симулировать магниторезонансные эксперименты. А размер моделируемого кластера частиц на каждой случайной реализации упирается только в объём доступной для размещения массивов памяти.

Для массовых рабочих станций, которыми мы сегодня у себя пользуемся, при 16 Гб RAM это где‑то в районе 12–14 частиц со спином 1/2, в зависимости от загрузки системы другими процессами. Немного, но если накопить статистику по реализациям, получается вполне достоверно. Таких работ относительно немного, но наша группа не одна на этом поприще, к счастью, есть с кем сравниваться в плане разработки и реализации методов. Есть, конечно, подходы и пакеты куда более серьёзные. Например, несравненный Spinach, на котором можно замоделировать, похоже, вообще всё что угодно, вплоть до полноразмерных белковых структур. Если, конечно, есть под рукой суперкомпьютер с GPU — гильбертово пространство квантовой системы от выбора алгоритма моделирования меньше не становится.

Сегодня этот же, по сути, метод частично живёт в виде кода на Python с NumPy и LAPACK, и потихоньку плодит с нашей помощью статьи и дипломы студентов. Чуть более гибко, чуть более просто писать, не думая о внутренней сущности отдельных команд и просто зная, что раз написано, например,np.kron— значит это точно отлаженное и надёжное кронекеровское перемножение матриц, а огромный кусок с методом вращений заменён на единственную строку с вызовомnp.linalg.eigh. По скорости же обе программы, если в Python сейчас оставить только функционал, соответствующий старому исходнику на АЛГОЛ, оказываются абсолютно сопоставимы.

А вот оживить программу, написанную полвека назад и сохранившуюся на пожелтевших страницах — по‑моему, это просто красиво!

Литература
Хеннер Е. К., Шапошников И. Г. Об одном численном методе статистической теории магнитного резонанса в твёрдых телах // Сб. Радиоспектроскопия – ПГУ, Пермь, 1976. – № 10. – С. 74–81.

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


  1. Worky
    26.05.2023 18:26
    +15

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


  1. quaer
    26.05.2023 18:26
    +1

    Сделайте для потомков полезную вещь: напишите конвертер с Fortran 90 на C или C++.

    Для Fortran 77 такое есть, а для Fortran 90 ещё нет.


    1. vadimr
      26.05.2023 18:26
      +1

      Это возможно только в не имеющем большого практического значения грубом приближении, так как в C и C++, в отличие от Фортрана, не определён порядок выполнения арифметических операций в выражении.


      1. quaer
        26.05.2023 18:26

        Разве? Результат математических операций зависит от порядка их написания в этих языках. Скобки также есть.


        1. vadimr
          26.05.2023 18:26

          В C и C++ порядок выполнения арифметических операций не определён, если алгебраически он не важен. Например, в выражении a+b+c компилятор Си вправе сначала сложить a и b, потом прибавить c, и также вправе сложить b и c, потом прибавить a. Компилятор Фортрана обязан складывать слева направо. Для плохо обусловленных численных задач (и при наличии побочных эффектов) это может быть важно.


          1. quaer
            26.05.2023 18:26

            В таких случаях охватывают скобками. Есть также флаги компилятора.


            1. vadimr
              26.05.2023 18:26

              Скобки в Си на это не влияют.


              1. quaer
                26.05.2023 18:26

                Operators can be regrouped according to the usual mathematical rules
                only where the operators really are associative or commutative.

                The additive operators + and - group left-to-right.

                A parenthesized expression is a primary expression whose type and value
                are identical to those of the enclosed expression. ... The parenthesized
                expression can be used in exactly the same contexts as those where the
                enclosed expression can be used, and with the same meaning, except as
                otherwise indicated.


                1. vadimr
                  26.05.2023 18:26
                  +1

                  Ну да, вы ж сами процитировали самое основное:

                  Operators can be regrouped according to the usual mathematical rules only where the operators really are associative or commutative.

                  Поскольку арифметическое сложение – ассоциативная операция (в теории), то компилятор вправе её перегруппировывать.

                  Компилятор C/C++ вправе переупорядочивать операции любым образом, который в принципе сохраняет результат. Может даже вообще выкидывать – например, из a+b-a может сделать b.

                  А левая ассоциативность относится к логическому порядку понимания семантики выражения (в C++ для сложных объектов выполняется также и физически, но для простых численных типов может выполняться переупорядочивание).

                  Вот как написано в стандарте Си:

                  Order of evaluation of the operands of any C operator, including the order of evaluation of function arguments in a function‑call expression, and the order of evaluation of the subexpressions within any expression is unspecified...

                  А вот что написано про Фортран:

                  the expression is evaluated from left to right, according to the following precedence among operators...


                  1. quaer
                    26.05.2023 18:26

                    Можно пример, когда использование скобок не решает проблему?


                    1. vadimr
                      26.05.2023 18:26

                      Да скобки вообще тут не причём и ничего не значат.

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

                      А если говорить про собственно последовательность вычислений, а не только применения арифметических операций, то вот, например:

                      Hidden text
                      $ cat eval.c
                      #include <stdio.h>
                      
                      int a () {
                        puts ("a");
                        return 1;
                      }
                      
                      int b () {
                        puts ("b");
                        return 2;
                      }
                      
                      int c () {
                        puts ("c");
                        return 3;
                      }
                      
                      int main () {
                        int s;
                        s = a()*(b()*c());
                        printf ("%d\n", s);
                        return 0;
                      }
                      
                      $ gcc eval.c
                      $ ./a.out
                      a
                      b
                      c
                      6

                      Это не к тому, что в Фортране кодогенерация устроена по-другому, а к тому, что скобки на неё не оказывают влияния.


                      1. quaer
                        26.05.2023 18:26

                        Проверил на Java, результат такой же. Вы мне картину мира сломали :) Век живи, век учись! Как же тогда правильно сильно маленькие и сильно большие числа совместно обрабатывать правильно?

                        А если, как советуют, добавить компилятору флаги?

                        -fassociative-math, -ffast-math and -ffloat-store


                      1. vadimr
                        26.05.2023 18:26

                        Флагами, скорее всего, можно решить эту проблему, но это ж не выход в обсуждаемой ситуации. Чтобы понимать, какие флаги надо ставить в каждом конкретном случае, надо знать Фортран и Си на уровне, который сделает ненужным автоматический переводчик.


                      1. quaer
                        26.05.2023 18:26

                        Есть какие-то рекомендации по написанию математических операций на сях? Скажем, имеем 2 малых величины m1, m2 и 2 больших величины, b1, b2.

                        Вместо (b1*b1)*(m1*m2) хотим (m1*b1)*(m2*b2) в предположении, что это уменьшит ошибку.


                      1. vadimr
                        26.05.2023 18:26

                        Обычно использование volatile отключает всю оптимизацию. Хотя это тоже не гарантировано стандартом.


  1. motomichael
    26.05.2023 18:26
    +1

    огромный кусок с методом вращений заменён на единственную строку с вызовомnp.linalg.eigh

    Вот интересно. Я сам никогда массивными расчетами не занимался, и с фортраном сталкивался только в университетском курсе программирования (достаточно бессмысленном, надо сказать). Но неоднократно слышал мнение, что фортран в научных расчетах не умирает, поскольку для него написаны мегатонны библиотек, от которых никто не может отказаться. И что, вы хотите сказать, что в библиотеке f90 не было более или менее стандартного способа получить собственные числа и векторы?


    1. kbtsiberkin Автор
      26.05.2023 18:26
      +1

      Да почему, LAPACK или MKL содержат всё нужное. Главное, не забывать RTFM.

      А раньше была либо мода, либо правила хорошего тона - каждый под свои задачи кодит сам. В итоге имеем на кафедре десятки версий одного и того же с непредсказуемым набором багов. На одном из своих предметов я перестал требовать написание, например, метода Рунге-Кутты-Мерсона, устав ежегодно видеть ужастики и под копирку слизанные друг у друга программы. Вроде бы учить на готовых библиотеках, но с "плохими" тестовыми задачами получается легче и эффективнее по затратам времени.


      1. motomichael
        26.05.2023 18:26
        +1

        Это да. У нас тут тоже есть некая программа для расчисления, так сказать, хода светил по небесной сфере. Телескоп наводить, куда надо. 250 килобайт вручную написанного некомментированного текста на f90 одним куском. У нее есть одна маленькая проблема: функции для работы с гражданским временем авторы почему-то решили написать сами. В те времена страна еще переходила каждый год с летнего времени на зимнее и обратно. И вот это они где-то там у себя захардкодили. Потом случился 2011 год... В общем, с тех пор это все обросло гроздьями внешних скриптов, которые с разной степенью успешности решают проблему. А в этом году вроде бы опять было предложение ввести переход на летнее время. Я как-то пытался с ней разобраться, но быстро сломался, заплакал и убежал.


        1. vadimr
          26.05.2023 18:26
          +1

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


    1. vadimr
      26.05.2023 18:26
      +1

      Эта программа на Алголе написана до появления стандартных библиотек Фортрана.


  1. vadimr
    26.05.2023 18:26
    +2

    В современном Фортране лучше не использовать специфические функции dabs, dsqrt и т.д. и константы вроде 1.0d0 – все стандартные функции полиморфны в отношении точности вещественных чисел. А так, если вы захотите в вашей программе поменять real*8 на real*10 или real*16, это не даст желаемого результата.


    1. kbtsiberkin Автор
      26.05.2023 18:26
      +1

      Это дурные привычки со студенчества, когда в наличии был жуткий Fortran Power Station 4.0. Постепенно переучиваюсь. Особенно удобно при работе с комплексными числами.


  1. Vitter
    26.05.2023 18:26

    del


  1. Vitter
    26.05.2023 18:26

    program NMR_rarefied
    implicit none
    real(8), dimension(:,:,:), allocatable :: dip, a1 ! dipolar interaction and additional
    real(8), dimension(:,:), allocatable :: sx, sy, sz, sI, sp, sm, o, u, v, w, & ! spin matrices and additional
                                            x, y, z, r, & ! r = {x, y, z}, distances between magnetic ions (MI)
                                            J0, & ! exchange interactions
                                            ReH, ImH, b, q, q2, & ! Hamiltonian parts and additional
                                            d0, d1 ! for Kronecker product procedure
    real(8), dimension(:), allocatable :: x1, y1, z1, & ! coordinates of MI
                                          RT2, RT3, RT4, RT1, sum_sqr, & ! for rotation method
                                          histp, hist ! partial and averaged histogram
    logical, dimension(:), allocatable :: mask1
    real(8), dimension(1:3) :: e1, e2, e3, d ! lattice basis
    real(8), dimension(1) :: i11, k11
    real(8) S, eps, & ! spin, precision
            alpha, beta, pd, & ! parameters of exchange (A*exp(-B*r)) and dipolar interaction (pd = (hbar*gamma)^2)
            res, & ! histogram resolution
            s1, s2, c1, c2, & ! rotation parameters
            m, e, c
    integer N, N1, N2, N3, NN, Nres, & ! total number of MI and their concentration
            i, j, k, i1, j1, k1, t, m3, ms, l, r1, ires, & ! counters
            f, fn, & ! 2S+1, (2S+1)^N
            nh ! number of histogram bars
    logical noconv
            
    !----------------------------------------------------
    ! read and set the initial parameters
    !----------------------------------------------------
    open(1, file="init.txt", form="formatted", action="read")
    read(1,*) eps, S, N, N1, N2, N3, nh, alpha, beta, pd, &
              e1, e2, e3, res, Nres
    close(1)
    
    f = 2*S + 1; fn = f**N
    call RANDOM_SEED()
    
    !----------------------------------------------------
    ! memory alloc
    !----------------------------------------------------
    allocate(sx(1:f,1:f)); allocate(sy(1:f,1:f)); allocate(sz(1:f,1:f))
    allocate(sI(1:f,1:f)); allocate(sp(1:f,1:f)); allocate(sm(1:f,1:f))
    allocate(o(1:f,1:f)); allocate(v(1:f,1:f)); allocate(w(1:f,1:f))
    allocate(dip(1:6,1:N,1:N)); allocate(J0(1:N,1:N)); allocate(u(1:N,1:N))
    allocate(x(1:N,1:N)); allocate(y(1:N,1:N)); allocate(z(1:N,1:N))
    allocate(r(1:N,1:N))
    allocate(x1(1:N)); allocate(y1(1:N)); allocate(z1(1:N))
    allocate(ReH(1:fn,1:fn)); allocate(ImH(1:fn,1:fn))
    allocate(b(1:fn,1:fn)); allocate(q(1:fn,1:fn)); allocate(q2(1:fn,1:fn))
    allocate(mask1(1:fn))
    allocate(a1(1:N,1:f,1:f))
    allocate(RT1(1:fn)); allocate(RT2(1:fn))
    allocate(RT3(1:fn)); allocate(RT4(1:fn))
    allocate(sum_sqr(1:fn))
    allocate(histp(1:nh)); allocate(hist(1:nh))
    
    hist = 0.0d0
    
    open(1, file="MI_coords.txt", form="formatted", action="write")
    open(2,file="E.txt", form="formatted", action="write")
    
    !----------------------------------------------------
    ! spin matrices initialization
    !----------------------------------------------------
    sx = 0.0d0; sy = 0.0d0; sz = 0.0d0; sp = 0.0d0; sm = 0.0d0; sI = 0.0d0
    do i = 1, f, 1
      sI(i,i) = 1.0d0
      sz(i,i) = S - (i-1)
      if(i <= f-1) sp(i,i+1) = dsqrt(dabs(S*(S+1.0d0) - real(i*(i+1))))
    enddo
    sm = transpose(sp)
    sx = 0.5d0*(sp + sm)
    sy = 0.5d0*(sm - sp)
    
    !----------------------------------------------------
    ires = 1
    REALIZATION: do while(ires <= Nres)
    !----------------------------------------------------
    ! generation of MI distribution in lattice
    !----------------------------------------------------
      x = 0.0d0; y = 0.0d0; z = 0.0d0; r = 0.0d0
      COORDS: do
      ! MI coordinates
        do i = 1, N, 1
          call RANDOM_NUMBER(d)
          d(1) = floor(d(1)*N1); d(2) = floor(d(2)*N2); d(3) = floor(d(3)*N3)
          x1(i) = d(1)*e1(1) + d(2)*e2(1) + d(3)*e3(1)
          y1(i) = d(1)*e1(2) + d(2)*e2(2) + d(3)*e3(2)
          z1(i) = d(1)*e1(3) + d(2)*e2(3) + d(3)*e3(3)
        enddo
      
      ! MI distances
        do i = 1, N, 1
          do j = 1, N, 1
            if(i /= j)then
              x(i,j) = x1(i) - x1(j)
              y(i,j) = y1(i) - y1(j)
              z(i,j) = z1(i) - z1(j)
              ! full interaction
              r(i,j) = dsqrt( x(i,j)**2 + y(i,j)**2 + z(i,j)**2 )
            endif
          enddo
          r(i,i) = 1.0d0
        enddo
    
        if(minval(r) > 0.5d0)exit
      enddo COORDS
    
      ! write MI coordinated
      write(1,'(A,I8)') "Realization ", ires
      do i = 1, N, 1
        write(1,'(I6, 3E20.6E3)') i, x1(i), y1(i), z1(i)
      enddo
      write(1,*)
      
    !----------------------------------------------------
    ! pair interaction evaluation
    !----------------------------------------------------
      ! isotropic part
      J0 = alpha*dexp(-beta*r)
      dip(1,:,:) = J0 + pd * (1.0d0 - 3.0d0*x**2 / r**2) / r**3
      dip(2,:,:) = J0 + pd * (1.0d0 - 3.0d0*y**2 / r**2) / r**3
      dip(3,:,:) = J0 + pd * (1.0d0 - 3.0d0*z**2 / r**2) / r**3
      ! anisotropic part
      dip(4,:,:) = -3.0d0*pd*x*y / r**5
      dip(5,:,:) = -3.0d0*pd*x*z / r**5
      dip(6,:,:) = -3.0d0*pd*y*z / r**5
    
    !----------------------------------------------------
    ! real part of the Hamiltonian matrix
    !----------------------------------------------------
      o = sz; call p78()
      ReH = b
      u = dip(1,:,:); v = sx; w = sx; call p56()
      u = dip(2,:,:); v = sy; w = -sy; call p56()
      u = dip(3,:,:); v = sz; w = sz; call p56()
      u = dip(5,:,:); v = sx; w = sz; call p56()
      q = ReH; ReH = 0.0d0
    
    !----------------------------------------------------
    ! imaginary part of the Hamiltonian matrix
    !----------------------------------------------------
      u = dip(4,:,:); v = sx; w = sy; call p56()
      u = dip(6,:,:); v = sy; w = sz; call p56()
      ImH = ReH; ReH = q
    
    !----------------------------------------------------
    ! Diagonalization of the Hamiltonian matrix
    !----------------------------------------------------
      t = 0; q = 0.0d0; q2 = 0.0d0
      do i = 1, fn, 1
        q(i,i) = 1.0d0
      enddo
    
      ! sum of squares by strings
      sum_sqr = 0.0d0
      do i = 1, fn, 1
        do j = 1, fn, 1
          if(i /= j) sum_sqr(i) = sum_sqr(i) + (ReH(i,j)**2 + ImH(i,j)**2)
        enddo
      enddo
      noconv = .false.
      
      ! rotation iterations
      ROTATION: do
        i11 = maxloc(sum_sqr); i1 = i11(1)
        mask1 = .true.; mask1(i1) = .false.
        k11 = maxloc(ReH(i1,:)**2 + ImH(i1,:)**2, mask=mask1); k1 = k11(1)
        m = maxval(ReH(i1,:)**2 + ImH(i1,:)**2, mask=mask1)
      
        if(m < eps)exit 
        if(t == 100000)then
          noconv = .true.
          exit
        endif
    
        mask1(k1) = .false.
        sum_sqr = sum_sqr - sum(ReH(:,i1)**2 + ImH(:,i1)**2, mask=mask1)
        
        i = i1; k = k1
      
        ! rotation parameters
        e = dabs(ReH(i,i) - ReH(k,k)) / dsqrt( (ReH(i,i) - ReH(k,k))**2 + 4.0d0*(ReH(i,k)**2 + ImH(i,k)**2) )
        s1 = dsqrt( 0.5d0*(1.0d0 - e) ); c1 = dsqrt( 0.5d0*(1.0d0 + e) )
        if(ReH(i,i) /= ReH(k,k)) s1 = sign(s1, ReH(i,i) - ReH(k,k))
        if(ReH(i,k) /= 0.0d0) s1 = sign(s1, ReH(i,k))
      
        s2 = ImH(i,k) / dsqrt(ReH(i,k)**2 + ImH(i,k)**2)
        if(ReH(i,k) /= 0.0d0) s2 = sign(s2, ReH(i,k))
      
        c2 = s1 * dabs(ReH(i,k)) / dsqrt(ReH(i,k)**2 + ImH(i,k)**2)
        s1 = s1*s2
      
        ! make the rotations
        RT1 = c1*q(:,i) + c2*q(:,k) + s1*q2(:,k)
        RT2 = -c2*q(:,i) + c1*q(:,k) + s1*q2(:,i)
        RT3 = -s1*q(:,k) + c1*q2(:,i) + c2*q2(:,k)
        RT4 = -s1*q(:,i) - c2*q2(:,i) + c1*q2(:,k)
        
        q(:,i) = RT1; q(:,k) = RT2; q2(:,i) = RT3; q2(:,k) = RT4
    
        RT1 = c1*ReH(:,i) + c2*ReH(:,k) + s1*ImH(:,k)
        RT2 = -c2*ReH(:,i) + c1*ReH(:,k) + s1*ImH(:,i)
        RT3 = -s1*ReH(:,k) + c1*ImH(:,i) + c2*ImH(:,k)
        RT4 = -s1*ReH(:,i) - c2*ImH(:,i) + c1*ImH(:,k)
    
        ReH(:,i) = RT1; ReH(:,k) = RT2; ImH(:,i) = RT3; ImH(:,k) = RT4
    
        RT1 = c1*ReH(i,:) + c2*ReH(k,:) - s1*ImH(k,:)
        RT2 = -c2*ReH(i,:) + c1*ReH(k,:) - s1*ImH(i,:)
        RT3 = c1*ImH(i,:) + c2*ImH(k,:) + s1*ReH(k,:)
        RT4 = -c2*ImH(i,:) + c1*ImH(k,:) + s1*ReH(i,:)
    
        ReH(i,:) = RT1; ReH(k,:) = RT2; ImH(i,:) = RT3; ImH(k,:) = RT4
        
        t = t+1
      
        mask1 = .true.; mask1(i1) = .false.; mask1(k1) = .false.
        sum_sqr = sum_sqr + sum(ReH(:,i1)**2 + ImH(:,i1)**2, mask = mask1) &
                  + sum(ReH(:,k1)**2 + ImH(:,k1)**2, mask = mask1)
       
        mask1 = .true.; mask1(i1) = .false.
        sum_sqr(i1) = sum(ReH(i1,:)**2 + ImH(i1,:)**2, mask = mask1)
      
        mask1 = .true.; mask1(k1) = .false.
        sum_sqr(k1) = sum(ReH(k1,:)**2 + ImH(k1,:)**2, mask = mask1)
      enddo ROTATION
    
    !----------------------------------------------------
      if(noconv)then
        print *, "Realization does not converge"
      else
        print *,ires, ", Rotation finished at ", t, " iterations"
    !----------------------------------------------------
    ! eigenvalues
    !----------------------------------------------------
        do i = 1, fn, 1
          RT1(i) = ReH(i,i)
        enddo
    
        ! write energy
        write(2,'(A,I8)') "Realization ", ires
        do i = 1, fn, 1
          write(2,'(I6,E20.6E3)') i, RT1(i)
        enddo
        write(2,*)
    
    !----------------------------------------------------
    ! evaluate probabilities and histogram amplitudes
    !----------------------------------------------------
        histp = 0.0d0
        o = sx
    
        call p78()
    
        ReH = matmul(b, q); ImH = matmul(b, q2)
        d1 = transpose(q); b = transpose(q2)
    
        q = matmul(d1, ReH) + matmul(b, ImH)
        q2 = matmul(d1, ImH) - matmul(b, ReH)
    
        do i = 1, fn, 1
          do j = 1, fn, 1
            c = q(i,j)**2 + q2(i,j)**2
            k = floor(1.0d0 + dabs(RT1(i) - RT1(j)) / res)
            if(k <= nh) histp(k) = histp(k) + c
          enddo
        enddo
    
        hist = hist + histp
        ires = ires + 1  
      endif
    enddo REALIZATION
    !----------------------------------------------------
    
    open(3,file="hist.txt", form="formatted", action="write")
    do i = 1, nh, 1
      write(3,'(2E20.6E3)') i*res, hist(i) / Nres
    enddo
    close(1); close(2); close(3)
    
    print *,"Histogram is written at hist.txt"
    
    !----------------------------------------------------
    ! memory clear
    !----------------------------------------------------
    deallocate(sx); deallocate(sy); deallocate(sz); deallocate(sI)
    deallocate(o); deallocate(v); deallocate(w)
    deallocate(ReH); deallocate(ImH); deallocate(b); deallocate(q); deallocate(q2)
    deallocate(mask1); deallocate(a1)
    deallocate(RT1); deallocate(RT2); deallocate(RT3); deallocate(RT4); deallocate(sum_sqr)
    deallocate(histp); deallocate(hist)
    deallocate(dip); deallocate(J0); deallocate(sp); deallocate(sm)
    deallocate(x1); deallocate(y1); deallocate(z1)
    deallocate(x); deallocate(y); deallocate(z); deallocate(r); deallocate(d1)
    
    contains
    !----------------------------------------------------
    ! Kronecker product of one spin and N-1 identity
    !----------------------------------------------------
    subroutine p78()
      b = 0.0d0
    
      do k = 1, N, 1
        a1(k,:,:) = sI
      enddo
      
      do k = 1, N, 1
        a1(k,:,:) = o
        call p34()
        a1(k,:,:) = sI
        b = b + d1
      enddo
    end subroutine p78
    
    !----------------------------------------------------
    ! Kronecker product of two spin and N-2 identity
    !----------------------------------------------------
    subroutine p56()
      do k = 1, N, 1
        do t = 1, N, 1
          if(k /= t)then
            a1(k,:,:) = v; a1(t,:,:) = w
          endif
          
          call p34()
                
          a1(k,:,:) = sI; a1(t,:,:) = sI
          ReH = ReH + u(k,t)*d1
        enddo
      enddo
    end subroutine p56
    
    !----------------------------------------------------
    ! Kronecker product of N matrices
    !----------------------------------------------------
    subroutine p34()
      if(allocated(d0)) deallocate(d0)
      if(allocated(d1)) deallocate(d1)
      
      allocate(d0(1:f,1:f)); d0 = 0.0d0
      allocate(d1(1:f**2,1:f**2)); d1 = 0.0d0
      
      d0 = a1(1,:,:)
      
      do m3 = 2, N, 1
        NN = f**(m3-1)
        do i = 1, NN, 1
          do j = 1, NN, 1
            do ms = 1, f, 1
              do l = 1, f, 1
                r1 = (i-1)*f + ms; i1 = (j-1)*f + l
                d1(r1,i1) = d0(i,j)*a1(m3,ms,l)
              enddo
            enddo
          enddo
        enddo
        
        if(m3 < N)then
          deallocate(d0); allocate(d0(1:f**m3,1:f**m3)); d0 = 0.0d0
          d0 = d1
          deallocate(d1); allocate(d1(1:f**(m3+1),1:f**(m3+1))); d1 = 0.0d0
        endif
        
      enddo
      deallocate(d0)
    end subroutine p34
    
    end program NMR_rarefied

    вы бы хоть расскрасили. Например это "CSS",