PREP 2013 Question Authoring - Archived

Matrix Mathobjects

Matrix Mathobjects

by Michael Doob -
Number of replies: 10
I have a few questions about matrices and mathobjects:

(1) Matrix allocation
I raised the question in the chat room of the dynamic allocation of matrices, as with the Hilbert matrix. The following seems to work OK:

   $nrows=6; $ncols=5;

   sub hilbert {
     1/($_[0]+$_[1]-1);
     }

   @W = (); 
   for my $j (1..$nrows) {
      $V = [];
      for my $i (1..$ncols) {push $V, hilbert($i,$j)};
      push @W, $V;
   }

You guys are the perl gurus and I'm way down the totem pole. Is there a better/cleaner way?

(2) Fractional mathobjects
In (1) the matrix entries are naturally fractions, and the matrix can be formed using "contextFraction.pl" and 

Context("Fraction");

in the obvious way:

   sub hilbert {
     Fraction(1,$_[0]+$_[1]-1);
     }   

This works perfectly, but the matrix can not be made into a matrix mathobject since these objects want real entries, and so the nice answer-checking properties (ans_array) of math objects are unavailable. Of course the matrix could be made into a math object by forming a new matrix with type casting: 

   sub hilbert {
     Real(Fraction(1,$_[0]+$_[1]-1));
     }

but then you lose the ability to put constraints on the fractions that are available in the Fractions context.

Is there any way around all of this? Maybe a new math object is needed to do this.

(3) I have essentially (weasel-word!) finished the routines to put an arbitrary matrix in reduced row echelon form. Is there any way to add local methods?

Cheers,
Michael 
In reply to Michael Doob

Re: Matrix Mathobjects

by Michael Gage -
Hi Michael,

I can give you some preliminary feedback:

(1) looks fine for the procedural approach.  You can also make it look more functional using perl's map function:  http://www.misc-perl-info.com/perl-map.html if you prefer.

e.g.   @array = map {1/$_} (1..5);
should give you (1/1, 1/2,1/3,1//4,1/5);
$array_ref = [ map {1/$_} (1..5) ];
gives you an array reference, looks a bit like python syntax.
All these methods work fine.

(2) This will be more difficult but doable.  We do allow complex values in matrices.  Davide will have an idea of how difficult adding Fractions would be.  Matrices were implemented in two different ways CPAN's Math::MatrixReal model (which I doctored slightly) and MathObjects.   Davide has done a lot to make these work together so that we get a superset of the capabilities of each, but there is more that could be done. (Look at http://search.cpan.org/~leto/Math-MatrixReal-2.09/lib/Math/MatrixReal.pm -- I believe that things like LR factorization are already possible.)  Allowing a fraction type has a natural use in teaching Gauss elimination so it would be worth some effort. Incidentally my own feeling (influenced by teaching from Strang's book) is that teaching LR factorization along with Gauss elimination is a good idea -- it gives a more profound understanding of matrix structure without a lot of extra work.  I would find it nice if your routines could allow for that as well.

Relevant files are:
https://github.com/openwebwork/pg/blob/master/macros/PGmorematrixmacros.pl
https://github.com/openwebwork/pg/blob/master/lib/Matrix.pm
and  https://github.com/openwebwork/pg/blob/master/lib/MatrixReal1.pm

(3)You can put routines in a macro file  labeled something like 
myMacros.pl  and place it in the templates/macros directory using
the File Manager.  That directory is searched first when you call
loadMacros()     One thing to be aware of.  The .pl files are treated as perl
files not as pg files   so for example if you include any tex commands you will
have to use formats such as  "\\int x^2 dx" to escape the backslash.  Likewise you use  \@array to create a reference to an array in a .pl file, but need to use 
~~@array to create the reference in a  .pg  file.  This means you might have to rewrite some of your subroutines slightly when they are moved to the .pl file from a .pg file.

The thinking is that you use TeX frequently in a .pg file (hence backslashes are automatically escaped for you) while in a macro  .pl file you use more perl and less TeX.  I still get caught by this -- even after all of these years. :-)

I hope this answers your question about local methods.

Hope this helps.

Mike

In reply to Michael Doob

Re: Matrix Mathobjects

by Davide Cervone -
Mike has already commented on (1) and (3), so let me say a little about (2). You can have Fractions in Matrices The best way to do it is to select the Fraction context (or Fraction-NoDecimals if you prefer), and enable Matrices in that context. You do that by using
    Context()->parens->set("[" => {formMatrix => 1});
This says that [...] is allowed to be used to form matrices, and that is needed for the parsing of student answers as matrices. Although you can always use Matrix() as a problem author, student answers can't produce matrices unless the context has this formMatrix value set. This is needed even if you are using ans_array as the answer array converts the student answer to use this notation and then parses it. WIthout that, you will get a message something like "Your answer isn't a matrix, it looks like a list of lists of numbers" or some such thing.

Without seeing the actual code you are using, that is the best I can do.

Davide

In reply to Davide Cervone

Re: Matrix Mathobjects

by Michael Doob -
Wow, Davide! A one-line solutions that works perfectly. Who would have thought?

Bravo and thanks.

Cheers,
Michael
In reply to Davide Cervone

Re: Matrix Mathobjects

by Michael Doob -
I've appended a small code sample. I've made it as small as I could and still illustrate my further questions. Usual excuses: unpolished, undocumented, first draft, yada, yada....

(1) Your one-line addition from yesterday seems to work perfectly, Davide. Thanks again.

(2) If the lines 

@W =(
 [Fraction(1,1),Fraction(2,1),Fraction(3,1),Fraction(4,1)],
 [Fraction(3,1),Fraction(3,1),Fraction(4,1),Fraction(5,1)], 
 [Fraction(-1,1),Fraction(-3,1),Fraction(2,1),Fraction(4,1)]
);

are replaced by the line

@W=([1,2,3,4],[3,3,4,5],[-1,-3,2,4]);

the entries are reals rather than fractions. Given that we are within a
Context("Fraction");
this surprised me. Should it have?

(3) if 
   $i=2;
   $j=Fraction(3,2);
What is the data type of $i*$j?

(4) Do you normally declare the context within a subroutine? 

(5) Not a question, but just a thank you for the prompt replies. 

-------------------- code sample ---------------------
##DESCRIPTION
##  Find the reduced row echelon form of a random matrix
##ENDDESCRIPTION

## Date('2013/06/18')
## Author('Michael Doob')
## Institution('University of Manitoba')

########################################################################

DOCUMENT();      

loadMacros(
   "PGstandard.pl",     # Standard macros for PG language
   "MathObjects.pl",
   "contextFraction.pl",
   "PGcourse.pl",      # Customization file for the course
);

# Print problem number and point value (weight) for the problem

TEXT(beginproblem());

# Show which answers are correct and which ones are incorrect
$showPartialCorrectAnswers = 1;

###############################################
#                   Setup                     #
###############################################

# From Davide Cervone (2013/06/17)
Context()->parens->set("[" => {formMatrix => 1});

Context("Numeric");
$nrows=6; $ncols=6;

Context("Fraction");

sub hilbert {
  Fraction(1,$_[0]+$_[1]-1);
  }

@H = (); 
for my $j (1..$nrows) {
   $V = [];
   for my $i (1..$ncols) {push $V, hilbert($i,$j)};
   push @H, $V;
}

$H = Matrix(@H);

### Here comes the test (non random) matrix 
$nrows=3; $ncols=4;
@W =(
   [Fraction(1,1),Fraction(2,1),Fraction(3,1),Fraction(4,1)],
   [Fraction(3,1),Fraction(3,1),Fraction(4,1),Fraction(5,1)], 
   [Fraction(-1,1),Fraction(-3,1),Fraction(2,1),Fraction(4,1)]
);


$M = Matrix(@W); # Keep a copy of original matrix

##############  Start: Elementary row operations subroutines #######

# rowswap(i,j) swaps rows i and j in the matrix $W
#   R_i <-> R_j
sub rowsswap { 
   my $i = $_[0];
   my $j = $_[1];
   my $tmp;
   for my $k (0..$ncols-1) {
      $tmp = $W[$i][$k];
      $W[$i][$k] = $W[$j][$k];
      $W[$j][$k] = $tmp;
   }
}

# rowmult(i,lambda) multiplies row i in the matrix $W by lambda
#   R_i <- \lambda R_i
sub rowmult {
   my $i = $_[0];
   my $lambda = $_[1];
   for my $k (0..$ncols-1) {
      $W[$i][$k] = $lambda*$W[$i][$k];
   }
}


# rowadd(i,j,lambda) adds lambda*(row j) to row i in the matrix $W 
#      R_i <- R_i+\lambda R_j
sub rowadd{
   my $i = $_[0];
   my $j = $_[1];
   my $lambda = $_[2];
   for my $k (0..$ncols-1) {
      $W[$i][$k] = $W[$i][$k] + $lambda*$W[$j][$k];
   }

##############  End: Elementary row operations subroutines #########



############### start: put $W in reduced row echelon form ###############
my $ipivot = 0; my $jpivot = 0;  
   while ($ipivot < $nrows && $jpivot < $ncols) {
      if ($W[$ipivot][$jpivot] != 0) {
         rowmult($ipivot, 1/$W[$ipivot][$jpivot]);
         for my $i (0..$nrows-1) {
             if ($i == $ipivot) { next;}
             rowadd($i, $ipivot, -$W[$i][$jpivot]);
          }
      $ipivot++; 
      } else {
      for my $i ($ipivot..$nrows-1) {
         if ($W[$i][$jpivot] != 0) {
            rowsswap($i,$ipivot);
            $jpivot--; # make up for the unneeded $jpivot++ at the end
            next;
            }
         }
      }
      $jpivot++; 
   }
############### finish: put $W in reduced row echelon form ###############


$R = Matrix(@W);

###############################################
#   Text of problem plus the answer box(es)   #
###############################################

Context()->texStrings;
BEGIN_TEXT

Here is the Hilbert matrix:
\[ H  =$H \]

Here is the original matrix:
\[ M = $M \]

$PAR
Here's the reduced row echelon form:

\[ R = $R \]
END_TEXT
Context()->normalStrings;

###############################################
#                  Answers                    #
###############################################



###############################################
#                 Solutions                   #
###############################################
Context()->texStrings;
BEGIN_SOLUTION
END_SOLUTION
ENDDOCUMENT();

In reply to Michael Doob

Re: Matrix Mathobjects

by Davide Cervone -
For (2), The Fraction context doesn't force everything to be fractions, it allows things to be fractions. You can still have reals and complexes and other types of objects in that context. There is also no problem in mixing reals with fractions, especially when the reals are integers. The integers will be promoted to Fraction objects when combined with a Fraction. (And a Fraction will be converted to a Real if combined with a Real that is not an integer). So a Matrix with some Real and some Fraction entries is perfectly OK.

In terms of student answers, a matrix with mixed Reals and Fractions also is OK. Each entry in the array of student answers will be computed as a Real or Fraction (depending on whether / is used), and combined into the final student Matrix. Reals and Fractions can be compared without error messages, so this can be compared to the correct Matrix just fine.

You may wish to use the Fraction-NoDecimals if you want to force students to enter fractions, but you could go either way.

Finally, note that

    @W=([1,2,3,4],[3,3,4,5],[-1,-3,2,4]);
is not actually using any MathObject yet (it is only when you do $R = Matrix(@W) that you get MathObjects as the entries. So technically, these are Perl reals, not MathObject Reals, and certainly wouldn't be Fractions at this point, since there are no MathObject constructors or Compute() calls.

For (3), the result will be a Fraction. Combining Fractions with integer-values Reals (or Perl reals) will result in Fraction objects.

For (4), The only reason to declare the Context within a subroutine is if you need to change it. The Context is global, so the subroutine can use it just like the outer code, and if you set the Context within a subroutine, it is set globally and will remain in effect after the subroutine ends. Note also that setting the context will cause any changes you have made to be lost (even if you change to same context).

Hope that helps.

Davide

In reply to Michael Doob

Re: Matrix Mathobjects

by Davide Cervone -
I have some comments about your code that might help you out. Most of cosmetic, but a couple are important.

  1. First, the line
        Context()->parens->set("[" => {formMatrix => 1});
    
    must come after selecting the Fraction context, so it should be
       Context("Fraction");
       Context()->parens->set("[" => {formMatrix => 1});
    
    Also, there is no need to select Numeric context since you don't actually create any MathObjects while in that context (the $nrows and $ncols are Perl integers, not MathObjects).

As discussed above, you don't need to make integers into fractions, though you can if you want, so
    @W=([1,2,3,4],[3,3,4,5],[-1,-3,2,4]);
is fine. (The reason this didn't work for you is actually later in the code; see below.)

Rather than using
    my $i = $_[0];
    my $j = $_[1];
you could use the shorter
    my ($i,$j) = @_;
to get the first two element of the arguments array (@_) as $i and $j.

You can do swap of two values without having to create a temporary variable. To swap $a and $b, you can do
    ($a,$b) = ($b,$a);
so you could do
    ($W[$i][$k],$W[$j][$k]) = ($W[$j][$k],$W[$i][$k]);
to swap these two entries. Combining this with the previous item shortens your rowswap macro to
    sub rowsswap { 
       my ($i,$j) = @_;
       for my $k (0..$ncols-1) {
         ($W[$i][$k],$W[$j][$k]) = ($W[$j][$k],$W[$i][$k]);
       }
    }

You can multiply a variable by a value using *=, so $W[$i][$k] = $lambda*$W[$i][$k]; could be shortened to $W[$i][$k] *= $lambda; (though not everyone likes this notation). Similarly, $W[$i][$k] = $W[$i][$k] + $lambda*$W[$j][$k]; could be replaced by $W[$i][$k] += $lambda*$W[$j][$k]; making your second two row macros be
    sub rowmult {
       my ($i,$lambda) = @_;
       for my $k (0..$ncols-1) {$W[$i][$k] *= $lambda}
    }
    
    sub rowadd {
       my ($i,$j,$lambda) = @_;
       for my $k (0..$ncols-1) {$W[$i][$k] += $lambda*$W[$j][$k]}
    }
The reason you had trouble with the reals versus fractions is in your pivoting algorithm where you use 1/$W[$ipivot][$jpivot]. Note that this only produces a fraction if $W[$ipivot][$jpivot] is a fraction; if it is a Perl real, then it is just Perl division and produces a Perl decimal real, not a MathObject Fraction.

You can fix this by using 1/Fraction($W[$ipivot][$jpivot]) instead. Then if $W[$ipivot][$jpivot] is a Perl integer, it will be made into a Fraction object, and one over it will again be a fraction. If it is already a Fraction, this will just return the original Fraction, no harm done. So you can start with a matrix of reals, and introduce Fractions into it only when needed.

Anyway, I think that should simplify your code a bit, and fix up a few minot details.

Davide

In reply to Davide Cervone

Re: Matrix Mathobjects

by Michael Doob -
Thanks very much for your reply. Your comments on coding are very helpful
(even if it does highlight my own inabilities sad).

(1) You can fix this by using 1/Fraction($W[$ipivot][$jpivot]) instead. 
That's a bulls-eye. Exactly the problem point.

(2) Concerning
   sub rowsswap { 
         my ($i,$j) = @_;
          for my $k (0..$ncols-1) {
         ($W[$i][$k],$W[$j][$k]) = ($W[$j][$k],$W[$i][$k]);
          }
       }
I guess it's also possible to just swap the hashes
   sub rowsswap { 
         my ($i,$j) = @_;
         ($W[$i],$W[$j]) = ($W[$j],$W[$i]);
          }
       }

(3) \(R= \)\{$R->ans_array\} 
When used with fractions, this sometimes works and sometime gives and error that the input is not in matrix form. I tried using  cmp on a ghost matrix with real entries, but this made no difference. At this point I still can't locate the exact condition that triggers the error. 

I'm not requiring the input to be as fractions for the problem I have in mind, but this might be a useful option to utilize in the future.


In reply to Michael Doob

Re: Matrix Mathobjects

by Michael Doob -
As to (3) above,

 \(R= \)\{$R->ans_array\}  generates an error if and only if the matrix has one row.


Recall that the original definition for the matrix is:

@W = (); 
for my $j (1..$nrows) {
   $V = [];
   for my $i (1..$ncols) {push $V, matrix_entry($i,$j)};
   push @W, $V;
}

Cheers,
Michael
In reply to Michael Doob

Re: Matrix Mathobjects

by Davide Cervone -
Yes, it turns out that there was an error in the processing of one-row matrices using ans_array. I submitted a patch last week, and it is in the 2.7 release. The changes are in pg/lib/Value/AnswerCheckers.pm. I think you should be able to replace your current copy with this version without trouble (but save your earlier copy just in case).

Davide
In reply to Michael Doob

Re: Matrix Mathobjects

by Davide Cervone -
For (2), yes, swapping the array references would work even better. Sorry I didn't suggest it. :-)

BTW, these are not hashes, but array references. Hashes are collections of name/value pairs stored in variables starting with % rather than @. You are just using arrays, not hashes.

Davide