aboutsummaryrefslogtreecommitdiff
path: root/question.php
diff options
context:
space:
mode:
authorJean-Michel Vedrine <vedrine@vedrine.org>2012-09-09 16:54:35 +0200
committerJean-Michel Vedrine <vedrine@vedrine.org>2012-09-09 16:54:35 +0200
commit96bee2b33467a1ce54f88c77b8a867ef1f7a9a97 (patch)
tree27fe7dd14fd3742ad3a9b0eed2b1adbcddd4c3a8 /question.php
First version of algebra question type initially written by Roger Moore for Moodle 2.3
Diffstat (limited to 'question.php')
-rw-r--r--question.php356
1 files changed, 356 insertions, 0 deletions
diff --git a/question.php b/question.php
new file mode 100644
index 0000000..12d27dd
--- /dev/null
+++ b/question.php
@@ -0,0 +1,356 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * algebra answer question definition class.
+ *
+ * @package qtype
+ * @subpackage algebra
+ * @author Roger Moore <rwmoore 'at' ualberta.ca>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/question/type/algebra/questiontype.php');
+require_once($CFG->dirroot . '/question/type/algebra/parser.php');
+require_once($CFG->dirroot . '/question/type/algebra/xmlrpc-utils.php');
+
+/**
+ * Represents an algebra question.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_algebra_question extends question_graded_by_strategy
+ implements question_response_answer_comparer {
+
+ /** @var array of question_answer. */
+ public $answers = array();
+ /** @var array of question_answer. */
+ public $variables = array();
+ public $compareby;
+ public $nchecks;
+ public $tolerance;
+ public $allowedfuncs;
+ public $disallow;
+ public $answerprefix;
+
+ public function __construct() {
+ parent::__construct(new question_first_matching_answer_grading_strategy($this));
+ }
+
+ public function get_expected_data() {
+ return array('answer' => PARAM_RAW_TRIMMED);
+ }
+
+ public function summarise_response(array $response) {
+ if (isset($response['answer'])) {
+ return $response['answer'];
+ } else {
+ return null;
+ }
+ }
+
+ public function is_complete_response(array $response) {
+ return array_key_exists('answer', $response) &&
+ ($response['answer'] || $response['answer'] === '0');
+ }
+
+ public function get_validation_error(array $response) {
+ if ($this->is_gradable_response($response)) {
+ return '';
+ }
+ return get_string('pleaseenterananswer', 'qtype_algebra');
+ }
+
+ /**
+ * Parses the given expression with the parser if required.
+ *
+ * This method will check to see if the argument it is given is already a parsed
+ * expression and if not will attempt to parse it.
+ *
+ * @param $expr expression which will be parsed
+ * @return top term of the parse tree or a string if an exception is thrown
+ */
+ public function parse_expression($expr) {
+ // Check to see if this is already a parsed expression
+ if(is_a($expr,'qtype_algebra_parser_term')) {
+ // It is a parsed expression so simply return it
+ return $expr;
+ }
+
+ // Create an array of variable names for the parser from the question if defined
+ $varnames=array();
+ if(isset($this->variables)) {
+ foreach($this->variables as $var) {
+ $varnames[]=$var->name;
+ }
+ }
+ // We now assume that we have a string to parse. Create a parser instance to
+ // to this and return the parser expression at the top of the parse tree
+ $p=new qtype_algebra_parser;
+ // Perform the actual parsing inside a try-catch block so that any exceptions
+ // can be caught and converted into errors
+ try {
+ return $p->parse($expr,$varnames);
+ } catch(Exception $e) {
+ // If the expression cannot be parsed then return a null term. This will
+ // make Moodle treat the answer as wrong.
+ // TODO: Would be nice to have support for 'invalid answer' in the quiz
+ // engine since an unparseable response is usually caused by a silly typo
+ return new qtype_algebra_parser_nullterm;
+ }
+ }
+
+ /**
+ * Parses the given expression with the parser if required.
+ *
+ * This method will parse the expression and return a TeX string
+ * or empty string
+ *
+ * @param $expr expression which will be parsed
+ * @return top term of the parse tree or a string if an exception is thrown
+ */
+ public function formated_expression($text) {
+
+ // Create an array of variable names for the parser from the question if defined
+ $varnames=array();
+ if(isset($this->variables)) {
+ foreach($this->variables as $var) {
+ $varnames[]=$var->name;
+ }
+ }
+ // We now assume that we have a string to parse. Create a parser instance to
+ // to this and return the parser expression at the top of the parse tree
+ $p=new qtype_algebra_parser;
+ // Perform the actual parsing inside a try-catch block so that any exceptions
+ // can be caught and converted into errors
+ try {
+ $exp = $p->parse($text, $varnames);
+ return '$$'.$exp->tex().'$$';
+ } catch(Exception $e) {
+ return '';
+ }
+ }
+
+ public function is_same_response(array $prevresponse, array $newresponse) {
+ // Check that both states have valid responses
+ if (!isset($prevresponse['answer']) or !isset($newresponse['answer'])) {
+ // At last one of the states did not have a response set so return false by default
+ return false;
+ }
+ // Parse the previous response
+ $expr=$this->parse_expression($prevresponse['answer']);
+ // Parse the new response
+ $testexpr=$this->parse_expression($newresponse['answer']);
+ // The type of comparison done depends on the comparision algorithm selected by
+ // the question. Use the defined algorithm to select which comparison function
+ // to call...
+ if($this->compareby == 'sage') {
+ // Uses an XML-RPC server with SAGE to perform a full symbollic comparision
+ return self::test_response_by_sage($expr,$testexpr);
+ } else if($this->compareby == 'eval') {
+ // Tests the response by evaluating it for a certain range of each variable
+ return self::test_response_by_evaluation($expr,$testexpr);
+ } else {
+ // Tests the response by performing a simple parse tree equivalence algorithm
+ return self::test_response_by_equivalence($expr,$testexpr);
+ }
+ }
+
+ public function get_answers() {
+ return $this->answers;
+ }
+
+ public function compare_response_with_answer(array $response, question_answer $answer) {
+ // Deal with the match anything answer by returning true
+ if ($answer->answer == '*') {
+ return true;
+ }
+ $expr=$this->parse_expression($response['answer']);
+ // Check that there is a response and if not return false. We do this here
+ // because even an empty response should match a widlcard answer.
+ if(is_a($expr,'qtype_algebra_parser_nullterm')) {
+ return false;
+ }
+
+ // Now parse the answer
+ $ansexpr=$this->parse_expression($answer->answer);
+ // The type of comparison done depends on the comparision algorithm selected by
+ // the question. Use the defined algorithm to select which comparison function
+ // to call...
+ if($this->compareby == 'sage') {
+ // Uses an XML-RPC server with SAGE to perform a full symbollic comparision
+ return self::test_response_by_sage($expr,$ansexpr);
+ } else if($this->compareby == 'eval') {
+ // Tests the response by evaluating it for a certain range of each variable
+ return self::test_response_by_evaluation($expr,$ansexpr);
+ } else {
+ // Tests the response by performing a simple parse tree equivalence algorithm
+ return self::test_response_by_equivalence($expr,$ansexpr);
+ }
+ }
+
+ /**
+ * Checks whether a response matches a given answer using SAGE
+ *
+ * This method will compare the given response to the given answer using the SAGE
+ * open source algebra computation software. The software is run by a remote
+ * XML-RPC server which is called with both the asnwer and the response and told to
+ * compare the two algebraic expressions.
+ *
+ * @return boolean true if the response matches the answer, false otherwise
+ */
+ function test_response_by_sage($response, $answer) {
+ // TODO: Store server information in the Moodle configuration
+ $request=array(
+ 'host' => 'localhost',
+ 'port' => 7777,
+ 'uri' => ''
+ );
+ // Sets the name of the method to call to full_symbolic_compare
+ $request['method']='full_symbolic_compare';
+ // Get a list of all the variables to declare
+ $vars=$response->get_variables();
+ $vars=array_merge($vars,array_diff($vars,$answer->get_variables()));
+ // Sets the arguments to the sage string of the response and the list of variables
+ $request['args']=array($answer->sage(),$response->sage(),$vars);
+ // Calls the XML-RPC method on the server and returns the response
+ return xu_rpc_http_concise($request)==0;
+ }
+
+ /**
+ * Checks whether a response matches a given answer using an evaluation method
+ *
+ * This method will compare the given response to the given answer by evaluating both
+ * for given values of the variables. Each variable must have a predefined range over
+ * which it can be checked and then both expressions will be evalutated several times
+ * using values randomly chosen to be within the range.
+ *
+ * @return boolean true if the response matches the answer, false otherwise
+ */
+ function test_response_by_evaluation($response, $answer) {
+ // Flag used to denote mismatch in response and answer
+ $same=true;
+ // Run the evaluation loop 10 times with different random variables...
+ for($i=0;$i<$this->nchecks;$i++) {
+ // Create an array to store the values of all the variables
+ $values=array();
+ // Loop over all the variables in the question
+ foreach($this->variables as $var) {
+ // Set the value of the variable to a random number between the min and max
+ $values[$var->name]=$var->min+lcg_value()*abs($var->max-$var->min);
+ }
+ $resp_value=$response->evaluate($values);
+ $ans_value=$answer->evaluate($values);
+ // Return false if only one of the reponse or answer gives NaN
+ if(is_nan($resp_value) xor is_nan($ans_value)) {
+ return false;
+ }
+ // Return false if only one of the reponse or answer is infinite
+ if(is_infinite($resp_value) xor is_infinite($ans_value)) {
+ return false;
+ }
+ // Use the fractional difference method if the answer has a value
+ // which is clearly distinguishable from zero
+ if(abs($ans_value)>1e-300) {
+ // Get the difference between the response and answer evaluations
+ $diff=abs(($resp_value-$ans_value)/$ans_value);
+ }
+ // Otherwise use an arbitrary minimum value
+ else {
+ // Get the difference between the response and answer evaluations
+ $diff=abs(($resp_value-$ans_value)/1e-300);
+ }
+ // Check to see if the difference is greater than tolerance
+ if($diff > $this->tolerance) {
+ // Return false since the formulae have been shown not to be the same
+ return false;
+ }
+ }
+ // We made it through the loop so now return true
+ return true;
+ }
+
+ /**
+ * Checks whether a response matches a given answer using a simple equivalence algorithm
+ *
+ * This method will compare the given response to the given answer by simply checking to
+ * see if the two parse trees are equivalent. This allows for a slightly more sophisticated
+ * check than a simple text compare but will not, neccessarily, catch two equivalent but
+ * different algebraic expressions.
+ *
+ * @return boolean true if the response matches the answer, false otherwise
+ */
+ public function test_response_by_equivalence($response, $answer) {
+ // Use the parser's equivalent method to see if the response is the same as the answer
+ return $response->equivalent($answer);
+ }
+
+ public function check_file_access($qa, $options, $component, $filearea,
+ $args, $forcedownload) {
+ if ($component == 'question' && $filearea == 'answerfeedback') {
+ $currentanswer = $qa->get_last_qt_var('answer');
+ $answer = $qa->get_question()->get_matching_answer(array('answer' => $currentanswer));
+ $answerid = reset($args); // itemid is answer id.
+ return $options->feedback && $answerid == $answer->id;
+
+ } else if ($component == 'question' && $filearea == 'hint') {
+ return $this->check_hint_file_access($qa, $options, $args);
+
+ } else {
+ return parent::check_file_access($qa, $options, $component, $filearea,
+ $args, $forcedownload);
+ }
+ }
+}
+
+/**
+ * Class to represent an algebra question variable, loaded from the question_algebra_variables table
+ * in the database.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_variable {
+ /** @var integer the answer id. */
+ public $id;
+
+ /** @var string the name. */
+ public $name;
+
+ /** @var string minimum value. */
+ public $min = '-';
+
+ /** @var string maximum value. */
+ public $max = '-';
+
+ /**
+ * Constructor.
+ * @param int $id the variable.
+ * @param string $name the name.
+ * @param string $min the minimum value.
+ * @param string $maximum value.
+ */
+ public function __construct($id, $name, $min, $max) {
+ $this->id = $id;
+ $this->name = $name;
+ $this->min = $min;
+ $this->max = $max;
+ }
+}