STACK3.3.3を利用する

14 9月

 

ubuntu14.04 で STACK 3.3.3 をインストールしてみたら、動作しませんでした。maximaのバージョンをubuntu14.04のものより上げる必要があるようで、maxima 5.36.0 に上げると、STACKが正常に動きました。しかし、この新しい maxima のバージョンでは、以前に書いた 数式自動採点用のコード(STACK のコードを利用している)が動作しないことが最近分かりました。つまり、次の ubuntu (多分 ubuntu 16.04)では、maxima のバージョンが 5.36.0 以上となるので、現在の自動採点のコードが動作しなくなりそうです。maxima の仕様に左右されるのは、この先かなり厄介ですが、とにかく対応しようと思います。以下、作業を記録します。 maxima 5.360 は、コンパイルしてインストールしました。そうすると、maximaの場所が変更されます。これまで /usr/bin/maximaだったのが, /usr/local/bin/maximaになります。バージョンを上げて上手く行った理由には、LISPの違いもあるのかもしれません。

上図のような、STACK の動作確認をするページがあります。moodle/question/type/stack/healthcheck.php が作成しているページです。これの中程に「CASをキャッシュしません」と書かれたところがあります。

その内容はコード中の下記の箇所が関連していると思われます。

// Test an *uncached* call to the CAS.  I.e. a genuine call to the process.
echo $OUTPUT->heading(stack_string(‘healthuncached’), 3);
echo html_writer::tag(‘p’, stack_string(‘healthuncachedintro’));
list($message, $genuinedebug, $result) = stack_connection_helper::stackmaxima_genuine_connect();
$summary[] = array($result, $message);
echo html_writer::tag(‘p’, $message);
echo output_debug(stack_string(‘debuginfo’), $genuinedebug);
$genuinecascall = $result;

この中で、 stack_connection_helper は、定義が connectorhelper.class.php にあります。このうち、 stackmaxima_genuine_connect() を下記に抜き出します。

 /**
* Really exectue a CAS command, regardless of the cache settings.
*/
public static function stackmaxima_genuine_connect() {
self::ensure_config_loaded();

// Put something non-trivial in the call.
$date = date(“Y-m-d H:i:s”);

$command = ‘cab:block([],print(“[TimeStamp= [ 0 ], Locals= [ 0=[ error= [“), ‘ .
‘cte(“CASresult”,errcatch(diff(x^n,x))), print(“1=[ error= [“), ‘ .
‘cte(“STACKversion”,errcatch(stackmaximaversion)), print(“2=[ error= [“), ‘ .
‘cte(“MAXIMAversion”,errcatch(MAXIMA_VERSION)), print(“3=[ error= [“), ‘ .
‘cte(“CAStime”,errcatch(CAStime:”‘.$date.'”)), print(“] ]”), return(true));’ .
“\n”;

// Really make sure there is no cache.
list($results, $debug) = self::stackmaxima_nocache_call($command);

$success = true;
$message = ”;
if (empty($results)) {
$message = stack_string(‘stackCas_allFailed’);
$success = false;
} else {
foreach ($results as $result) {
if (‘CASresult’ === $result[‘key’]) {
if ($result[‘value’] != ‘n*x^(n-1)’) {
$success = false;
}
} else if (‘CAStime’ === $result[‘key’]) {
if ($result[‘value’] != ‘”‘.$date.'”‘) {
$success = false;
}
} else if (‘STACKversion’ !== $result[‘key’] && ‘MAXIMAversion’ !== $result[‘key’]) {
$success = false;
}
}
}

if ($success) {
$message = stack_string(‘healthuncachedstack_CAS_ok’);
} else {
$message .= stack_string(‘healthuncachedstack_CAS_not’);
}

return array($message, $debug, $success);
}

 $command という変数がありますが、中身は Maxima のコードです。この内容を計算して画面に出力しているようです。

( cte() 関数は、stackmaxima.mac の中で定義されています。errcatch() 関数は Maxima の関数で、カッコ内の式を順に評価して、もしエラーが生じなければ結果のリストを返し、途中でエラーが生じたら空のリストを返します。cte() 関数の中身は下記。

/* This function executes ex, which is assumed to be a stack expression  */
/* which is surrounded by errcatch.  Hence we end up with a list.        */
cte(var,ex) := block([str],
print(“], key= [“),
print(var),
print(“]”),
if ex = [] then block(
ex:STACKERROR,
print(“, value = [], display = []”)
)
else block(
print(“, value = [“),
print(string(ex[1])),
print(“], display = [“),
print(StackDISP(ex[1], “”)),
print(“]”),
ex:ex[1]
),
print(“], “),
return(ex)
)$

ここから次に下記のコードで

// Really make sure there is no cache.
list($results, $debug) = self::stackmaxima_nocache_call($command);

おなじクラス内の stackmaxima_nocache_call($command) に移ります。すぐ上で定義されているのですが、下記のような内容です。

 /**
* Exectue a CAS command, without any caching.
*/
private static function stackmaxima_nocache_call($command) {
self::ensure_config_loaded();

$configcache = self::$config->casresultscache;
$casdebugging = self::$config->casdebugging;
self::$config->casresultscache = ‘none’;
self::$config->casdebugging = true;

$connection = self::make();
$results = $connection->compute($command);

self::$config->casresultscache = $configcache;
self::$config->casdebugging = $casdebugging;

$debug = $connection->get_debuginfo();
return array($results, $debug);
}

ここから、 compute($command) が呼び出されて、これの定義は connector.class.php にあって、下記の内容です。

 /* @see stack_cas_connection::compute() */
public function compute($command) {

$context = “Platform: “. stack_connection_helper::get_platform() . “\n”;
$context .= “Maxima shell command: “. $this->command . “\n”;;
$context .= “Maxima initial command: “. $this->initcommand . “\n”;
$context .= “Maxima timeout: “. $this->timeout;
$this->debug->log(‘Context used’, $context);

$this->debug->log(‘Maxima command’, $command);

$rawresult = $this->call_maxima($command);
$this->debug->log(‘CAS result’, $rawresult);

$unpackedresult = $this->unpack_raw_result($rawresult);
$this->debug->log(‘Unpacked result as’, print_r($unpackedresult, true));

if (!stack_connection_helper::check_stackmaxima_version($unpackedresult)) {
stack_connection_helper::warn_about_version_mismatch($this->debug);
}

return $unpackedresult;
}

それらしい、文字列が並んでいます。画面に出力された文字列と同じです。その後、これ自身の call_maxima($command) を呼び出しているのですが、実際の中身は connector.unix.class.php に書かれています。その内容を下記にあげます。

 /* @see stack_cas_connection_base::call_maxima() */
protected function call_maxima($command) {

$ret = false;
$err = ”;
$cwd = null;
$env = array(‘why’ => ‘itworks’);

$descriptors = array(
0 => array(‘pipe’, ‘r’),
1 => array(‘pipe’, ‘w’),
2 => array(‘pipe’, ‘w’));
$casprocess = proc_open($this->command, $descriptors, $pipes, $cwd, $env);

if (!is_resource($casprocess)) {
throw new stack_exception(‘stack_cas_connection: could not open a CAS process’);
}

if (!fwrite($pipes[0], $this->initcommand)) {
throw new stack_exception(‘stack_cas_connection: could not write to the CAS process.’);
}
fwrite($pipes[0], $command);
fwrite($pipes[0], ‘quit();’.”\n\n”);

$ret = ”;
// Read output from stdout.
$starttime = microtime(true);
$continue   = true;

if (!stream_set_blocking($pipes[1], false)) {
$this->debug->log(”, ‘Warning: could not stream_set_blocking to be FALSE on the CAS process.’);
}

while ($continue and !feof($pipes[1])) {

$now = microtime(true);

if (($now – $starttime) > $this->timeout) {
$procarray = proc_get_status($casprocess);
if ($procarray[‘running’]) {
proc_terminate($casprocess);
}
$continue = false;
} else {
$out = fread($pipes[1], 1024);
if (” == $out) {
// Pause.
usleep(1000);
}
$ret .= $out;
}

}

if ($continue) {
fclose($pipes[0]);
fclose($pipes[1]);
$this->debug->log(‘Timings’, “Start: {$starttime}, End: {$now}, Taken = ” .
($now – $starttime));

} else {
// Add sufficient closing ]’s to allow something to be un-parsed from the CAS.
// WARNING: the string ‘The CAS timed out’ is used by the cache to search for a timeout occurrence.
$ret .= ‘ The CAS timed out. ] ] ] ]’;
}

return $ret;
}
}

proc_open($this->command, $descriptors, $pipes, $cwd, $env) が Maxima を起動して、コマンドを受け付ける状態にしているところです(proc_open は php のコマンド)。その中に、一部 if 文に埋もれていますが、下記のコードがあります。

fwrite($pipes[0], $this->initcommand)
fwrite($pipes[0], $command)
fwrite($pipes[0], ‘quit();’.”\n\n”)

 

$pipes[0] とは、先に起動して開いた Maxima のパイプで、このコードが Maxima に命令を送っているところです。内容をモニターしてみると、$this->initcommand は下記のようなもので moodledata/stackmaximalocal.mac を読み込んでいました。

load("/var/www/moodledata/stack/maximalocal.mac"); 

次の $command は先の cab:block([],  . . .  で始まる Maxima のコマンドです。最後はパイプを閉じています。依って、端末から Maxima を起動して、下記のコマンドを Maxima に送り込むと同じ出力が得られます(ダブルクオートが変な文字になっています。修正する必要があります)。

load(“/var/www/moodledata/stack/maximalocal.mac”);
cab:block([],print(“[TimeStamp= [ 0 ], Locals= [ 0=[ error= [“), cte(“CASresult”,errcatch(diff(x^n,x))), print(“1=[ error= [“), cte(“STACKversion”,errcatch(stackmaximaversion)), print(“2=[ error= [“), cte(“MAXIMAversion”,errcatch(MAXIMA_VERSION)), print(“3=[ error= [“), cte(“CAStime”,errcatch(CAStime:”2015-10-01 17:03:01″)), print(“] ]”), return(true));

 

出力は下記。長すぎて入りきっていません。

 

cab:block([],  . . .  で始まるコマンドはデーターベースに記録されています。 mdl_qtype_stack_cas_cache というテーブルに保存されています。ここを検索して、該当のものがあれば、処理せずに返事を返すようです。

次に、cab:block([],  . . .  で始まるコマンドの作成のところを考えます。

上図は、評価関数の動作を試すページで、 moodle/question/type/stack/answertests.php が作成しています。コードからたどってみます。下記が評価テストを実行している箇所です。

list($passed, $error, $rawmark, $feedback, $ansnote) = stack_answertest_test_data::run_test($test);

この stack_answertest_test_data クラスの定義が answertestfixtures.class.php にあって、run_test の中で、さらに

$anst = new stack_ans_test_controller($test->name, $test->studentanswer,
$test->teacheranswer, new stack_options(), $test->options);

$result   = $anst->do_test();

$test というのが、1個1個の評価テストの材料で、answertestfixtures.class.php 自身に値が書かれているのですが、下記のようなものです。

array(‘AlgEquiv’, ‘x-1’, ‘(x^2-1)/(x+1)’, 1, ”, ”)

この値を元にして、先の cab:block([],  . . .  で始まるコマンドが作成されていると思います。

stack_ans_test_controller クラスの定義が、controller.class.php にあって、初期化において下記のクラスが引用されて、

$this->at = new stack_answertest_general_cas($sans, $tans, ‘ATAlgEquiv’, false, $casoption, $options);

do_test において、

public function do_test() {
$result = $this->at->do_test();
return $result;
}

依って、次に stack_answertest_general_cas クラスですが、これは at_general_cas.class.php にあって、 do_test において

$session = new stack_cas_session($cts, $this->options, 0);

この引数にある $cts の中身は、要素3個の配列で、それぞれ stack_cas_casstring というクラスで、例えば下記のような内容です。

array (size=3)
0 =>
object(stack_cas_casstring)[910]
private ‘rawcasstring’ => string ‘STACKSA:1/0’ (length=11)
private ‘casstring’ => string ‘1/0’ (length=3)
private ‘valid’ => boolean true
private ‘key’ => string ‘STACKSA’ (length=7)
private ‘errors’ => null
private ‘value’ => null
private ‘display’ => null
private ‘answernote’ =>
array (size=0)
empty
private ‘feedback’ => null
1 =>
object(stack_cas_casstring)[913]
private ‘rawcasstring’ => string ‘STACKTA:0’ (length=9)
private ‘casstring’ => string ‘0’ (length=1)
private ‘valid’ => boolean true
private ‘key’ => string ‘STACKTA’ (length=7)
private ‘errors’ => null
private ‘value’ => null
private ‘display’ => null
private ‘answernote’ =>
array (size=0)
empty
private ‘feedback’ => null
2 =>
object(stack_cas_casstring)[914]
private ‘rawcasstring’ => string ‘result:StackReturn(ATEqualComAss(STACKSA,STACKTA))’ (length=50)
private ‘casstring’ => string ‘StackReturn(ATEqualComAss(STACKSA,STACKTA))’ (length=43)
private ‘valid’ => boolean true
private ‘key’ => string ‘result’ (length=6)
private ‘errors’ => null
private ‘value’ => null
private ‘display’ => null
private ‘answernote’ =>
array (size=0)
empty
private ‘feedback’ => null

それぞれ、学生の解答、先生の準備した答え、STACKの返事?に関するもののようです。$this->options の中身は下記のようなものでした。

object(stack_options)[912]
private ‘options’ =>
array (size=9)
‘display’ =>
array (size=6)
‘type’ => string ‘list’ (length=4)
‘value’ => string ‘LaTeX’ (length=5)
‘strict’ => boolean true
‘values’ =>
array (size=3)

‘caskey’ => string ‘OPT_OUTPUT’ (length=10)
‘castype’ => string ‘string’ (length=6)
‘multiplicationsign’ =>
array (size=6)
‘type’ => string ‘list’ (length=4)
‘value’ => string ‘dot’ (length=3)
‘strict’ => boolean true
‘values’ =>
array (size=3)

‘caskey’ => string ‘make_multsgn’ (length=12)
‘castype’ => string ‘fun’ (length=3)
‘complexno’ =>
array (size=6)
‘type’ => string ‘list’ (length=4)
‘value’ => string ‘i’ (length=1)
‘strict’ => boolean true
‘values’ =>
array (size=4)

‘caskey’ => string ‘make_complexJ’ (length=13)
‘castype’ => string ‘fun’ (length=3)
‘inversetrig’ =>
array (size=6)
‘type’ => string ‘list’ (length=4)
‘value’ => string ‘cos-1’ (length=5)
‘strict’ => boolean true
‘values’ =>
array (size=3)

‘caskey’ => string ‘make_arccos’ (length=11)
‘castype’ => string ‘fun’ (length=3)
‘floats’ =>
array (size=6)
‘type’ => string ‘boolean’ (length=7)
‘value’ => int 1
‘strict’ => boolean true
‘values’ =>
array (size=0)

‘caskey’ => string ‘OPT_NoFloats’ (length=12)
‘castype’ => string ‘ex’ (length=2)
‘sqrtsign’ =>
array (size=6)
‘type’ => string ‘boolean’ (length=7)
‘value’ => boolean true
‘strict’ => boolean true
‘values’ =>
array (size=0)

‘caskey’ => string ‘sqrtdispflag’ (length=12)
‘castype’ => string ‘ex’ (length=2)
‘simplify’ =>
array (size=6)
‘type’ => string ‘boolean’ (length=7)
‘value’ => boolean false
‘strict’ => boolean true
‘values’ =>
array (size=0)

‘caskey’ => string ‘simp’ (length=4)
‘castype’ => string ‘ex’ (length=2)
‘assumepos’ =>
array (size=6)
‘type’ => string ‘boolean’ (length=7)
‘value’ => boolean false
‘strict’ => boolean true
‘values’ =>
array (size=0)

‘caskey’ => string ‘assume_pos’ (length=10)
‘castype’ => string ‘ex’ (length=2)
‘matrixparens’ =>
array (size=6)
‘type’ => string ‘list’ (length=4)
‘value’ => string ‘[‘ (length=1)
‘strict’ => boolean true
‘values’ =>
array (size=5)

‘caskey’ => string ‘lmxchar’ (length=7)
‘castype’ => string ‘exs’ (length=3)

STACK のセッティングに関するもののようですが、下記のページの下方で設定する内容ではないかと思います。

上記キャプチャーの設定に関連するものは、/var/www/html/moodle/question/type/stack/settings.php や /var/www/html/moodle/question/type/stack/stack/options.class.php です。この辺は後でまた利用しないといけない部分です。

話を

$session = new stack_cas_session($cts, $this->options, 0);

に戻します。もう少し引用します。

$session = new stack_cas_session($cts, $this->options, 0);
$session->instantiate();
$this->debuginfo = $session->get_debuginfo();

stack_cas_session は cassession.class.php に定義があります。このクラスを作成した後,instantiate()を実行しているのですが,このinstantiate()の中を見ると,

$connection = stack_connection_helper::make();
$results = $connection->compute($this->construct_maxima_command());
$this->debuginfo = $connection->get_debuginfo();

この中にある、construct_maxima_command()の内容は下記です。

private function construct_maxima_command() {
// Ensure that every command has a valid key.

$casoptions = $this->options->get_cas_commands();

$csnames = $casoptions['names'];
$csvars = $casoptions['commands'];
$cascommands = '';
$caspreamble = '';

$cascommands .= ', print("-1=[ error= ["), cte("__stackmaximaversion",errcatch(__stackmaximaversion:stackmaximaversion)) ';

$i = 0;
foreach ($this->session as $cs) {
if ('' == $cs->get_key()) {
$label = 'dumvar'.$i;
} else {
$label = $cs->get_key();
}

// Replace any ?'s with a safe value.
$cmd = str_replace('?', 'QMCHAR', $cs->get_casstring());
// Strip off any []s at the end of a variable name.
// These are used to assign elements of lists and matrices, but this breaks Maxima's block command.
if (false === strpos($label, '[')) {
$cleanlabel = $label;
} else {
$cleanlabel = substr($label, 0, strpos($label, '['));
}

// Now we do special things if we have a command to re-order expressions.
if (false !== strpos($cmd, 'ordergreat') || false !== strpos($cmd, 'orderless')) {
// These commands must be in a separate block, and must only appear once.
$caspreamble = $cmd."$\n";
$cmd = '0';
}

$csnames .= ", $cleanlabel";
$cascommands .= ", print(\"$i=[ error= [\"), cte(\"$label\",errcatch($label:$cmd)) ";
$i++;

}

$cass = $caspreamble;
$cass .= 'cab:block([ RANDOM_SEED';
$cass .= $csnames;
$cass .= '], stack_randseed(';
$cass .= $this->seed.')'.$csvars;
$cass .= ", print(\"[TimeStamp= [ $this->seed ], Locals= [ \") ";
$cass .= $cascommands;
$cass .= ", print(\"] ]\") , return(true) ); \n ";

return $cass;
}

ここで、Maxima のコマンドが組み立てられているようです。以下、繰り返しもありますが、この中で利用されている変数などを見ていきます。まず、最初の方の、

        $casoptions = $this->options->get_cas_commands();
        $csnames = $casoptions[‘names’];
        $csvars  = $casoptions[‘commands’];

$csnames の内容は、例えば

, OPT_NoFloats, sqrtdispflag, simp, assume_pos, lmxchar

$csvars の内容は

, make_multsgn(“dot”), make_complexJ(“i”), make_arccos(“cos-1”), OPT_NoFloats:true, sqrtdispflag:true, simp:false, assume_pos:false, lmxchar:”[“

です。これは options.class.php で作られるもので、$this->options とは stack_options クラスです。これは簡単に利用できそうで、問題なし。

次に、ループになっている部分ですが、下記の

        $i = 0;
        foreach ($this->session as $cs) {
            if (” == $cs->get_key()) {
                $label = ‘dumvar’.$i;
            } else {
                $label = $cs->get_key();
            }

            // Replace any ?’s with a safe value.
            $cmd = str_replace(‘?’, ‘QMCHAR’, $cs->get_casstring());

この中の $this->session ですが、そこから取り出した $cs に関する $cs->get_key() の値は、”STACKSA” や “STACKTA” や “result” です。つまり、この $this->session は、at_general_cas.class.php の do_test において 作られる $cts です($cs は stack_cas_casstring クラス)。この $cts に相当するものは、作る必要があります。他、この construct_maxima_command() の中で、問題になりそうな変数はないように思えます。

話を at_general_cas.class.php の do_test() まで戻します。とりあえず do_test() を全部下記にあげます。

/**
*
*
* @return bool
* @access public
*/
public function do_test() {

if ('' == trim($this->sanskey)) {
$this->aterror      = stack_string('TEST_FAILED', array('errors' => stack_string("AT_EmptySA")));
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => stack_string("AT_EmptySA")));
$this->atansnote    = $this->casfunction.'TEST_FAILED:Empty SA.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

if ('' == trim($this->tanskey)) {
$this->aterror      = stack_string('TEST_FAILED', array('errors' => stack_string("AT_EmptyTA")));
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => stack_string("AT_EmptyTA")));
$this->atansnote    = $this->casfunction.'TEST_FAILED:Empty TA.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

if ($this->processcasoptions) {
if (null == $this->atoption or '' == $this->atoption) {
$this->aterror      = 'TEST_FAILED';
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => stack_string("AT_MissingOptions")));
$this->atansnote    = 'STACKERROR_OPTION.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
} else {
// Validate with teacher privileges, strict syntax & no automatically adding stars.
$ct  = new stack_cas_casstring($this->atoption);

if (!$ct->get_valid('t', true, 1)) {
$this->aterror      = 'TEST_FAILED';
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => ''));
$this->atfeedback  .= stack_string('AT_InvalidOptions', array('errors' => $ct->get_errors()));
$this->atansnote    = 'STACKERROR_OPTION.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}
}
$atopt = $this->atoption;
$ta   = "[$this->tanskey,$atopt]";
} else {
$ta = $this->tanskey;
}

// Sort out options.
if (null === $this->options) {
$this->options = new stack_options();
}
if (!(null === $this->simp)) {
$this->options->set_option('simplify', $this->simp);
}

$cascommands = array();
$cascommands[] = "STACKSA:$this->sanskey";
$cascommands[] = "STACKTA:$ta";
$cascommands[] = "result:StackReturn({$this->casfunction}(STACKSA,STACKTA))";

$cts = array();
foreach ($cascommands as $com) {
$cs    = new stack_cas_casstring($com);
$cs->get_valid('t', true, 0);
$cts[] = $cs;
}

$session = new stack_cas_session($cts, $this->options, 0);
$session->instantiate();
$this->debuginfo = $session->get_debuginfo();

if ('' != $session->get_errors_key('STACKSA')) {
$this->aterror      = 'TEST_FAILED';
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => $session->get_errors_key('STACKSA')));
$this->atansnote    = $this->casfunction.'_STACKERROR_SAns.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

if ('' != $session->get_errors_key('STACKTA')) {
$this->aterror      = 'TEST_FAILED';
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => $session->get_errors_key('STACKTA')));
$this->atansnote    = $this->casfunction.'_STACKERROR_TAns.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

$sessionvars = $session->get_session();
$result = $sessionvars[2];

if ('' != $result->get_errors()) {
$this->aterror      = 'TEST_FAILED';
if ('' != trim($result->get_feedback())) {
$this->atfeedback = $result->get_feedback();
} else {
$this->atfeedback = stack_string('TEST_FAILED', array('errors' => $result->get_errors()));
}
$this->atansnote    = trim($result->get_answernote());
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

$this->atansnote  = trim($result->get_answernote());

// Convert the Maxima string 'true' to PHP true.
if ('true' == $result->get_value()) {
$this->atmark = 1;
} else {
$this->atmark = 0;
}
$this->atfeedback = $result->get_feedback();
$this->atvalid    = $result->get_valid();

if ($this->atmark) {
return true;
} else {
return false;
}
}

 

stack_string() は locallib.php で定義されていて、抜き出すと下記のようなものです。

/**
* Equivalent to get_string($key, 'qtype_stack', $a), but this method ensure that
* any equations in the string are displayed properly.
* @param string $key the string name.
* @param mixed $a (optional) any values to interpolate into the string.
* @return string the language string
*/
function stack_string($key, $a = null) {
return stack_maths::process_lang_string(get_string($key, 'qtype_stack', $a));
}

基本的には、moodle の get_string() 関数です。これは特定のメッセージを返すもので、その返す内容は下記にあります。

/var/www/html/moodle/question/type/stack/lang/en/qtype_stack.php

この日本語版が、moodledata/lang/ja/qtype_stack.php にあります。これを書き換えるのは容易な感じです。

しかし、$ct  = new stack_cas_casstring($this->atoption); などにある stack_cas_casstring() は厄介です。この定義は casstring.class.php にあります。 その内容は、学生や教師の解答や正解の内容をチェックするもののようです。利用したい内容ですが、これを動かすには関連するコードが多くてかなり大変です。ゆくゆくは利用するとして、今回はこのセキュリティー関係の機能をカットします。

 

長くなりますが maxima に送るコマンドを組み立てる php を下記に挙げます。casstring.class.php と options.class.php も含めたzip ファイルはここです。この2つのファイルは多少書き換えました。

<?php

require_once('casstring.class.php');

require_once('options.class.php');

// 下記のデータは answertestfixtures.class.php から

//$rawdata = array('AlgEquiv', 'a^b*a^c', 'a^(b+c)', 1, '', '');

//$rawdata = array('AlgEquiv', 'integerp(3.1)', 'true', 0, '', '');

//$rawdata = array('AlgEquiv', 'abs(x)', 'sqrt(x^2)', 1, '', '');

//$rawdata = array('EqualComAss', '2/4', '1/2', 0, '', 'Numbers');

$rawdata = array('FacForm', '24*(x-1/4)', '24*x-6', 1, 'x', 'Factors over other fields');

$test = new stdClass();
$test->name          = $rawdata[0];
$test->studentanswer = $rawdata[1];
$test->teacheranswer = $rawdata[2];
$test->expectedscore = $rawdata[3];
$test->options       = $rawdata[4];
$test->notes         = $rawdata[5];

$options = null;
$expectedscore = $test->expectedscore;
$seed = 0;
$notes = $test->notes;

$param = ATconstruct($test->name, $test->studentanswer, $test->teacheranswer, $test->options, $options);

$sanskey = $param->sans;
$tanskey = $param->tans;
$casfunction = $param->casfunction;
$processcasoptions = $param->processcasoptions;
$options = $param->options;
$casoption = $param->casoption;
$simp = $param->simp;
$requiredoptions = $param->requiredoptions;

if ($processcasoptions) {
$ta   = "[$tanskey,$casoption]";
} else {
$ta = $tanskey;
}

// Sort out options.

if (null === $options) {
$options = new stack_options();
}

if (!(null === $simp)) {
$options->set_option('simplify', $simp);
}

$cascommands = array();
$cascommands[] = "STACKSA:$sanskey";
$cascommands[] = "STACKTA:$ta";
$cascommands[] = "result:StackReturn({$casfunction}(STACKSA,STACKTA))";

$keys = array();
$keys[] = "STACKSA";
$keys[] = "STACKTA";
$keys[] = "result";

$casstring = array();
$casstring[] = $sanskey;
$casstring[] = $ta;
$casstring[] = "StackReturn({$casfunction}(STACKSA,STACKTA))";

$cts = array();
$i = 0;

foreach ($cascommands as $com) {

$cs    = new stack_cas_casstring($com);

//$cs->get_valid('t', true, 0); // この機能は削除した

$cs->set_key($keys[$i]);

$cs->set_casstring($casstring[$i]);

$cts[] = $cs;

$i++;
}

$str = construct_maxima_command($cts, $options, $seed);

echo $str."\n";

function ATconstruct($anstest = null, $sans = null, $tans = null, $casoption = null, $options = null) {

// ($anstest = null, $sans = null, $tans = null, $options = null, $casoption = null)

switch($anstest) {
case 'AlgEquiv':
return new parameter_constract($sans, $tans, 'ATAlgEquiv', false, $casoption, $options);
break;

// 修正したところ Dimension 用の部分

case 'Dimension':
return new parameter_constract($sans, $tans, 'ATAlgEquiv', false, $casoption, $options);
break;

case 'EqualComAss':
return new parameter_constract($sans, $tans, 'ATEqualComAss', false, $casoption, $options, false);
break;

case 'CasEqual':
return new parameter_constract($sans, $tans, 'ATCASEqual', false, $casoption, $options, false);
break;

case 'SameType':
return new parameter_constract($sans, $tans, 'ATSameType', false, $casoption, $options);
break;

case 'SubstEquiv':
return new parameter_constract($sans, $tans, 'ATSubstEquiv', false, $casoption, $options);
break;

case 'Expanded':
return new parameter_constract($sans, $tans, 'ATExpanded', false, $casoption, $options);
break;

case 'FacForm':
return new parameter_constract($sans, $tans, 'ATFacForm', true, $casoption, $options, false, true);
break;

case 'SingleFrac':
return new parameter_constract($sans, $tans, 'ATSingleFrac', false, $casoption, $options, false);
break;

case 'PartFrac':
return new parameter_constract($sans, $tans, 'ATPartFrac',
true, $casoption, $options, true, false, true);
break;

case 'CompSquare':
return new parameter_constract($sans, $tans, 'ATCompSquare',
true, $casoption, $options, true, false, true);
break;

case 'String':
//require_once(__DIR__ . '/atstring.class.php');
//return new stack_anstest_atstring($sans, $tans, $options, $casoption);
return null;
break;

case 'StringSloppy':
//require_once(__DIR__ . '/stringsloppy.class.php');
//return new stack_anstest_stringsloppy($sans, $tans, $options, $casoption);
return null;
break;

case 'RegExp':
//require_once(__DIR__ . '/atregexp.class.php');
//return new stack_anstest_atregexp($sans, $tans, $options, $casoption);
return null;
break;

case 'Diff':
return new parameter_constract($sans, $tans, 'ATDiff', true, $casoption, $options, false, true);
break;

case 'Int':
return new parameter_constract($sans, $tans, 'ATInt', true, $casoption, $options, false, true);
break;

case 'GT':
return new parameter_constract($sans, $tans, 'ATGT', false, $casoption, $options);
break;

case 'GTE':
return new parameter_constract($sans, $tans, 'ATGTE', false, $casoption, $options);
break;

case 'NumAbsolute':
if (trim($casoption) == '') {
$casoption = '0.05';
}
return new parameter_constract($sans, $tans, 'ATNumAbsolute', true, $casoption, $options, true, true);
break;

case 'NumRelative':
if (trim($casoption) == '') {
$casoption = '0.05';
}
return new parameter_constract($sans, $tans, 'ATNumRelative', true, $casoption, $options, true, true);
break;

case 'NumSigFigs':
return new parameter_constract($sans, $tans, 'ATNumSigFigs', true, $casoption, $options, true, true);
break;

case 'NumDecPlaces':
//require_once(__DIR__ . '/atdecplaces.class.php');
//$this->at = new stack_anstest_atdecplaces($sans, $tans, $options, $casoption);
return null;
break;

case 'LowestTerms':
return new parameter_constract($sans, $tans, 'ATLowestTerms', false, $casoption, $options, 0);
break;

case 'SysEquiv':
return new parameter_constract($sans, $tans, 'ATSysEquiv', false, $casoption, $options);
break;

default:
return 'stack_ans_test_controller: called with invalid answer test name: '.$anstest;
break;
}
}

// 元は、class stack_answertest_general_cas

class parameter_constract {

public $sans;
public $tans;

/**
* @var string The name of the cas function this answer test uses.
*/
public $casfunction;

/**
* $var bool Are options processed by the CAS.
*/
public $processcasoptions;

public $casoption;
public $options;

/**
* $var bool If this variable is set to true or false we override the
*      simplification options in the CAS variables.
*/
public $simp;

/**
* $var bool Are options required for this test.
*/
public $requiredoptions;

/**
* @param  string $sans
* @param  string $tans
* @param  string $casoption
*/
public function __construct($sans, $tans, $casfunction, $processcasoptions = false, $casoption = null, $options = null, $simp = false, $requiredoptions = false) {

$this->sans       = $sans;
$this->tans       = $tans;

$this->casfunction       = $casfunction;
$this->processcasoptions = $processcasoptions;

$this->casoption       = $casoption;
$this->options       = $options;

$this->simp              = (bool) $simp;

$this->requiredoptions   = $requiredoptions;
}
}

/**
* Creates the string which Maxima will execute
*
* @return string
*/
function construct_maxima_command($cts, $options, $seed) {
// Ensure that every command has a valid key.

$casoptions = $options->get_cas_commands();

$csnames = $casoptions['names'];
$csvars  = $casoptions['commands'];
$cascommands = '';
$caspreamble = '';

$cascommands .= ', print("-1=[ error= ["), cte("__stackmaximaversion",errcatch(__stackmaximaversion:stackmaximaversion)) ';

$i = 0;
foreach ($cts as $cs) {
if ('' == $cs->get_key()) {
$label = 'dumvar'.$i;
} else {
$label = $cs->get_key();
}

// Replace any ?'s with a safe value.
$cmd = str_replace('?', 'QMCHAR', $cs->get_casstring());
// Strip off any []s at the end of a variable name.
// These are used to assign elements of lists and matrices, but this breaks Maxima's block command.
if (false === strpos($label, '[')) {
$cleanlabel = $label;
} else {
$cleanlabel = substr($label, 0, strpos($label, '['));
}

// Now we do special things if we have a command to re-order expressions.
if (false !== strpos($cmd, 'ordergreat') || false !== strpos($cmd, 'orderless')) {
// These commands must be in a separate block, and must only appear once.
$caspreamble = $cmd."$\n";
$cmd = '0';
}

$csnames   .= ", $cleanlabel";
$cascommands .= ", print(\"$i=[ error= [\"), cte(\"$label\",errcatch($label:$cmd)) ";
$i++;

}

$cass  = $caspreamble;
$cass .= 'cab:block([ RANDOM_SEED';
$cass .= $csnames;
$cass .= '], stack_randseed(';
$cass .= $seed.')'.$csvars;
$cass .= ", print(\"[TimeStamp= [ $seed ], Locals= [ \") ";
$cass .= $cascommands;
$cass .= ", print(\"] ]\") , return(true) ); \n ";

return $cass;
}

 

環境の違いで動かないことも考えられますが、そのへんは後で確認します。

次に、作成した Maxima コマンドを送るところです。 connector.unix.class.php にある call_maxima( ) を利用してみました。

ほとんどが先のコードと重なるので、下記に Maxima にコマンドを送る関数の所だけをあげます。

// connector.unix.class.php から作成

function call_maxima($initcommand, $command, $debug, $timeout) {

$ret = false;
$err = '';
$cwd = null;
$env = array('why' => 'itworks');

$descriptors = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'));

$casprocess = proc_open('/usr/local/bin/maxima', $descriptors, $pipes, $cwd, $env);

fwrite($pipes[0], $initcommand);
fwrite($pipes[0], $command);
fwrite($pipes[0], 'quit();'."\n\n");

$ret = '';
// Read output from stdout.
$starttime = microtime(true);
$continue   = true;

if (!stream_set_blocking($pipes[1], false)) {
$debug->log('', 'Warning: could not stream_set_blocking to be FALSE on the CAS process.');
}

while ($continue and !feof($pipes[1])) {

$now = microtime(true);

if (($now - $starttime) > $timeout) {
$procarray = proc_get_status($casprocess);
if ($procarray['running']) {
proc_terminate($casprocess);
}
$continue = false;
} else {
$out = fread($pipes[1], 1024);
if ('' == $out) {
// Pause.
usleep(1000);
}
$ret .= $out;
}

}

if ($continue) {
fclose($pipes[0]);
fclose($pipes[1]);
$debug->log('Timings', "Start: {$starttime}, End: {$now}, Taken = " .($now - $starttime));

} else {
// Add sufficient closing ]'s to allow something to be un-parsed from the CAS.
// WARNING: the string 'The CAS timed out' is used by the cache to search for a timeout occurrence.
$ret .= ' The CAS timed out. ] ] ] ]';
}

return $ret;

}

 

引数の $initcommand の中身は, ‘load(“/var/www/html/for_stack333/moodledata/stack/maximalocal.mac”);’ です。次の $command は先に組み上げた Maxima コマンド(cab:block([],  . . . で始まるもの)です。次の  $debug は stack_debug_log_base クラスで、あとで書きます。 $timeout はコード中のループの最大時間を決めるもののようで、使用するときは適当に 30 と入れています。

さて、$debug ですが、ログを貯めていく変数で、先ほど書いたように stack_debug_log_base クラスで、utils.class.php に定義されています。コードでは log() メソッドが使われているのですが、そのままでは利用できなかったので(html_writer が必要)、多少書き換えました。それでも moodle の s( ) が必要で、これは weblib.php にあります。weblib.php からは s( )、p( ) を残して他を削除しました。

 

実行時の画面です。最後の出力、上記のコードで言えば、 $ret 変数の内容を出力しています。すべてのコードの zip ファイルをここにおきます。ただ、maximalocal.mac にフォルダー名等が直接記入してあるので、適当な変更が必要です。他にも maxima のインストール場所に依存する箇所があります。

 

続いて、Maxima の計算結果の後処理のところです。 stack_cas_session ( cassession.class.php で定義)に戻ります。このクラスの instantiate() は,

/* This is the function which actually sends the commands off to Maxima. */
public function instantiate() {
if (null === $this->valid) {
$this->validate();
}
if (!$this->valid) {
return false;
}
// Lazy instantiation - only do this once...
// Empty session.  Nothing to do.
if ($this->instantiated || null === $this->session) {
return true;
}

$connection = stack_connection_helper::make();
$results = $connection->compute($this->construct_maxima_command());
$this->debuginfo = $connection->get_debuginfo();

// Now put the information back into the correct slots.
$session    = $this->session;
$newsession = array();
$newerrors  = '';
$allfail    = true;
$i          = 0;

// We loop over each entry in the session, not over the result.
// This way we can add an error for missing values.
foreach ($session as $cs) {
$gotvalue = false;

if ('' == $cs->get_key()) {
$key = 'dumvar'.$i;
} else {
$key = $cs->get_key();
}

if (array_key_exists($i, $results)) {
$allfail = false; // We at least got one result back from the CAS!

$result = $results["$i"]; // GOCHA!  results have string represenations of numbers, not int....

if (array_key_exists('value', $result)) {
$val = str_replace('QMCHAR', '?', $result['value']);
$cs->set_value($val);
$gotvalue = true;
}

if (array_key_exists('display', $result)) {
// Need to add this in here also because strings may contain question mark characters.
$disp = str_replace('QMCHAR', '?', $result['display']);
$cs->set_display($disp);
}

if (array_key_exists('valid', $result)) {
$cs->set_valid($result['valid']);
}

if (array_key_exists('answernote', $result)) {
$cs->set_answernote($result['answernote']);
}

if (array_key_exists('feedback', $result)) {
$cs->set_feedback($result['feedback']);
}

if ('' != $result['error'] and false === strstr($result['error'], 'clipped')) {
$cs->add_errors($result['error']);
$cs->decode_maxima_errors($result['error']);
$newerrors .= stack_maxima_format_casstring($cs->get_raw_casstring());
$newerrors .= ' '.stack_string("stackCas_CASErrorCaused") .
' ' . $result['error'] . ' ';
}
} else if (!$gotvalue) {
$errstr = stack_string("stackCas_failedReturn").' '.stack_maxima_format_casstring($cs->get_raw_casstring());
$cs->add_errors($errstr);
$cs->set_answernote('CASFailedReturn');
$newerrors .= $errstr;
}

$newsession[] = $cs;
$i++;
}
$this->session = $newsession;

if ('' != $newerrors) {
$this->errors .= '<span>'.stack_string('stackCas_CASError').'</span>'.$newerrors;
}
if ($allfail) {
$this->errors = '<span>'.stack_string('stackCas_allFailed').'</span>';
}

$this->instantiated = true;
}

 

16行目あたりの compute($this->construct_maxima_command() が、Maxima で計算をするところです。 stack_connection_helper は、定義が connectorhelper.class.php にあります。compute( ) は connector.class.php に定義されているようです。下記にあげます。

/* @see stack_cas_connection::compute() */
public function compute($command) {

$context = "Platform: ". stack_connection_helper::get_platform() . "\n";
$context .= "Maxima shell command: ". $this->command . "\n";;
$context .= "Maxima initial command: ". $this->initcommand . "\n";
$context .= "Maxima timeout: ". $this->timeout;
$this->debug->log('Context used', $context);

$this->debug->log('Maxima command', $command);

$rawresult = $this->call_maxima($command);
$this->debug->log('CAS result', $rawresult);

$unpackedresult = $this->unpack_raw_result($rawresult);
$this->debug->log('Unpacked result as', print_r($unpackedresult, true));

if (!stack_connection_helper::check_stackmaxima_version($unpackedresult)) {
stack_connection_helper::warn_about_version_mismatch($this->debug);
}

return $unpackedresult;
}

call_maxima($command) からの返事 $rawresult を、unpack_raw_result に渡しています。unpack_raw_resultを下記にあげます。

protected function unpack_raw_result($rawresult) {
$result = '';
$errors = false;

if ('' == trim($rawresult)) {
$this->debug->log('Warning, empty result!', 'unpack_raw_result: completely empty result was returned by the CAS.');
return array();
}

// Check we have a timestamp & remove everything before it.
$ts = substr_count($rawresult, '[TimeStamp');
if ($ts != 1) {
$this->debug->log('', 'unpack_raw_result: no timestamp returned. Data returned was: '.$rawresult);
return array();
} else {
$result = strstr($rawresult, '[TimeStamp'); // Remove everything before the timestamp.
}

$result = trim(str_replace('#', '', $result));
$result = trim(str_replace("\n", '', $result));

$unp = $this->unpack_helper($result);

if (array_key_exists('Locals', $unp)) {
$uplocs = $unp['Locals']; // Grab the local variables.
unset($unp['Locals']);
} else {
$uplocs = '';
}

// Now we need to turn the (error,key,value,display) tuple into an array.
$locals = array();

foreach ($this->unpack_helper($uplocs) as $var => $valdval) {
if (is_array($valdval)) {
$errors["CAS"] = "unpack_raw_result: CAS failed to generate any useful output.";
} else {
if (preg_match('/.*\[.*\].*/', $valdval)) {
// There are some []'s in the string.
$loc = $this->unpack_helper($valdval);
if ('' == trim($loc['error'])) {
unset($loc['error']);
}
$locals[$var] = $loc;

} else {
$errors["LocalVarGet$var"] = "Couldn't unpack the local variable $var from the string $valdval.";
}
}
}

// Next process and tidy up these values.
foreach ($locals as $i => &$local) {

if (isset($local['error'])) {
$local['error'] = $this->tidy_error($local['error']);
} else {
$local['error'] = '';
}
// If there are plots in the output.
$plot = isset($local['display']) ? substr_count($local['display'], '<img') : 0;
if ($plot > 0) {
// Plots always contain errors, so remove.
$local['error'] = '';
// For mathml display, remove the mathml that is inserted wrongly round the plot.
$local['display'] = str_replace('<math xmlns=\'http://www.w3.org/1998/Math/MathML\'>',
'', $local['display']);
$local['display'] = str_replace('</math>', '', $local['display']);

// For latex mode, remove the mbox.
// This handles forms: \mbox{image} and (earlier?) \mbox{{} {image} {}}.
$local['display'] = preg_replace("|\\\mbox{({})? (<html>.+</html>) ({})?}|", "$2", $local['display']);

if ($this->wwwroothasunderscores) {
$local['display'] = str_replace($this->wwwrootfixupfind,
$this->wwwrootfixupreplace, $local['display']);
}
}
}
return $locals;
}

ここで、call_maxima( ) からの返事を配列に作りなしているようです。unpack_raw_result 以外にも、同じクラス内の unpack_helper と tidy_error も必要です。これらを多少書き換えました。

とりあえず、unpack_raw_result から得られる結果を表示するところまでを書いて見ました。下記に実行時の画面キャプチャーをあげます。上記のコードで言えば $locals を var_dump( ) で表示しています。すべてのコードはここです。

続いてこの後の部分です。Maxima から返ってきたメッセージを対応する言葉に変換する機能などが含まれると思われます(’ATList_wrongentries’ などを対応するメッセージに変換する機能)。

 

再び引用しますが、cassession.class.php の instantiate() を下記にあげます。

/* This is the function which actually sends the commands off to Maxima. */
public function instantiate() {
if (null === $this->valid) {
$this->validate();
}
if (!$this->valid) {
return false;
}
// Lazy instantiation - only do this once...
// Empty session.  Nothing to do.
if ($this->instantiated || null === $this->session) {
return true;
}

$connection = stack_connection_helper::make();
$results = $connection->compute($this->construct_maxima_command());
$this->debuginfo = $connection->get_debuginfo();

// Now put the information back into the correct slots.
$session    = $this->session;
$newsession = array();
$newerrors  = '';
$allfail    = true;
$i          = 0;

// We loop over each entry in the session, not over the result.
// This way we can add an error for missing values.
foreach ($session as $cs) {
$gotvalue = false;

if ('' == $cs->get_key()) {
$key = 'dumvar'.$i;
} else {
$key = $cs->get_key();
}

if (array_key_exists($i, $results)) {
$allfail = false; // We at least got one result back from the CAS!

$result = $results["$i"]; // GOCHA!  results have string represenations of numbers, not int....

if (array_key_exists('value', $result)) {
$val = str_replace('QMCHAR', '?', $result['value']);
$cs->set_value($val);
$gotvalue = true;
}

if (array_key_exists('display', $result)) {
// Need to add this in here also because strings may contain question mark characters.
$disp = str_replace('QMCHAR', '?', $result['display']);
$cs->set_display($disp);
}

if (array_key_exists('valid', $result)) {
$cs->set_valid($result['valid']);
}

if (array_key_exists('answernote', $result)) {
$cs->set_answernote($result['answernote']);
}

if (array_key_exists('feedback', $result)) {
$cs->set_feedback($result['feedback']);
}

if ('' != $result['error'] and false === strstr($result['error'], 'clipped')) {
$cs->add_errors($result['error']);
$cs->decode_maxima_errors($result['error']);
$newerrors .= stack_maxima_format_casstring($cs->get_raw_casstring());
$newerrors .= ' '.stack_string("stackCas_CASErrorCaused") .
' ' . $result['error'] . ' ';
}
} else if (!$gotvalue) {
$errstr = stack_string("stackCas_failedReturn").' '.stack_maxima_format_casstring($cs->get_raw_casstring());
$cs->add_errors($errstr);
$cs->set_answernote('CASFailedReturn');
$newerrors .= $errstr;
}

$newsession[] = $cs;
$i++;
}
$this->session = $newsession;

if ('' != $newerrors) {
$this->errors .= '<span>'.stack_string('stackCas_CASError').'</span>'.$newerrors;
}
if ($allfail) {
$this->errors = '<span>'.stack_string('stackCas_allFailed').'</span>';
}

$this->instantiated = true;
}

 

まず16行目の compute() の結果を $results に入れています。その中身は unpack_raw_result() の返事です。28行目では、$session のそれぞれの要素に関してループします。要素を $cs としていますが、$cs は stack_cas_casstring クラス です。この $cs に $results の結果を格納していきます。変更後の内容を画面出力させたもののキャプチャーを下記にあげます。$session は3個の要素なのですが、画面に入りきらなかったので、後ろの方の2項の内容が表示されています。先の $locals は配列で、これはstack_cas_casstring クラスという違いもあります。

上記は、stack_cas_session クラスの instantiate() を実行したところなわけですが、これを遡って元に戻ります。これの呼び出し元は、例えば stack_answertest_general_cas クラス(at_general_cas.class.php)の do_test() です。下記に引用します。

/**
*
*
* @return bool
* @access public
*/
public function do_test() {

if ('' == trim($this->sanskey)) {
$this->aterror      = stack_string('TEST_FAILED', array('errors' => stack_string("AT_EmptySA")));
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => stack_string("AT_EmptySA")));
$this->atansnote    = $this->casfunction.'TEST_FAILED:Empty SA.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

if ('' == trim($this->tanskey)) {
$this->aterror      = stack_string('TEST_FAILED', array('errors' => stack_string("AT_EmptyTA")));
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => stack_string("AT_EmptyTA")));
$this->atansnote    = $this->casfunction.'TEST_FAILED:Empty TA.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

if ($this->processcasoptions) {
if (null == $this->atoption or '' == $this->atoption) {
$this->aterror      = 'TEST_FAILED';
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => stack_string("AT_MissingOptions")));
$this->atansnote    = 'STACKERROR_OPTION.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
} else {
// Validate with teacher privileges, strict syntax & no automatically adding stars.
$ct  = new stack_cas_casstring($this->atoption);

if (!$ct->get_valid('t', true, 1)) {
$this->aterror      = 'TEST_FAILED';
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => ''));
$this->atfeedback  .= stack_string('AT_InvalidOptions', array('errors' => $ct->get_errors()));
$this->atansnote    = 'STACKERROR_OPTION.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}
}
$atopt = $this->atoption;
$ta   = "[$this->tanskey,$atopt]";
} else {
$ta = $this->tanskey;
}

// Sort out options.
if (null === $this->options) {
$this->options = new stack_options();
}
if (!(null === $this->simp)) {
$this->options->set_option('simplify', $this->simp);
}

$cascommands = array();
$cascommands[] = "STACKSA:$this->sanskey";
$cascommands[] = "STACKTA:$ta";
$cascommands[] = "result:StackReturn({$this->casfunction}(STACKSA,STACKTA))";

$cts = array();
foreach ($cascommands as $com) {
$cs    = new stack_cas_casstring($com);
$cs->get_valid('t', true, 0);
$cts[] = $cs;
}

$session = new stack_cas_session($cts, $this->options, 0);
$session->instantiate();
$this->debuginfo = $session->get_debuginfo();

if ('' != $session->get_errors_key('STACKSA')) {
$this->aterror      = 'TEST_FAILED';
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => $session->get_errors_key('STACKSA')));
$this->atansnote    = $this->casfunction.'_STACKERROR_SAns.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

if ('' != $session->get_errors_key('STACKTA')) {
$this->aterror      = 'TEST_FAILED';
$this->atfeedback   = stack_string('TEST_FAILED', array('errors' => $session->get_errors_key('STACKTA')));
$this->atansnote    = $this->casfunction.'_STACKERROR_TAns.';
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

$sessionvars = $session->get_session();
$result = $sessionvars[2];

if ('' != $result->get_errors()) {
$this->aterror      = 'TEST_FAILED';
if ('' != trim($result->get_feedback())) {
$this->atfeedback = $result->get_feedback();
} else {
$this->atfeedback = stack_string('TEST_FAILED', array('errors' => $result->get_errors()));
}
$this->atansnote    = trim($result->get_answernote());
$this->atmark       = 0;
$this->atvalid      = false;
return null;
}

$this->atansnote  = trim($result->get_answernote());

// Convert the Maxima string 'true' to PHP true.
if ('true' == $result->get_value()) {
$this->atmark = 1;
} else {
$this->atmark = 0;
}
$this->atfeedback = $result->get_feedback();
$this->atvalid    = $result->get_valid();

if ($this->atmark) {
return true;
} else {
return false;
}
}

 

do_test() は、76行目です。以降の機能を利用していきますが(利用しないものもあります)、おおよそはエラーの検出と変数への値の格納でしょうか。98行目の $result は評価結果が入った stack_answertest_general_cas クラス です。103行目などで、get_feedback() メソッドが利用されているのですが、このとき得られる値($this->atfeedback の値)は、例えば “stack_trans(‘ATList_wrongentries’ , !quot!\[\left[ 1 , 2 , {\color{red}{\underline{4}}} \right] \]!quot! );” などで、まだメッセージに成っていません。依って更に遡る必要があります。

 

stack_answertest_general_cas クラスの呼び出しもとは、例えば stack_ans_test_controller クラス(controller.class.php で定義)です。このコンストラクターで呼び出しています。さらに、stack_ans_test_controller クラスの呼び出しもとを探ると、stack_answertest_test_data クラス(answertestfixtures.class.php で定義)で、その run_test() を引用します。

 

public static function run_test($test) {
$anst = new stack_ans_test_controller($test->name, $test->studentanswer,
$test->teacheranswer, new stack_options(), $test->options);

// The false clause is useful for developers to track down which test case is breaking Maxima.
if (true) {
$result   = $anst->do_test(); // This actually executes the answer test in the CAS.
$errors   = $anst->get_at_errors();
$rawmark  = $anst->get_at_mark();
$feedback = $anst->get_at_feedback();
$ansnote  = $anst->get_at_answernote();
} else {
$feedback = 'AT'.$test->name.'('.$test->studentanswer.','.$test->teacheranswer.');';
$result   = true; // This actually executes the answer test in the CAS.
$errors   = '';
$rawmark  = 0;
$ansnote  = '';
}

$passed = false;
if ($rawmark === $test->expectedscore) {
$passed = true;
}

// The test failed, and we expected it to fail.
if ($errors === 'TEST_FAILED') {
if (-1 === $test->expectedscore) {
$passed = true;
} else {
$passed = false;
}
}
// These tests are all expected to fail, so we make them all pass.
if (-2 === $test->expectedscore) {
$passed = true;
}

return array($passed, $errors, $rawmark, $feedback, $ansnote);
}

10行目の $feedback は、例えば

“赤字で示された部分が不正解です。\[\left[ 1 , 2 , {\color{red}{\underline{4}}} \right] \]”

上記のように、メッセージに変換されています。依って、この run_test() の内容を利用していけば良さそうです。stack_ans_test_controller クラスの get_at_feedback() を見てみると、

 

/**
*
*
* @return string
* @access public
*/
public function get_at_feedback() {
return stack_maxima_translate($this->at->get_at_feedback());
}

 

ここにある、stack_maxima_translate() は、locallib.php にあります。一緒にstack_trans() も必要 で、その内容は下記のようなものです。

/**
* Translates a string taken as output from Maxima.
*
* This function takes a variable number of arguments, the first of which is assumed to be the identifier
* of the string to be translated.
*/
function stack_trans() {
$nargs = func_num_args();

if ($nargs > 0) {
$arglist = func_get_args();
$identifier = func_get_arg(0);
$a = array();
if ($nargs > 1) {
for ($i = 1; $i < $nargs; $i++) {
$index = $i - 1;
$a["m{$index}"] = func_get_arg($i);
}
}
$return = stack_string($identifier, $a);
echo $return;
}
}

function stack_maxima_translate($rawfeedback) {

if (strpos($rawfeedback, 'stack_trans') === false) {
return trim($rawfeedback);
} else {
$rawfeedback = str_replace('[[', '', $rawfeedback);
$rawfeedback = str_replace(']]', '', $rawfeedback);
$rawfeedback = str_replace('\n', '', $rawfeedback);
$rawfeedback = str_replace('\\', '\\\\', $rawfeedback);
$rawfeedback = str_replace('!quot!', '"', $rawfeedback);

ob_start();
eval($rawfeedback);
$translated = ob_get_contents();
ob_end_clean();

return trim($translated);
}
}

 

とくに問題なく利用できそうです。

 

メッセージに変換する際の元データが、 qtype_stack.php にあるのですが、この日本語版では、$a となるべきところが a となっている箇所があるなど、$が抜けているところがあるようです。いくらか修正したのですが、全部は訂正できていません。

下記が最終コードの実行画面です。出力の最後の方だけ見えています。$atmark が正解不正解を示し、$atvalid は評価が成功したことを表す変数です。

読み込むファイルも含めた全部のコードはここです。繰り返しですが、maximalocal.mac に moodledata フォルダーや maxima ファイルの場所が書き込まれています。他にも関数call_maxima に maxima のインストール場所に依存する箇所があります。利用する際には適当に変更してください。

 

長くなりますが、 上記の画面で実行したファイル(test.php)をあげます。 学生の解答や先生の正解が Maxima の式として正しくないときには、エラーが生じて動作しないかもしれません。(これまでもやっていたことですが)何か前処理をして、式として正しいかどうかを確認しておく必要はあります。

<?php

require_once('locallib.php');        // stack_maxima_format_casstring と stack_string クラスの定義
require_once('outputcomponents.php');    // html_writer クラスの定義
require_once('get_config.php');        // get_config() の定義
require_once('my_get_string.php');    // get_string() の定義
require_once('weblib.php');        // s() の定義
require_once('utils.class.php');    // stack_debug_log の定義
require_once('casstring.class.php');
require_once('options.class.php');

// 下記のデータは answertestfixtures.class.php から

//$rawdata = array('AlgEquiv', 'a^b*a^c', 'a^(b+c)', 1, '', '');
//$rawdata = array('AlgEquiv', 'integerp(3.1)', 'true', 0, '', '');
//$rawdata = array('AlgEquiv', 'abs(x)', 'sqrt(x^2)', 1, '', '');
//$rawdata = array('EqualComAss', '2/4', '1/2', 0, '', 'Numbers');
//$rawdata = array('FacForm', '24*(x-1/4)', '24*x-6', 1, 'x', 'Factors over other fields');
//$rawdata = array('AlgEquiv', '[1,2,4]', '[1,2,3]', 0, '', '');
//$rawdata = array('AlgEquiv', 'x^(1/2)', 'sqrt(x)', 1, '', 'Powers and roots');
//$rawdata = array('AlgEquiv', '1/0', '1', -1, '', '');
//$rawdata = array('AlgEquiv', 'x-1)^2', '(x-1)^2', -1, '', '');
//$rawdata = array('AlgEquiv', 'a^b*a^c', 'a^(b+c)', 1, '', '');
//$rawdata = array('AlgEquiv', 'x', '{1,2,3}', 0, '', 'Sets');
//$rawdata = array('AlgEquiv', '[1,2]', '[1,2,3]', 0, '', '');
$rawdata = array('AlgEquiv', '[1,2,4]', '[1,2,3]', 0, '', '');
//$rawdata = array('AlgEquiv', '{1,2}', '{1,2,3}', 0, '', '');
//$rawdata = array('AlgEquiv', 'matrix([epsilon[2],2],[2,x^2])', 'matrix([epsilon[0],2],[2,x^3])', 0, '', '');
//$rawdata = array('AlgEquiv', '1', 'x=1', 0, '', 'Equations');
//$rawdata = array('AlgEquiv', 'f(x)=x^2', 'f(x):=x^2', 0, '', '');
//$rawdata = array('AlgEquiv', 'sqrt(12)', '2*sqrt(3)', 1, '', 'Surds');
//$rawdata = array('SubstEquiv', 'X^2+1', 'x^2+1', 1, '', '');
//$rawdata = array('SubstEquiv', 'x^2+y', 'a^2+b', 1, '', '');
//$rawdata = array('EqualComAss', '2/4', '1/2', 0, '', 'Numbers');
//$rawdata = array('EqualComAss', 'x+1', 'y=2*x+1', 0, '', 'Equations');
//$rawdata = array('EqualComAss', '[2*x+1,2]', '[1+x*2,2]', 1, '', 'Lists');
//$rawdata = array('CasEqual', '0.5', '1/2', 0, '', 'Mix of floats and rational numbers');
//$rawdata = array('SameType', 'x>2', 'x>1', 1, '', '');
//$rawdata = array('SysEquiv', '[90=v*t,90=(v+5)*(t*x-1/4)]', '[90=v*t,90=(v+5)*(t-1/4)]', 0, '', '');
//$rawdata = array('SysEquiv', '[90=v*t,90=(v+5)*(t-1/4),90=(v+6)*(t-1/5)]', '[90=v*t,90=(v+5)*(t-1/4)]', 0, '', '');
//$rawdata = array('Expanded', '(x-1)*(x+1)', '0', 0, '', '');
//$rawdata = array('Expanded', 'x^2-a*x-b*x+a*b', '0', 1, '', '');
//$rawdata = array('FacForm', '2*(x^2-3*x)', '2*x*(x-3)', 0, 'x', '');
//$rawdata = array('FacForm', '(y-4)*y*(3*y+6)', '3*(y-4)*y*(y+2)', 0, 'y', '');
//$rawdata = array('CompSquare', 'x^2-2*x+2', '(x-1)^2+1', 0, 'x', '');
//$rawdata = array('SingleFrac', 'a+1/2', '(2*a+1)/2', 0, '', '');
//$rawdata = array('SingleFrac', '(1/2)/(3/4)', '2/3', 0, '', 'Fractions within fractions');
//$rawdata = array('PartFrac', '1/(x-1)-(x+1)/(x^2+1)', '2/((x-1)*(x^2+1))', 1, 'x', 'Irreducible quadratic in denominator');
//$rawdata = array('PartFrac', '1/(2*(x-1))+x/(2*(x^2+1))', '1/((x-1)*(x^2+1))', 0, 'x', '');
//$rawdata = array('PartFrac', '1/(n-1)-1/n^2', '1/((n+1)*n)', 0, 'n', '');
//$rawdata = array('Diff', 'x^4/4', '3*x^2', 0, 'x', '');
//$rawdata = array('Diff', '6000*(x-a)^5999', '6000*(x-a)^5999', 1, 'x', '');
//$rawdata = array('Int', 'x^3/3+c+1', 'x^3/3', 1, 'x', '');
//$rawdata = array('Int', 'x^3/3+c+k', 'x^3/3', 0, 'x', '');
//$rawdata = array('Int', 'x^3/3*c', 'x^3/3', 0, 'x', '');
//$rawdata = array('Int', 'ln(x)+c', 'ln(k*abs(x))', 0, 'x', '');
//$rawdata = array('Int', '((5*%e^7*x-%e^7)*%e^(5*x))', '((5*%e^7*x-%e^7)*%e^(5*x))/25+c', 0, '[x,x*%e^(5*x+7)]', '');
//$rawdata = array('GT', 'pi', '3', 1, '', '');
//$rawdata = array('NumRelative', '1.05', '1', 1, '', '');
//$rawdata = array('NumRelative', '1.05', '1', 1, '0.1', 'Options passed');
//$rawdata = array('NumRelative', '[3,1.414]', '[pi,sqrt(2)]', 0, '0.01', '');
//$rawdata = array('NumSigFigs', '3.142', 'pi', 1, '4', '');
//$rawdata = array('NumSigFigs', '3141', '3.1415927', 0, '4', '');
//$rawdata = array('LowestTerms', '2/4', '0', 0, '', '');

$test = new stdClass();
$test->name          = $rawdata[0];
$test->studentanswer = $rawdata[1];
$test->teacheranswer = $rawdata[2];
$test->expectedscore = $rawdata[3];
$test->options       = $rawdata[4];
$test->notes         = $rawdata[5];

$options = null;
$expectedscore = $test->expectedscore;
$seed = 0;
$notes = $test->notes;

$param = ATconstruct($test->name, $test->studentanswer, $test->teacheranswer, $test->options, $options);

$sanskey = $param->sans;
$tanskey = $param->tans;
$casfunction = $param->casfunction;
$processcasoptions = $param->processcasoptions;
$options = $param->options;
$casoption = $param->casoption;
$simp = $param->simp;
$requiredoptions = $param->requiredoptions;

if ($processcasoptions) {
$ta   = "[$tanskey,$casoption]";
} else {
$ta = $tanskey;
}

// Sort out options.

if (null === $options) {
$options = new stack_options();
}

if (!(null === $simp)) {
$options->set_option('simplify', $simp);
}

$cascommands = array();
$cascommands[] = "STACKSA:$sanskey";
$cascommands[] = "STACKTA:$ta";
$cascommands[] = "result:StackReturn({$casfunction}(STACKSA,STACKTA))";

$keys = array();
$keys[] = "STACKSA";
$keys[] = "STACKTA";
$keys[] = "result";

$casstring = array();
$casstring[] = $sanskey;
$casstring[] = $ta;
$casstring[] = "StackReturn({$casfunction}(STACKSA,STACKTA))";

$cts = array();
$i = 0;

foreach ($cascommands as $com) {

$cs    = new stack_cas_casstring($com);

//$cs->get_valid('t', true, 0); // この機能は削除した

$cs->set_key($keys[$i]);

$cs->set_casstring($casstring[$i]);

$cts[] = $cs;

$i++;
}

$secondcommand = construct_maxima_command($cts, $options, $seed);

echo $secondcommand."\n\n";

$a_debug_log = new stack_debug_log_base();

$initcommand = 'load("/var/www/html/for_stack333/moodledata/stack/maximalocal.mac");';

$after_string = call_maxima($initcommand, $secondcommand, $a_debug_log, 30);

echo $after_string."\n";

$results = unpack_raw_result($after_string, $a_debug_log);

var_dump($results);

// cassession.class.php の instantiate() から

// Now put the information back into the correct slots.
$session    = $cts;
$newsession = array();
$newerrors  = '';
$allfail    = true;
$i          = 0;

$errors = ""; // どう利用するのか不明。

// We loop over each entry in the session, not over the result.
// This way we can add an error for missing values.

foreach ($session as $cs) {

$gotvalue = false;

if ('' == $cs->get_key()) {
$key = 'dumvar'.$i;
} else {
$key = $cs->get_key();
}

if (array_key_exists($i, $results)) {
$allfail = false; // We at least got one result back from the CAS!

$result = $results["$i"]; // GOCHA!  results have string represenations of numbers, not int....

if (array_key_exists('value', $result)) {
$val = str_replace('QMCHAR', '?', $result['value']);
$cs->set_value($val);
$gotvalue = true;
}

if (array_key_exists('display', $result)) {
// Need to add this in here also because strings may contain question mark characters.
$disp = str_replace('QMCHAR', '?', $result['display']);
$cs->set_display($disp);
}

if (array_key_exists('valid', $result)) {
$cs->set_valid($result['valid']);
}

if (array_key_exists('answernote', $result)) {
$cs->set_answernote($result['answernote']);
}

if (array_key_exists('feedback', $result)) {
$cs->set_feedback($result['feedback']);
}

if ('' != $result['error'] and false === strstr($result['error'], 'clipped')) {
$cs->add_errors($result['error']);
$cs->decode_maxima_errors($result['error']);
$newerrors .= stack_maxima_format_casstring($cs->get_raw_casstring());
$newerrors .= ' '.stack_string("stackCas_CASErrorCaused") . ' ' . $result['error'] . ' ';
}
} else if (!$gotvalue) {
$errstr = stack_string("stackCas_failedReturn").' '.stack_maxima_format_casstring($cs->get_raw_casstring());
$cs->add_errors($errstr);
$cs->set_answernote('CASFailedReturn');
$newerrors .= $errstr;
}

$newsession[] = $cs;
$i++;
}

$session = $newsession;

if ('' != $newerrors) {
$errors .= '<span>'.stack_string('stackCas_CASError').'</span>'.$newerrors;
}

if ($allfail) {
$errors = '<span>'.stack_string('stackCas_allFailed').'</span>';
}

echo "\n\n".'$errors : '.$errors."\n\n";

var_dump($session);

//at_general_cas.class.php の do_test() から

$aterror      = null;
$atfeedback   = null;
$atansnote    = null;
$atmark       = 0;
$atvalid      = null;

if ('' != $session[2]->get_errors()) {

$aterror      = 'TEST_FAILED';

if ('' != trim($session[2]->get_feedback())) {
$atfeedback = $session[2]->get_feedback();
} else {
$atfeedback = stack_string('TEST_FAILED', array('errors' => $session[2]->get_errors()));
}

$atansnote    = trim($session[2]->get_answernote());
$atmark       = 0;
$atvalid      = false;
//return null;

} else {

$atansnote  = trim($session[2]->get_answernote());

// Convert the Maxima string 'true' to PHP true.
if ('true' == $session[2]->get_value()) {
$atmark = 1;
} else {
$atmark = 0;
}

$atfeedback = $session[2]->get_feedback();
$atfeedback = stack_maxima_translate($atfeedback);

$atvalid    = $session[2]->get_valid();
}

//if ($atmark) {
//return true;
//} else {
//return false;
//}

echo "\n\n";
echo '$aterror    = '.$aterror."\n";
echo '$atfeedback = '.$atfeedback."\n";
echo '$atansnote  = '.$atansnote."\n";
echo '$atmark     = '.$atmark."\n";
echo '$atvalid    = '.$atvalid."\n";

function ATconstruct($anstest = null, $sans = null, $tans = null, $casoption = null, $options = null) {

// ($anstest = null, $sans = null, $tans = null, $options = null, $casoption = null)

switch($anstest) {
case 'AlgEquiv':
return new parameter_constract($sans, $tans, 'ATAlgEquiv', false, $casoption, $options);
break;

// 修正したところ Dimension 用の部分

case 'Dimension':
return new parameter_constract($sans, $tans, 'ATAlgEquiv', false, $casoption, $options);
break;

case 'EqualComAss':
return new parameter_constract($sans, $tans, 'ATEqualComAss', false, $casoption, $options, false);
break;

case 'CasEqual':
return new parameter_constract($sans, $tans, 'ATCASEqual', false, $casoption, $options, false);
break;

case 'SameType':
return new parameter_constract($sans, $tans, 'ATSameType', false, $casoption, $options);
break;

case 'SubstEquiv':
return new parameter_constract($sans, $tans, 'ATSubstEquiv', false, $casoption, $options);
break;

case 'Expanded':
return new parameter_constract($sans, $tans, 'ATExpanded', false, $casoption, $options);
break;

case 'FacForm':
return new parameter_constract($sans, $tans, 'ATFacForm', true, $casoption, $options, false, true);
break;

case 'SingleFrac':
return new parameter_constract($sans, $tans, 'ATSingleFrac', false, $casoption, $options, false);
break;

case 'PartFrac':
return new parameter_constract($sans, $tans, 'ATPartFrac',
true, $casoption, $options, true, false, true);
break;

case 'CompSquare':
return new parameter_constract($sans, $tans, 'ATCompSquare',
true, $casoption, $options, true, false, true);
break;

case 'String':
//require_once(__DIR__ . '/atstring.class.php');
//return new stack_anstest_atstring($sans, $tans, $options, $casoption);
return null;
break;

case 'StringSloppy':
//require_once(__DIR__ . '/stringsloppy.class.php');
//return new stack_anstest_stringsloppy($sans, $tans, $options, $casoption);
return null;
break;

case 'RegExp':
//require_once(__DIR__ . '/atregexp.class.php');
//return new stack_anstest_atregexp($sans, $tans, $options, $casoption);
return null;
break;

case 'Diff':
return new parameter_constract($sans, $tans, 'ATDiff', true, $casoption, $options, false, true);
break;

case 'Int':
return new parameter_constract($sans, $tans, 'ATInt', true, $casoption, $options, false, true);
break;

case 'GT':
return new parameter_constract($sans, $tans, 'ATGT', false, $casoption, $options);
break;

case 'GTE':
return new parameter_constract($sans, $tans, 'ATGTE', false, $casoption, $options);
break;

case 'NumAbsolute':
if (trim($casoption) == '') {
$casoption = '0.05';
}
return new parameter_constract($sans, $tans, 'ATNumAbsolute', true, $casoption, $options, true, true);
break;

case 'NumRelative':
if (trim($casoption) == '') {
$casoption = '0.05';
}
return new parameter_constract($sans, $tans, 'ATNumRelative', true, $casoption, $options, true, true);
break;

case 'NumSigFigs':
return new parameter_constract($sans, $tans, 'ATNumSigFigs', true, $casoption, $options, true, true);
break;

case 'NumDecPlaces':
//require_once(__DIR__ . '/atdecplaces.class.php');
//$this->at = new stack_anstest_atdecplaces($sans, $tans, $options, $casoption);
return null;
break;

case 'LowestTerms':
return new parameter_constract($sans, $tans, 'ATLowestTerms', false, $casoption, $options, 0);
break;

case 'SysEquiv':
return new parameter_constract($sans, $tans, 'ATSysEquiv', false, $casoption, $options);
break;

default:
return 'stack_ans_test_controller: called with invalid answer test name: '.$anstest;
break;
}
}

// 元は、class stack_answertest_general_cas

class parameter_constract {

public $sans;
public $tans;

/**
* @var string The name of the cas function this answer test uses.
*/
public $casfunction;

/**
* $var bool Are options processed by the CAS.
*/
public $processcasoptions;

public $casoption;
public $options;

/**
* $var bool If this variable is set to true or false we override the
*      simplification options in the CAS variables.
*/
public $simp;

/**
* $var bool Are options required for this test.
*/
public $requiredoptions;

/**
* @param  string $sans
* @param  string $tans
* @param  string $casoption
*/
public function __construct($sans, $tans, $casfunction, $processcasoptions = false, $casoption = null, $options = null, $simp = false, $requiredoptions = false) {

$this->sans       = $sans;
$this->tans       = $tans;

$this->casfunction       = $casfunction;
$this->processcasoptions = $processcasoptions;

$this->casoption       = $casoption;
$this->options       = $options;

$this->simp              = (bool) $simp;

$this->requiredoptions   = $requiredoptions;
}
}

/**
* Creates the string which Maxima will execute
*
* @return string
*/
function construct_maxima_command($cts, $options, $seed) {
// Ensure that every command has a valid key.

$casoptions = $options->get_cas_commands();

$csnames = $casoptions['names'];
$csvars  = $casoptions['commands'];
$cascommands = '';
$caspreamble = '';

$cascommands .= ', print("-1=[ error= ["), cte("__stackmaximaversion",errcatch(__stackmaximaversion:stackmaximaversion)) ';

$i = 0;
foreach ($cts as $cs) {
if ('' == $cs->get_key()) {
$label = 'dumvar'.$i;
} else {
$label = $cs->get_key();
}

// Replace any ?'s with a safe value.
$cmd = str_replace('?', 'QMCHAR', $cs->get_casstring());
// Strip off any []s at the end of a variable name.
// These are used to assign elements of lists and matrices, but this breaks Maxima's block command.
if (false === strpos($label, '[')) {
$cleanlabel = $label;
} else {
$cleanlabel = substr($label, 0, strpos($label, '['));
}

// Now we do special things if we have a command to re-order expressions.
if (false !== strpos($cmd, 'ordergreat') || false !== strpos($cmd, 'orderless')) {
// These commands must be in a separate block, and must only appear once.
$caspreamble = $cmd."$\n";
$cmd = '0';
}

$csnames   .= ", $cleanlabel";
$cascommands .= ", print(\"$i=[ error= [\"), cte(\"$label\",errcatch($label:$cmd)) ";
$i++;

}

$cass  = $caspreamble;
$cass .= 'cab:block([ RANDOM_SEED';
$cass .= $csnames;
$cass .= '], stack_randseed(';
$cass .= $seed.')'.$csvars;
$cass .= ", print(\"[TimeStamp= [ $seed ], Locals= [ \") ";
$cass .= $cascommands;
$cass .= ", print(\"] ]\") , return(true) ); \n ";

return $cass;
}

// connector.unix.class.php から作成

function call_maxima($initcommand, $command, $debug, $timeout) {

$ret = false;
$err = '';
$cwd = null;
$env = array('why' => 'itworks');

$descriptors = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'));

$casprocess = proc_open('/usr/local/bin/maxima', $descriptors, $pipes, $cwd, $env);

fwrite($pipes[0], $initcommand);
fwrite($pipes[0], $command);
fwrite($pipes[0], 'quit();'."\n\n");

$ret = '';
// Read output from stdout.
$starttime = microtime(true);
$continue   = true;

if (!stream_set_blocking($pipes[1], false)) {
$debug->log('', 'Warning: could not stream_set_blocking to be FALSE on the CAS process.');
}

while ($continue and !feof($pipes[1])) {

$now = microtime(true);

if (($now - $starttime) > $timeout) {
$procarray = proc_get_status($casprocess);
if ($procarray['running']) {
proc_terminate($casprocess);
}
$continue = false;
} else {
$out = fread($pipes[1], 1024);
if ('' == $out) {
// Pause.
usleep(1000);
}
$ret .= $out;
}

}

if ($continue) {
fclose($pipes[0]);
fclose($pipes[1]);
$debug->log('Timings', "Start: {$starttime}, End: {$now}, Taken = " .($now - $starttime));

} else {
// Add sufficient closing ]'s to allow something to be un-parsed from the CAS.
// WARNING: the string 'The CAS timed out' is used by the cache to search for a timeout occurrence.
$ret .= ' The CAS timed out. ] ] ] ]';
}

return $ret;

}

// connector.class.php から持ってきた
/**
* Top level Maxima-specific function used to parse CAS output into an array.
*
* @param array $rawresult Raw CAS output
* @return array
*/
function unpack_raw_result($rawresult, $debug) {

$result = '';
$errors = false;

if ('' == trim($rawresult)) {
$debug->log('Warning, empty result!', 'unpack_raw_result: completely empty result was returned by the CAS.');
return array();
}

// Check we have a timestamp & remove everything before it.
$ts = substr_count($rawresult, '[TimeStamp');
if ($ts != 1) {
$debug->log('', 'unpack_raw_result: no timestamp returned. Data returned was: '.$rawresult);
return array();
} else {
$result = strstr($rawresult, '[TimeStamp'); // Remove everything before the timestamp.
}

$result = trim(str_replace('#', '', $result));
$result = trim(str_replace("\n", '', $result));

$unp = unpack_helper($result);

if (array_key_exists('Locals', $unp)) {
$uplocs = $unp['Locals']; // Grab the local variables.
unset($unp['Locals']);
} else {
$uplocs = '';
}

// Now we need to turn the (error,key,value,display) tuple into an array.
$locals = array();

foreach (unpack_helper($uplocs) as $var => $valdval) {
if (is_array($valdval)) {
$errors["CAS"] = "unpack_raw_result: CAS failed to generate any useful output.";
} else {
if (preg_match('/.*\[.*\].*/', $valdval)) {
// There are some []'s in the string.
$loc = unpack_helper($valdval);
if ('' == trim($loc['error'])) {
unset($loc['error']);
}
$locals[$var] = $loc;

} else {
$errors["LocalVarGet$var"] = "Couldn't unpack the local variable $var from the string $valdval.";
}
}
}

// Next process and tidy up these values.
foreach ($locals as $i => &$local) {

if (isset($local['error'])) {
$local['error'] = tidy_error($local['error']);
} else {
$local['error'] = '';
}
// If there are plots in the output.
$plot = isset($local['display']) ? substr_count($local['display'], '<img') : 0;
if ($plot > 0) {
// Plots always contain errors, so remove.
$local['error'] = '';
// For mathml display, remove the mathml that is inserted wrongly round the plot.
$local['display'] = str_replace('<math xmlns=\'http://www.w3.org/1998/Math/MathML\'>',
'', $local['display']);
$local['display'] = str_replace('</math>', '', $local['display']);

// For latex mode, remove the mbox.
// This handles forms: \mbox{image} and (earlier?) \mbox{{} {image} {}}.
$local['display'] = preg_replace("|\\\mbox{({})? (<html>.+</html>) ({})?}|", "$2", $local['display']);

//if ($this->wwwroothasunderscores) {
//    $local['display'] = str_replace($this->wwwrootfixupfind,
//            $this->wwwrootfixupreplace, $local['display']);
//}
}
}
return $locals;
}

function unpack_helper($rawresultfragment) {
// Take the raw string from the CAS, and unpack this into an array.
$offset = 0;
$rawresultfragmentlen = strlen($rawresultfragment);
$unparsed = '';
$errors = '';

if ($eqpos = strpos($rawresultfragment, '=', $offset)) {
// Check there are ='s.
do {
$gb = stack_utils::substring_between($rawresultfragment, '[', ']', $eqpos);
$val = substr($gb[0], 1, strlen($gb[0]) - 2);
$val = trim($val);

if (preg_match('/[-A-Za-z0-9].*/', substr($rawresultfragment, $offset, $eqpos - $offset), $regs)) {
$var = trim($regs[0]);
} else {
$var = 'errors';
$errors['LOCVARNAME'] = "Couldn't get the name of the local variable.";
}

$unparsed[$var] = $val;
$offset = $gb[2];
} while (($eqpos = strpos($rawresultfragment, '=', $offset)) && ($offset < $rawresultfragmentlen));

} else {
$errors['PREPARSE'] = "There are no ='s in the raw output from the CAS!";
}

if ('' != $errors) {
$unparsed['errors'] = $errors;
}

return $unparsed;
}

/**
* Deals with Maxima errors. Enables some translation.
*
* @param string $errstr a Maxima error string
* @return string
*/
function tidy_error($errstr) {

// This case arises when we use a numerical text for algebraic equivalence.
if (strpos($errstr, 'STACK: ignore previous error.') !== false) {
return '';
}

if (strpos($errstr, '0 to a negative exponent') !== false) {
$errstr = stack_string('Maxima_DivisionZero');
}
return $errstr;
}

 

これで終了だと思ったのですが、ubuntu1510 で動かしてみたらうまくいかず、もう少し書きます。

以下は、上手く動いている時の、動き出しのところのキャプチャーです。maxima のバージョンは 5.360、Lisp は SBCL 1.1.14.debian です。場所は/usr/local/bin/maximaで、ソースからコンパイルして導入したmaxima です。

ubuntu1510 では、ソフトウェアセンターから入る maxima は 5.361 で、LISP はGCL2.6.12 です。場所は /usr/bin/maxima です。この場所も問題ですが、LISPの違いも問題になりそうです。

sbcl を利用する maxima をコンパイルするときは下記。ソースを展開したフォルダーに移動して、2,3コマンドを打ちます。

cd ./maxima-5.36.0

./configure –enable-sbcl

make

sudo make install

GCLでのコンパイルが上手くいきません。GCLのコンパイルができたら、比較ができるのですが,現況は原因が maxima なのか LISP なのかはっきりしません。

いまのところ maxima は、ubuntu ソフトウェアセンターから導入するのではなくて、ソースから sbcl を使用するようにコンパイルして使うのが良さそうです。