diff --git a/lib/Value/AnswerChecker.pm b/lib/Value/AnswerChecker.pm
index 3ed4f18535..46a3fa57ea 100644
--- a/lib/Value/AnswerChecker.pm
+++ b/lib/Value/AnswerChecker.pm
@@ -2050,48 +2050,44 @@ sub cmp_postprocess {
$self->cmp_Error($ans, "The dimension of your result is incorrect");
}
-#
-# Diagnostics for Formulas
-#
+# Diagnostics for Formulas.
sub cmp_diagnostics {
- my $self = shift;
- my $ans = shift;
- my $isEvaluator = (ref($ans) =~ /Evaluator/) ? 1 : 0;
- my $hash = $isEvaluator ? $ans->rh_ans : $ans;
- my $diagnostics = $self->{context}->diagnostics->merge("formulas", $self, $hash);
+ my ($self, $ans) = @_;
+ my $isEvaluator = ref($ans) =~ /Evaluator/ ? 1 : 0;
+ my $hash = $isEvaluator ? $ans->rh_ans : $ans;
+ my $diagnostics = $self->{context}->diagnostics->merge('formulas', $self, $hash);
my $formulas = $diagnostics->{formulas};
return unless $formulas->{show};
- my $output = "";
+ my $output = '';
if ($isEvaluator) {
- #
- # The tests to be performed when the answer checker is created
- #
- $self->getPG('loadMacros("PGgraphmacros.pl")');
+ # The tests to be performed when the answer checker is created.
+ $self->getPG('loadMacros("plots.pl")');
my ($inputs) = $self->getPG('$inputs_ref');
my $process = $inputs->{checkAnswers} || $inputs->{previewAnswers} || $inputs->{submitAnswers};
if ($formulas->{checkNumericStability} && !$process) {
- ### still needs to be written
+ ### Still needs to be written.
}
} else {
- #
- # The checks to be performed when an answer is submitted
- #
+ # The checks to be performed when an answer is submitted.
my $student = $ans->{student_formula};
- #
- # Get the test points
- #
- my @names = $self->{context}->variables->names;
- my $vx = (keys(%{ $self->{variables} }))[0];
- my $vi = 0;
- while ($names[$vi] ne $vx) { $vi++ }
- my $points = [ map { $_->[$vi] } @{ $self->{test_points} } ];
- my @params = $self->{context}->variables->parameters;
- @names = $self->{context}->variables->variables;
- #
- # The graphs of the functions and errors
- #
+ # Get the test points. This requires first determining what variables are in use and ignore parameters.
+ my $points = [];
+ my @params = $self->{context}->variables->parameters;
+ my %pnames = map { $_ => 1 } @params;
+ my @names = $self->{context}->variables->variables;
+ my @variables = grep { !$pnames{$_} } keys %{ $self->{variables} };
+
+ # If a single variable is found, use its test points for the graph.
+ # Functions with more than one variable are not graphed so test points are not needed.
+ if (scalar(@variables) == 1) {
+ my $vi = 0;
+ while ($names[$vi] ne $variables[0]) { $vi++ }
+ $points = [ map { $_->[$vi] } @{ $self->{test_points} } ];
+ }
+
+ # The graphs of the functions and errors.
if ($formulas->{showGraphs}) {
my @G = ();
if ($formulas->{combineGraphs}) {
@@ -2099,7 +2095,7 @@ sub cmp_diagnostics {
@G,
$self->cmp_graph(
$diagnostics, [ $student, $self ],
- title => 'Student Answer (red)
Correct Answer (green)
',
+ title => 'Student Answer (red)
Correct Answer (green)',
points => $points,
showDomain => 1
)
@@ -2131,157 +2127,152 @@ sub cmp_diagnostics {
)
);
}
- $output .=
- '
'
- . ''
- . join(' | ', @G)
- . '
';
+ $output .= join('', @G);
}
- #
- # The adaptive parameters
- #
+ # The adaptive parameters.
if ($formulas->{showParameters} && scalar(@params) > 0) {
- $output .= '
Adaptive Parameters: ';
- $output .= join(" ", map { " $params[$_]: " . $self->{parameters}[$_] } (0 .. $#params));
- $output .= ' |
';
+ $output .=
+ 'Adaptive Parameters
'
+ . join("
", map { " $params[$_]: " . $self->{parameters}[$_] } 0 .. $#params)
+ . '
';
}
- #
- # The test points and values
- #
- my @rows = ();
- my $colsep = ' | ';
+ # The test points and values.
+ my @rows = ();
my @P =
(map { (scalar(@{$_}) == 1) ? $_->[0] : $self->Package("Point")->make(@{$_}) } @{ $self->{test_points} });
- my @i = sort { $P[$a] <=> $P[$b] } (0 .. $#P);
- foreach $p (@P) {
- if (Value::isValue($p) && $p->length > 2) { $p = $p->string; $p =~ s|,|, |g }
- }
- my $zeroLevelTol = $self->{context}{flags}{zeroLevelTol};
- $self->{context}{flags}{zeroLevelTol} = 0; # always show full resolution in the tables below
- my $names = join(',', @names);
- $names = '(' . $names . ')' if scalar(@names) > 1;
-
- $student->createPointValues($self->{test_points}, 0, 1, 1) unless $student->{test_values};
-
- my $cv = $self->{test_values};
- my $sv = $student->{test_values};
- my $av = $self->{test_adapt} || $cv;
-
- if ($formulas->{showTestPoints}) {
- my @p = ("$names:", (map { $P[ $i[$_] ] } (0 .. $#P)));
- push(@rows, ' | | ' . join($colsep, @p) . ' |
');
- push(@rows, '' . join($colsep, (" ") x scalar(@p)) . ' |
');
- push(
- @rows,
- '| '
- . join($colsep,
- ($av == $cv) ? "Correct Answer:" : "Adapted Answer:",
- map { Value::isNumber($av->[ $i[$_] ]) ? $av->[ $i[$_] ] : "undefined" } (0 .. $#P))
- . ' |
'
- );
- push(
- @rows,
- '| '
- . join($colsep,
- "Student Answer:",
- map { Value::isNumber($sv->[ $i[$_] ]) ? $sv->[ $i[$_] ] : "undefined" } (0 .. $#P))
- . ' |
'
- );
- }
- #
- # The absolute errors (colored by whether they are ok or too big)
- #
- if ($formulas->{showAbsoluteErrors}) {
- my @p = ("Absolute Error:");
- my $tolerance = $self->getFlag('tolerance');
- my $tolType = $self->getFlag('tolType');
- my $error;
- foreach my $j (0 .. $#P) {
- if (Value::isNumber($sv->[ $i[$j] ])) {
- $error = CORE::abs($av->[ $i[$j] ] - $sv->[ $i[$j] ]);
- $error =
- ''
- . $error
- . ''
- if $tolType eq 'absolute';
- } else {
- $error = "---";
+ if (@P) {
+ my @i = sort { $P[$a] <=> $P[$b] } 0 .. $#P;
+ my @points = ([] x scalar(@names));
+ for my $p (@P) {
+ my @pt = Value::isValue($p) ? $p->value : ($p);
+ for (0 .. $#pt) {
+ push(@{ $points[$_] }, $pt[$_]);
}
- push(@p, $error);
}
- push(@rows, '| ' . join($colsep, @p) . ' |
');
- }
- #
- # The relative errors (colored by whether they are OK or too big)
- #
- if ($formulas->{showRelativeErrors}) {
- my @p = ("Relative Error:");
- my $tolerance = $self->getFlag('tolerance');
- my $tol;
- my $tolType = $self->getFlag('tolType');
- my $error;
- my $zeroLevel = $self->getFlag('zeroLevel');
- foreach my $j (0 .. $#P) {
- if (Value::isNumber($sv->[ $i[$j] ])) {
- my $c = $av->[ $i[$j] ];
- my $s = $sv->[ $i[$j] ];
- if (CORE::abs($cv->[ $i[$j] ]->value) < $zeroLevel || CORE::abs($s->value) < $zeroLevel) {
- $error = CORE::abs($c - $s);
- $tol = $zeroLevelTol;
+ my $zeroLevelTol = $self->{context}{flags}{zeroLevelTol};
+ $self->{context}{flags}{zeroLevelTol} = 0; # Always show full resolution in the tables below.
+
+ $student->createPointValues($self->{test_points}, 0, 1, 1) unless $student->{test_values};
+
+ my $cv = $self->{test_values};
+ my $sv = $student->{test_values};
+ my $av = $self->{test_adapt} || $cv;
+
+ if ($formulas->{showTestPoints}) {
+ for my $k (0 .. $#names) {
+ # Only show variables that are used in either answer or student formula.
+ push(@rows, [ $names[$k], map { $points[$k][ $i[$_] ] } 0 .. $#P ])
+ if $self->{variables}{ $names[$k] } || $student->{variables}{ $names[$k] };
+ }
+ push(
+ @rows,
+ [
+ $av == $cv ? 'Correct Answer' : 'Adapted Answer',
+ map { Value::isNumber($av->[ $i[$_] ]) ? $av->[ $i[$_] ] : 'undefined' } 0 .. $#P
+ ]
+ );
+ push(
+ @rows,
+ [
+ 'Student Answer',
+ map { Value::isNumber($sv->[ $i[$_] ]) ? $sv->[ $i[$_] ] : 'undefined' } 0 .. $#P
+ ]
+ );
+ }
+
+ # The absolute errors (colored by whether they are OK or too big).
+ if ($formulas->{showAbsoluteErrors}) {
+ my @p = ();
+ my $tolerance = $self->getFlag('tolerance');
+ my $tolType = $self->getFlag('tolType');
+ my $error;
+ for my $j (0 .. $#P) {
+ if (Value::isNumber($sv->[ $i[$j] ])) {
+ $error = CORE::abs($av->[ $i[$j] ] - $sv->[ $i[$j] ]);
+ $error =
+ ''
+ . $error
+ . ''
+ if $tolType eq 'absolute';
} else {
- $error = CORE::abs(($c - $s) / ($c || 1E-10));
- $tol = $tolerance;
+ $error = "---";
}
- $error = '' . $error . ''
- if $tolType eq 'relative';
- } else {
- $error = "---";
+ push(@p, "$error");
+ }
+ push(@rows, [ 'Absolute Error', @p ]);
+ }
+
+ # The relative errors (colored by whether they are OK or too big).
+ if ($formulas->{showRelativeErrors}) {
+ my @p = ();
+ my $tolerance = $self->getFlag('tolerance');
+ my $tol;
+ my $tolType = $self->getFlag('tolType');
+ my $error;
+ my $zeroLevel = $self->getFlag('zeroLevel');
+ for my $j (0 .. $#P) {
+ if (Value::isNumber($sv->[ $i[$j] ])) {
+ my $c = $av->[ $i[$j] ];
+ my $s = $sv->[ $i[$j] ];
+ if (CORE::abs($cv->[ $i[$j] ]->value) < $zeroLevel || CORE::abs($s->value) < $zeroLevel) {
+ $error = CORE::abs($c - $s);
+ $tol = $zeroLevelTol;
+ } else {
+ $error = CORE::abs(($c - $s) / ($c || 1E-10));
+ $tol = $tolerance;
+ }
+ $error =
+ '' . $error . ''
+ if $tolType eq 'relative';
+ } else {
+ $error = "---";
+ }
+ push(@p, "$error");
}
- push(@p, $error);
+ push(@rows, [ 'Relative Error', @p ]);
}
- push(@rows, '| ' . join($colsep, @p) . ' |
');
+ $self->{context}{flags}{zeroLevelTol} = $zeroLevelTol;
}
- $self->{context}{flags}{zeroLevelTol} = $zeroLevelTol;
- #
- # Put the data into a table
- #
+
+ # Put the data into a table.
if (scalar(@rows)) {
- $output .=
- '
'
- . join(' | ', @rows)
- . '
';
+ my $tdstyle = 'style="padding:5px"';
+ my $trstyle = 'style="border:1px solid black"';
+ $output .= '';
+ for my $row (@rows) {
+ my $header = shift(@$row);
+ $output .=
+ "| $header | "
+ . join(" | ", @$row)
+ . ' |
';
+ }
+ $output .= '
';
}
}
- #
- # Put all the diagnostic output into a frame
- #
+
+ # Put all the diagnostic output into a frame.
return unless $output;
$output =
- ''
- . 'Diagnostics for '
- . $self->string . ':'
- . ''
+ ''
+ . ' Diagnostics for '
+ . $self->string
+ . ' '
. $output
- . ' |
';
+ . '';
$self->getPG('$PG')->debug_message($output);
}
-#
-# Draw a graph from a given Formula object
-#
+# Draw a graph from a given Formula object.
sub cmp_graph {
- my $self = shift;
- my $diagnostics = shift;
- my $F1 = shift;
+ my ($self, $diagnostics, $F1, @opts) = @_;
my $F2;
- ($F1, $F2) = @{$F1} if (ref($F1) eq 'ARRAY');
- #
- # Get the various options
- #
- my %options = (title => '', points => [], @_);
+ ($F1, $F2) = @{$F1} if ref($F1) eq 'ARRAY';
+
+ # Get the various options.
+ my %options = (title => '', points => [], @opts);
my $graphs = $diagnostics->{graphs};
my $limits = $graphs->{limits};
my $size = $graphs->{size};
@@ -2291,49 +2282,50 @@ sub cmp_graph {
my $clip = $options{clip};
my ($my, $My) = (0, 0);
my ($mx, $Mx);
- my $dx;
- my $f;
- my $y;
my @pnames = $self->{context}->variables->parameters;
my @pvalues = ($self->{parameters} ? @{ $self->{parameters} } : (0) x scalar(@pnames));
- my $x = "";
+ my $x = '';
- #
- # Find the max and min values of the function
- #
- foreach $f ($F1, $F2) {
+ # Find the max and min values of the function.
+ for my $f ($F1, $F2) {
next unless defined($f);
- foreach my $v (keys(%{ $f->{variables} })) {
+ for my $v (keys(%{ $f->{variables} })) {
if ($v ne $x && !$f->{context}->variables->get($v)->{parameter}) {
if ($x) {
- warn "Only formulas with one variable can be graphed" unless $self->{graphWarning};
+ my $warn = $self->{graphWarning};
$self->{graphWarning} = 1;
- return "";
+ return $warn
+ ? ''
+ : '
'
+ . 'Only formulas with one variable can be graphed.
';
}
$x = $v;
}
}
unless ($f->typeRef->{length} == 1) {
- warn "Only real-valued functions can be graphed" unless $self->{graphWarning};
+ my $warn = $self->{graphWarning};
$self->{graphWarning} = 1;
- return "";
+ return $warn
+ ? ''
+ : ''
+ . 'Only formulas with one variable can be graphed.
';
}
$x = ($f->{context}->variables->names)[0] unless $x;
$limits = [ $self->getVariableLimits($x) ] unless $limits;
$limits = $limits->[0] while ref($limits) eq 'ARRAY' && ref($limits->[0]) eq 'ARRAY';
($mx, $Mx) = @{$limits};
- $dx = ($Mx - $mx) / $steps;
+ my $dx = ($Mx - $mx) / $steps;
if ($f->isConstant) {
- $y = $f->eval;
+ my $y = $f->eval;
$my = $y if $y < $my;
$My = $y if $y > $My;
} else {
my $F = $f->perlFunction(undef, [ $x, @pnames ]);
- foreach my $i (0 .. $steps - 1) {
- $y = eval { &{$F}($mx + $i * $dx, @pvalues) };
+ for my $i (0 .. $steps - 1) {
+ my $y = eval { &{$F}($mx + $i * $dx, @pvalues) };
next unless defined($y) && Value::isNumber($y);
$my = $y if $y < $my;
$My = $y if $y > $My;
@@ -2349,80 +2341,58 @@ sub cmp_graph {
}
$my = -$My / 10 if $my > -$My / 10;
$My = -$my / 10 if $My < -$my / 10;
- my $a = $self->Package("Real")->new(($My - $my) / ($Mx - $mx));
+ my $a = $self->Package('Real')->new(($My - $my) / ($Mx - $mx));
- #
- # Create the graph itself, with suitable title
- #
+ # Create the graph itself, with suitable title.
my $grf = $self->getPG('$_grf_ = {n => 0}');
$grf->{Goptions} = [
- $mx, $my, $Mx, $My,
- axes => $graphs->{axes},
- grid => $graphs->{grid},
- size => $size,
+ xmin => $mx,
+ xmax => $Mx,
+ xtick_delta => ($Mx - $mx) / $graphs->{grid}->[0],
+ xminor => 0,
+ xlabel => "\\($x\\)",
+ ymin => $my,
+ ymax => $My,
+ ytick_delta => ($My - $my) / $graphs->{grid}->[1],
+ yminor => 0,
+ ytick_label_format => abs($My) < 0.01 ? 'scinot' : 'decimal',
+ ylabel => '\(f\)'
];
- $grf->{params} = {
- names => [ $x, @pnames ],
- values => { map { $pnames[$_] => $pvalues[$_] } (0 .. scalar(@pnames) - 1) },
- };
- $grf->{G} = $self->getPG('init_graph(@{$_grf_->{Goptions}})');
- $grf->{G}->imageName($grf->{G}->imageName . '-' . time()); # avoid browser cache
- $self->cmp_graph_function($grf, $F2, "green", $steps, $points) if defined($F2);
- $self->cmp_graph_function($grf, $F1, "red", $steps, $points);
- my $image = $self->getPG('alias(insertGraph($_grf_->{G}))');
- $image =
- '
';
+ $grf->{variable_name} = $x;
+ $grf->{params} = { map { $pnames[$_] => $pvalues[$_] } 0 .. $#pnames };
+ $grf->{G} = $self->getPG('Plot(@{$_grf_->{Goptions}})');
+ $self->cmp_graph_function($grf, $F2, $mx, $Mx, 'green', $points) if defined($F2);
+ $self->cmp_graph_function($grf, $F1, $mx, $Mx, 'red', $points);
+ my $image = '' . $self->getPG('image($_grf_->{G})') . '
';
my $title = $options{title};
- $title .= '' if $title;
- $title .= "Domain: [$mx,$Mx]
" if $options{showDomain};
- $title .= "Range: [$my,$My]
Aspect ratio: $a:1";
- return '' . $image . ' ' . $title . ' | ';
+ $title = "$title
" if $title;
+ return
+ ''
+ . $title
+ . ($options{showDomain} ? "Domain: [$mx,$Mx]
" : '')
+ . "Range: [$my,$My]
Aspect ratio: $a:1"
+ . $image
+ . '
';
}
-#
-# Add a function to a graph object, and plot the points
-# that are used to test the function
-#
+# Add a function to a Plots::Plot object, and plot the points
+# that are used to test the function.
sub cmp_graph_function {
- my $self = shift;
- my $grf = shift;
- my $F = shift;
- my $color = shift;
- my $steps = shift;
- my $points = shift;
- $grf->{n}++;
- my $Fn = "F" . $grf->{n};
- $grf->{$Fn} = $F;
- my $f;
+ my ($self, $grf, $F, $min, $max, $color, $points) = @_;
if ($F->isConstant) {
my $y = $F->eval;
- $f = $self->getPG('new Fun(sub {' . $y . '},$_grf_->{G})');
+ $grf->{G}->add_dataset([ $min, $y ], [ $max, $y ], color => $color);
} else {
- my $X = $grf->{params}{names}[0];
- $f =
- $self->getPG('new Fun(sub {Parser::Evaluate($_grf_->{'
- . $Fn . '},'
- . $X
- . '=>shift,%{$_grf_->{params}{values}})},$_grf_->{G})');
- foreach my $x (@{$points}) {
- my $y = Parser::Evaluate($F, ($X) => $x, %{ $grf->{params}{values} });
+ my $X = $grf->{variable_name};
+ $F = $F->substitute(%{ $grf->{params} }) if %{ $grf->{params} };
+ $grf->{G}->add_function($F, $X, $min, $max, color => $color);
+ for my $x (@{$points}) {
+ my $y = Parser::Evaluate($F, ($X) => $x);
next unless defined($y) && Value::isNumber($y);
- $grf->{x} = $x;
- $grf->{'y'} = $y;
- my $C = $self->getPG('new Circle($_grf_->{x},$_grf_->{y},4,"' . $color . '","' . $color . '")');
- $grf->{G}->stamps($C);
+ $grf->{G}->add_point($x, $y, color => $color);
}
}
- $f->color($color);
- $f->weight(2);
- $f->steps($steps);
}
#
diff --git a/lib/Value/Context/Diagnostics.pm b/lib/Value/Context/Diagnostics.pm
index e095de3daa..f5e66d9a65 100644
--- a/lib/Value/Context/Diagnostics.pm
+++ b/lib/Value/Context/Diagnostics.pm
@@ -31,7 +31,6 @@ sub new {
limits => undef,
size => 250,
grid => [ 10, 10 ],
- axes => [ 0, 0 ],
},
@_,
);