ARPACK depends on LAPACK’s internal random number generators

  • dlarnv
  • slarnv
  • zlarnv
  • clarnv

which use the raw routines

  • dlaruv
  • slaruv

This is true even if you use Julia to initialize the starting vector using Julia’s random number generator. The reason is because ARPACK needs to be able to get random data to restart the process is it detects an invariant subspace. This suggests two approaches: call LAPACK’s RNG from Julia, or port LAPACK’s RNG to Julia. Why do the simple thing when the more complicated thing is so much more fun?

Aside, best guess on LAPACK names: _larnv = lapack random normal vector and _laruv = lapack random uniform vector

Calling LAPACK’s random number generator from julia

The tricky bit about calling LAPACK’s RNG from Julia is the seed value. This is just 4 ints, which we’d like to be stack allocated. Unfortunately, we won’t be able to get that for reasons that are internal to Julia. (Blah blah, stack vs. heap and gc and guarantees…)

Here’s some code that works though.

import LinearAlgebra

_dlarnv_blas!(idist::Int,
       iseed::Ref{NTuple{4,Int}},
      n::Int,
      x::Vector{Float64}) =
  ccall((LinearAlgebra.BLAS.@blasfunc("dlarnv_"), LinearAlgebra.BLAS.libblas), Cvoid,
    (Ref{LinearAlgebra.BlasInt}, Ptr{LinearAlgebra.BlasInt}, Ref{LinearAlgebra.BlasInt}, Ptr{Float64}),
    idist, iseed, n, x)

x = zeros(5); _dlarnv!(1,Base.Ref(tuple(1,2,3,4)),length(x),x)

using BenchmarkTools
@btime _dlarnv_blas!(1, tseed, length(z), z) setup=begin
    z=zeros(128);     
    tseed=Base.Ref(tuple(1,3,5,7));
end

This uses NTuple as the holder for the 4 ints we use as the seed. At this point, I thought the following should work…

mutable struct MySeedState
  seed::NTuple{4,Int}
end

function myrand!(x::Vector{Float64}, state::MySeedState)
  iseed = Base.Ref(state.seed) # this will allocate :(

  _dlarnv_blas!(2, iseed, length(x), x)
  state.seed = iseed[]
end

using BenchmarkTools
@btime myrand!(z, tstate) setup=begin z=zeros(128); tstate=MySeedState(tuple(1,3,5,7)); end

But – see above – I learned when we want to use this is that allocating a Ref can actually allocate in Julia, even when it’s an itsbits type.

julia> isbitstype(NTuple{4,Int})
true

To avoid this, we can just allocate the ref as part of the mutable struct.

mutable struct MyRefSeedState
  seed::Ref{NTuple{4,Int}}
end

function myrand!(x::Vector{Float64}, state::MyRefSeedState)
  _dlarnv_blas!(2, state.seed, length(x), x)
end

using BenchmarkTools
@btime myrand!(z, tstate) setup=begin
    z=zeros(128);
    tstate=MyRefSeedState(Base.Ref(tuple(1,3,5,7)));
end

Either way, it probably isn’t that big of a deal, but I’d like to be clear on where the allocations are happening. e.g. one-time thread local setup is okay. Random internal allocations are not okay.

It’s puzzling to me why this causes an allocation as this seems like it can be safely done on the stack and the same allocation doesn’t occur for types like Int.

Porting LAPACK’s random number generator to Julia.

Since the routine is sufficiently simple, we can actually just port the random number generator directly.

Aside – that this is actually a weakness as Julia’s RNGs are much better. On the other hand, these are just used to generate data that is not-orthogonal to a desired invariant subspace (with very very high probability). So the quality of the RNG does not need to be statistical.

The underlying double-precision random number generator is dlaruv not dlarnv. The second function dlarnv is a wrapper that does seemingly odd things like work in 128-length segments, until you realize that dlaruv only works in 128-length segments. (Weird!) Anyhoo.

Here’s a link to the double precision generator dlaruv.f in OpenBLAS

The routine is fairly simple. Have a set of fixed values and do some integer operations/etc. to get pseudorandom values out. Specifically > This routine uses a multiplicative congruential method with modulus 2**48 and multiplier 33952834046453 (see G.S.Fishman, 'Multiplicative congruential random number generators with modulus 2**b: an exhaustive analysis for b = 32 and a partial analysis for b = 48', Math. Comp. 189, pp 331-344, 1990).

See the appendix for code to extract this fixed array of values.

We set these values into a constant global array

const _dlaruv_mm=tuple(tuple(494,322,2508,2549), ...

julia> typeof(_dlaruv_mm)
NTuple{128, NTuple{4, Int64}}

Next up, we port the actual RNG. In this case, I was actually able to copy/paste much of the FORTRAN code.

function _dlaruv!(iseed::Base.RefValue{NTuple{4,Int}}, n::Int, x::Vector{Float64})
  IPW2=4096
  R = 1/IPW2
  i1 = iseed[][1]
  i2 = iseed[][2]
  i3 = iseed[][3]
  i4 = iseed[][4]
  # setup scope for final call in julia
  IT1 = i1
  IT2 = i2
  IT3 = i3
  IT4 = i4
  @inbounds for i=1:min(n,length(_dlaruv_mm))
    # directly copied/pasted from fortran, hence the caps
    while true
      # Multiply the seed by i-th power of the multiplier modulo 2**48
      IT4 = i4*_dlaruv_mm[i][4]
      IT3 = IT4 ÷ IPW2
      IT4 = IT4 - IPW2*IT3
      IT3 = IT3 + i3*_dlaruv_mm[i][4] + i4*_dlaruv_mm[i][3]
      IT2 = IT3 ÷ IPW2
      IT3 = IT3 - IPW2*IT2
      IT2 = IT2 + i2*_dlaruv_mm[i][4] + i3*_dlaruv_mm[i][3] + i4*_dlaruv_mm[i][2]
      IT1 = IT2 ÷ IPW2
      IT2 = IT2 - IPW2*IT1
      IT1 = IT1 + i1*_dlaruv_mm[i][4] + i2*_dlaruv_mm[i][3] + i3*_dlaruv_mm[i][2] + i4*_dlaruv_mm[i][1]
      IT1 = mod( IT1, IPW2 )

      # Convert 48-bit integer to a real number in the interval (0,1)
      x[i] = R*( Float64( IT1 )+R*( Float64( IT2 )+R*( Float64( IT3 )+R*
                 Float64( IT4 ) ) ) )
      if x[i] == 1
        # handle the case where the value is different
        i1 += 2
        i2 += 2
        i3 += 2
        i4 += 2
      else
        break
      end
    end
  end
  iseed[] = tuple(IT1,IT2,IT3,IT4) # update the seed
end

This uses the global constant array to avoid dealing with that big array of values each function call; otherwise, the only difference is that we use a while loop instead of a GOTO loop in fortran.

This gives exactly the same results as the FORTRAN call

_dlaruv_blas!(
      iseed::Ref{NTuple{4,Int}},
      n::Int,
      x::Vector{Float64}) =
  ccall((LinearAlgebra.BLAS.@blasfunc("dlaruv_"), LinearAlgebra.BLAS.libblas), Cvoid,
    (Ptr{LinearAlgebra.BlasInt}, Ref{LinearAlgebra.BlasInt}, Ptr{Float64}),
    iseed, n, x)

seed1 = Base.RefValue(tuple(1,3,5,7))
seed2 = Base.RefValue(tuple(1,3,5,7))

x1 = zeros(97); _dlaruv_blas!(seed1, length(x1), x1)
x2 = similar(x1); _dlaruv!(seed2, length(x2), x2)

@assert x1 == x2
@assert seed1[] == seed2[]

x3 = zeros(143); _dlaruv_blas!(seed1, length(x3), x3)
x4 = zeros(axes(x3)...); _dlaruv!(seed2, length(x4), x4)

@assert x3 == x4
@assert seed1[] == seed2[]

Awesome. But if you look at dlarnv, it also has a statically allocated length 128 array that is not going to be fun in Julia. Doable, but not fun… nor at all necessary. We can actually streamline things by just manually inlining the RNG directly into dlarnv. The only thing we have to remember is that we need to “refresh” the seed every 128 RV generations.

Since ARPACK only ever calls dlarnv with idist=2, we are just going to port that code.

function _dlarnv_idist_2!(iseed::Base.RefValue{NTuple{4,Int}}, n::Int, x::Vector{Float64})
  # manually inline dlaruv to avoid excess cruft...
  IPW2=4096
  R = 1/IPW2
  i1 = iseed[][1]
  i2 = iseed[][2]
  i3 = iseed[][3]
  i4 = iseed[][4]
  # setup scope for final call in julia
  IT1 = i1
  IT2 = i2
  IT3 = i3
  IT4 = i4

  nrv = 0

  @inbounds for i=1:n
    rv = 1.0
    nrv += 1
    if nrv > length(_dlaruv_mm)
      nrv = 1  # update seed information
      i1 = IT1
      i2 = IT2
      i3 = IT3
      i4 = IT4
    end

    while true
      # Multiply the seed by i-th power of the multiplier modulo 2**48
      IT4 = i4*_dlaruv_mm[nrv][4]
      IT3 = IT4 ÷ IPW2
      IT4 = IT4 - IPW2*IT3
      IT3 = IT3 + i3*_dlaruv_mm[nrv][4] + i4*_dlaruv_mm[nrv][3]
      IT2 = IT3 ÷ IPW2
      IT3 = IT3 - IPW2*IT2
      IT2 = IT2 + i2*_dlaruv_mm[nrv][4] + i3*_dlaruv_mm[nrv][3] + i4*_dlaruv_mm[nrv][2]
      IT1 = IT2 ÷ IPW2
      IT2 = IT2 - IPW2*IT1
      IT1 = IT1 + i1*_dlaruv_mm[nrv][4] + i2*_dlaruv_mm[nrv][3] + i3*_dlaruv_mm[nrv][2] + i4*_dlaruv_mm[nrv][1]
      IT1 = mod( IT1, IPW2 )

      # Convert 48-bit integer to a real number in the interval (0,1)
      rv = R*( Float64( IT1 )+R*( Float64( IT2 )+R*( Float64( IT3 )+R*
               Float64( IT4 ) ) ) )
      if rv == 1
        # handle the case where the value is different
        i1 += 2
        i2 += 2
        i3 += 2
        i4 += 2
      else
        break
      end
    end
    # now, rv is a random value
    # port the line from `dlarnv` for idist=2
    x[i] = 2*rv - 1
  end
  iseed[] = tuple(IT1,IT2,IT3,IT4) # update the seed
end

This works great!

seed1 = Base.RefValue(tuple(1,3,5,7))
seed2 = Base.RefValue(tuple(1,3,5,7))

x1 = zeros(501); _dlarnv_blas!(2, seed1, length(x1), x1)
x2 = similar(x1); _dlarnv_idist_2!(seed2, length(x2), x2)

@assert x1 == x2
@assert seed1[] == seed2[]

##
x3 = zeros(143); _dlarnv_blas!(2, seed1, length(x3), x3)
x4 = zeros(axes(x3)...); _dlarnv_idist_2!(seed2, length(x4), x4)

@assert x3 == x4
@assert seed1[] == seed2[]    

Exactly the same values and exactly the same seeds.

Things that might happen in the future… I may pull out the key piece of the random number generator into a separate function…

rv,IT1,IT2,IT3,IT4,i1,i2,i3,i4 = _dlaruv_rng_step(
    nrv,IT1,IT2,IT3,IT4,i1,i2,i3,i4)

Well… I tried that, and it turns out slower, which brings us to …

Julia is faster than Fortran.

What’s neat about inlining that code is we get a small performance boost.

using BenchmarkTools
@btime _dlarnv_idist_2!(tseed, length(z), z) setup=begin
  z=zeros(501);
  tseed=Base.Ref(tuple(1,3,5,7)); end
@btime _dlarnv_blas!(2, tseed, length(z), z) setup=begin
  z=zeros(501);
  tseed=Base.Ref(tuple(1,3,5,7)); end

Gives …

Julia 6.686 μs (0 allocations: 0 bytes)

Fortran 7.333 μs (0 allocations: 0 bytes)

So Julia for the win!

(For reference, with the _dlaruv_rng_step idea, it was 7.0 microseconds, so just a nudge slower. Not sure why as it would seem to inline to exactly the same code. )

Appendix

Getting the values from the fortran code.

Here is how we extracted that big array of constant values from the FORTRAN code for dlaruv. I believe it’s the same array for slaruv too, but I’d need to double-check that. So this code is helpful there.

code = raw"""
DATA               ( MM( 1, J ), J = 1, 4 ) / 494, 322, 2508,
$                   2549 /
DATA               ( MM( 2, J ), J = 1, 4 ) / 2637, 789, 3754,
$                   1145 /
DATA               ( MM( 3, J ), J = 1, 4 ) / 255, 1440, 1766,
$                   2253 /
DATA               ( MM( 4, J ), J = 1, 4 ) / 2008, 752, 3572,
$                   305 /
DATA               ( MM( 5, J ), J = 1, 4 ) / 1253, 2859, 2893,
$                   3301 /
DATA               ( MM( 6, J ), J = 1, 4 ) / 3344, 123, 307,
$                   1065 /
DATA               ( MM( 7, J ), J = 1, 4 ) / 4084, 1848, 1297,
$                   3133 /
DATA               ( MM( 8, J ), J = 1, 4 ) / 1739, 643, 3966,
$                   2913 /
DATA               ( MM( 9, J ), J = 1, 4 ) / 3143, 2405, 758,
$                   3285 /
DATA               ( MM( 10, J ), J = 1, 4 ) / 3468, 2638, 2598,
$                   1241 /
DATA               ( MM( 11, J ), J = 1, 4 ) / 688, 2344, 3406,
$                   1197 /
DATA               ( MM( 12, J ), J = 1, 4 ) / 1657, 46, 2922,
$                   3729 /
DATA               ( MM( 13, J ), J = 1, 4 ) / 1238, 3814, 1038,
$                   2501 /
DATA               ( MM( 14, J ), J = 1, 4 ) / 3166, 913, 2934,
$                   1673 /
DATA               ( MM( 15, J ), J = 1, 4 ) / 1292, 3649, 2091,
$                   541 /
DATA               ( MM( 16, J ), J = 1, 4 ) / 3422, 339, 2451,
$                   2753 /
DATA               ( MM( 17, J ), J = 1, 4 ) / 1270, 3808, 1580,
$                   949 /
DATA               ( MM( 18, J ), J = 1, 4 ) / 2016, 822, 1958,
$                   2361 /
DATA               ( MM( 19, J ), J = 1, 4 ) / 154, 2832, 2055,
$                   1165 /
DATA               ( MM( 20, J ), J = 1, 4 ) / 2862, 3078, 1507,
$                   4081 /
DATA               ( MM( 21, J ), J = 1, 4 ) / 697, 3633, 1078,
$                   2725 /
DATA               ( MM( 22, J ), J = 1, 4 ) / 1706, 2970, 3273,
$                   3305 /
DATA               ( MM( 23, J ), J = 1, 4 ) / 491, 637, 17,
$                   3069 /
DATA               ( MM( 24, J ), J = 1, 4 ) / 931, 2249, 854,
$                   3617 /
DATA               ( MM( 25, J ), J = 1, 4 ) / 1444, 2081, 2916,
$                   3733 /
DATA               ( MM( 26, J ), J = 1, 4 ) / 444, 4019, 3971,
$                   409 /
DATA               ( MM( 27, J ), J = 1, 4 ) / 3577, 1478, 2889,
$                   2157 /
DATA               ( MM( 28, J ), J = 1, 4 ) / 3944, 242, 3831,
$                   1361 /
DATA               ( MM( 29, J ), J = 1, 4 ) / 2184, 481, 2621,
$                   3973 /
DATA               ( MM( 30, J ), J = 1, 4 ) / 1661, 2075, 1541,
$                   1865 /
DATA               ( MM( 31, J ), J = 1, 4 ) / 3482, 4058, 893,
$                   2525 /
DATA               ( MM( 32, J ), J = 1, 4 ) / 657, 622, 736,
$                   1409 /
DATA               ( MM( 33, J ), J = 1, 4 ) / 3023, 3376, 3992,
$                   3445 /
DATA               ( MM( 34, J ), J = 1, 4 ) / 3618, 812, 787,
$                   3577 /
DATA               ( MM( 35, J ), J = 1, 4 ) / 1267, 234, 2125,
$                   77 /
DATA               ( MM( 36, J ), J = 1, 4 ) / 1828, 641, 2364,
$                   3761 /
DATA               ( MM( 37, J ), J = 1, 4 ) / 164, 4005, 2460,
$                   2149 /
DATA               ( MM( 38, J ), J = 1, 4 ) / 3798, 1122, 257,
$                   1449 /
DATA               ( MM( 39, J ), J = 1, 4 ) / 3087, 3135, 1574,
$                   3005 /
DATA               ( MM( 40, J ), J = 1, 4 ) / 2400, 2640, 3912,
$                   225 /
DATA               ( MM( 41, J ), J = 1, 4 ) / 2870, 2302, 1216,
$                   85 /
DATA               ( MM( 42, J ), J = 1, 4 ) / 3876, 40, 3248,
$                   3673 /
DATA               ( MM( 43, J ), J = 1, 4 ) / 1905, 1832, 3401,
$                   3117 /
DATA               ( MM( 44, J ), J = 1, 4 ) / 1593, 2247, 2124,
$                   3089 /
DATA               ( MM( 45, J ), J = 1, 4 ) / 1797, 2034, 2762,
$                   1349 /
DATA               ( MM( 46, J ), J = 1, 4 ) / 1234, 2637, 149,
$                   2057 /
DATA               ( MM( 47, J ), J = 1, 4 ) / 3460, 1287, 2245,
$                   413 /
DATA               ( MM( 48, J ), J = 1, 4 ) / 328, 1691, 166,
$                   65 /
DATA               ( MM( 49, J ), J = 1, 4 ) / 2861, 496, 466,
$                   1845 /
DATA               ( MM( 50, J ), J = 1, 4 ) / 1950, 1597, 4018,
$                   697 /
DATA               ( MM( 51, J ), J = 1, 4 ) / 617, 2394, 1399,
$                   3085 /
DATA               ( MM( 52, J ), J = 1, 4 ) / 2070, 2584, 190,
$                   3441 /
DATA               ( MM( 53, J ), J = 1, 4 ) / 3331, 1843, 2879,
$                   1573 /
DATA               ( MM( 54, J ), J = 1, 4 ) / 769, 336, 153,
$                   3689 /
DATA               ( MM( 55, J ), J = 1, 4 ) / 1558, 1472, 2320,
$                   2941 /
DATA               ( MM( 56, J ), J = 1, 4 ) / 2412, 2407, 18,
$                   929 /
DATA               ( MM( 57, J ), J = 1, 4 ) / 2800, 433, 712,
$                   533 /
DATA               ( MM( 58, J ), J = 1, 4 ) / 189, 2096, 2159,
$                   2841 /
DATA               ( MM( 59, J ), J = 1, 4 ) / 287, 1761, 2318,
$                   4077 /
DATA               ( MM( 60, J ), J = 1, 4 ) / 2045, 2810, 2091,
$                   721 /
DATA               ( MM( 61, J ), J = 1, 4 ) / 1227, 566, 3443,
$                   2821 /
DATA               ( MM( 62, J ), J = 1, 4 ) / 2838, 442, 1510,
$                   2249 /
DATA               ( MM( 63, J ), J = 1, 4 ) / 209, 41, 449,
$                   2397 /
DATA               ( MM( 64, J ), J = 1, 4 ) / 2770, 1238, 1956,
$                   2817 /
DATA               ( MM( 65, J ), J = 1, 4 ) / 3654, 1086, 2201,
$                   245 /
DATA               ( MM( 66, J ), J = 1, 4 ) / 3993, 603, 3137,
$                   1913 /
DATA               ( MM( 67, J ), J = 1, 4 ) / 192, 840, 3399,
$                   1997 /
DATA               ( MM( 68, J ), J = 1, 4 ) / 2253, 3168, 1321,
$                   3121 /
DATA               ( MM( 69, J ), J = 1, 4 ) / 3491, 1499, 2271,
$                   997 /
DATA               ( MM( 70, J ), J = 1, 4 ) / 2889, 1084, 3667,
$                   1833 /
DATA               ( MM( 71, J ), J = 1, 4 ) / 2857, 3438, 2703,
$                   2877 /
DATA               ( MM( 72, J ), J = 1, 4 ) / 2094, 2408, 629,
$                   1633 /
DATA               ( MM( 73, J ), J = 1, 4 ) / 1818, 1589, 2365,
$                   981 /
DATA               ( MM( 74, J ), J = 1, 4 ) / 688, 2391, 2431,
$                   2009 /
DATA               ( MM( 75, J ), J = 1, 4 ) / 1407, 288, 1113,
$                   941 /
DATA               ( MM( 76, J ), J = 1, 4 ) / 634, 26, 3922,
$                   2449 /
DATA               ( MM( 77, J ), J = 1, 4 ) / 3231, 512, 2554,
$                   197 /
DATA               ( MM( 78, J ), J = 1, 4 ) / 815, 1456, 184,
$                   2441 /
DATA               ( MM( 79, J ), J = 1, 4 ) / 3524, 171, 2099,
$                   285 /
DATA               ( MM( 80, J ), J = 1, 4 ) / 1914, 1677, 3228,
$                   1473 /
DATA               ( MM( 81, J ), J = 1, 4 ) / 516, 2657, 4012,
$                   2741 /
DATA               ( MM( 82, J ), J = 1, 4 ) / 164, 2270, 1921,
$                   3129 /
DATA               ( MM( 83, J ), J = 1, 4 ) / 303, 2587, 3452,
$                   909 /
DATA               ( MM( 84, J ), J = 1, 4 ) / 2144, 2961, 3901,
$                   2801 /
DATA               ( MM( 85, J ), J = 1, 4 ) / 3480, 1970, 572,
$                   421 /
DATA               ( MM( 86, J ), J = 1, 4 ) / 119, 1817, 3309,
$                   4073 /
DATA               ( MM( 87, J ), J = 1, 4 ) / 3357, 676, 3171,
$                   2813 /
DATA               ( MM( 88, J ), J = 1, 4 ) / 837, 1410, 817,
$                   2337 /
DATA               ( MM( 89, J ), J = 1, 4 ) / 2826, 3723, 3039,
$                   1429 /
DATA               ( MM( 90, J ), J = 1, 4 ) / 2332, 2803, 1696,
$                   1177 /
DATA               ( MM( 91, J ), J = 1, 4 ) / 2089, 3185, 1256,
$                   1901 /
DATA               ( MM( 92, J ), J = 1, 4 ) / 3780, 184, 3715,
$                   81 /
DATA               ( MM( 93, J ), J = 1, 4 ) / 1700, 663, 2077,
$                   1669 /
DATA               ( MM( 94, J ), J = 1, 4 ) / 3712, 499, 3019,
$                   2633 /
DATA               ( MM( 95, J ), J = 1, 4 ) / 150, 3784, 1497,
$                   2269 /
DATA               ( MM( 96, J ), J = 1, 4 ) / 2000, 1631, 1101,
$                   129 /
DATA               ( MM( 97, J ), J = 1, 4 ) / 3375, 1925, 717,
$                   1141 /
DATA               ( MM( 98, J ), J = 1, 4 ) / 1621, 3912, 51,
$                   249 /
DATA               ( MM( 99, J ), J = 1, 4 ) / 3090, 1398, 981,
$                   3917 /
DATA               ( MM( 100, J ), J = 1, 4 ) / 3765, 1349, 1978,
$                   2481 /
DATA               ( MM( 101, J ), J = 1, 4 ) / 1149, 1441, 1813,
$                   3941 /
DATA               ( MM( 102, J ), J = 1, 4 ) / 3146, 2224, 3881,
$                   2217 /
DATA               ( MM( 103, J ), J = 1, 4 ) / 33, 2411, 76,
$                   2749 /
DATA               ( MM( 104, J ), J = 1, 4 ) / 3082, 1907, 3846,
$                   3041 /
DATA               ( MM( 105, J ), J = 1, 4 ) / 2741, 3192, 3694,
$                   1877 /
DATA               ( MM( 106, J ), J = 1, 4 ) / 359, 2786, 1682,
$                   345 /
DATA               ( MM( 107, J ), J = 1, 4 ) / 3316, 382, 124,
$                   2861 /
DATA               ( MM( 108, J ), J = 1, 4 ) / 1749, 37, 1660,
$                   1809 /
DATA               ( MM( 109, J ), J = 1, 4 ) / 185, 759, 3997,
$                   3141 /
DATA               ( MM( 110, J ), J = 1, 4 ) / 2784, 2948, 479,
$                   2825 /
DATA               ( MM( 111, J ), J = 1, 4 ) / 2202, 1862, 1141,
$                   157 /
DATA               ( MM( 112, J ), J = 1, 4 ) / 2199, 3802, 886,
$                   2881 /
DATA               ( MM( 113, J ), J = 1, 4 ) / 1364, 2423, 3514,
$                   3637 /
DATA               ( MM( 114, J ), J = 1, 4 ) / 1244, 2051, 1301,
$                   1465 /
DATA               ( MM( 115, J ), J = 1, 4 ) / 2020, 2295, 3604,
$                   2829 /
DATA               ( MM( 116, J ), J = 1, 4 ) / 3160, 1332, 1888,
$                   2161 /
DATA               ( MM( 117, J ), J = 1, 4 ) / 2785, 1832, 1836,
$                   3365 /
DATA               ( MM( 118, J ), J = 1, 4 ) / 2772, 2405, 1990,
$                   361 /
DATA               ( MM( 119, J ), J = 1, 4 ) / 1217, 3638, 2058,
$                   2685 /
DATA               ( MM( 120, J ), J = 1, 4 ) / 1822, 3661, 692,
$                   3745 /
DATA               ( MM( 121, J ), J = 1, 4 ) / 1245, 327, 1194,
$                   2325 /
DATA               ( MM( 122, J ), J = 1, 4 ) / 2252, 3660, 20,
$                   3609 /
DATA               ( MM( 123, J ), J = 1, 4 ) / 3904, 716, 3285,
$                   3821 /
DATA               ( MM( 124, J ), J = 1, 4 ) / 2774, 1842, 2046,
$                   3537 /
DATA               ( MM( 125, J ), J = 1, 4 ) / 997, 3987, 2107,
$                   517 /
DATA               ( MM( 126, J ), J = 1, 4 ) / 2573, 1368, 3508,
$                   3017 /
DATA               ( MM( 127, J ), J = 1, 4 ) / 1148, 1848, 3525,
$                   2141 /
DATA               ( MM( 128, J ), J = 1, 4 ) / 545, 2366, 3801,
$                   1537 /
"""

lines = split(code, "\n"; keepempty=false)
for i = 1:2:length(lines)
  l1 = lines[i]
  l2 = lines[i+1] # next line  with last piece
  vals13str = split(l1, "/")[2] # values 1 to 3
  vals13 = parse.(Int, split(vals13str, ","; keepempty=false))
  vals14 = push!(vals13, parse(Int, split(l2[2:end])[1]))
  println("tuple(", join(vals14, ","), "),")
end