(2) |
For E(Ln,i), we have the different values:
The values considered in computing the energy are:
and are destabilizing energies (>0) whereas and are stabilizing (<0). The second algorithm operates in a similar manner to the first, except that instead of having only one matrix Energy and one matrix Previous, we have two pairs (one corresponding to S, and one corresponding to L): Energy, Loop, Prev_energy and Prev_loop. Moreover, Prev_energy and Prev_loop are now 3-dimensional arrays; that means that, for given values of n and i, Prev_energy[n, i] and Prev_loop[n, i] are two arrays of pointers: this is to handle the fact that there may be several solutions. Note also that the pointers in Prev_loop are now represented by a couple of integers: this is due to the large number of possible solutions at each step. For given values of n and i, the program first computes E(Ln,i) and then E(Sn,i) (the latter requiring known values of the former). These prev-matrices are a compact way of storing all the solutions of minimal energy. Actually they form a DAG (directed acyclic graph). Note that although this DAG contains all the solutions, listing them could very well be exponential.
The above program uses about the same approach as in the previous algorithm except that there is an additional function: compute_loop that takes into account all the additional elements mentionned in section 2.1.
If transition = 0, a random element of the array previous_energy[length, i] is considered; according to its value, 1 or 2 may occur: 1 indicates that the procedure undergoes a loop mode, 2 means the considered part of the sequence is split in two parts.
If transition = 1,then, 3, 4 or 5 occurs: 3 corresponds to a hairpin loop, 4 to a multiloop, and 5 to a bulge or an interior loop.
procedure find_couple(length, i , seq, transition)
begin
ifthen
begin
if transition = 0 then
begin
k = previous_energy[length, i, random]
if k = 0 then find_couple(length,
I,
seq, 1)
(1)
else
begin
find_couple(k, i, seq, 0)
find_couple(length-k, i+k, seq, 0)
(2)
end
end
else
begin
create_couple(i,i+length-1)
(p,q) = previous_loop[length, i, random]
if p=-1 then
begin
if q=-1 then end
(3)
else
begin
find_couple(q, i+1, seq, 0)
find_couple(n-q-2, i+q+1, seq, 0)
(4)
end
end
else find_couple(length-p-q,
i+p, 1)
(5)
end
end
end
In this procedure, only one random solution is traced. Indeed, all the solutions are contained in the two matrices prev_energy and prev_loop, but it would take an exponential time to list them all. This solution is obtained by using the random numbers that are generated by the function random.
This graph corresponds to the following secondary structure:
diagonal[length, i, l] = with l corresponding to p+q.
This matrix is computed on the fly in the main loop (on length and i): l may vary from 1 to n, so the complexity is O(n). Once this matrix is known, the formula for the interior loop becomes: . The complexity time becomes O(n). So, instead of having a n2-complexity, we have a single loop and a n-complex function within two embedded loops; this reduces the complexity from O(n4) to O(n3).
Actually, the length-index of diagonal is not even needed since the recursion depth is 1: thus we may reduce the storage to diagonal1[i,l] and diagonal2[i,l]. Of course, a prev_diagonal matrix is parallel to diagonal, so that p and q may be recovered, and solutions traced.
The other function find_couple has the same complexity as in the previous algorithm: O(n2). So, the highest complexity O(n3) prevails.
Sometimes, a user wishes to determine the best secondary structure according to certain requirements. For instance he may want to have a helix between nucleotides p and q. To make this task simpler, a linear notation for displaying the structure is introduced:
This sequence has 11 different solutions :
The following table presents the actual times in seconds for the SGI-Onyx
machine
200 | 400 | 600 | 800 | 1000 | |
1 | 3.3 | 24.9 | 86.7 | 216.4 | 461.1 |
2 | 3.3 | 17.9 | 55.1 | 130.6 | 244.1 |
5 | 3.3 | 13.4 | 35.1 | 74.4 | 141.7 |
8 | 3.3 | 12.9 | 31.3 | 63.2 | 111.1 |
The algorithm has cubic complexity, so the execution time may be approximated by a polynomial: .
When dealing with nproc parallel processors, the execution time Tnprocmay be approximated by: . Indeed, the parallel instructions are executed within the second loop of the algorithm, this means that they have an effect on the quadratic and cubic coefficients of the polynomial.
The following curves represents: :
The values obtained for have been estimated by polynomial interpolation. The above figure clearly shows that for larger n and nproc, the results approximate the value 1.0 which is the expected asymptotic value of .
Therefore, the model is satisfactory, but not accurate enough to estimate the speed for larger numbers of processors. Let us make a more accurate analysis of the algorithm: inside the two outermost loops, we have the call of a function and a loop. The execution time is: C*length+D.
This component is executed (n-length)
times; therefore the execution time becomes: (n-length)(C*length+D)+E,
the multiplicative constant being included in C and D. In
addition, one should take into account an embedded n-loop. So, the
final execution time is:
.
Again, the multiplicative constant is included in C, D
and E. The parallel part operates on the second loop; the approximate
parallel execution time becomes:
These results are much more accurate for the larger lengths (above 800),
the fact that the estimates have an odd behaviour for lengths under 800
is due to the fact that the above formula remains an approximation; the
errors are reduced as n increases. We may now make estimates for
the algorithm's efficiency as the number of processors is increased:
These curves show that for a string of length n=1000, the number of processors needed to obtain significant speed-up is no larger than 10. Nevertheless, when n=4000, gains are possible beyond 50 processors.