acer

32400 Reputation

29 Badges

19 years, 344 days
Ontario, Canada

Social Networks and Content at Maplesoft.com

MaplePrimes Activity


These are replies submitted by acer

@Christopher2222 I would say that this Document is an important example of current GUI performance.

If I download and run your latest revision, which is a Document with 2D Math input for the coded procedure, I see following.

I first split the proc defn out from the actual call to IsotopeTable(). That's key to seeing one big effect. It takes the Standard GUI 1.2 CPU seconds and 1.9 real-time (wall clock) seconds just to parse and print that proc code. That is...worrying. I call it a bug.

So I converted the code to 1D Maple Notation. And run it in a Worksheet (just because I find it easier to navigate and execute). Then the proc definition code runs lightning fast, in effectively zero time. Same for Standard as for Classic.

Then I call IsotopeTable(116), in all three variations: 2D code in Document, 1D code in Standard Worksheet, and 1D code in Classic GUI. It plots and displays a result, after a few seconds. The Classic GUI remains very smooth and responsive. The 1D Worksheet and the 2D Document instances both gets a bit sticky and doesn't scroll smoothly whenever the plot transitions from being "off screen" to "on screen" or is half-visible.

If I add more calls to IsotopeTable() then Classic stays smooth and responsive while Standard gets harder and harder to scoll and manipulate. I've heard it suggested that one of the problems seen here is that Standard plots are only ever probe-able. If there were a way to throw up a plots in Standard that only allowed as much post-display interaction as happens in Classic then maybe they would not bring the GUI to its knees so easily.

Here is a 1D editing of the code. Since most of the points in c are very small then it runs faster if order in which the conditionals are tested is reversed from the original code. Also the conditionals now do only one check for each elif (this follows up my earlier comment). You don't need to mess with op(b) just to get the dimensions of b, and note the difference between op(1,b) and op(b)[1]. And you only need compute the dimensions of b once. And now it utilizes `uses`. It's not very much faster, as a lot of time is still spent inside listdensityplot. But maybe it looks a little cleaner.

Try running it in Classic and a Worksheet, to see the differences. And compare with the earlier 2D version that you uploaded (for which you'd have to insert the time() and time[real]() calls in order to measure just the 2D proc parsing and printing portion.

restart:st, str := time(), time[real]():
 IsotopeTable := proc(elements)
 local a,b,c,d,i,j,ja,jb,jc,jd,sy,sd,sh,sm,Isoj,nIsoj,
       maxopopb1,maxopopb2;
 uses ScientificConstants, plots;
 a := table();
 for j to elements do
    Isoj := GetIsotopes(element = j);
    nIsoj := nops([Isoj]);
    if nIsoj <> 1 then
       for i from op(Isoj[1]) to op(Isoj[nIsoj]) do
          try
             a[i, j] := convert(rhs(rhs(GetElement([j, i], halflife)[2])[1]),
                                'units',
                                rhs(rhs(GetElement([j, i], halflife)[2])[3]), s)
          catch:
             try
                if is(GetElement([j, i], abundance) <> 0) then
                   a[i, j] := 0.10e12
                end if
             catch:
                a[i, j] := 1
             end try
          end try
       end do
    elif nIsoj = 1 then
       i := op(Isoj);
       try
          a[i, j] := convert(rhs(rhs(GetElement([j, i], halflife)[2])[1]),
                             'units',
                             rhs(rhs(GetElement([j, i], halflife)[2])[3]), s)
       catch:
          try
             if is(GetElement([j, i], abundance) <> 0) then
                a[i, j] := 0.10e12
             end if
          catch:
             a[i, j] := 1
          end try
       end try
    end if
 end do;
 b := Array(convert(a,array), datatype=float[8]);
 
 # We could alternatively use rtable_dims(b)
 maxopopb1 := op([2,1,2],b); #max(op(op(b)[1]));
 maxopopb2 := op([2,2,2],b); #max(op(op(b)[2]));
 
 c := Array(1 .. maxopopb1, 1 .. maxopopb2);
 sy := convert(1, 'units', year, second);
 sd := convert(1, 'units', day, second);
 sh := 3600; sm := 60;
 
 for i to maxopopb1 do
 for j to maxopopb2 do
 if b[i, j] = 0.0 then c[i, j] := 13
 elif b[i, j] < 0.1e-2 then c[i, j] := 3
 elif b[i, j] < 0.1e-1 then c[i, j] := 4
 elif b[i, j] < .1 then c[i, j] := 5
 elif b[i, j] < 1 then c[i, j] := 7
 elif b[i, j] < sm then c[i, j] := 8
 elif b[i, j] < sh then c[i, j] := 10
 elif b[i, j] < sd then c[i, j] := 15
 elif b[i, j] < sy then c[i, j] := 18
 else c[i, j] := 20 # b[i, j] >= sy
 end if end do end do;
 
 ja := [13, 3, 4, 5, 6, 7, 8, 10, 15, 18, 20];
 jb := [seq(plottools[rectangle](
          [maxopopb1+5, (1/11)*maxopopb2*i-(1/11)*maxopopb2],
          [maxopopb1+15, (1/11)*maxopopb2*i],
                                 color = COLOR(HUE, (1/20)*op(i, ja))),
            i = 1 .. nops(ja))];
 jc := maxopopb1+20;
 jd := textplot({
   [jc, (1/2)*maxopopb2, "unknown"], [jc, (1/22)*maxopopb2, "---"],
   [jc, (3/22)*maxopopb2, "0 - 0.001"], [jc, (5/22)*maxopopb2, "0.001 - 0.01"],
   [jc, (7/22)*maxopopb2, "0.01 - 0.1"], [jc, (9/22)*maxopopb2, "0.1 - 1"],
   [jc, (13/22)*maxopopb2, "seconds"], [jc, (15/22)*maxopopb2, "minutes"],
   [jc, (17/22)*maxopopb2, "hours"], [jc, (19/22)*maxopopb2, "days"],
   [jc, (21/22)*maxopopb2, "years"]}, align = right);
 d := listdensityplot(c, colorstyle = HUE, range = 0 .. 20,
         title = ["Half-Life Table of the Isotopes",
         font = [TIMES, BOLD, 20]],
         labels = ["Atomic Mass \n            A",
                   "Atomic Number \n              Z"]);
 display(d, jb, jd);
 end proc:
 time()-st, time[real]()-str;
 st,ba:=time(),kernelopts(bytesalloc):
    IsotopeTable(116);
 time()-st,kernelopts(bytesalloc)-ba;

acer

That is quite fun. Please don't mind if I make a few really minor suggestions.

Is there an `option remember` on (or under) convert/units? I ask because the code is converting each of 1 {year,day,hour,minute} into seconds. And it does that inside a double (nested) loop, hence repeating each of those conversions tens of thousands of times for your original size. It would look much cleaner (and be more good efficient practice) if instead it computed those values just once outside the loops, assigning to a few locals say.

Is it necessary to make `c` a table rather than an outright Array? Is it necessary to call plots[display] when forming `jb`?

In the loop in which you assign entries of `c`, using if..elif.. could it be that most all the conditions are unnecessarily being tested twice? I mean, consider the fact that you've ordered all the conditionals (great). So if b[i,j] is found to not be in a given range (b[i,j]<A and b[i,j]>=B) then why subsequently retest b[i,j]<B, given that you've already sorted the conditionals so that A>B>C, etc?

It's not great form to load packages outside the proc definition (if members are used in that proc) when it can be done some much more reliably and cleanly inside the proc body with the `uses` construct.

Maybe you could label the axes more helpfully, for the non-expert?

acer

It might do with some improvement.

If I may make some suggestions:

Use the modern return statement, not the RETURN() routine which has been out of fashion for about 10 years.

It's not a good idea to load a package like LinearAlgebra outside of the proc which relies on it. Use the modern `use` or `uses` facilities instead, or just utilize the full long command names in the proc body.

It's a bad idea to unprotect D (even if it's not necessary and you don't end up assigning to it, like in your code).

The code is missing the semicolon or colon terminatior in the proc's penultimate line, making it a syntax error. And the RETURN function call is missing its close-bracket, also a syntax error.

The Pinv and P locals are not strictly necessary, as they get assigned but then only used the once, in the very same line. So that line could just utilize MatrixInverse(Evecs) and Evecs in the line, and save the pair of locals.

The routine posted computes Pinv and then throws it away. That's a waste. Either use the eigenvalues to produce the diagonal factor, or return Pinv. Or normalize P so that (provided it's columns are linearly independent and a full span. (I suggest testing for diagonalizability in some other way, pls see below.)

A good routine for this would distinguish between the exact symbolic and the pure floating-point cases.

I don't like the use of computing Pinv just so that one can test the eigenvectors for linear independence & full span. I'd suggest instead to use JordanForm in the exact or symbolic case (with a simple test that the J is of type 'Matrix'('diagonal')) or to use SchurForm in the pure float case. For the float case it may depend on one's needs and goals, since diagonalizability is another ballgame there. (I also doub't I'd compute P^(-1).M.P when the diagonal factor is available by the eigenvalues -- it's an overly costly way to test.)

I'm not trying to sound harsh, but I don't quite see the point of such a new routine. What would it given that is either not a) available by other direct means, or b) not best-advised?

acer

The rather simple reason why the repetitions work (and of which length) came to me while waiting at the supermarket checkout. Isn't that a good reason to do math, so as to avoid being bored while waiting in queues?

acer

The rather simple reason why the repetitions work (and of which length) came to me while waiting at the supermarket checkout. Isn't that a good reason to do math, so as to avoid being bored while waiting in queues?

acer

It worked fine for me in 12.02, Windows 32bit  (XP).

acer

It worked fine for me in 12.02, Windows 32bit  (XP).

acer

I had written myself a similar procedure, this evening. But then I noticed that it had an unfortunate behaviour, shared with your submission. Namely what happens to `z` below,

> restart;
> RemoveAllAssumptions:=proc() local A,Ls,L,Lc,indx;
>    Ls:=map(convert,[anames('user')],string);
>    L:=ListTools:-Enumerate([anames('user')]);
>    Lc:=select(hastype,eval(L),`local`);
>    if Lc=[] then NULL
>    else
>       indx:=map2(op,1,Lc);
>       A:=[seq(Ls[i],i=indx)];
>       unassign(op(map(parse,A)));
>    end if
> end proc:

> z:=y;
                               y

> assume(x>0);

> y:=x;
                               x

> z;
                               x

> RemoveAllAssumptions();

> z;
                               z

> y;
                               y

I could see that one might reasonably argue that, after RemoveAllAssumptions is called above, y is unassigned. (How would the procedure know, otherwise, whether to make y take as some new value the global x, or some procedure's escaped local x, etc?) But it doesn't look right to me that z gets unassigned in the above example.

I'm sure that a robust version, which somehow preserves the desired previous assignments, can be written. But offhand I could only see kludgy ways to handle some stranger cases.

Of course, your routine may suffice for the Orginal Poster. (Although, maybe his assumed names are all simply indexed and some loop would also do.)

acer

I had written myself a similar procedure, this evening. But then I noticed that it had an unfortunate behaviour, shared with your submission. Namely what happens to `z` below,

> restart;
> RemoveAllAssumptions:=proc() local A,Ls,L,Lc,indx;
>    Ls:=map(convert,[anames('user')],string);
>    L:=ListTools:-Enumerate([anames('user')]);
>    Lc:=select(hastype,eval(L),`local`);
>    if Lc=[] then NULL
>    else
>       indx:=map2(op,1,Lc);
>       A:=[seq(Ls[i],i=indx)];
>       unassign(op(map(parse,A)));
>    end if
> end proc:

> z:=y;
                               y

> assume(x>0);

> y:=x;
                               x

> z;
                               x

> RemoveAllAssumptions();

> z;
                               z

> y;
                               y

I could see that one might reasonably argue that, after RemoveAllAssumptions is called above, y is unassigned. (How would the procedure know, otherwise, whether to make y take as some new value the global x, or some procedure's escaped local x, etc?) But it doesn't look right to me that z gets unassigned in the above example.

I'm sure that a robust version, which somehow preserves the desired previous assignments, can be written. But offhand I could only see kludgy ways to handle some stranger cases.

Of course, your routine may suffice for the Orginal Poster. (Although, maybe his assumed names are all simply indexed and some loop would also do.)

acer

One useful technique is to re-use float[8] (or other hardware datatype) Matrix/Vector/Array workspaces.

This can save on the total memory allocation needed.

There is a useful command, ArrayTools:-Fill, which can be utilized to zero-out an existing Matrix/Vector/Array.

Things can get more complicated when the individual tasks require varying sizes of workspace. In such cases in can still be useful and efficient to re-use a single instance of the workspace. Of course, in such a scenario the code may have to handle the fact that the workspace is larger than necessary (see Historical Note below).

When using separate workspace objects then the memory performance and usage can depend on whether or not one creates & unassigns one's workspaces in order of descending size. The following comparison illustrates that the worse scenario is to create then in order of ascending size, because doing so can involve much more total memory allocation (due to the mentioned problem of "memory fragmentation").

Case of generating separate workspaces, in order of increaing size,

> restart:
> for i from 1 to 10 do
>   M:=Matrix(2000+i*100,datatype=float[8]):
>   # some task involving a clean M
>   M:='M': dummy1: dummy2: dummy3:
>   gc():
> end do:
> kernelopts(bytesalloc);
                           527992392

Case of generating separate workspaces, in order of decreasing size,

> restart:
> for i from 1 to 10 do
>   M:=Matrix(3000-i*100,datatype=float[8]):
>   # some task involving a clean M
>   M:='M': dummy1: dummy2: dummy3:
>   gc():
> end do:
kernelopts(bytesalloc);
                            68144960

Case of re-using single (largest required) workspace,

> restart:
> M:=Matrix(2900,datatype=float[8]): # largest needed
> for i from 1 to 10 do
>   # some task involving a clean M
>   ArrayTools:-Fill(2900*2900,0.0,M);
>   # no `gc` needed to clear out M!
> end do:
> kernelopts(bytesalloc);
                            68144960

Historical Note: Old-timer Fortran programmers would often write code in which single larger-than-always-necessary blocks of memory were allocated up front. The size would depend on the largest size that would ever be required. That large full block would be used and re-used within the individual tasks. Indeed, a lot of classic code like LAPACK is implemented in this style. Assuming Fortran-order storage, many LAPACK functions have associated with each array argument a triple of scalars to denote the size. For example, 2D array A would have `lda`, `m`, and `n'. The `lda` meant the leading dimension of A, or in other words the height of columns since `Fortran order` means `stored by column`. The `n` meant number of columns and could be used naturally. But the `m` meant the height of the data for the given task, and its value could be much less than `lda`. In this scheme the data for each individual task could be stored and accessed in the upper portion of the (much larger than necessary) full array block. The functions were written to know how to use `lda` and `m` as appropriate, according to the stride needed to jump from one column to another of the actual data subblock actually in use. One would use `m` as the number of entries of any particular column, but the code would use `lda` as the number of places to jump in order to walk along a particular row.

  | A[1,1]  ..  A[1,j]  ..   A[1,n] |
  |   .           .            .    |
  |   .           .            .    |
  | A[i,1]  ..  A[i,j]  ..   A[i,n] |
  |   .           .            .    |
  |   .           .            .    |
  | A[m,1]  ..  A[m,j]  ..   A[m,n] |
  |   .                             |
  |   .                             |
  |   .                             |
  | A[lda,1]  ..        .. A[lda,n] |

In the above example, A would declared as size (lda,n).

acer

One useful technique is to re-use float[8] (or other hardware datatype) Matrix/Vector/Array workspaces.

This can save on the total memory allocation needed.

There is a useful command, ArrayTools:-Fill, which can be utilized to zero-out an existing Matrix/Vector/Array.

Things can get more complicated when the individual tasks require varying sizes of workspace. In such cases in can still be useful and efficient to re-use a single instance of the workspace. Of course, in such a scenario the code may have to handle the fact that the workspace is larger than necessary (see Historical Note below).

When using separate workspace objects then the memory performance and usage can depend on whether or not one creates & unassigns one's workspaces in order of descending size. The following comparison illustrates that the worse scenario is to create then in order of ascending size, because doing so can involve much more total memory allocation (due to the mentioned problem of "memory fragmentation").

Case of generating separate workspaces, in order of increaing size,

> restart:
> for i from 1 to 10 do
>   M:=Matrix(2000+i*100,datatype=float[8]):
>   # some task involving a clean M
>   M:='M': dummy1: dummy2: dummy3:
>   gc():
> end do:
> kernelopts(bytesalloc);
                           527992392

Case of generating separate workspaces, in order of decreasing size,

> restart:
> for i from 1 to 10 do
>   M:=Matrix(3000-i*100,datatype=float[8]):
>   # some task involving a clean M
>   M:='M': dummy1: dummy2: dummy3:
>   gc():
> end do:
kernelopts(bytesalloc);
                            68144960

Case of re-using single (largest required) workspace,

> restart:
> M:=Matrix(2900,datatype=float[8]): # largest needed
> for i from 1 to 10 do
>   # some task involving a clean M
>   ArrayTools:-Fill(2900*2900,0.0,M);
>   # no `gc` needed to clear out M!
> end do:
> kernelopts(bytesalloc);
                            68144960

Historical Note: Old-timer Fortran programmers would often write code in which single larger-than-always-necessary blocks of memory were allocated up front. The size would depend on the largest size that would ever be required. That large full block would be used and re-used within the individual tasks. Indeed, a lot of classic code like LAPACK is implemented in this style. Assuming Fortran-order storage, many LAPACK functions have associated with each array argument a triple of scalars to denote the size. For example, 2D array A would have `lda`, `m`, and `n'. The `lda` meant the leading dimension of A, or in other words the height of columns since `Fortran order` means `stored by column`. The `n` meant number of columns and could be used naturally. But the `m` meant the height of the data for the given task, and its value could be much less than `lda`. In this scheme the data for each individual task could be stored and accessed in the upper portion of the (much larger than necessary) full array block. The functions were written to know how to use `lda` and `m` as appropriate, according to the stride needed to jump from one column to another of the actual data subblock actually in use. One would use `m` as the number of entries of any particular column, but the code would use `lda` as the number of places to jump in order to walk along a particular row.

  | A[1,1]  ..  A[1,j]  ..   A[1,n] |
  |   .           .            .    |
  |   .           .            .    |
  | A[i,1]  ..  A[i,j]  ..   A[i,n] |
  |   .           .            .    |
  |   .           .            .    |
  | A[m,1]  ..  A[m,j]  ..   A[m,n] |
  |   .                             |
  |   .                             |
  |   .                             |
  | A[lda,1]  ..        .. A[lda,n] |

In the above example, A would declared as size (lda,n).

acer

Now I'm all keen to know whether the answer is not just something obvious like -315.19 kJ/mol

Please let us know, when you find out.

acer

Now I'm all keen to know whether the answer is not just something obvious like -315.19 kJ/mol

Please let us know, when you find out.

acer

@PatrickT Your large test is no longer appropriate, in the sense that the inputs are floats. LinearAlgebra converts that to float[8] and bang, is done. Anyone can take the pure loop version, Compile it after typing it, and get similar. Or they could write a precompiled external routine for this (like LinearAlgebra has). There's nothing wrong with floats, but it's an apple and oranges situation.

It shouldn't be surprising that the result object's size may be very different according to whether it is datatype=anything or not. It makes a difference1

The versions users posted are all (obviously, I say) designed for the exact case (like your symbolic example).

Also, did you try the new submission KPKP above, on your example that for which you wrote that nothing else succeeded? On my i7 it took 7 secs and the memory fit in about 250Mb Ithink it was. Of course. it too should be altered for float/float[8].

An appropriate large test/comparison of the original posted codes would be exact or symbolic.also, you could reasonably use routine `KP` in an above response since that is like LinearAlgebra's exact code in KroneckerProduct.

Alternatively, large float tests could be done on new submissions tailored for that type. Apples to apples. Oranges to oranges.

You could also use kernelopts(bytesalloc) to compute a memory difference, ie an allocation increase incurred for each operation.

@hirnyk Yes, it is important for several reasons.

  • Just because you cannot imagine a situation where highly repeated use of a low-level function is necessary does not in any way mean that no situation would likely ever require it. There are many more programming situations than you or I or anyone else here can imagine. That is why programming is so important to Maple (and, incidentally, why it should be treated as much, much more important that program-free "Clickable Math").
  • The procedure in question is simple and basic. So the issue of how it may be optimized is very general, and the answers to that in turn are very likely broadly applicable. Observe how the very fastest code solutions to this question rely on kernel built-ins and inplace semantics. Observe how slower code solutions mostly involve a lot of temporary garbage production. There are important, re-usable lessons to be learnt from all this.
  • As Patrick expressed, such comparisons are also useful for learning Maple.
First 447 448 449 450 451 452 453 Last Page 449 of 592