diff options
author | Jean-Michel Vedrine <vedrine@vedrine.org> | 2012-09-09 16:54:35 +0200 |
---|---|---|
committer | Jean-Michel Vedrine <vedrine@vedrine.org> | 2012-09-09 16:54:35 +0200 |
commit | 96bee2b33467a1ce54f88c77b8a867ef1f7a9a97 (patch) | |
tree | 27fe7dd14fd3742ad3a9b0eed2b1adbcddd4c3a8 /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.php | 356 |
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; + } +} |