From: dpvc v. a. <we...@ma...> - 2010-04-01 00:43:55
|
Log Message: ----------- Provide a limited context in which rational functions can be specified Modified Files: -------------- pg/macros: PGcomplexmacros.pl contextLimitedPolynomial.pl Added Files: ----------- pg/macros: contextRationalFunction.pl Revision Data ------------- Index: contextLimitedPolynomial.pl =================================================================== RCS file: /webwork/cvs/system/pg/macros/contextLimitedPolynomial.pl,v retrieving revision 1.23 retrieving revision 1.24 diff -Lmacros/contextLimitedPolynomial.pl -Lmacros/contextLimitedPolynomial.pl -u -r1.23 -r1.24 --- macros/contextLimitedPolynomial.pl +++ macros/contextLimitedPolynomial.pl @@ -50,6 +50,56 @@ sub _contextLimitedPolynomial_init {LimitedPolynomial::Init()}; # don't load it again ################################################## + +package LimitedPolynomial; + +# +# Mark a variable as having power 1 +# Mark a number as being present (when strict coefficients are used) +# Mark a monomial as having its given powers +# +sub markPowers { + my $self = shift; + if ($self->class eq 'Variable') { + my $vIndex = LimitedPolynomial::getVarIndex($self); + $self->{index} = $vIndex->{$self->{name}}; + $self->{exponents} = [(0) x scalar(keys %{$vIndex})]; + $self->{exponents}[$self->{index}] = 1; + } elsif ($self->class eq 'Number') { + my $vIndex = LimitedPolynomial::getVarIndex($self); + $self->{exponents} = [(0) x scalar(keys %{$vIndex})]; + } + if ($self->{exponents}) { + my $power = join(',',@{$self->{exponents}}); + $self->{powers}{$power} = 1; + } +} + +# +# Get a hash of variable names that point to indices +# within the array of powers for a monomial +# +sub getVarIndex { + my $self = shift; + my $equation = $self->{equation}; + if (!$equation->{varIndex}) { + $equation->{varIndex} = {}; my $i = 0; + foreach my $v ($equation->{context}->variables->names) + {$equation->{varIndex}{$v} = $i++} + } + return $equation->{varIndex}; +} + +# +# Check for a constant expression +# +sub isConstant { + my $self = shift; + return 1 if $self->{isConstant} || $self->class eq 'Constant'; + return scalar(keys(%{$self->getVariables})) == 0; +} + +################################################## # # Handle common checking for BOPs # @@ -62,7 +112,7 @@ # sub _check { my $self = shift; - my $super = ref($self); $super =~ s/LimitedPolynomial/Parser/; + my $super = ref($self); $super =~ s/^.*?::/Parser::/; &{$super."::_check"}($self); if (LimitedPolynomial::isConstant($self->{lop}) && LimitedPolynomial::isConstant($self->{rop})) { @@ -133,62 +183,12 @@ $self->Error("Can't use '%s' between constants",$self->{bop}); } -################################################## - -package LimitedPolynomial; - -# -# Mark a variable as having power 1 -# Mark a number as being present (when strict coefficients are used) -# Mark a monomial as having its given powers -# -sub markPowers { - my $self = shift; - if ($self->class eq 'Variable') { - my $vIndex = LimitedPolynomial::getVarIndex($self); - $self->{index} = $vIndex->{$self->{name}}; - $self->{exponents} = [(0) x scalar(keys %{$vIndex})]; - $self->{exponents}[$self->{index}] = 1; - } elsif ($self->class eq 'Number') { - my $vIndex = LimitedPolynomial::getVarIndex($self); - $self->{exponents} = [(0) x scalar(keys %{$vIndex})]; - } - if ($self->{exponents}) { - my $power = join(',',@{$self->{exponents}}); - $self->{powers}{$power} = 1; - } -} - -# -# Get a hash of variable names that point to indices -# within the array of powers for a monomial -# -sub getVarIndex { - my $self = shift; - my $equation = $self->{equation}; - if (!$equation->{varIndex}) { - $equation->{varIndex} = {}; my $i = 0; - foreach my $v ($equation->{context}->variables->names) - {$equation->{varIndex}{$v} = $i++} - } - return $equation->{varIndex}; -} - -# -# Check for a constant expression -# -sub isConstant { - my $self = shift; - return 1 if $self->{isConstant} || $self->class eq 'Constant'; - return scalar(keys(%{$self->getVariables})) == 0; -} - ############################################## # # Now we get the individual replacements for the operators # that we don't want to allow. We inherit everything from # the original Parser::BOP class, and just add the -# polynomial checks here. Note that checkpolynomial +# polynomial checks here. Note that checkPolynomial # only gets called if at least one of the terms is not # a number. # @@ -248,7 +248,7 @@ $self->Error("In a polynomial, you can only divide by numbers") unless LimitedPolynomial::isConstant($r); $self->Error("You can only divide a single term by a number") - if $l->{isPoly} && $l->{isPoly} == 1; + if $l->{isPoly} && $l->{isPoly} != 2; $self->{isPoly} = $l->{isPoly}; $self->{powers} = $l->{powers}; delete $l->{powers}; $self->{exponents} = $l->{exponents}; delete $l->{exponents}; @@ -299,18 +299,18 @@ sub _check { my $self = shift; - my $super = ref($self); $super =~ s/LimitedPolynomial/Parser/; + my $super = ref($self); $super =~ s/^.*?::/Parser::/; &{$super."::_check"}($self); my $op = $self->{op}; return if LimitedPolynomial::isConstant($op); - $self->Error("You can only use '%s' with monomials",$self->{def}{string}) - if $op->{isPoly}; $self->{isPoly} = 2; $self->{powers} = $op->{powers}; delete $op->{powers}; $self->{exponents} = $op->{exponents}; delete $op->{exponents}; + return if $self->checkPolynomial; + $self->Error("You can only use '%s' with monomials",$self->{def}{string}); } -sub checkPolynomial {return 0} +sub checkPolynomial {return !(shift)->{op}{isPoly}} ############################################## Index: PGcomplexmacros.pl =================================================================== RCS file: /webwork/cvs/system/pg/macros/PGcomplexmacros.pl,v retrieving revision 1.14 retrieving revision 1.15 diff -Lmacros/PGcomplexmacros.pl -Lmacros/PGcomplexmacros.pl -u -r1.14 -r1.15 --- macros/PGcomplexmacros.pl +++ macros/PGcomplexmacros.pl @@ -90,7 +90,148 @@ =cut +my %cplx_context = ( + 'std' => 'Complex', + 'strict' => 'LimitedComplex-strict', + 'strict_polar' => 'LimitedComplex-polar', + 'strict_cartesian' => 'LimitedComplex-cartesian', + 'strict_num_polar' => 'LimitedComplex-polar-strict', + 'strict_num_cartesian' => 'LimitedComplex-cartesian-strict', +); + sub cplx_cmp { + return original_cplx_cmp(@_) if $main::useOldAnswerMacros; + + my $correctAnswer = shift; + my %cplx_params = @_; + + # + # Get default options + # + assign_option_aliases( \%cplx_params, + 'reltol' => 'relTol', + ); + set_default_options(\%cplx_params, + 'tolType' => (defined($cplx_params{tol}) ) ? 'absolute' : 'relative', + # default mode should be relative, to obtain this tol must not be defined + 'tolerance' => $main::numAbsTolDefault, + 'relTol' => $main::numRelPercentTolDefault, + 'zeroLevel' => $main::numZeroLevelDefault, + 'zeroLevelTol' => $main::numZeroLevelTolDefault, + 'format' => $main::numFormatDefault, + 'debug' => 0, + 'mode' => 'std', + 'strings' => undef, + ); + my $format = $cplx_params{'format'}; + my $mode = $cplx_params{'mode'}; + + if( $cplx_params{tolType} eq 'relative' ) { + $cplx_params{'tolerance'} = .01*$cplx_params{'relTol'}; + } + + my $context = $cplx_context{$mode}; + unless ($context) {$context = "Complex"; warn "Unknown mode '$mode'"} + $context = $Parser::Context::Default::context{$context}->copy; + + # + # Set the format for the context + # + $context->{format}{number} = $cplx_params{'format'} if $cplx_params{'format'}; + + # + # Add the strings to the context + # + if ($cplx_params{strings}) { + foreach my $string (@{$cplx_params{strings}}) { + my %tex = ($string =~ m/(-?)inf(inity)?/i)? (TeX => "$1\\infty"): (); + $context->strings->add(uc($string) => {%tex}) + unless $context->strings->get(uc($string)); + } + } + + # + # Set the tolerances + # + if ($cplx_params{tolType} eq 'absolute') { + $context->flags->set( + tolerance => $cplx_params{tolerance}, + tolType => 'absolute', + ); + } else { + $context->flags->set( + tolerance => .01*$cplx_params{tolerance}, + tolType => 'relative', + ); + } + $context->flags->set( + zeroLevel => $cplx_params{zeroLevel}, + zeroLevelTol => $cplx_params{zeroLevelTol}, + ); + + # + # Get the proper Parser object for the professor's answer + # using the initialized context + # + my $oldContext = Parser::Context->current(\%main::context,$context); my $z; + if (ref($correctAnswer) eq 'Complex') { + $z = Value::Complex->new($correctAnswer->Re,$correctAnswer->Im); + } else { + $z = Value::Formula->new($correctAnswer); + die "The professor's answer can't be a formula" unless $z->isConstant; + $z = $z->eval; $z = new Value::Complex($z) unless Value::class($z) eq 'String'; + } + $z->{correct_ans} = $correctAnswer; + + # + # Get the answer checker from the parser object + # + my $cmp = $z->cmp; + $cmp->install_pre_filter(sub { + my $rh_ans = shift; + $rh_ans->{original_student_ans} = $rh_ans->{student_ans}; + $rh_ans->{original_correct_ans} = $rh_ans->{correct_ans}; + return $rh_ans; + }); + $cmp->install_post_filter(sub { + my $rh_ans = shift; my $z = $rh_ans->{student_value}; + # + # Stringify student answer (use polar form if student did) + # + if (ref($z) && $z->isNumber) { + $z = Value::Complex->new($z); # promote real to complex + if ($rh_ans->{original_student_ans} =~ m/(^|[^a-zA-Z])e\s*(\^|\*\*)/) { + my ($a,$b) = ($z->mod,$z->arg); + unless ($context->flag('strict_numeric')) { + my $rt = (new Complex($z->Re->value,$z->Im->value))->stringify_polar; + ($a,$b) = ($rt =~ m/\[(.*),(.*)\]/); + } + $a = Value::Real->new($a)->string; + $b = Value::Real->new($b)->string if Value::matchNumber($b); + if ($b eq '0') { + $rh_ans->{student_ans} = $a; + } else { + if ($a eq '1') {$a = ''} elsif ($a eq '-1') {$a = '-'} else {$a .= '*'} + if ($b eq '1') {$b = 'i'} elsif ($b eq '-1') {$b = '(-i)'} else {$b = "($b i)"} + $rh_ans->{student_ans} = $a.'e^'.$b; + } + } else { + $rh_ans->{student_ans} = $rh_ans->{student_value}->string; + } + } + return $rh_ans; + }); + $cmp->{debug} = $cplx_params{debug}; + Parser::Context->current(\%main::context,$oldContext); + + return $cmp; +} + +# +# The original version, for backward compatibility +# (can be removed when the Parser-based version is more fully tested.) +# +sub original_cplx_cmp { my $correctAnswer = shift; my %cplx_params = @_; --- /dev/null +++ macros/contextRationalFunction.pl @@ -0,0 +1,184 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2010 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader: pg/macros/contextRationalFunction.pl,v 1.1 2010/04/01 00:21:45 dpvc Exp $ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + +=head1 NAME + +contextRationalFunction.pl - Only allow rational functions + (and their products and powers) + +=head1 DESCRIPTION + +Implements a context in which students can only enter rational +functions, with some control over whether a single division is +allowed, or whether products of rational functions are allowed. + +Select the context using: + + Context("RationalFunction"); + +The RationalFunction context supports all the flags of the +PolynomialFactors context, except for strictDivision, since rational +functions allow division of polynomials. + +In addition, there is a singleQuotients flag that controls whether +products of rational functions are allowed or not. By default, they +are allowed, but you can set this flag to 1 in order to force the +student answer to be as a single fraction. + +Finally, there is also a strict context that does not allow +operations even within the coefficients. Select it using: + + Context("RationalFunction-Strict"); + +In addition to disallowing operations within the coefficients, this +context does not reduce constant operations (since they are not +allowed), and sets the singlePowers, singleFactors, singleQuotients, +and stricPowers flags automatically. In addition, it disables all the +functions, though they can be re-enabled, if needed. + +=cut + +################################################## + +loadMacros( + "MathObjects.pl", + "contextPolynomialFactors.pl" +); + +sub _contextRationalFunction_init {RationalFunction::Init()} + +############################################## + +package RationalFunction::BOP::multiply; +our @ISA = qw(PolynomialFactors::BOP::multiply); + +sub checkFactors { + my $self = shift; my ($l,$r) = @_; + $self->SUPER::checkFactors($l,$r); + if (($l->{isPoly}||0) >= 6 || ($r->{isPoly}||0) >= 6) { + $self->Error("You can not use multiplication with rational functions as operands ". + "(do you need parentheses around the denominator?)") + if $self->context->flag("singleQuotients"); + $self->{isPoly} = 7; # product containing rational functions + } + return 1; +} + +############################################## + +package RationalFunction::BOP::divide; +our @ISA = qw(PolynomialFactors::BOP::divide); + +sub checkPolynomial { + my $self = shift; my ($l,$r) = ($self->{lop},$self->{rop}); + if ((!$l->{isPoly} || $l->{isPoly} == 2) && LimitedPolynomial::isConstant($r)) { + $self->{isPoly} = $l->{isPoly}; + $self->{powers} = $l->{powers}; delete $l->{powers}; + $self->{exponents} = $l->{exponents}; delete $l->{exponents}; + } elsif (($l->{isPoly}||0) >= 6 || ($r->{isPoly}||0) >= 6) { + $self->Error("Only one polynomial division is allowed in a rational function"); + } else { + PolynomialFactors::markFactor($l); + PolynomialFactors::markFactor($r); + $self->checkFactors($l,$r); + } + return 1; +} + +sub checkFactors { + my $self = shift; my ($l,$r) = @_; + my $single = $self->context->flag("singleFactors"); + $self->Error("Only one constant multiple or fraction is allowed (combine or cancel them)") + if $l->{factors}{0} && $r->{factors}{0} && $self->context->flag("singleFactors"); + $self->{factors} = $l->{factors}; delete $l->{factors}; + foreach my $factor (keys %{$r->{factors}}) { + if ($single && $self->{factors}{$factor}) { + $self->Error("Each factor can appear only once (combine or cancel like factors)") unless $factor eq "0"; + $self->Error("Only one constant coefficient or negation is allowed (combine or cancel them)"); + } + $self->{factors}{$factor} = 1; + } + delete $r->{factors}; + $self->{isPoly} = 6; # rational function + return 1; +} + +############################################## + +package RationalFunction::BOP::power; +our @ISA = qw(PolynomialFactors::BOP::power); + +sub checkPolynomial { + my $self = shift; my ($l,$r) = ($self->{lop},$self->{rop}); + $self->SUPER::checkPolynomial; + $self->{isPoly} = 6 if ($l->{isPoly}||0) >= 6; + return 1; +} + +############################################## + +package RationalFunction::UOP::minus; +our @ISA = qw(PolynomialFactors::UOP::minus); + +sub checkPolynomial { + my $self = shift; + $self->SUPER::checkPolynomial; + $self->{isPoly} = 6 if ($self->{op}{isPoly}||0) >= 6; + return 1; +} + +############################################## + +package RationalFunction; +our @ISA = ('PolynomialFactors'); + +sub Init { + # + # Build the new context that calls the + # above classes rather than the usual ones + # + + my $context = $main::context{RationalFunction} = Parser::Context->getCopy("PolynomialFactors"); + $context->{name} = "RationalFunction"; + $context->operators->set( + '*' => {class => 'RationalFunction::BOP::multiply'}, + '* ' => {class => 'RationalFunction::BOP::multiply'}, + ' *' => {class => 'RationalFunction::BOP::multiply'}, + ' ' => {class => 'RationalFunction::BOP::multiply'}, + '/' => {class => 'RationalFunction::BOP::divide'}, + ' /' => {class => 'RationalFunction::BOP::divide'}, + '/ ' => {class => 'RationalFunction::BOP::divide'}, + '^' => {class => 'RationalFunction::BOP::power'}, + '**' => {class => 'RationalFunction::BOP::power'}, + 'u-' => {class => 'RationalFunction::UOP::minus'}, + ); + $context->flags->set(strictPowers => 1); + + # + # A context where coefficients can't include operations + # + $context = $main::context{"RationalFunction-Strict"} = $context->copy; + $context->flags->set( + strictCoefficients => 1, + singlePowers => 1, singleFactors => 1, singleQuotients => 1, + reduceConstants => 0, + ); + $context->functions->disable("All"); # can be re-enabled if needed + + main::Context("RationalFunction"); ### FIXME: probably should require author to set this explicitly +} + +1; |