WeBWorK Problems

Algebra problems: radicals, factorizing, simplifying

Algebra problems: radicals, factorizing, simplifying

by Rob Owen -
Number of replies: 23
Hello to all, I'm a new Webwork user at my university and I have a few questions about implementing Webwork problems for our college algebra course. The first concerns concerns solving equations by radicals, say solving x^2 + 3x - 3 = 0 both in terms of radicals and to two decimal places. The latter is relatively trivial since we can input the solutions as $exact_sol1 = "(-3 + sqrt(21))/2" and then $round_sol is simply the exact solution rounded to 2dp. The former, however, is giving me hives: is there any good way to determine whether the given solution was given as an exact algebraic expression instead of as a decimal? I haven't settled on any method in particular, but I have the following ideas: a) check the exact string submitted by the student for particular literals ("sqrt", "**", "^"); b) disallow certain literals (e.g. "."); c) require certain functions (e.g. exponentiation or sqrt); d) disallow certain functions (don't know which yet). Anyone have any ideas?

[In a similar vein: is there any way to require the answer be given in fractional form, e.g. 4/5, and not decimal form, e.g. 0.8, short of explicitly requiring a numerator and denominator?]

The second question has been addressed here previously but I'm curious as to the conclusions people drew: what is the best way to implement factorizing in webwork? For example, given x^2 + 2x - 3, what's the best way to write/accept x-1 & x+3 as the solutions? In particular, I'm worried about clever ways of breaking this, like x-1 and (x^2 + 2x - 3)/(x-1), as well as potentially accepting factorization-up-to-unit, like 2x - 2 and (x/2 + 3/2).

Finally -- and this one's going to be a monster -- a number of problems that we're trying to implement involve "simplifying" the given expression. Obviously "simplifying" is an ill-defined term in the first place; I'm particularly concerned about implementing it in webwork, though. My current plan is to accept their answer as a formula then, in addition to checking its correctness as a function, impose some heuristic on its functional tree (e.g. depth no greater than four & no more than 15 nodes). Is this a good idea? If so, how does one go about implementing this in webwork? I sort of know how to do it when the required answer involves nothing but BOPs -- though I could definitely use a primer if anyone has the chance -- but I'm not sure how to implement it in general.

I'd greatly appreciate an answer as soon as possible since the summer curriculum just got changed and I need to get some of this problems up and running by next week. Thank you very much for your time, and for the efforts you've all made in creating webwork.
In reply to Rob Owen

Re: Algebra problems: radicals

by Davide Cervone -
You have asked several questions, and I will answer them individually in separate responses to make threading work better.

The first question is about disallowing decimals in the answers. If you use the MathObjects rather than the traditional answer checkers, you will be able to exercise more control over what the students can enter. For example:

   loadMacros("MathObjects.pl");

   Context("Numeric");
   Parser::Number::NoDecimals;

   ANS(Compute("(-3+sqrt(21))/2")->cmp);
will force the students to enter the answer without using explicit decimals.

You are also allowed to limit the operations and functions that the student can access. For example, you can disable exponentiation via:

    Context()->operators->remove('^','**');
so students would have to use sqrt() rather than raising to the 1/2 power, for example.

To limit the functions available, you can use

    Context()->functions->disable("sqrt")
to disallow the sqrt() function, or
  Context()->functions->disable("Trig");
to disabled all the trig functions, or
  Context()->functions->disable("Trig");
  Context()->functions->enable("sin","cos");
to allow only the sin() and cos() trig functions, or
  Context()->functions->disable("All");
to allow no function calls. There was a discussion of this at one point, but I can't find the reference to it. There are a variety of categories that can be used; see the file pg/lib/Parser/Context/Functions.pm for the complete list.

Davide

In reply to Davide Cervone

Re: Algebra problems: radicals

by Curtis Card -
Could you tell me where to locate the macro MathObjects.pl. I found that it just loads Parser.pl which loads Value.pl, but there wasn't anything in the MathObjects.pl.html file to do this. I'm new to Webwork and don't know how to do this myself. I'm trying to setup the instructiveProblems/NewProblems from the rochester_problib and they require MathObjects.pl

Thanks
In reply to Rob Owen

Re: Algebra problems: 2-place decimals

by Davide Cervone -
You mention that the 2-decimal-place answer check will be easy, but this is actually more subtle than you might think. If you simply check against the answer 1.41, for example, do you want 1.411 to be marked correct? How about 1.409? Depending on how the tolerances are set, these might be marked as correct answers. Even if you lower the tolerances, you might still allow answers you don't want, like 1.4099999. And there is also the issue of the student entering sqrt(2), which you might not want.

You can force the student to enter just a single number by disabling all the operators and functions. There is already a context that does this:

    Context("LimitedNumeric");
It corresponds to num_cmp's "strict" mode (in fact, this is the context that implements that mode behind the scenes).

It is possible to overcome the issues of tolerances I describe above by starting with the LimitedNumeric context, and setting a special value that is used to check the numbers entered in a formula:

    Context("LimitedNumeric");
    Context()->flags->set(
      NumberCheck => sub {
        my $self = shift;
        $self->Error("You should enter numbers with at most 2 decimal places")
         if $self->{value_string} =~ m/~~.~~d~~d~~d/;
      },
      tolType => 'absolute',
      tolerance => .005,
    );

    $ans = Real(prfmt(sqrt(2),"%.2f"));
    ANS($ans->cmp);
This restricts the student to entering only numbers with at most 2 decimal places, and uses absolute tolerances that should only match the correct answer.

Davide

In reply to Davide Cervone

Re: Algebra problems: 2-place decimals

by Rob Owen -
[I'll reply to these individually to keep the threading intact.]

Our basic feeling on the 2dp answer is simply to disallow the
student from inputting the answer from their calculator wholesale, or from entering sqrt(2), by setting the tolerance sufficiently fine. You're quite right that doing an exactly 2dp answer is hard; my solution was simply to write a custom answer checker that required the literal string of the students answer [pulled out of the answer hash] to pattern match (after stripping out whitespace) and evaluate correctly. Something like

sub decimalChecker {
my $correct = shift; my $student = shift; my $ah = shift;
my $originalAnswer = $ah->{original_student_ans};
$originalAnswer =~ s/~~s+//g;
return ($student == $correct && $originalAnswer =~ /^~~d*~~.~~d~~d$/;
}

as the answer checker. I think this covers the bases, but maybe I've missed something?

Also, for future reference: can the answer checkers receive parameters
from a MathObject? That is, can I do something like

ANS( Value("sqrt(2)")->cmp( checker => ~~&decimalChecker(6) );

so that I could, for example, write a single routine that would
automatically check answers to n dp?
In reply to Rob Owen

Re: Algebra problems: 2-place decimals

by Davide Cervone -
I think this covers the bases, but maybe I've missed something?

Assuming you have used LimitedNumeric context, then I think your method will be OK. A slightly easier final check would be

    return($originalAnswer eq $correct->string);
which avoids going through the fuzzy real check unnecessarily (it could return false if the tolerances aren't set correctly, even when the answer is "right"). Actually, the "->string" is not strictly necessary since the correct answer will be stringified automatically.

I assume that if the answer is 1.41 and the student answers 1.410 you want to mark that as false? This is what your code currently will do, so it will be important to make it clear in the wording of the problem or students may become confused (and irate).

You might also consider setting the LaTeX preview string so that it shows what the student actually typed (since it will lose the trailing 0's otherwise and may cause confusion in that case when their answer is marked wrong especially when the correct answer looks identical). You could even set the student_ans field so it contains the trailing 0's as well.

My version of the answer checker is the following:

    sub decimalChecker {
      my ($correct,$student,$ans) = @_;
      return 0 unless $correct->class eq $student->class;
      my $original = $ans->{original_student_ans}; $original =~ s/~~s+//g;
      $ans->{preview_latex_string} = $original;
      return $original eq $correct;
    }


can the answer checkers receive parameters from a MathObject?

Not the way you have suggested. You can't mix getting a pointer to a subroutine with passing arguments to the subroutine like that. You can, however, add more flags to the cmp call and they will be available in the answer hash:

    ANS(Real(1.41)->cmp(decimals=>2,checker=>sub {
      my ($correct,$student,$ans) = @_;
      my $decimals = $ans->{decimals};
      ...
    }));
(It would also be possible to make a decimalChecker subroutine that returns a code reference to a subroutine that does the correct number of digits, something like
    sub decimalChecker {
      my $n = shift;
      sub {
         my ($correct,$student,$ans) = @_;
         ... (uses $n internally)
      }
    }
and then you can use ANS(Real(1.41)->cmp(checker=>decimalChecker(2))) but it is probably not worth it. If you do, you should be sure to set the correct_ans field of the answer hash so that the student will see the correct number of digits when he views the correct answer.)

I have a better method that I will post later, but I have to run off to graduation now.

Davide

In reply to Rob Owen

Re: Algebra problems: 2-place decimals

by Davide Cervone -
PS, Value("sqrt(2)") won't work, as there is no Value() function. You should use Real(sqrt(2)) or (less efficient) Real("sqrt(2)"), or Compute("sqrt(2)"). I don't recommend any of them for this problem, however, as the first two will have the correct answer shown as 1.41421, even when the checker is asking for a different number of digits (confusing to the students). The latter is particularly bad, as the correct answer would be shown as "sqrt(2)" even though you want the student to give a decimal answer (never good to show the correct answer in the wrong form). Finally, if you are using LimitedNumeric context, then neither of the later two will work (both will produce run-time errors when the problem is displayed) because sqrt is not defined in that context, and indeed neither is the parenthesis needed to obtain the function call.

Also, if you are going to be asking for exactly the number of digits you want, you probably want to set

    Context()->{format}{number} = "";
rather than the default "%g" so that numbers will be shown in their full precision rather than only to 6 digits or so.

Davide

In reply to Rob Owen

Re: Algebra problems: 2-place decimals

by Davide Cervone -
There is a slicker way to do use MathObjects to do this, and that is to define a new class that is a limited-precision class. Here's one way to do it.

First, make a file called "fixedPrecision.pl" in your course's templates/macros directory that contains the following:

    sub _fixedPrecision_init {}

    package FixedPrecision;
    our @ISA = ("Value::Real");

    sub new {
      my $self = shift; my $class = ref($self) || $self;
      my $x = shift; my $n = shift;
      Value::Error("Too many arguments") if scalar(@_) > 0;
      if (defined($n)) {
        $x = main::prfmt($x,"%.${n}f");
      } else {
        $x =~ s/\s+//g;
        my ($int,$dec) = split(/\./,$x);
        $n = length($dec);
      }
      $self = bless $self->SUPER::new($x), $class;
      $self->{decimals} = $n; $self->{isValue} = 1;
      return $self;
    }

    sub string {
      my $self = shift;
      main::prfmt($self->value,"%.".$self->{decimals}."f");
    }

    sub compare {
      my ($l,$r,$flag) = @_;
      if ($l->promotePrecedence($r)) {return $r->compare($l,!$flag)}
      $l cmp $r;
    }

    package FixedPrecisionNumber;
    our @ISA = ("Parser::Number");

    sub new {
      my $self = shift; my $class = ref($self) || $self;
      my $equation = shift; my $context = $equation->{context};
      $self = bless $self->SUPER::new($equation,@_), $class;
      $self->{value} = FixedPrecision->new($self->{value_string});
      return $self;
    }

    sub string {(shift)->{value}->string(@_)}
    sub TeX {(shift)->{value}->TeX(@_)}

    package main;

    Context()->{parser}{Number} = "FixedPrecisionNumber";
    sub FixedPrecision {FixedPrecision->new(@_)};

    1;
(I've attached a copy of this file for your convenience.)

This file defined a new MathObject class (FixedPrecision) that is a subclass of the Real class that adds a new field {decimals} that determines how many decimal places to use. It also creates a Parser class that replaces the standard Number class that uses the new FixedPrecision object to implement numbers in the Parser.

By default, FixedPrecsion(n) will take however many decimal digits there are in n as the number of digits, or you can specify the number explicitly as FixedPrecision(n,digits). For example, FixedPrecision(sqrt(2),3) would produce a value 1.414 that the student must match exactly.

You should use this within the LimitedNumeric context, as in the following example:

    DOCUMENT();

    loadMacros(
      "PGstandard.pl",
      "Parser.pl",
    );

    TEXT(beginproblem);

    Context("LimitedNumeric");
    loadMacros("fixedPrecision.pl");  # must come after Context is set.

    $n = FixedPrecision(sqrt(2),3);

    BEGIN_TEXT
    \($n\) = \{ans_rule(10)\}
    END_TEXT

    ANS($n->cmp);

    ENDDOCUMENT();
The FixedPrecision class overrides the string and compare methods to implement the fixed-precision output and to do a string comparison rather than a (fuzzy) numeric comparison, so that only the exact number of digits will be marked correct.

Note that this is not a full implementation of a MathObject class, which would have to be more sophisticated in order to handle things like addition, subtraction, and so on correctly. This means it can only be used in the LimitedNumeric context where those operations will never be performed. This class also won't handle scientific notation properly. It would be possible to make a more complete implementation of this idea, but there are details to be worked out.

Davide

In reply to Davide Cervone

Re: Algebra problems: 2-place decimals

by Rob Owen -
[Edited for clarity & correctness]

It's been a while since I checked back in here, but: we've implemented the FixedPrecision class as you recommended above, and everything works fine provided the answers are all positive. If the student enters a *negative* answer, however, the student's answer becomes artificially truncated by the comparitor. Specifically, a negative answer is automatically truncated to 0 dp, irrespective of the parameters of the FixedPrecision object. So, for example, code like

$py3 = FixedPrecision(-.9165234, 3);
ANS( $py3->cmp );

won't accept anything, because the student's answer can be, at best, -1. The only way an answer of -.917 can be accepted is to write something like

$py3 = FixedPrecision(-.9165234, 0);
ANS( $py3->cmp );

which (obviously) isn't what we want.

I haven't followed the boards in a while so I don't know if there's something bigger and better out there, but could someone please advise on how to fix this problem?

Thanks much,

-- Robert


In reply to Rob Owen

Re: Algebra problems: 2-place decimals

by Davide Cervone -
I'm not able to reproduce the problem. When I use the the example you give above, it works for me, and I'm able to answer -.917 as expected. Have you modified the fixedPrecision.pl file at all? Can you post a complete example file, in case there is something else going on that is causing the problem? Also, what version of pg are you using? (When did you last update?)

Davide
In reply to Davide Cervone

Re: Algebra problems: 2-place decimals

by Rob Owen -
Here's the version of fixedPrecision.pl we're using. I believe -- though I could not swear to it -- that it was unaltered since you gave it to me last year.

sub _fixedPrecision_init {}

package FixedPrecision;
our @ISA = ("Value::Real");

sub new {
my $self = shift; my $class = ref($self) || $self;
my $x = shift; my $n = shift;
Value::Error("Too many arguments") if scalar(@_) > 0;
if (defined($n)) {
$x = main::prfmt($x,"%.${n}f");
} else {
$x =~ s/\s+//g;
my ($int,$dec) = split(/\./,$x);
$n = length($dec);
}
$self = bless $self->SUPER::new($x), $class;
$self->{decimals} = $n; $self->{isValue} = 1;
return $self;
}

sub string {
my $self = shift;
main::prfmt($self->value,"%.".$self->{decimals}."f");
}

sub compare {
my ($l,$r,$flag) = @_;
if ($l->promotePrecedence($r)) {return $r->compare($l,!$flag)}
$l cmp $r;
}

package FixedPrecisionNumber;
our @ISA = ("Parser::Number");

sub new {
my $self = shift; my $class = ref($self) || $self;
my $equation = shift; my $context = $equation->{context};
$self = bless $self->SUPER::new($equation,@_), $class;
$self->{value} = FixedPrecision->new($self->{value_string});
return $self;
}

sub string {(shift)->{value}->string(@_)}
sub TeX {(shift)->{value}->TeX(@_)}

package main;

Context()->{parser}{Number} = "FixedPrecisionNumber";

sub FixedPrecision {FixedPrecision->new(@_)};

1;

And here's some of the code in which it's being implemented:

loadMacros("PG.pl",
"PGbasicmacros.pl",
"PGchoicemacros.pl",
"PGanswermacros.pl",
"PGauxiliaryFunctions.pl",
"PGgraphmacros.pl",

"Parser.pl",
"UWMadisonMacros.pl");

# Should be MathObjects nowadays, I believe
# UWMadisonMacros isn't actually being used in this problem, I'm just including it for completeness

$pxexact = "2/5";
$pyexact = "-sqrt(1-($pxexact)**2)";
$pxapprox = 0.400;
$pyapprox = -sqrt(1-$pxapprox**2);

$qxexact = $pyexact;
$qyexact = $pyexact;
$qxapprox = $pyapprox;
$qyapprox = $pyapprox;

$rxexact = $qxexact;
$ryexact = "-" . $pxexact;
$rxapprox = $qxapprox;
$ryapprox = - $pxapprox;

# Ask for exact answers here...
# This works fine

ANS(num_cmp($pxexact));
ANS(num_cmp($pyexact));

ANS(num_cmp($qxexact));
ANS(num_cmp($qyexact));

ANS(num_cmp($rxexact));
ANS(num_cmp($ryexact));

# Ask for approximate answers here
# Want answers to 3dp

Context("LimitedNumeric");
loadMacros("fixedPrecision.pl"); # must come after Context is set.

$px3 = FixedPrecision($pxapprox, 3); #student must enter .400 not .4
$py3 = FixedPrecision($pyapprox, 3);

ANS($px3->cmp);
ANS($py3->cmp);

$qx3 = FixedPrecision($qxapprox, 3);
$qy3 = FixedPrecision($qyapprox, 3);

ANS($qx3->cmp);
ANS($qy3->cmp);

$rx3 = FixedPrecision($rxapprox, 3);
$ry3 = FixedPrecision($ryapprox, 3); # Student must enter .400 not .4
ANS($rx3->cmp);
ANS($ry3->cmp);

Any of the negative numbers entered by the student in the second half of the problem will be automatically rounded to the nearest integer. Positive answers are not rounded at all, but only correct if entered to 3dp (as they should be).

[And I don't recall offhand how to check which version of PG we're using. Would you mind reminding me? We're in the 2.x but -- especially not having been on the project for a few months -- I'd be reluctant to guess.]
In reply to Rob Owen

Re: Algebra problems: 2-place decimals

by Davide Cervone -
Sorry, it still works correctly for me. I can enter -.917 for these answers without trouble. I'm still wondering of there is a version issue.

You could use cvs status on one of the files in the pg directory to see whether there are any sticky tags set (like rel-2.4.0 or something like that). If not, then do cvs status on pg/lib/Value/AnswerChecker.pm and tell me what the working revision number is.

Davide
In reply to Davide Cervone

Re: Algebra problems: 2-place decimals

by Rob Owen -
Let's see: the pg directory is itself /pg-2.4.0/ and the version resulting from cvs'ing AnswerChecker.pm is


File: AnswerChecker.pm Status: Up-to-date

Working revision: 1.91.2.1
Repository revision: 1.91.2.1 /webwork/cvs/system/pg/lib/Value/AnswerChecker.pm,v
Sticky Tag: rel-2-4-dev (branch: 1.91.2)
Sticky Date: (none)
Sticky Options: (none)
In reply to Rob Owen

Re: Algebra problems: 2-place decimals

by Davide Cervone -
Sorry for the delay in answering. I got overloaded there for a while.

There have been a number of important changes to the MathObjects since version 1.91 of AnswerChecker.pm, so I suspect the differences we are seeing are due to that. The "up-to-date" indicator is that the file you have is up to date as far as the rel-2.4-dev release is concerned. The current HEAD version is 1.115.

I would suggest updating to the HEAD version of PG; it seems to be in a pretty stable state right now. That can be done without updating the rest of WeBWorK, but might not be something you want to do in the middle of the semester, so you may want to wait until the end of the term for that. I think you will find that the FixePrecision object will work correctly with the more current version of MathObjects.

Davide
In reply to Rob Owen

Re: Algebra problems: forcing fractions

by Davide Cervone -
You ask whether it is possible to force a fractional answer like 4/5 but disallow 0.8. The traditional answer checker has a "frac" mode, but this does not prevent 0.8 from being entered (this is the "LimitedNumeric-Fraction" context). Fortunately, however, there is a context that does what you want:
    Context("LimitedNumeric-StrictFraction");
This disables all functions and operations (except those needed for fractions), and sets the context's NumberCheck to one that prevents decimal numbers from being entered. (There is no mode that corresponds to this in the traditional num_cmp() checker.)

I think this context may still have "pi" and "e" defined in it, so students can enter fractions that include these (e.g., pi/2), but that is unlikely. You can disable them with

    Context("LimitedNumeric-StrictFraction");
    Context()->constants->remove("pi","e");

    ANS(Compute("4/5")->cmp);
The Compute function creates an appropriate MathObject from the string provided, and in addition makes that string the correct answer string, rather than 0.8 which it would be if you used Real(4/5) instead. (At some point I need to make an honest fraction MathObject class.)

Hope that does what you want.

Davide

In reply to Rob Owen

Re: Algebra problems: factoring

by Davide Cervone -
You ask about how to request a factorization of a polynomial.

This was discussed a couple of summers ago at

    http://webwork.maa.org/moodle/mod/forum/discuss.php?d=2608
and there are a number of code examples that illustrate various approaches to the problem. I think the MultiPart ones are your best choices, but you may have different needs.

Since that discussion, we now have a context that allows only polynomial answers:

    loadMacros("contextLimitedPolynomial.pl");
    Context("LimitedPolynomial");
You can use this rather than Context("Numeric") to force the individual responses to be polynomials (preventing answers like (x^2 + 2x - 3)/(x-1) from being entered). It would also be possible to extend the LimitedPolynomial context to allow products of polynomials (or even limiting to monic polynomials, or linear ones), but that is not currently available, and I'm not sure when I'll get to it.

Davide

In reply to Rob Owen

Re: Algebra problems: simplifying

by Davide Cervone -
Your question about simplification is the difficult one. I don't see any way to do it in general. Your suggestions of counting nodes or limiting depths are interesting, but I'm not sure they will be adequate. If you have specific simplifications you want the students to perform, it might be possible to check for that.

The Formula MathObject contains the parse tree for the formula, and it is possible to inspect the structure of the tree, but there are no pre-defined methods for traversing the tree, so it is not easy to do. But you at least you don't have to parse the equation yourself. Analyzing the structure algebraically can be quite tricky, and your students will almost certainly come up with situations you didn't think of.

I am a little concerned that you need to have these by next week. Writing significantly new answer checkers like this is a subtle and time-consuming process. I would not expect to have production versions in that short a time.

Good luck with your course.

Davide
In reply to Davide Cervone

Re: Algebra problems: simplifying

by Rob Owen -
It could be the innocence of ignorance but it didn't seem particularly hard to traverse the tree, at least if it's built of BOPs and UOPs. I had to spend an hour or two reading the source but that much at least seemed self-explanatory. Mind, I haven't tried to make it work for function calls like sin( x^2 + sqrt(x) ) or f(x, y, z) so this could all come crashing down at any moment, but that's a bridge to burn another day.

[And yes, this relies crucially on the fact that the MathObject comes pre-parsed. Just thinking about parsing by hand gives me the chills.]

At the moment, btw, we're implementing a mix of computational complexity and pattern matching as a rough heuristic to define "simplified", since we can already compare the answers as functions. It's risky in the sense that we're assuming people will go wrong in predictable ways, but given the context it's about as good as we're gonna get. Once again I find myself using the $ah->{original_student_ans}; it seems deprecated in the code (and in these forums) but it's just so useful for these kinds of questions. Is there anything I should be careful of when using the answer hash beyond the obvious (e.g. don't write over the student's answer)? Is it considered déclassé? Or have I just been looking in the wrong places?

Also, since I didn't say so above: thank you very much for your time. Given the time-frame I'm working in, your speedy response was a godsend.
In reply to Rob Owen

Re: Algebra problems: simplifying

by Davide Cervone -
It is always hard to know what programming background someone has when they write a question in these forums. You are comfortable looking through the source code, so obviously you are a reasonably experienced programmer, and so you will not find it too daunting to traverse the parse tree. Not everyone here is in the same position, so I was trying to be cautious in my response.

The main issue is to know the various node types (BOP, UOP, List, etc.) and how to find the leaves from each type. You have already worked out BOP and UOP. The other nodes correspond to the various subclasses of Parser::Item (all in the top level of pg/lib/Parser/). It's not too bad, really, but nothing is documented, and you have to do all the tree traversal by hand. The parser was not designed with user-traversal in mind, but this is not the first time it has come up, and I should think about making an API to access it.

Concerning $ah->{original_student_ans}, I don't have any problem with your using it, but you might consider using the parsed student answer to produce its corresponding string rather than the original string. This will be more "normalized" in the sense that all the operations are shown consistently and without extra spacing, and so on, so you don't have to worry about irregularities of the student's input.

For example, if you have used my ($correct,$student,$ah) = @_; in a custom checker, then you can use $student->string to get the string version of the student's answer. If I recall correctly, this is also what has been set as $ah->{student_ans} initially.

There are also two other values in the answer-hash that might be of interest: $ah->{student_formula} and $ah->{student_value}. The latter is what gets passed to $student, and is the MathObject holding the student's answer (it will be a Real, Complex, Vector, etc.). If the student's answer is a numeric one (as opposed to a Formula), then all you get is the final answer, not the parse tree that produced it. The $ah->{student_formula} is the parsed version of the student's answer, even if it ends up producing a numeric result. (If it is a formula, then $ah->{student_value} and $ah->{student_formula} will be the same.) This lets you analyze the student's answer even when it is not a formula.

Note that if the student gives a constant answer, even when the answer checker is looking for a Formula, $ah->{student_value} (and so $student) will be a Value object rather than a Parser object, and so it will not have a {tree} field. You may want to use $ah->{student_formula} rather than $student if you are going to traverse the tree. Otherwise, use $student = main::Formula($student) to force it to be a (constant-valued) formula rather than a constant so that it has a {tree} to work with.

Good luck with your project.

Davide
In reply to Davide Cervone

Re: Algebra problems: simplifying

by Lars Jensen -
Hi Everyone,

Is anyone aware of any library factoring problems that use the (newer) methods described above. The old factoring problems usually require that each factor is entered in a separate box, and are pretty confusing for students.

Thanks for any help.

Lars.
In reply to Lars Jensen

Re: Algebra problems: simplifying

by Robin Cruz -

I have some factoring problems for an Intermediate Algebra course which use MathObjects.  I hope to have them out to the library by the first part of August 2008.  You may look into them at

http://docralph.collegeofidaho.edu/webwork2/Intermediate_Algebra/

You may log in with any of the usernames: student1, student2, student3, student4 or student5

The password for all is: practice

Currently, the problems are all "pinked" since I have some unfinished work to do yet and there is a file missing which makes the config file "unhappy" -- I don't think the factoring problems need this file.  So, the problems should work.  I hope to get back to work on these now that my classes are over for the year.

--rac

In reply to Rob Owen

Re: Algebra problems: factorizing

by Alex Jordan -
I stumbled on something that I want to share. I haven't looked at everyone's ideas about this issue, but I think this idea below is new to the thread, and handles the factoring issue quite nicely.

When I first started WeBWorK I knew no Perl, and thought there was no difference between == and eq. Early on, I once wrote a custom answer checker and used eq when I probably would have been using == had I known any better. But upon later review of this problem I realized eq was making the factoring business work almost beautifully.

Simple example first:
Send $ans to a custom checker, where $ans = Compute("2(x+1)"). Have a custom checker simply compare $correct->reduce with $student->reduce using eq instead of ==, and here's what happens:
1. You're using a Math Object, so Math Object syntax error messages still work.
2. These things count as correct:
2(x+1)
2 ( x + 1)
2*(x+1)
([([2])]) * (1*x+1)
3. These things do not, (but *would* with ==) :
(x+1)*2
2(1+x)
2x+2

Despite the last examples being functionally equivalent Formulas to 2(x+1), they are not counted as correct because their reduced strings are different. And yet the ->reduce method removes most of the annoying types of equivalency, like extra space, multiplication by 1, and extra parens, to name a few. Even this small example could be the answer to a beginning algebra "distribute out the greatest common factor" problem.



But you can take this concept and apply it to (x+1)(x+2) in a factoring problem. It's functionally equivalent to x^2+3x+2, but you don't want that to count as correct. You only want:
(x+1)(x+2) (x+1)(2+x)
(1+x)(x+2) (1+x)(2+x)
(x+2)(x+1) (x+2)(1+x)
(2+x)(x+1) (2+x)(1+x)

to count as correct, including all variations with more parens, the inclusion of a * between factors, and other things that don't matter when using eq on reduced Formulas.


Using this idea, I wrote a problem that can take up to three factors, each having up to three terms, present the product, and ask for a factorization. As far as I can tell, it works exactly the way any of us would want it to. I'll include it below. It does not try to manually list out permutations like those 8 above, but rather uses permutation hashes to automatically generate them in an array.

The bigger discovery that I want to share is how using eq might solve certain problems with Math Objects, when they become "too forgiving" with equivalency. eq is a little more discriminatory than ==.

Alex
_______________________________
Here is the code for that factoring problem. I'd love to hear if anyone has a way to:
-upgrade it to any number of factors.
-upgrade it to factors with any number of terms
-allow (1+x)^2 to be entered that way, instead of forced as something like (1+x)(1+x)






# DESCRIPTION
# WeBWorK problem written by Alex Jordan
# Portland Community College
# ENDDESCRIPTION

## DBsubject('Algebra')
## DBchapter('Polynomial and Rational Functions')
## DBsection('Polynomial Functions')
## KEYWORDS('factoring')
## Author('Alex Jordan')
## Institution('PCC')




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

DOCUMENT();

loadMacros(
"PGstandard.pl",
"MathObjects.pl",
"PGpolynomialmacros.pl",

);

##############################################
#This problem displays an expanded polynomial and asks for it to be factored. The answer is a Formula and will give appropriate syntax error messages for Formula in Numeric context. The order of factors does not matter. The order of terms within any factor does not matter.

#The answer must be a product of binomials and trinomials, perhaps with a monomial. If constants can be distributed out they must be, and they must be simplified. Having more than three factors, or using factors with more than three terms would require more than just surface alteration, but is theoretically do-able. However, an array of super factorial size is being created here, so maybe moving up to more factors would be a bad idea. As it is, 3 factors each with three terms makes an array of size (3!)^4. You might want to stick to no more than one trinomial.

#To use this problem for <= 3 factors with <= 3 terms,

#1. Fill in the coefficient array @coef below to reflect the factors. The factors should already be irreducible, except that a monomial divisor is allowed in any *one* factor, and a constant divisor is allowed in any factor. If you want a doubled or tripled factor like (ax+b)^2, it will have to be written by students as a product: (ax+b)(ax+b) [or (ax+b)(b + xa), etc]. Take all of this into consideration when randomizing the coefficients.

#2. If you change the number of factors down to two or one, leave trivial [1] rows in @coef.



#Define an hash of hashes for permutations in S_3, S_2.
#Perl might have a classier way to do this so that it wouldn't be tedious to #upgrade to four factors.

%S3 = (A => {0 => 0, 1 => 1, 2 => 2}, B => {0 => 0, 1 => 2, 2 => 1},
C => {0 => 1, 1 => 0, 2 => 2}, D => {0 => 1, 1 => 2, 2 => 0},
E => {0 => 2, 1 => 1, 2 => 0}, F => {0 => 2, 1 => 0, 2 => 1},);
%S2 = (A => {0 => 0, 1 => 1}, B => {0 => 1, 1 => 0},);
%S1 = (A => {0 => 0},);



Context("Numeric");
Context()->reduction->set('(-x)-y'=>0, '(-x)+y'=>0);
#Not desired reductions for this kind of problem.


@coef = ([2,4,10], #One factor is (x+4), etc. Could be randomized.
[1,4],
[3,0]);

$n = $#coef;



#Get the array of coefficients for the product

@prod = PolyMult(~~@{$coef[0]},~~@{$coef[1]});
@prod = PolyMult(~~@prod,~~@{$coef[2]});

#Make the presented polynomial. I'm using PolyString (included in
#PGpolynomialmacros.pl) but it currently doesn't handle negative Math Objects
#correctly. That could easily be fixed. Anyway, for this current example,
#keep the original coefficients as Perl reals.

$polynomial = Formula(PolyString(~~@prod))->reduce;



#Pull out any constants that can be pulled out.
$const = 1;
foreach my $i (0..$n) {
my $z = @{$coef[$i]}-1;
my $g = 1;
if ($z > 0) {
$g = gcd(@{$coef[$i]});
foreach my $j (2..$z) {
$g = gcd($g,$coef[$i]->[$j]);
}
}
else {$g = $coef[$i]->[0];};
$const = $const * $g;

my @temp = ();
foreach my $j (0..$z) {
@temp = (@temp,($coef[$i]->[$j])/$g);
};
$coef[$i] = [ @temp ];
}

#A Formula reduction pushes constants to the left anyway, so we won't worry about all the places student could conceivable put this constant.




#Upgrade @coef so that each entry is a Formula with the proper
#power of x attached. Remove all empty slots, like 0*x.

@terms =([]);

foreach my $i (0..$n) {
my @temp = ();
my $temp2 = @{$coef[$i]}-1;
foreach my $j (0..$temp2) {
my $c = $coef[$i]->[$j];
my $d = $temp2-$j;
if ($c != 0) {
@temp = (@temp, Formula("$c x^{$d}")->reduce);
};
};
$terms[$i] = [ @temp ];
};


#Make the array of acceptable alternative factorizations.

#First, make a two-dimensional array indexed by the original factors,
#then by order of terms within that factor. If the factorization includes a
#trinomial, that row should be handled differently.

@altF = ([]);




foreach my $i (0..$n) {
my $t = @{$terms[$i]};
if ($t == 1) { #for monomial factors
$altF[$i] = [ ( $terms[$i]->[0] ) ];
};

if ($t == 2) { #for binomial factors
my @tmp = ();
foreach my $j (A,B) {
@tmp = (@tmp, ($terms[$i]->[$S2{$j}{0}]
+ $terms[$i]->[$S2{$j}{1}])->reduce );
};
$altF[$i] = [ @tmp ];
};

if ($t == 3) { #for trinomial factors
my @tmp = ();
foreach my $j (A..F) {
@tmp = (@tmp, Compute($terms[$i]->[$S3{$j}{0}] + $terms[$i]->[$S3{$j}{1}]
+ $terms[$i]->[$S3{$j}{2}])->reduce );
};
$altF[$i] = [ @tmp ];
};

};


#Make a one-dimensional array with all legal alternative factorizations.
#We will make use of a two-dimensional array of indices
$t0 = @{$altF[0]}-1;
$t1 = @{$altF[1]}-1;
$t2 = @{$altF[2]}-1;

@indices = ([0..$t0],[0..$t1],[0..$t2],);

@altFtions = ();

foreach my $k (A..F) {
foreach my $m0 (0..$t0) {
foreach my $m1 (0..$t1) {
foreach my $m2 (0..$t2) {

@altFtions = (@altFtions, $const
*($altF[$S3{$k}{0}]->[$indices[$S3{$k}{0}]->[$m0]])
*($altF[$S3{$k}{1}]->[$indices[$S3{$k}{1}]->[$m1]])
*($altF[$S3{$k}{2}]->[$indices[$S3{$k}{2}]->[$m2]])
);
};
};
};
};

#Pick the version that is displayed as correct. Return the turned-off reductions.

Context()->reduction->set('(-x)-y'=>1, '(-x)+y'=>1);
$factored = Compute("$altFtions[0]")->reduce;


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

Context()->texStrings;

BEGIN_TEXT


Factor this polynomial.

$PAR


$SPACE $SPACE $SPACE \( $polynomial \)

$PAR


\{ans_rule(30)\}







END_TEXT

Context()->normalStrings;

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



ANS($factored->cmp(typeMatch=>Formula("x"), checker => sub {
my ($correct,$student,$ans) = @_;
my $ret = 0;
my $l = 0;
while (($ret != 1) && ($l <= $#altFtions)) {
if ($student->reduce eq $altFtions[$l]->reduce) {$ret = 1;};
$l = $l + 1;
}
Value->Error("You haven't factored completely.")
if (($ret == 0) && ($student == $correct));

Value->Error("Your factorization is incorrect.")
if ($student != $correct);

return $ret
}));


ENDDOCUMENT();

In reply to Alex Jordan

Re: Algebra problems: factorizing

by Paul Pearson -
Dear Alex,

That's an interesting observation about the distributive law. For polynomial factoring, there is a new way (as of spring 2010) that is much easier and much better.

http://webwork.maa.org/wiki/FactoredPolynomial1

http://webwork.maa.org/wiki/FactoringAndExpanding

I've spent a considerable amount of time the past year updating the wiki pages:

http://webwork.maa.org/wiki/SubjectAreaTemplates

http://webwork.maa.org/wiki/IndexOfProblemTechniques

If there are things you (and everyone reading this message) would like to be able to do that are not addressed by the wiki, please post your questions to this forum. Who knows? Maybe someone else has already figured out a good solution to your problem. Maybe I've figured it out but didn't put anything up on the wiki about it. Etc.

Best Regards,

Paul Pearson
In reply to Paul Pearson

Re: Algebra problems: factorizing

by Alex Jordan -
Paul,

Thanks for those links. I was wondering where a directory was for all those examples. I've hit a couple of them through Google, but I never knew where the directory was. (Now of course I see that there's a link at the top of each example.)

And thanks for the work too. That catalog will be a great resource. I've bookmarked it!

Alex