diff --git a/README.md b/README.md index 4ba1fe01..d6bd22c2 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,19 @@ Next you can add choices, which the users will have to rate later on. After the rating period has finished, you can allocate the users automatically or manually. Upon publishing the results, users will be able to see which choice they have been allocated to. For more information please visit the [moodle wiki](https://docs.moodle.org/31/en/Ratingallocate). +Configuration +============= +General +--------- +You can configure ``mod/ratingallocate`` using moodles administration interface. Three different solvers are available for selection. For LP it is necessary to configure the way it will use the external LP solver by selecting an executor. + + +Webservice backend +------------ +Using the webservice executor mod_ratingallocate can use an external lp solver which is not on the same machine as the moodle instance. +A working webserver with PHP is needed. Clone the mod_ratingallocate into the document root and take a look into ``webservice/config.php`` for configuring the executors backend. A strong secret and HTTPS are recommended. + + Moodle version ====================== The plugin is continously tested with all moodle versions, which are security supported by the moodle headquarter. @@ -30,8 +43,8 @@ In addition to all stable branches the version is also tested against the master Algorithm ========= -This module uses a modified Edmonds-karp algorithm to solve the minimum-cost flow problem. Augmenting paths are found using Bellman-Ford, but the user ratings are multiplied with -1 first. - -Worst-Case complexity is O(m^2n^2) with m,n being number of edges (#users+#choices+#ratings_users_gave) and nodes (2+#users+#choices) in the graph. +Using the Edmonds-Karp algorithm the Worst-Case complexity is O(m^2n^2) with m,n being number of edges (#users+#choices+#ratings_users_gave) and nodes (2+#users+#choices) in the graph. Distributing 500 users to 21 choices takes around 11sec. +Measurements for using the LP solver or Ford-Fulkerson are not available. + diff --git a/classes/local/group.php b/classes/local/group.php new file mode 100644 index 00000000..0e88a665 --- /dev/null +++ b/classes/local/group.php @@ -0,0 +1,153 @@ +. + +namespace mod_ratingallocate\local; + +class group { + + private $id = ''; + private $limit = 0; + private $assigned_users = []; + + /** + * Creates a new group + * + * @param $id Id of the group + * @param $limit Group limit + */ + public function __construct($id, $limit = 0) { + $this->id = $id; + $this->set_limit($limit); + } + + /** + * Returns the group id + * + * @return Id of the group + */ + public function get_id() { + return $this->id; + } + + /** + * Checks if the group has a limit (limit is zero) + * + * @return True if the group has a limit + */ + public function has_limit() { + return $this->limit != 0; + } + + /** + * Returns the group limit + * + * @return Group limit + */ + + public function get_limit() { + return $this->limit; + } + + /** + * Checks if the group is empty + * + * @return True if the group is empty + */ + public function is_empty() { + return empty($this->assigned_users); + } + + /** + * Checks if the group is full (limit has been reached) + * + * @return True if the group is full + */ + public function is_full() { + if($this->limit == 0) + return false; + + return count($this->assigned_users) == $this->limit; + } + + /** + * Sets the group limit (zero for no limit) + * + * @param $limit The new group limit + * @throws exception if the group limit is negative + */ + public function set_limit($limit) { + if($limit < 0) + throw new \exception('Limit cannot be negative!'); + + $this->limit = $limit; + } + + /** + * Returns an array of users which are assigned to the group + * + * @return Array of users which are assigned to the group + */ + public function get_assigned_users() { + return $this->assigned_users; + } + + /** + * Adds an assigned user to the group + * + * @param $user User that gets added to the group + * + * @throws exception if the group limit has been reached or the user has been already assigned to a group + */ + public function add_assigned_user(&$user) { + if($this->exists_assigned_user($user)) + return; + + if($this->is_full()) + throw new \exception('Limit has been reached!'); + + if($user->get_assigned_group() && $user->get_assigned_group() != $this) + throw new \exception('User has been already assigned to another group!'); + + $user->set_assigned_group($this); + $this->assigned_users[$user->get_id()] = $user; + } + + /** + * Checks if user belongs to this group + * + * @param $user User that gets checked + * + * @return True if user belongs to this group + */ + public function exists_assigned_user($user) { + return isset($this->assigned_users[$user->get_id()]); + } + + /** + * Removes a user from the group + * + * @param $user User that gets removed from the group + * + * @throws exception If user was not assigned from the group + */ + public function remove_assigned_user($user) { + if(!$this->exists_assigned_user($user)) + throw new \exception('User has not been assigned to this group!'); + + unset($this->assigned_users[$user->get_id()]); + } + +} \ No newline at end of file diff --git a/classes/local/lp/engine.php b/classes/local/lp/engine.php new file mode 100644 index 00000000..9e847d78 --- /dev/null +++ b/classes/local/lp/engine.php @@ -0,0 +1,39 @@ +. + +namespace mod_ratingallocate\local\lp; + +abstract class engine { + + /** + * Reads the content of the stream and returns the variables and their optimized values + * + * @param $stream Output stream of the program that was executed + * + * @return Array of variables and their values + */ + abstract public function read($stream); + + /** + * Returns the command that gets executed + * + * @param $input_file Name of the input file + * + * @returns Command as a string + */ + abstract public function get_command($input_file); + +}; \ No newline at end of file diff --git a/classes/local/lp/engines/cplex.php b/classes/local/lp/engines/cplex.php new file mode 100644 index 00000000..5abd857b --- /dev/null +++ b/classes/local/lp/engines/cplex.php @@ -0,0 +1,55 @@ +. + +namespace mod_ratingallocate\local\lp\engines; + +class cplex extends \mod_ratingallocate\local\lp\engine { + + /** + * Returns the command that gets executed + * + * @param $input_file Name of the input file + * + * @returns Command as a string + */ + public function get_command($input_file) { + return "cplex -c \"read $input_file\" \"optimize\" \"display solution variables -\""; + } + + /** + * Reads the content of the stream and returns the variables and their optimized values + * + * @param $stream Output stream of the program that was executed + * + * @return Array of variables and their values + */ + public function read($stream) { + $content = stream_get_contents($stream); + $solution = []; + + foreach(array_slice(explode("\n", substr($content, strpos($content, "Solution Value"))), 1) as $variable) { + $parts = explode(' ', preg_replace('!\s+!', ' ', $variable)); + + if(count($parts) < 2) + break; + + $solution[$parts[0]] = intval($parts[1]); + } + + return $solution; + } + +} \ No newline at end of file diff --git a/classes/local/lp/engines/scip.php b/classes/local/lp/engines/scip.php new file mode 100644 index 00000000..f081577f --- /dev/null +++ b/classes/local/lp/engines/scip.php @@ -0,0 +1,55 @@ +. + +namespace mod_ratingallocate\local\lp\engines; + +class scip extends \mod_ratingallocate\local\lp\engine { + + /** + * Returns the command that gets executed + * + * @param $input_file Name of the input file + * + * @returns Command as a string + */ + public function get_command($input_file) { + return "scip -f $input_file"; + } + + /** + * Reads the content of the stream and returns the variables and their optimized values + * + * @param $stream Output stream of the program that was executed + * + * @return Array of variables and their values + */ + public function read($stream) { + $content = stream_get_contents($stream); + $solution = []; + + foreach(array_slice(explode("\n", substr($content, strpos($content, 'objective value:'))), 1) as $variable) { + $parts = explode(' ', preg_replace('!\s+!', ' ', $variable)); + + if(empty($parts[0])) + break; + + $solution[$parts[0]] = $parts[1]; + } + + return $solution; + } + +} \ No newline at end of file diff --git a/classes/local/lp/executor.php b/classes/local/lp/executor.php new file mode 100644 index 00000000..6144d5cc --- /dev/null +++ b/classes/local/lp/executor.php @@ -0,0 +1,96 @@ +. + +namespace mod_ratingallocate\local\lp; + +abstract class executor { + + private $engine = null; + + /** + * Creates an executor instance + * + * @param $engine Engine that is used + * + * @return Executor instance + */ + public function __construct($engine = null) { + $this->set_engine($engine); + } + + /** + * Sets the engine used by the executor + */ + public function set_engine($engine) { + $this->engine = $engine; + } + + /** + * Returns the engine + * + * @return Engine + */ + public function get_engine() { + return $this->engine; + } + + /** + * Runs the distribution with user and group objects and assigns users to their selected groups + * + * @param $users Array of users + * @param $groups Array of groups + * @param $weighter Weighter instance + */ + public function solve_objects(&$users, &$groups, $weighter) { + $values = $this->solve_linear_program(utility::create_linear_program($users, $groups, $weighter)); + + utility::assign_groups($values, $users, $groups); + } + + /** + * Runs the distribution with a linear program and returns variables and their values + * + * @param $linear_program Linear program that gets solved + * @param $executor Executor that is used + * + * @return Array of variables and their value + */ + public function solve_linear_program($linear_program) { + return $this->solve_lp_file($linear_program->write()); + } + + /** + * Runs the distribution with a lp file and returns variables and their values + * + * @param $linear_program Linear program that gets solved + * @param $executor Executor that is used + * + * @return Array of variables and their value + */ + public function solve_lp_file($lp_file) { + return $this->get_engine()->read($this->solve($lp_file)); + } + + /** + * Solves the lp file + * + * @param $lp_file Content of the lp file + * + * @return Array of variables and their value + */ + abstract public function solve($lp_file); + +} \ No newline at end of file diff --git a/classes/local/lp/executors/local.php b/classes/local/lp/executors/local.php new file mode 100644 index 00000000..bc1d09b4 --- /dev/null +++ b/classes/local/lp/executors/local.php @@ -0,0 +1,69 @@ +. + +namespace mod_ratingallocate\local\lp\executors; + +class local extends \mod_ratingallocate\local\lp\executor { + + private $local_file = null; + + /** + * Creates a local executor + * + * @param $engine Engine that is used + * @param $local_path Path of the local file used for solver execution + * + * @return Local executor instance + */ + public function __construct($engine = null) { + parent::__construct($engine); + + $this->local_file = tmpfile(); + } + + public function __destruct() { + unlink($this->get_local_path().'.lp'); + unlink($this->get_local_path()); + } + + /** + * Returns the path of the local file + * + * @return Local path + */ + public function get_local_path() { + return stream_get_meta_data($this->local_file)['uri']; + } + + /** + * Executes engine command on a remote machine using a webservice + * + * @param $engine Engine that is used + * @param $lp_file Content of the LP file + * + * @return Stream of stdout + */ + public function solve($lp_file) { + $path = $this->get_local_path().'.lp'; + $local_file = fopen($path, 'w+'); + + fwrite($local_file, $lp_file); + fseek($local_file, 0); + + return popen($this->get_engine()->get_command($path), 'r'); + } + +} diff --git a/classes/local/lp/executors/ssh.php b/classes/local/lp/executors/ssh.php new file mode 100644 index 00000000..78e4a188 --- /dev/null +++ b/classes/local/lp/executors/ssh.php @@ -0,0 +1,93 @@ +. + +namespace mod_ratingallocate\local\lp\executors; + +class ssh extends \mod_ratingallocate\local\lp\executor { + + private $connection = null; + private $local_file = null; + + /** + * Creates a ssh executor + * + * @param $engine Engine that is used + * @param $connection SSH connection + * + * @return ssh executor instance + */ + public function __construct($engine = null, $connection = null) { + parent::__construct($engine); + + $this->local_file = tmpfile(); + + $this->set_connection($connection); + } + + /** + * Sets the SSH connection + * + * @return SSH connection + */ + public function set_connection($connection) { + $this->connection = $connection; + } + + /** + * Returns the SSH connection + * + * @return SSH connection + */ + public function get_connection() { + return $this->connection; + } + + /** + * Returns the handle of the local file + * + * @return Handle of the local file + */ + public function get_local_file() { + return $this->local_file; + } + + /** + * Returns the path of the local file + * + * @return Local path + */ + public function get_local_path() { + return stream_get_meta_data($this->get_local_file())['uri']; + } + + /** + * Executes engine command on a remote machine using SSH + * + * @param $engine Engine that is used + * @param $lp_file Content of the LP file + * + * @return Stream of stdout + */ + public function solve($lp_file) { + $remote_path = trim(stream_get_contents($this->get_connection()->execute('mktemp --suffix=.lp'))); + + fwrite($this->get_local_file(), $lp_file); + fseek($this->get_local_file(), 0); + $this->get_connection()->send_file($this->get_local_path(), $remote_path); + + return $this->get_connection()->execute($this->get_engine()->get_command($remote_path)); + } +} diff --git a/classes/local/lp/executors/webservice/backend.php b/classes/local/lp/executors/webservice/backend.php new file mode 100644 index 00000000..509489a5 --- /dev/null +++ b/classes/local/lp/executors/webservice/backend.php @@ -0,0 +1,103 @@ +. + +namespace mod_ratingallocate\local\lp\executors\webservice; + +class backend +{ + private $engine = null; + private $secret = null; + + /** + * Creates a webservice backend + * + * @param $engine Engine which is used by the backend + * @param $secret Secret used for backend protection + * @param $local_path Local path where a temporary lp file is stored + * + * @return Webservice backend instance + */ + public function __construct($engine = null, $secret = null) { + $this->set_engine($engine); + $this->set_secret($secret); + } + + /** + * Sets the engine used by the backend + * + * @param $engine Engine + */ + public function set_engine($engine) { + $this->engine = $engine; + } + + /** + * Returns the engine used by the backend + * + * @return Engine + */ + public function get_engine() { + return $this->engine; + } + + /** + * Sets the webservices secret + * + * @param $secret Webservice secret + */ + public function set_secret($secret) { + $this->secret = $secret; + } + + /** + * Returns the webservice secret + * + * @return Webservice secret + */ + public function get_secret() { + return $this->secret; + } + + /** + * Handles an incoming request + */ + public function main() { + if(!$this->verify_secret()) { + http_response_code(401); + echo 'Unauthorized'; + + return; + } + + if(isset($_POST['lp_file'])) { + $executor = new \mod_ratingallocate\local\lp\executors\local($this->get_engine()); + fpassthru($executor->solve($_POST['lp_file'])); + } + } + + /** + * Verifys the secret + * + * @return true if secret was verified successfully + */ + private function verify_secret() { + if($this->get_secret() === null) + return true; + + return isset($_POST['secret']) && $this->get_secret() === $_POST['secret']; + } + +} diff --git a/classes/local/lp/executors/webservice/connector.php b/classes/local/lp/executors/webservice/connector.php new file mode 100644 index 00000000..8f0a5544 --- /dev/null +++ b/classes/local/lp/executors/webservice/connector.php @@ -0,0 +1,112 @@ +. + +namespace mod_ratingallocate\local\lp\executors\webservice; + +class connector extends \mod_ratingallocate\local\lp\executor { + + const CURL_TIMEOUT = 5; + + private $uri = ''; + private $secret = null; + + /** + * Creates a webservice connector + */ + public function __construct($engine = null, $uri = '', $secret = null) { + parent::__construct($engine); + $this->set_uri($uri); + $this->set_secret($secret); + } + + /** + * Sets the webservices secret + * + * @param $secret Webservice secret + */ + public function set_secret($secret) { + $this->secret = $secret; + } + + /** + * Returns the webservices secret + * + * @return Secret of the backend + */ + public function get_secret() { + return $this->secret; + } + + /** + * Sets the uri to the backend + * + * @param $uri URI to the backend + */ + public function set_uri($uri) { + $this->uri = $uri; + } + + /** + * Returns the uri to the backend + * + * @return URI of the backend + */ + public function get_uri() { + return $this->uri; + } + + /** + * Executes engine command on a remote machine using a webservice + * + * @param $engine Engine that is used + * @param $lp_file Content of the LP file + * + * @throws exception If status code is not 200 (OK) + * + * @return Stream of stdout + */ + public function solve($lp_file) { + $handle = curl_init(); + + curl_setopt_array($handle, [ + CURLOPT_URL => $this->get_uri(), + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POSTFIELDS => ['lp_file' => $lp_file, + 'secret' => $this->get_secret()], + CURLOPT_CONNECTTIMEOUT => self::CURL_TIMEOUT, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_SSL_VERIFYPEER => 0, + ]); + + $result = curl_exec($handle); + + if(($status = curl_getinfo($handle, CURLINFO_HTTP_CODE)) != 200) { + $error = curl_error($handle); + curl_close($handle); + throw new \exception("Could not retrieve solution (HTTP Status: $status, cURL: ".$error.", result: $result)!"); + } + + curl_close($handle); + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $result); + rewind($stream); + + return $stream; + } + +} diff --git a/classes/local/lp/linear_program.php b/classes/local/lp/linear_program.php new file mode 100644 index 00000000..ede37d30 --- /dev/null +++ b/classes/local/lp/linear_program.php @@ -0,0 +1,177 @@ +. + +namespace mod_ratingallocate\local\lp; + +class linear_program { + const MAXIMUM_LINE_INPUT = 510; + const MAXIMUM_NAME_LENGTH = 255; + + const NONE = -1; + const MINIMIZE = 0; + const MAXIMIZE = 1; + + const BINARY = 0; + const INTEGER = 1; + const REAL = 2; + const COMPLEX = 3; + + private $objective_method = self::NONE; + private $objective_function = ''; + + private $constraints = []; + private $bounds = []; + private $variables = []; + + public function set_objective_method($objective_method) { + $this->objective_method = $objective_method; + } + + public function get_objective_method() { + return $this->objective_method; + } + + public function set_objective_function($objective_function) { + $this->objective_function = $objective_function; + } + + public function get_objective_function() { + return $this->objective_function; + } + + public function set_objective($objective_method, $objective_function) { + $this->set_objective_method($objective_method); + $this->set_objective_function($objective_function); + } + + public function set_constraints($constraints) { + $this->constraints = $constraints; + } + + public function get_constraints() { + return $this->constraints; + } + + public function add_constraint($constraint) { + $this->constraints[] = $constraint; + } + + public function set_bounds($bounds) { + $this->bounds = $bounds; + } + + public function get_bounds() { + return $this->bounds; + } + + public function add_bound($bound) { + $this->bounds[] = $bound; + } + + public function set_variables($variables) { + $this->variables = $variables; + } + + public function get_variables() { + return $this->variables; + } + + public function get_variable_names() { + return array_map(function($x) { + return $x['name']; + }, $this->variables); + } + + public function add_variable($variable, $type = self::REAL) { + if(strlen($variable) > self::MAXIMUM_NAME_LENGTH) + throw new \exception('Name length exceeds the maximum!'); + + $this->variables[$variable] = ['type' => $type, 'name' => $variable]; + } + + public function write_objective_method() { + if($this->get_objective_method() == linear_program::MINIMIZE) + return 'Minimize'; + elseif($this->get_objective_method() == linear_program::MAXIMIZE) + return 'Maximize'; + + throw new \exception('Linear program objectives method is invalid!'); + } + + public function write_objective_function() { + return $this->prepare_term($this->get_objective_function()); + } + + public function write_objective() { + if(empty($this->get_objective_function())) + throw new \exception('Linear program objectives function is invalid!'); + + return $this->write_objective_method($this->get_objective_method())."\n".$this->write_objective_function()."\n"; + } + + public function write_constraints() { + if(empty($this->get_constraints())) + return ''; + + return "Subject To\n".implode(array_map(function($constraint) { + return $this->prepare_term($constraint)."\n"; + }, $this->get_constraints())); + } + + public function write_bounds() { + if(empty($this->bounds)) + return ''; + + return "Bounds\n".implode(array_map(function($bound) { + return $this->prepare_term($bound)."\n"; + }, $this->get_bounds())); + } + + public function write_variables() { + if(empty($this->get_variables())) + return ''; + + return "General\n".$this->prepare_string(array_map(function($x) { return $x['name'].' '; }, $this->get_variables()))."\n"; + } + + public function write() { + return $this->write_objective() + .$this->write_constraints() + .$this->write_bounds() + .$this->write_variables().'End'; + } + + public function prepare_term($term) { + return $this->prepare_string(preg_split('$([\+\-])$', str_replace(['*', ' '], '', $term), null, PREG_SPLIT_DELIM_CAPTURE)); + } + + public function prepare_string($array) { + $line_size = 0; + + return array_reduce($array, function($carry, $x) use (&$line_size) { + $x_length = strlen($x); + + if(($line_size + $x_length + 1) > self::MAXIMUM_LINE_INPUT) { + $line_size = 0; + $x = "\n".$x; + $carry = trim($carry); + } + + $line_size += $x_length; + return $carry.$x; + }, ''); + } +}; \ No newline at end of file diff --git a/classes/local/lp/utility.php b/classes/local/lp/utility.php new file mode 100644 index 00000000..b41184fa --- /dev/null +++ b/classes/local/lp/utility.php @@ -0,0 +1,178 @@ +. + +namespace mod_ratingallocate\local\lp; + +class utility +{ + + /** + * Translates a user and a group object to a name + * + * @param $user User object + * @param $group Group object + * + * @return Name + */ + public static function translate_to_name($user, $group) { + return 'x_'.$user->get_id().'_'.$group->get_id(); + } + + /** + * Translate a name to a user and a group object + * + * @param $name Name which gets translated + * @param $users Array of users + * @param $groups Array of groups + * + * @return Array containing translated user as the first element and translated group as the second + */ + public static function translate_from_name($name, $users, $groups) { + $explode = explode('_', $name); + + if(count($explode) < 3) + return [null, null]; + + return [$users[$explode[1]], $groups[$explode[2]]]; + } + + /** + * Adds the objective function to the linear program + * + * @param $linear_program Linear program the objective function is added to + * @param $users Array of users + * @param $groups Array of groups + * @param $weighter Weighter object for the weighting process + */ + public static function add_objective_function(&$linear_program, $users, $groups, $weighter) { + $objective_function = ''; + + foreach($users as $user) { + foreach($groups as $group) { + $weight = $weighter->apply($user->get_priority($group)); + + if(!empty($objective_function) && $weight != 0) + $objective_function .= '+'; + + if($weight == 1) + $objective_function .= self::translate_to_name($user, $group); + else if($weight != 0) + $objective_function .= $weight.'*'.self::translate_to_name($user, $group); + } + } + + $linear_program->set_objective(\mod_ratingallocate\local\lp\linear_program::MAXIMIZE, $objective_function); + } + + /** + * Adds constraints to the linear program + * + * @param $linear_program Linear program the constraints are added to + * @param $users Array of users + * @param $groups Array of groups + */ + public static function add_constraints(&$linear_program, $users, $groups) { + foreach($groups as $group) { + $lhs = ''; + + foreach($users as $user) { + if(!empty($lhs)) + $lhs .= '+'; + + $lhs .= self::translate_to_name($user, $group); + } + + $linear_program->add_constraint("$lhs <= {$group->get_limit()}"); + } + } + + /** + * Adds bounds to the linear program + * + * @param $linear_program Linear program the bounds are added to + * @param $users Array of users + * @param $groups Array of groups + */ + public static function add_bounds(&$linear_program, $users, $groups) { + foreach($users as $user) + foreach($groups as $group) + $linear_program->add_bound('0 <= '.self::translate_to_name($user, $group)); + + foreach($users as $user) { + $lhs = ''; + + foreach($groups as $group) { + if(!empty($lhs)) + $lhs .= '+'; + + $lhs .= self::translate_to_name($user, $group); + } + + $linear_program->add_constraint("$lhs <= 1"); + } + } + + /** + * Adds variables to the linear program + * + * @param $linear_program Linear program the bounds are added to + * @param $users Array of users + * @param $groups Array of groups + */ + public static function add_variables(&$linear_program, $users, $groups) { + foreach($users as $user) + foreach($groups as $group) + $linear_program->add_variable(self::translate_to_name($user, $group)); + } + + /** + * Creates a fully configured linear program + * + * @param $groups Array of groups + * @param $users Array of $users + * @param $weighter Weighter object + * + * @return Fully configured linear program + */ + public static function create_linear_program(&$users, &$groups, $weighter) { + $linear_program = new \mod_ratingallocate\local\lp\linear_program(); + + self::add_objective_function($linear_program, $users, $groups, $weighter); + self::add_constraints($linear_program, $users, $groups); + self::add_bounds($linear_program, $users, $groups); + self::add_variables($linear_program, $users, $groups); + + return $linear_program; + } + + /** + * Assigns a group determined by $solution to each user + * + * @param $solution Array of solutions + * @param $users Array of users + * @param $groups Array of groups + */ + public static function assign_groups($solution, &$users, &$groups) { + foreach($solution as $key => $value) { + if($value) { + list($user, $group) = self::translate_from_name($key, $users, $groups); + + if($user && $group) + $user->set_assigned_group($group); + } + } + } +} \ No newline at end of file diff --git a/classes/local/lp/weighter.php b/classes/local/lp/weighter.php new file mode 100644 index 00000000..9d90c006 --- /dev/null +++ b/classes/local/lp/weighter.php @@ -0,0 +1,42 @@ +. + +namespace mod_ratingallocate\local\lp; + +/** + * Abstract class which defines abstract methods for weighter + */ +abstract class weighter { + + /** + * Applys a concrete value for x + * + * @param $x Value for x + * + * @return Function value for x + */ + abstract public function apply($x); + + /** + * Returns the functional representation as a string + * + * @param $variable_name The name of the variable + * + * @return Functional representation as a string + */ + abstract public function to_string($variable_name = 'x'); + +}; \ No newline at end of file diff --git a/classes/local/lp/weighters/identity_weighter.php b/classes/local/lp/weighters/identity_weighter.php new file mode 100644 index 00000000..2343febe --- /dev/null +++ b/classes/local/lp/weighters/identity_weighter.php @@ -0,0 +1,31 @@ +. + +namespace mod_ratingallocate\local\lp\weighters; + +/** + * Class which represents an identity weighter + */ +class identity_weighter extends polynomial_weighter { + + /** + * Creates an identity weighter + */ + public function __construct() { + parent::__construct([1, 0]); + } + +}; diff --git a/classes/local/lp/weighters/polynomial_weighter.php b/classes/local/lp/weighters/polynomial_weighter.php new file mode 100644 index 00000000..1f8a0ca0 --- /dev/null +++ b/classes/local/lp/weighters/polynomial_weighter.php @@ -0,0 +1,90 @@ +. + +namespace mod_ratingallocate\local\lp\weighters; + +/** + * Class which represents a polynomial weighter + */ +class polynomial_weighter extends \mod_ratingallocate\local\lp\weighter { + + /** + * Coeefficients, that represent the polynom + */ + private $coefficients = []; + + /** + * Creates a polynomial weighter + * + * @param $coefficients Coefficients, which reprent the polynom + */ + public function __construct($coefficients) { + $this->coefficients = array_reverse($coefficients); + } + + /** + * Returns the coefficients of the polynom + * + * @return Array of coefficients + */ + public function get_coefficients() { + return $this->coefficients; + } + + /** + * Applys a concrete value for x + * + * @param $x Value for x + * + * @return Function value for x + */ + public function apply($x) { + $weight = 0; + + for($i = 0; $i < count($this->coefficients); ++$i) + $weight += $this->coefficients[$i] * pow($x, $i); + + return $weight; + } + + /** + * Returns the functional representation as a string + * + * @param $variable_name The name of the variable + * + * @return Functional representation as a string + */ + public function to_string($variable_name = 'x') { + $string = ''; + + $coefficients_size = count($this->coefficients); + + for($i = 0; $i < $coefficients_size; ++$i) { + if($this->coefficients[$i] == 0) + continue; + + if(!empty($string)) + $string .= '+'; + + $string .= $this->coefficients[$i] == 1 ? '' : $this->coefficients[$i].'*'; + $string .= $variable_name; + $string .= $i != 1 ? '^'.$i : ''; + } + + return $string; + } + +}; \ No newline at end of file diff --git a/classes/local/ssh/authentication.php b/classes/local/ssh/authentication.php new file mode 100644 index 00000000..e6ac52af --- /dev/null +++ b/classes/local/ssh/authentication.php @@ -0,0 +1,39 @@ +. + +namespace mod_ratingallocate\local\ssh; + +class authentication { + + private $username = ''; + + public function __construct($username) { + $this->username = $username; + } + + public function set_username($username) { + $this->username = $username; + } + + public function get_username() { + return $this->username; + } + + public function authenticate($connection) { + return ssh2_auth_none($connection, $this->username); + } + +} \ No newline at end of file diff --git a/classes/local/ssh/connection.php b/classes/local/ssh/connection.php new file mode 100644 index 00000000..88a514f8 --- /dev/null +++ b/classes/local/ssh/connection.php @@ -0,0 +1,136 @@ +. + +namespace mod_ratingallocate\local\ssh; + +class connection { + + private $address = ''; + private $fingerprint = false; + private $authentication = null; + + private $handle = null; + + /** + * Creates a new SSH connection + * + * @param $address Address of the ssh server + * @param $authentication Authentication method + * @param $fingerprint Fingerprint of the ssh server (null for none) + * + * @throws exception If the connection to the ssh server could not be established + * @throws exception If authentication failed + */ + public function __construct($address, $authentication, $fingerprint = null) { + $this->address = $address; + $this->authentication = $authentication; + $this->fingerprint = $fingerprint; + + $this->handle = \ssh2_connect($this->address); + + if($this->fingerprint && ssh2_fingerprint($this->handle) != $this->fingerprint) + throw new \exception("Fingerprints do not match!"); + + if(!$this->handle) + throw new \exception("Could not connect to ssh server with address {$this->address}!"); + + if(!$this->authentication->authenticate($this->handle)) + throw new \exception('Authentication failed!'); + } + + /** + * Returns the address of the ssh server + * + * @return SSH server address + */ + public function get_address() { + return $this->address; + } + + /** + * Returns the fingerprint of the ssh server + * + * @return SSH server fingerprint + */ + public function get_fingerprint() { + return $this->fingerprint; + } + + /** + * Returns the authentication method of the ssh server + * + * @return Authentication method + */ + public function get_authentication() { + return $this->authentication; + } + + /** + * Returns the connection handle of the ssh server + * + * @return SSH server connection handle + */ + public function get_handle() { + return $this->handle; + } + + /** + * Executes a command + * + * @param $command Command which gets executed + * + * @throws exception If the command could not be executed + * + * @return Stream handle + */ + public function execute($command) { + $stream = \ssh2_exec($this->handle, $command); + + if(!$stream) + throw new \exception("Could not execute the command {$this->command}!"); + + stream_set_blocking($stream, true); + + return $stream; + } + + /** + * Sends a file to a remote server + * + * @param $local_path Local path + * @param $remote_path Remote path + * + * @throws exception If file was not transmitted successfully + */ + public function send_file($local_path, $remote_path) { + if(!ssh2_scp_send($this->handle, $local_path, $remote_path)) + throw new \exception('Error sending file!'); + } + + /** + * Receives a file from a remote server + * + * @param $remote_path Remote path + * @param $local_path Local path + * + * @throws exception If file was not transmitted successfully + */ + public function receive_file($remote_path, $local_path) { + if(!ssh2_scp_recv($this->handle, $remote_path, $local_path)) + throw new \exception('Error receiving file!'); + } + +} \ No newline at end of file diff --git a/classes/local/ssh/password_authentication.php b/classes/local/ssh/password_authentication.php new file mode 100644 index 00000000..d05c43b3 --- /dev/null +++ b/classes/local/ssh/password_authentication.php @@ -0,0 +1,40 @@ +. + +namespace mod_ratingallocate\local\ssh; + +class password_authentication extends authentication { + + private $password = ''; + + public function __construct($username, $password) { + parent::__construct($username); + $this->password = $password; + } + + public function set_password($password) { + $this->password = $password; + } + + public function get_password() { + return $this->password; + } + + public function authenticate($connection) { + return ssh2_auth_password($connection, $this->get_username(), $this->get_password()); + } + +} \ No newline at end of file diff --git a/classes/local/ssh/public_key_authentication.php b/classes/local/ssh/public_key_authentication.php new file mode 100644 index 00000000..9e0662f4 --- /dev/null +++ b/classes/local/ssh/public_key_authentication.php @@ -0,0 +1,61 @@ +. + +namespace mod_ratingallocate\local\ssh; + +class public_key_authentication extends authentication { + + private $public_key = ''; + private $private_key = ''; + private $passphrase = ''; + + public function __construct($username, $public_key, $private_key, $passphrase = '') { + parent::__construct($username); + + $this->public_key = $public_key; + $this->private_key = $private_key; + $this->passphrase = $passphrase; + } + + public function set_public_key($public_key) { + $this->public_key = $public_key; + } + + public function get_public_key() { + return $this->public_key; + } + + public function set_private_key($private_key) { + $this->private_key = $private_key; + } + + public function get_private_key() { + return $this->private_key; + } + + public function set_passphrase($passphrase) { + $this->passphrase = $passphrase; + } + + public function get_passphrase() { + return $this->passphrase; + } + + public function authenticate($connection) { + return ssh2_auth_pubkey_file($connection, $this->get_username(), $this->get_public_key(), $this->get_private_key(), $this->get_passphrase()); + } + +} \ No newline at end of file diff --git a/classes/local/user.php b/classes/local/user.php new file mode 100644 index 00000000..0474510b --- /dev/null +++ b/classes/local/user.php @@ -0,0 +1,212 @@ +. + +namespace mod_ratingallocate\local; + +class user { + + private $id = -1; + private $selected_groups = []; + private $assigned_group = null; + + /** + * Creates a user + * + * @param $id id of the user + * @param $selected_groups Selected groups of the user + */ + public function __construct($id, $selected_groups = []) { + $this->id = $id; + $this->set_selected_groups($selected_groups); + } + + /** + * Returns the id of the user + * + * @return Id of the user + */ + public function get_id() { + return $this->id; + } + + /** + * Sets selected groups of user + * + * @param $selected_groups Array of selected groups + */ + public function set_selected_groups($selected_groups) { + $this->clear_selected_groups(); + + foreach($selected_groups as &$value) + $this->add_selected_group($value); + } + + /** + * Clears selected groups of user + */ + public function clear_selected_groups() { + $this->selected_groups = []; + } + + /** + * Returns selected groups + * + * @return Array of selected groups + */ + public function get_selected_groups() { + return array_map(function($x){return $x['group'];}, $this->selected_groups); + } + + /** + * Adds a group choice + * + * @param $group Group that is selected by the user + * @param $priority Selections priority + * + * @throws exception If group has already been selected by the user + */ + public function add_selected_group(&$group, $priority = 1) { + if($this->exists_selected_group($group)) + throw new \exception('Group has already been selected by the user!'); + + $this->selected_groups[$group->get_id()] = ['group' => &$group, 'priority' => 0]; + + try { + $this->set_priority($group, $priority); + } + catch(exception $e) { + $this->remove_selected_group($group); + throw $e; + } + } + + /** + * Checks if a group was selected by the user + * + * @return True of group was selected + */ + public function exists_selected_group($group) { + return isset($this->selected_groups[$group->get_id()]); + } + + /** + * Removes a choice from selected groups + * + * @param $group group that gets removed from selected groups + * + * @throws exception If group has not been selected by the user + */ + public function remove_selected_group($group) { + if(!$this->exists_selected_group($group)) + throw new \exception('Group has not been selected by the user!'); + + unset($this->selected_groups[$group->get_id()]); + } + + /** + * Sets the priority for the given group + * + * @param $group Group whichs priority get changed + * @param $priority New priority + * + * @throws exception If group has not been selected by the user + * @throws exception If priority is not numeric + * @throws exception If priority is less than zero + * @throws exception If priority is zero + */ + public function set_priority($group, $priority) { + if(!$this->exists_selected_group($group)) + throw new \exception('Group has not been selected by the user!'); + + if(!is_numeric($priority)) + throw new \exception('Priority is not numeric!'); + + if($priority < 0) + throw new \exception('Priority is not positive!'); + + if($priority == 0) + throw new \exception('Cannot set priority to zero!'); + + $this->selected_groups[$group->get_id()]['priority'] = $priority; + } + + /** + * Returns the priority for the given group, which is 0 if the group was not selected by the user + * + * @param $group Group + * + * @return Priority for the given group + */ + public function get_priority($group) { + if(!$this->exists_selected_group($group)) + return 0; + + return $this->selected_groups[$group->get_id()]['priority']; + } + + /** + * Returns the assigned group + * + * @return Assigned group(null for none) + */ + public function get_assigned_group() { + return $this->assigned_group; + } + + /** + * Assigns a group to the user, removing the user from the current assigned group and adding + * the user to the newly assigned group + * + * @param $group Group that the user gets added to + */ + public function set_assigned_group(&$group) { + if($group == $this->get_assigned_group()) + return; + + if($this->assigned_group) + $this->assigned_group->remove_assigned_user($this); + + $this->assigned_group = null; + + if($group) { + $this->assigned_group = $group; + $group->add_assigned_user($this); + } + } + + /** + * Checks if assigned group is an element of the selected groups and therefor if the choice is satisfied by + * the assigned group + * + * @return True If the choice is satisfied + */ + public function is_choice_satisfied() { + return (!$this->assigned_group ? false : $this->exists_selected_group($this->assigned_group)); + } + + /** + * Returns the choice satisfaction, representing the satisfaction with the assigned group + * + * @return Between 0 and 1, representing how satisfying the assigned group is + */ + public function get_choice_satisfaction() { + if(!$this->is_choice_satisfied()) + return 0; + + return $this->get_priority($this->assigned_group) / max(array_map(function($x) { return $x['priority']; }, $this->selected_groups)); + } + +} \ No newline at end of file diff --git a/classes/local/utility.php b/classes/local/utility.php new file mode 100644 index 00000000..e4b83ef2 --- /dev/null +++ b/classes/local/utility.php @@ -0,0 +1,66 @@ +. + +namespace mod_ratingallocate\local; + +class utility { + + public static function transform_from_users_and_groups($users, $groups) { + $allocation = array_map(function($x) { return $x->get_id(); }, $groups); + array_walk($allocation, function(&$x, $y) { $x = []; }); + + foreach($users as $user) + if($user->is_choice_satisfied()) + $allocation[$user->get_assigned_group()->get_id()][] = $user->get_id(); + + return $allocation; + } + + public static function transform_to_users_and_groups($choices, $ratings) { + $groups = self::transform_to_groups($choices); + $users = self::transform_to_users($ratings); + + self::transform_user_selection($ratings, $users, $groups); + + return [&$users, &$groups]; + } + + public static function transform_to_users($ratings) { + $users = []; + + foreach(array_unique(array_map(function($x) { return $x->userid; }, $ratings)) as $id) + $users[$id] = new \mod_ratingallocate\local\user($id); + + return $users; + } + + public static function transform_to_groups($choices) { + $groups = []; + + foreach($choices as $choice) + if(!isset($choices->active) || $choice->active) + $groups[$choice->id] = new \mod_ratingallocate\local\group($choice->id, $choice->maxsize); + + return $groups; + } + + public static function transform_user_selection($ratings, &$users, &$groups) { + foreach($ratings as $rating) + if($rating->rating > 0) + $users[$rating->userid]->add_selected_group($groups[$rating->choiceid], $rating->rating); + } + +} \ No newline at end of file diff --git a/classes/task/cron_task.php b/classes/task/cron_task.php index ce12e06f..928acb6d 100644 --- a/classes/task/cron_task.php +++ b/classes/task/cron_task.php @@ -73,7 +73,7 @@ public function execute() { if ($ratingallocate->ratingallocate->runalgorithmbycron === "1" && $ratingallocate->get_algorithm_status() === \mod_ratingallocate\algorithm_status::notstarted) { // Run allocation. - $ratingallocate->distrubute_choices(); + $ratingallocate->distribute_choices(); } } return true; diff --git a/lang/en/ratingallocate.php b/lang/en/ratingallocate.php index 3af071e8..6b8030f9 100644 --- a/lang/en/ratingallocate.php +++ b/lang/en/ratingallocate.php @@ -46,6 +46,34 @@ $string['algorithmtimeout'] = 'Algorithm timeout'; $string['configalgorithmtimeout'] = 'The time in seconds after which the algorithm is assumed to be stuck. The current run is terminated and marked as failed.'; +$string['edmonds_karp'] = 'Edmonds-Karp'; +$string['ford_fulkerson'] = 'Ford-Fulkerson'; +$string['lp'] = 'LP'; +$string['solver'] = 'Solver'; +$string['solver_description'] = 'Solver for distributing users into groups.'; +$string['general'] = 'General'; +$string['engine'] = 'Engine'; +$string['engine_description'] = 'Engine for optimizing linear programs.'; +$string['executor'] = 'Executor'; +$string['executor_description'] = 'Executor for executing the engine.'; +$string['scip'] = 'SCIP'; +$string['cplex'] = 'CPLEX'; +$string['local'] = 'Local'; +$string['local_description'] = 'Executor for running the selected engine locally.'; +$string['ssh'] = 'SSH'; +$string['ssh_description'] = 'Executor for connecting to a remote engine by using SSH.'; +$string['ssh_address'] = 'SSH Address'; +$string['ssh_address_description'] = 'Address of a ssh server, the executor connects to for running the selected engine.'; +$string['ssh_username'] = 'SSH username'; +$string['ssh_username_description'] = 'Username of the ssh user.'; +$string['ssh_password'] = 'SSH password'; +$string['ssh_password_description'] = 'Password of the ssh user.'; +$string['webservice'] = 'Webservice'; +$string['webservice_description'] = 'Executor for connecting to a remote engine by using HTTP or HTTPS.'; +$string['secret'] = 'Secret'; +$string['secret_description'] = 'Webservices backend secret, used for protecting it.'; +$string['uri'] = 'URI'; +$string['uri_description'] = 'URI of webservice backend.'; // // $string['choicestatusheading'] = 'Status'; diff --git a/locallib.php b/locallib.php index 0323c21c..f0afb368 100644 --- a/locallib.php +++ b/locallib.php @@ -41,6 +41,7 @@ // Takes care of loading all the solvers. require_once(dirname(__FILE__) . '/solver/ford-fulkerson-koegel.php'); require_once(dirname(__FILE__) . '/solver/edmonds-karp.php'); +require_once(dirname(__FILE__) . '/solver/lp-solver.php'); // Now come all the strategies. require_once(dirname(__FILE__) . '/strategy/strategy01_yes_no.php'); @@ -199,7 +200,7 @@ private function process_action_start_distribution() { if (has_capability('mod/ratingallocate:start_distribution', $this->context)) { /* @var mod_ratingallocate_renderer */ $renderer = $this->get_renderer(); - if ($this->get_algorithm_status() === \mod_ratingallocate\algorithm_status::running) { + /*if ($this->get_algorithm_status() === \mod_ratingallocate\algorithm_status::running) { // Don't run, if an instance is already running. $renderer->add_notification(get_string('algorithm_already_running', ratingallocate_MOD_NAME)); } else if ($this->ratingallocate->runalgorithmbycron === "1" && @@ -207,14 +208,14 @@ private function process_action_start_distribution() { ) { // Don't run, if the cron has not started yet, but is set as priority. $renderer->add_notification(get_string('algorithm_scheduled_for_cron', ratingallocate_MOD_NAME)); - } else { + } else*/ if(true) { $this->origdbrecord->{this_db\ratingallocate::ALGORITHMSTATUS} = \mod_ratingallocate\algorithm_status::running; $DB->update_record(this_db\ratingallocate::TABLE, $this->origdbrecord); // Try to get some more memory, 500 users in 10 groups take about 15mb. raise_memory_limit(MEMORY_EXTRA); core_php_time_limit::raise(); // Distribute choices. - $timeneeded = $this->distrubute_choices(); + $timeneeded = $this->distribute_choices(); // Logging. $event = \mod_ratingallocate\event\distribution_triggered::create_simple( @@ -826,28 +827,51 @@ public function get_ratings_for_rateable_choices() { return $fromraters; } + /** + * Returns solver instance, based on $CFG + */ + public function get_solver() { + global $CFG; + + switch($CFG->ratingallocate_solver) { + case 'lp': + return new solver_lp(); + + case 'ford_fulkerson': + return new solver_ford_fulkerson(); + + case 'edmonds_karp': + default: return new solver_edmonds_karp(); + } + } + /** * distribution of choices for each user * take care about max_execution_time and memory_limit */ - public function distrubute_choices() { + public function distribute_choices() { require_capability('mod/ratingallocate:start_distribution', $this->context); + $distributor = $this->get_solver(); + // Set algorithm status to running. $this->origdbrecord->algorithmstatus = \mod_ratingallocate\algorithm_status::running; $this->origdbrecord->algorithmstarttime = time(); $this->db->update_record(this_db\ratingallocate::TABLE, $this->origdbrecord); - $distributor = new solver_edmonds_karp(); - // $distributor = new solver_ford_fulkerson(); - $timestart = microtime(true); - $distributor->distribute_users($this); - $timeneeded = (microtime(true) - $timestart); - // echo memory_get_peak_usage(); + try { + $timestart = microtime(true); + $distributor->distribute_users($this); + $timeneeded = (microtime(true) - $timestart); - // Set algorithm status to finished. - $this->origdbrecord->algorithmstatus = \mod_ratingallocate\algorithm_status::finished; - $this->db->update_record(this_db\ratingallocate::TABLE, $this->origdbrecord); + // Set algorithm status to finished. + $this->origdbrecord->algorithmstatus = \mod_ratingallocate\algorithm_status::finished; + $this->db->update_record(this_db\ratingallocate::TABLE, $this->origdbrecord); + } + catch(\exception $e) { + $this->set_algorithm_failed(); + throw $e; + } return $timeneeded; } @@ -1365,6 +1389,52 @@ public function addtestdata() { $transaction->allow_commit(); } + /** + * Returns an array of settigs for the used strategy + */ + public function get_settingfields() { + $strategy = $this->get_strategy_class(); + $array = $strategy->get_dynamic_settingfields(); + + if(empty($array)) + $array = $strategy->get_static_settingfields(); + + if(empty($array)) + $array = $strategy->get_default_settingfields(); + + return $array; + } + + /** + * Returns an array of available choices + */ + public function get_available_ratings() { + return array_filter($this->get_settingfields(), function($value, $key) { + return is_numeric($key); + }, ARRAY_FILTER_USE_BOTH); + } + + /** + * Another internal helper to populate the database with random data + */ + public function add_test_data($overwrite = true) { + $transaction = $this->db->start_delegated_transaction(); + $ratings = array_keys($this->get_available_ratings()); + + foreach(get_enrolled_users($this->context) as $user) { + foreach($this->get_choices() as $choice) { + $rating = new stdclass(); + $rating->userid = $user->id; + $rating->choiceid = $choice->id; + $rating->rating = $ratings[array_rand($ratings)]; + + $this->db->insert_record('ratingallocate_ratings', $rating); + } + } + + $transaction->allow_commit(); + } + /** * Lazy load the page renderer and expose the renderer to plugin. * diff --git a/settings.php b/settings.php index b1de9b71..0045c7fd 100644 --- a/settings.php +++ b/settings.php @@ -3,12 +3,41 @@ * Admin settings for mod_ratingallocate * * @package mod_ratingallocate - * @copyright 2015 Tobias Reischmann + * @copyright 2015 Tobias Reischmann, 2017 Justus Flerlage * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { + $t = function($name) { + return get_string($name, 'ratingallocate'); + }; + + $settings->add(new admin_setting_heading('ratingallocate_general', $t('general'), '')); + $options = ['edmonds_karp' => $t('edmonds_karp'), + 'ford_fulkerson' => $t('ford_fulkerson'), + 'lp' => $t('lp')]; + $settings->add(new admin_setting_configselect('ratingallocate_solver', $t('solver'), $t('solver_description'), 'edmonds_karp', $options)); $settings->add(new admin_setting_configtext('ratingallocate_algorithm_timeout', get_string('algorithmtimeout', 'ratingallocate'), - get_string('configalgorithmtimeout', 'ratingallocate'), 600, PARAM_INT)); + get_string('configalgorithmtimeout', 'ratingallocate'), 600, PARAM_INT)); + + $settings->add(new admin_setting_heading('ratingallocate_lp', $t('lp'), '')); + $options = ['scip' => $t('scip'), + 'cplex' => $t('cplex')]; + $settings->add(new admin_setting_configselect('ratingallocate_engine', $t('engine'), $t('engine_description'), 'scip', $options)); + $options = ['local' => $t('local'), + 'ssh' => $t('ssh'), + 'webservice' => $t('webservice')]; + $settings->add(new admin_setting_configselect('ratingallocate_executor', $t('executor'), $t('executor_description'), 'local', $options)); + + $settings->add(new admin_setting_heading('ratingallocate_webservice', $t('webservice'), $t('webservice_description'))); + $settings->add(new admin_setting_configpasswordunmask('ratingallocate_secret', $t('secret'), $t('secret_description'), '', PARAM_TEXT)); + $settings->add(new admin_setting_configtext('ratingallocate_uri', $t('uri'), $t('uri_description'), 'http://localhost/moodle-mod_ratingallocate/webservice', PARAM_TEXT)); + + $settings->add(new admin_setting_heading('ratingallocate_ssh', $t('ssh'), $t('ssh_description'))); + $settings->add(new admin_setting_configtext('ratingallocate_ssh_address', $t('ssh_address'), $t('ssh_address_description'), null, PARAM_TEXT)); + $settings->add(new admin_setting_configtext('ratingallocate_ssh_username', $t('ssh_username'), $t('ssh_username_description'), null, PARAM_TEXT)); + $settings->add(new admin_setting_configpasswordunmask('ratingallocate_ssh_password', $t('ssh_password'), $t('ssh_password_description'), null, PARAM_TEXT)); + + unset($t); } diff --git a/solver/lp-solver.php b/solver/lp-solver.php new file mode 100644 index 00000000..00aec5b0 --- /dev/null +++ b/solver/lp-solver.php @@ -0,0 +1,89 @@ +. + +/** + * + * Contains a solver which distributes by using external lp solvers + * + * @package mod_ratingallocate + * @subpackage mod_ratingallocate + * @copyright 2017 Justus Flerlage + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(__FILE__) . '/../locallib.php'); +require_once(dirname(__FILE__) . '/solver-template.php'); + +//function dbg_msg($obj) { +// echo "
", print_r($obj), "
"; +//} + +class solver_lp extends distributor { + + public function get_name() { + return 'lp'; + } + + public function get_ssh_connection() { + global $CFG; + + return new \mod_ratingallocate\local\ssh\connection($CFG->ratingallocate_ssh_address, + $this->get_ssh_authentication()); + } + + public function get_ssh_authentication() { + global $CFG; + + return new \mod_ratingallocate\local\ssh\password_authentication($CFG->ratingallocate_ssh_username, + $CFG->ratingallocate_ssh_password); + } + + public function get_executor($engine) { + global $CFG; + + switch($CFG->ratingallocate_executor) + { + case 'webservice': + return new \mod_ratingallocate\local\lp\executors\webservice\connector($engine, $CFG->ratingallocate_uri, $CFG->ratingallocate_secret); + + case 'ssh': + return new \mod_ratingallocate\local\lp\executors\ssh($engine, $this->get_ssh_connection(), $CFG->ratingallocate_remote_path); + } + + return new \mod_ratingallocate\lp\local\executors\local($engine, $CFG->ratingallocate_local_path); + } + + public function get_engine() { + global $CFG; + + $engine_path = "\\mod_ratingallocate\\local\\lp\\engines\\{$CFG->ratingallocate_engine}"; + + return new $engine_path(); + } + + public function compute_distribution($choicerecords, $ratings, $usercount) { + list($users, $groups) = \mod_ratingallocate\local\utility::transform_to_users_and_groups($choicerecords, $ratings); + + $engine = $this->get_engine(); + $executor = $this->get_executor($engine); + + $executor->solve_objects($users, $groups, new \mod_ratingallocate\local\lp\weighters\identity_weighter()); + + return \mod_ratingallocate\local\utility::transform_from_users_and_groups($users, $groups); + } + +} \ No newline at end of file diff --git a/tests/cplex.log b/tests/cplex.log new file mode 100644 index 00000000..7c31fd41 --- /dev/null +++ b/tests/cplex.log @@ -0,0 +1,36 @@ +Welcome to IBM(R) ILOG(R) CPLEX(R) Interactive Optimizer 12.6.0.0 + with Simplex, Mixed Integer & Barrier Optimizers +5725-A06 5725-A29 5724-Y48 5724-Y49 5724-Y54 5724-Y55 5655-Y21 +Copyright IBM Corp. 1988, 2013. All Rights Reserved. + +Type 'help' for a list of available commands. +Type 'help' followed by a command name for more +information on commands. + +CPLEX> Problem 'file.lp' read. +Read time = 0.00 sec. (0.00 ticks) +CPLEX> Found incumbent of value -2.000000 after 0.00 sec. (0.00 ticks) +Tried aggregator 1 time. +MIP Presolve eliminated 1 rows and 2 columns. +All rows and columns eliminated. +Presolve time = 0.00 sec. (0.00 ticks) + +Root node processing (before b&c): + Real time = 0.00 sec. (0.00 ticks) +Parallel b&c, 4 threads: + Real time = 0.00 sec. (0.00 ticks) + Sync time (average) = 0.00 sec. + Wait time (average) = 0.00 sec. + ------------ +Total (root+branch&cut) = 0.00 sec. (0.00 ticks) + +Solution pool: 3 solutions saved. + +MIP - Integer optimal solution: Objective = 3.0000000000e+00 +Solution time = 0.00 sec. Iterations = 0 Nodes = 0 +Deterministic time = 0.00 ticks (4.93 ticks/sec) + +CPLEX> Incumbent solution +Variable Name Solution Value +x1 5.000000 +x2 2.000000 diff --git a/tests/generator/lib.php b/tests/generator/lib.php index 9e3b0d49..c5f2ff24 100644 --- a/tests/generator/lib.php +++ b/tests/generator/lib.php @@ -319,7 +319,7 @@ public function __construct(advanced_testcase $tc, $moduledata = null, $choiceda // allocate choices $ratingallocate = mod_ratingallocate_generator::get_ratingallocate_for_user($tc, $this->moddb, $this->teacher); - $timeneeded = $ratingallocate->distrubute_choices(); + $timeneeded = $ratingallocate->distribute_choices(); $tc->assertGreaterThan(0, $timeneeded); $tc->assertLessThan(1.0, $timeneeded, 'Allocation is very slow'); $this->allocations = $ratingallocate->get_allocations(); diff --git a/tests/group_test.php b/tests/group_test.php new file mode 100644 index 00000000..6e67942a --- /dev/null +++ b/tests/group_test.php @@ -0,0 +1,159 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +class group_test extends basic_testcase { + + private $group1 = null; + private $group2 = null; + + private $user1= null; + private $user2= null; + + /** + * @covers \mod_ratingallocate\local\group::__construct + */ + protected function setUp() { + $this->group1 = new \mod_ratingallocate\local\group(1); + $this->group2 = new \mod_ratingallocate\local\group(2); + + $this->user1 = new \mod_ratingallocate\local\user(1, []); + $this->user2 = new \mod_ratingallocate\local\user(2, []); + } + + /** + * @covers \mod_ratingallocate\local\group::has_limit + */ + public function test_initial_limit() { + $this->assertFalse($this->group1->has_limit()); + } + + /** + * @covers \mod_ratingallocate\local\group::get_assigned_users + */ + public function test_assigned_users_initially_empty() { + $this->assertEmpty($this->group1->get_assigned_users()); + } + + /** + * @covers \mod_ratingallocate\local\group::has_limit + */ + public function test_initial_size() { + $this->assertTrue($this->group1->is_empty()); + } + + /** + * @covers \mod_ratingallocate\local\group::set_limit + * @covers \mod_ratingallocate\local\group::get_limit + */ + public function test_valid_limit() { + $this->group1->set_limit(1000); + $this->assertEquals(1000, $this->group1->get_limit()); + } + + /** + * @covers \mod_ratingallocate\local\group::set_limit + * @expectedException exception + */ + public function test_invalid_limit() { + $this->group1->set_limit(-1); + } + + /** + * @covers \mod_ratingallocate\local\group::add_assigned_user + * @covers \mod_ratingallocate\local\group::exists_assigned_user + */ + public function test_assign_one_user() { + $this->group1->add_assigned_user($this->user1); + $this->assertTrue($this->group1->exists_assigned_user($this->user1)); + } + + /** + * @depends test_assign_one_user + * @covers \mod_ratingallocate\local\group::add_assigned_user + * @covers \mod_ratingallocate\local\group::get_assigned_users + */ + public function test_assign_multiple_users() { + $this->group1->add_assigned_user($this->user1); + $this->group1->add_assigned_user($this->user2); + $this->assertContains($this->user1, $this->group1->get_assigned_users()); + } + + /** + * @depends test_valid_limit + * @depends test_assign_one_user + * @covers \mod_ratingallocate\local\group::add_assigned_user + * @covers \mod_ratingallocate\local\group::is_full + */ + public function test_full_group() { + $this->group1->set_limit(1); + $this->group1->add_assigned_user($this->user1); + $this->assertTrue($this->group1->is_full()); + } + + /** + * @depends test_assign_one_user + * @depends test_valid_limit + * @covers \mod_ratingallocate\local\group::add_assigned_user + * @expectedException exception + */ + public function test_assign_to_full_group() { + $this->group1->set_limit(1); + $this->group1->add_assigned_user($this->user1); + $this->group1->add_assigned_user($this->user2); + } + + /** + * @depends test_assign_one_user + * @covers \mod_ratingallocate\local\group::add_assigned_user + * @expectedException exception + */ + public function test_assign_assigned_user() { + $this->group1->add_assigned_user($this->user1); + $this->group2->add_assigned_user($this->user1); + } + + /** + * @covers \mod_ratingallocate\local\group::add_assigned_user + * @covers \mod_ratingallocate\local\group::get_assigned_users + */ + public function test_double_assign() { + $this->group1->add_assigned_user($this->user1); + $this->group1->add_assigned_user($this->user1); + $this->assertContains($this->user1, $this->group1->get_assigned_users()); + } + + /** + * @covers \mod_ratingallocate\local\group::remove_assigned_user + * @covers \mod_ratingallocate\local\group::exists_assigned_user + */ + public function test_remove_valid_user() { + $this->group1->add_assigned_user($this->user1); + $this->group1->remove_assigned_user($this->user1); + $this->assertFalse($this->group1->exists_assigned_user($this->user1)); + } + + /** + * @depends test_assigned_users_initially_empty + * @covers \mod_ratingallocate\local\group::remove_assigned_user + * @expectedException exception + */ + public function test_remove_invalid_user() { + $this->group1->remove_assigned_user($this->user1); + } + +} diff --git a/tests/locallib_test.php b/tests/locallib_test.php index 217457c6..37db36df 100644 --- a/tests/locallib_test.php +++ b/tests/locallib_test.php @@ -95,7 +95,7 @@ public function test_simple() { // Allocate choices. $ratingallocate = mod_ratingallocate_generator::get_ratingallocate_for_user($this, $mod, $teacher); - $timeneeded = $ratingallocate->distrubute_choices(); + $timeneeded = $ratingallocate->distribute_choices(); $this->assertGreaterThan(0, $timeneeded); $this->assertLessThan(0.1, $timeneeded, 'Allocation is very slow'); diff --git a/tests/lp_engine_test.php b/tests/lp_engine_test.php new file mode 100644 index 00000000..fd43a8e6 --- /dev/null +++ b/tests/lp_engine_test.php @@ -0,0 +1,68 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +class lp_engine_test extends basic_testcase { + + private $cplex = null; + private $scip = null; + + public function setUp() { + $this->cplex = new \mod_ratingallocate\local\lp\engines\cplex(); + $this->scip = new \mod_ratingallocate\local\lp\engines\scip(); + } + + /** + * @covers \mod_ratingallocate\local\lp\engines\scip::read + */ + public function test_scip_read() { + $handle = fopen(__DIR__.'/scip.log', 'r'); + $solution = $this->scip->read($handle); + + $this->assertEquals($solution['x1'], 5); + $this->assertEquals($solution['x2'], 2); + + fclose($handle); + } + + /** + * @covers \mod_ratingallocate\local\lp\engines\cplex::read + */ + public function test_cplex_read() { + $handle = fopen(__DIR__.'/cplex.log', 'r'); + $solution = $this->cplex->read($handle); + + $this->assertEquals($solution['x1'], 5); + $this->assertEquals($solution['x2'], 2); + + fclose($handle); + } + + /** + * @covers \mod_ratingallocate\local\lp\engines\cplex::get_command + */ + public function test_cplex_command() { + $this->assertNotEmpty($this->cplex->get_command('file.lp')); + } + + /** + * @covers \mod_ratingallocate\local\lp\engines\scip::get_command + */ + public function test_scip_command() { + $this->assertNotEmpty($this->scip->get_command('file.lp')); + } +} \ No newline at end of file diff --git a/tests/lp_executor_test.php b/tests/lp_executor_test.php new file mode 100644 index 00000000..9ed3cb70 --- /dev/null +++ b/tests/lp_executor_test.php @@ -0,0 +1,61 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +class lp_executor_test extends basic_testcase { + + private $local = null; + private $ssh = null; + private $webservice = null; + + public function setUp() { + $this->local = new \mod_ratingallocate\local\lp\executors\local(); + $this->ssh= new \mod_ratingallocate\local\lp\executors\ssh(); + $this->webservice = new \mod_ratingallocate\local\lp\executors\webservice\connector(); + } + + /** + * @covers \mod_ratingallocate\local\lp\executor::set_engine + * @covers \mod_ratingallocate\local\lp\executor::get_engine + */ + public function test_engine() { + $this->local->set_engine(new stdClass()); + $this->assertEquals($this->local->get_engine(), new stdClass()); + } + + /** + * @covers \mod_ratingallocate\local\lp\executors\local::get_local_path + */ + public function test_local_file_path() { + $this->assertNotEmpty($this->local->get_local_path()); + } + + /** + * @covers \mod_ratingallocate\local\lp\executors\ssh::get_local_path + */ + public function test_ssh_file_path() { + $this->assertNotEmpty($this->ssh->get_local_path()); + } + + /** + * @covers \mod_ratingallocate\local\lp\executors\ssh::get_local_path + */ + public function test_ssh_file() { + $this->assertNotNull($this->ssh->get_local_file()); + } + +} \ No newline at end of file diff --git a/tests/lp_linear_program_test.php b/tests/lp_linear_program_test.php new file mode 100644 index 00000000..1c1063ce --- /dev/null +++ b/tests/lp_linear_program_test.php @@ -0,0 +1,166 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +class lp_linear_program_test extends basic_testcase { + + private $linear_program = null; + + public function setUp() { + $this->linear_program = new mod_ratingallocate\local\lp\linear_program(); + + $this->linear_program->set_objective_method(\mod_ratingallocate\local\lp\linear_program::MAXIMIZE); + $this->linear_program->set_objective_function('3x1+4x2'); + + $this->linear_program->add_constraint('x1 + x2 < 100'); + $this->linear_program->add_bound('x2 < 4'); + $this->linear_program->add_variable('x1'); + $this->linear_program->add_variable('x2'); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_program::set_objective_method + * @covers \mod_ratingallocate\local\lp\linear_program::get_objective_method + */ + public function test_objective_method() { + $this->linear_program->set_objective_method(\mod_ratingallocate\local\lp\linear_program::MINIMIZE); + $this->assertEquals($this->linear_program->get_objective_method(), \mod_ratingallocate\local\lp\linear_program::MINIMIZE); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_program::set_objective_function + * @covers \mod_ratingallocate\local\lp\linear_program::get_objective_function + */ + public function test_objective_function() { + $this->linear_program->set_objective_function('2x+3'); + $this->assertEquals($this->linear_program->get_objective_function(), '2x+3'); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_program::add_constraint + * @covers \mod_ratingallocate\local\lp\linear_program::get_constraints + */ + public function test_constraints() { + $this->linear_program->add_constraint('x1 + x2 < 100'); + $this->assertContains('x1 + x2 < 100', $this->linear_program->get_constraints()); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_program::set_constraints + * @covers \mod_ratingallocate\local\lp\linear_program::get_constraints + */ + public function test_constraints2() { + $this->linear_program->set_constraints(['x1 + x2 < 100']); + $this->assertContains('x1 + x2 < 100', $this->linear_program->get_constraints()); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_program::add_bound + * @covers \mod_ratingallocate\local\lp\linear_program::get_bounds + */ + public function test_bounds() { + $this->linear_program->add_bound('x2 > 25'); + $this->assertContains('x2 > 25', $this->linear_program->get_bounds()); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_program::set_bounds + * @covers \mod_ratingallocate\local\lp\linear_program::get_bounds + */ + public function test_bounds2() { + $this->linear_program->set_bounds(['x2 > 25']); + $this->assertContains('x2 > 25', $this->linear_program->get_bounds()); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_program::add_variable + * @covers \mod_ratingallocate\local\lp\linear_program::get_variable_names + */ + public function test_variables() { + $this->linear_program->add_variable('x2'); + $this->assertContains('x2', $this->linear_program->get_variable_names()); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_program::add_variable + * @expectedException exception + */ + public function test_variable_long_name() { + $this->linear_program->add_variable('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_programm::write_objective_method + */ + public function test_write_objective_method() { + $this->assertEquals('maximize', strtolower($this->linear_program->write_objective_method())); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_programm::write_objective_method + */ + public function test_write_objective_method2() { + $this->linear_program->set_objective_method(\mod_ratingallocate\local\lp\linear_program::MINIMIZE); + $this->assertEquals('minimize', strtolower($this->linear_program->write_objective_method())); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_programm::write_objective_method + * @expectedException exception + */ + public function test_write_objective_method3() { + $this->linear_program->set_objective_method(\mod_ratingallocate\local\lp\linear_program::NONE); + $this->linear_program->write_objective_method(); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_programm::write_objective + * @expectedException exception + */ + public function test_write_objective_function() { + $this->linear_program->set_objective_function(''); + $this->linear_program->write_objective(); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_programm::write_constraints + */ + public function test_write_constraints() { + $this->assertEquals($this->linear_program->write_constraints(), "Subject To\nx1+x2<100\n"); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_programm::write_bounds + */ + public function test_write_bounds() { + $this->assertEquals($this->linear_program->write_bounds(), "Bounds\nx2<4\n"); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_programm::write_variables + */ + public function test_write_variables() { + $this->assertEquals($this->linear_program->write_variables(), "General\nx1 x2 \n"); + } + + /** + * @covers \mod_ratingallocate\local\lp\linear_programm::write + */ + public function test_write() { + $this->assertNotEmpty($this->linear_program->write()); + } +} \ No newline at end of file diff --git a/tests/lp_utility_test.php b/tests/lp_utility_test.php new file mode 100644 index 00000000..7177233e --- /dev/null +++ b/tests/lp_utility_test.php @@ -0,0 +1,67 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +class lp_utility_test extends basic_testcase { + + private $users = null; + private $groups = null; + + protected function setUp() { + $this->users = []; + $this->groups = []; + + for($i = 0; $i < 2; ++$i) + $this->groups[] = new \mod_ratingallocate\local\group($i); + + for($i = 0; $i < 4; ++$i) { + $this->users[] = new \mod_ratingallocate\local\user($i, $this->groups); + $this->users[$i]->set_assigned_group($this->groups[rand() % count($this->groups)]); + } + } + + /** + * @covers \mod_ratingallocate\local\lp\utility::translate_to_name + */ + public function test_to_translation() { + $this->assertEquals('x_2_1', \mod_ratingallocate\local\lp\utility::translate_to_name($this->users[2], $this->groups[1])); + } + + /** + * @covers \mod_ratingallocate\local\lp\utility::translate_from_name + */ + public function test_from_translation() { + $this->assertEquals([$this->users[2], $this->groups[1]], \mod_ratingallocate\local\lp\utility::translate_from_name('x_2_1', $this->users, $this->groups)); + } + + /** + * @covers \mod_ratingallocate\local\lp\utility::assign_groups + */ + public function test_assigning_groups() { + \mod_ratingallocate\local\lp\utility::assign_groups(['x_2_1' => 1], $this->users, $this->groups); + $this->assertSame($this->users[2]->get_assigned_group(), $this->groups[1]); + } + + /** + * @covers \mod_ratingallocate\local\lp\utility::create_linear_program + */ + public function test_linear_program_creation() { + $linear_program = \mod_ratingallocate\local\lp\utility::create_linear_program($this->users, $this->groups, new \mod_ratingallocate\local\lp\weighters\identity_weighter()); + $this->assertNotNull($linear_program); + } + +} \ No newline at end of file diff --git a/tests/lp_weighter_test.php b/tests/lp_weighter_test.php new file mode 100644 index 00000000..40814f3a --- /dev/null +++ b/tests/lp_weighter_test.php @@ -0,0 +1,54 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +class lp_weighter_test extends basic_testcase { + + private $weighter1 = null; + private $weighter2 = null; + + /** + * @covers \mod_ratingallocate\local\lp\weighters\polynomial_weighter::__construct + * @covers \mod_ratingallocate\local\lp\weighters\identity_weighter::__construct + */ + protected function setUp() { + $this->weighter1 = new \mod_ratingallocate\local\lp\weighters\polynomial_weighter([4, 2, 0]); + $this->weighter2 = new \mod_ratingallocate\local\lp\weighters\identity_weighter(); + } + + /** + * @covers \mod_ratingallocate\local\lp\weighters\polynomial_weighter::apply + */ + public function test_polynomial_apply() { + $this->assertEquals($this->weighter1->apply(3), 4*3*3 + 2*3); + } + + /** + * @covers \mod_ratingallocate\local\lp\weighters\polynomial_weighter::apply + */ + public function test_polynomial_to_string() { + $this->assertEquals($this->weighter1->to_string('y'), '2*y+4*y^2'); + } + + /** + * @covers \mod_ratingallocate\local\lp\weighters\identity_weighter::apply + */ + public function test_identity_apply() { + $this->assertEquals($this->weighter2->apply(4), 4); + } + +} \ No newline at end of file diff --git a/tests/mod_ratingallocate_lp_utility_test.php b/tests/mod_ratingallocate_lp_utility_test.php new file mode 100644 index 00000000..3003acf3 --- /dev/null +++ b/tests/mod_ratingallocate_lp_utility_test.php @@ -0,0 +1,67 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +class mod_ratingallocate_lp_utility_test extends basic_testcase { + + private $users = null; + private $groups = null; + + protected function setUp() { + $this->users = []; + $this->groups = []; + + for($i = 0; $i < 2; ++$i) + $this->groups[] = new \mod_ratingallocate\local\group($i); + + for($i = 0; $i < 4; ++$i) { + $this->users[] = new \mod_ratingallocate\local\user($i, $this->groups); + $this->users[$i]->set_assigned_group($this->groups[rand() % count($this->groups)]); + } + } + + /** + * @covers \mod_ratingallocate\local\lp\utility::translate_to_name + */ + public function test_to_translation() { + $this->assertEquals('x_2_1', \mod_ratingallocate\local\lp\utility::translate_to_name($this->users[2], $this->groups[1])); + } + + /** + * @covers \mod_ratingallocate\local\lp\utility::translate_from_name + */ + public function test_from_translation() { + $this->assertEquals([$this->users[2], $this->groups[1]], \mod_ratingallocate\local\lp\utility::translate_from_name('x_2_1', $this->users, $this->groups)); + } + + /** + * @covers \mod_ratingallocate\local\lp\utility::assign_groups + */ + public function test_assigning_groups() { + \mod_ratingallocate\local\lp\utility::assign_groups(['x_2_1' => 1], $this->users, $this->groups); + $this->assertSame($this->users[2]->get_assigned_group(), $this->groups[1]); + } + + /** + * @covers \mod_ratingallocate\local\lp\utility::create_linear_program + */ + public function test_linear_program_creation() { + $linear_program = \mod_ratingallocate\local\lp\utility::create_linear_program($this->users, $this->groups, new \mod_ratingallocate\local\lp\weighters\identity_weighter()); + $this->assertNotNull($linear_program); + } + +} \ No newline at end of file diff --git a/tests/scip.log b/tests/scip.log new file mode 100644 index 00000000..d171a609 --- /dev/null +++ b/tests/scip.log @@ -0,0 +1,221 @@ +SCIP version 3.1.0 [precision: 8 byte] [memory: block] [mode: optimized] [LP solver: SoPlex 2.0.0] [GitHash: 577ee45] +Copyright (c) 2002-2014 Konrad-Zuse-Zentrum fuer Informationstechnik Berlin (ZIB) + +External codes: + Readline 6.3 GNU library for command line editing (gnu.org/s/readline) + SoPlex 2.0.0 Linear Programming Solver developed at Zuse Institute Berlin (soplex.zib.de) [GitHash: 568f354] + cppad-20140000.1 Algorithmic Differentiation of C++ algorithms developed by B. Bell (www.coin-or.org/CppAD) + ZLIB 1.2.8 General purpose compression library by J. Gailly and M. Adler (zlib.net) + GMP 6.1.2 GNU Multiple Precision Arithmetic Library developed by T. Granlund (gmplib.org) + ZIMPL 3.3.2 Zuse Institute Mathematical Programming Language developed by T. Koch (zimpl.zib.de) + +user parameter file not found - using default parameters + +read problem +============ + +original problem has 2 variables (0 bin, 2 int, 0 impl, 0 cont) and 1 constraints + +solve problem +============= + +feasible solution found by trivial heuristic after 0.0 seconds, objective value -2.000000e+00 +presolving: +(round 1) 1 del vars, 1 del conss, 0 add conss, 1 chg bounds, 0 chg sides, 0 chg coeffs, 0 upgd conss, 0 impls, 0 clqs +presolving (2 rounds): + 2 deleted vars, 1 deleted constraints, 0 added constraints, 1 tightened bounds, 0 added holes, 0 changed sides, 0 changed coefficients + 0 implications, 0 cliques +presolved problem has 0 variables (0 bin, 0 int, 0 impl, 0 cont) and 0 constraints +transformed objective value is always integral (scale: 1) +Presolving Time: 0.00 + + time | node | left |LP iter|LP it/n| mem |mdpt |frac |vars |cons |cols |rows |cuts |confs|strbr| dualbound | primalbound | gap +t 0.0s| 1 | 0 | 0 | - | 164k| 0 | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -- | 3.000000e+00 | Inf + 0.0s| 1 | 0 | 0 | - | 164k| 0 | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3.000000e+00 | 3.000000e+00 | 0.00% + +SCIP Status : problem is solved [optimal solution found] +Solving Time (sec) : 0.00 +Solving Nodes : 1 +Primal Bound : +3.00000000000000e+00 (3 solutions) +Dual Bound : +3.00000000000000e+00 +Gap : 0.00 % + +primal solution: +================ + +objective value: 3 +x1 5 (obj:1) +x2 2 (obj:-1) + +Statistics +========== + +SCIP Status : problem is solved [optimal solution found] +Total Time : 0.00 + solving : 0.00 + presolving : 0.00 (included in solving) + reading : 0.00 + copying : 0.00 (0 times copied the problem) +Original Problem : + Problem name : file.lp + Variables : 2 (0 binary, 2 integer, 0 implicit integer, 0 continuous) + Constraints : 1 initial, 1 maximal + Objective sense : maximize +Presolved Problem : + Problem name : t_file.lp + Variables : 0 (0 binary, 0 integer, 0 implicit integer, 0 continuous) + Constraints : 0 initial, 0 maximal +Presolvers : ExecTime SetupTime Calls FixedVars AggrVars ChgTypes ChgBounds AddHoles DelCons AddCons ChgSides ChgCoefs + boundshift : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + components : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + convertinttobin : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + domcol : 0.00 0.00 1 0 0 0 0 0 0 0 0 0 + dualinfer : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + gateextraction : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + implics : 0.00 0.00 2 0 0 0 0 0 0 0 0 0 + inttobinary : 0.00 0.00 1 0 0 0 0 0 0 0 0 0 + trivial : 0.00 0.00 2 0 0 0 0 0 0 0 0 0 + dualfix : 0.00 0.00 2 2 0 0 0 0 0 0 0 0 + genvbounds : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + probing : 0.00 0.00 0 0 0 0 0 0 0 0 0 0 + pseudoobj : 0.00 0.00 2 0 0 0 1 0 0 0 0 0 + linear : 0.00 0.00 0 0 0 0 0 0 1 0 0 0 + root node : - - - 0 - - 0 - - - - - +Constraints : Number MaxNumber #Separate #Propagate #EnfoLP #EnfoPS #Check #ResProp Cutoffs DomReds Cuts Applied Conss Children + integral : 0 0 0 0 0 0 5 0 0 0 0 0 0 0 + countsols : 0 0 0 0 0 0 5 0 0 0 0 0 0 0 +Constraint Timings : TotalTime SetupTime Separate Propagate EnfoLP EnfoPS Check ResProp SB-Prop + integral : 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 + countsols : 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +Propagators : #Propagate #ResProp Cutoffs DomReds + dualfix : 0 0 0 0 + genvbounds : 0 0 0 0 + obbt : 0 0 0 0 + probing : 0 0 0 0 + pseudoobj : 0 0 0 0 + redcost : 0 0 0 0 + rootredcost : 0 0 0 0 + vbounds : 0 0 0 0 +Propagator Timings : TotalTime SetupTime Presolve Propagate ResProp SB-Prop + dualfix : 0.00 0.00 0.00 0.00 0.00 0.00 + genvbounds : 0.00 0.00 0.00 0.00 0.00 0.00 + obbt : 0.00 0.00 0.00 0.00 0.00 0.00 + probing : 0.00 0.00 0.00 0.00 0.00 0.00 + pseudoobj : 0.00 0.00 0.00 0.00 0.00 0.00 + redcost : 0.00 0.00 0.00 0.00 0.00 0.00 + rootredcost : 0.00 0.00 0.00 0.00 0.00 0.00 + vbounds : 0.00 0.00 0.00 0.00 0.00 0.00 +Conflict Analysis : Time Calls Success DomReds Conflicts Literals Reconvs ReconvLits LP Iters + propagation : 0.00 0 0 - 0 0.0 0 0.0 - + infeasible LP : 0.00 0 0 - 0 0.0 0 0.0 0 + bound exceed. LP : 0.00 0 0 - 0 0.0 0 0.0 0 + strong branching : 0.00 0 0 - 0 0.0 0 0.0 0 + pseudo solution : 0.00 1 0 - 0 0.0 0 0.0 - + applied globally : 0.00 - - 0 0 0.0 - - - + applied locally : - - - 0 0 0.0 - - - +Separators : ExecTime SetupTime Calls Cutoffs DomReds Cuts Applied Conss + cut pool : 0.00 0 - - 0 - - (maximal pool size: 0) + cgmip : 0.00 0.00 0 0 0 0 0 0 + clique : 0.00 0.00 0 0 0 0 0 0 + closecuts : 0.00 0.00 0 0 0 0 0 0 + cmir : 0.00 0.00 0 0 0 0 0 0 + flowcover : 0.00 0.00 0 0 0 0 0 0 + gomory : 0.00 0.00 0 0 0 0 0 0 + impliedbounds : 0.00 0.00 0 0 0 0 0 0 + intobj : 0.00 0.00 0 0 0 0 0 0 + mcf : 0.00 0.00 0 0 0 0 0 0 + oddcycle : 0.00 0.00 0 0 0 0 0 0 + rapidlearning : 0.00 0.00 0 0 0 0 0 0 + strongcg : 0.00 0.00 0 0 0 0 0 0 + zerohalf : 0.00 0.00 0 0 0 0 0 0 +Pricers : ExecTime SetupTime Calls Vars + problem variables: 0.00 - 0 0 +Branching Rules : ExecTime SetupTime BranchLP BranchExt BranchPS Cutoffs DomReds Cuts Conss Children + allfullstrong : 0.00 0.00 0 0 0 0 0 0 0 0 + cloud : 0.00 0.00 0 0 0 0 0 0 0 0 + fullstrong : 0.00 0.00 0 0 0 0 0 0 0 0 + inference : 0.00 0.00 0 0 0 0 0 0 0 0 + leastinf : 0.00 0.00 0 0 0 0 0 0 0 0 + mostinf : 0.00 0.00 0 0 0 0 0 0 0 0 + pscost : 0.00 0.00 0 0 0 0 0 0 0 0 + random : 0.00 0.00 0 0 0 0 0 0 0 0 + relpscost : 0.00 0.00 0 0 0 0 0 0 0 0 +Primal Heuristics : ExecTime SetupTime Calls Found + LP solutions : 0.00 - - 0 + pseudo solutions : 0.00 - - 0 + strong branching : 0.00 - - 0 + actconsdiving : 0.00 0.00 0 0 + clique : 0.00 0.00 0 0 + coefdiving : 0.00 0.00 0 0 + crossover : 0.00 0.00 0 0 + dins : 0.00 0.00 0 0 + dualval : 0.00 0.00 0 0 + feaspump : 0.00 0.00 0 0 + fixandinfer : 0.00 0.00 0 0 + fracdiving : 0.00 0.00 0 0 + guideddiving : 0.00 0.00 0 0 + intdiving : 0.00 0.00 0 0 + intshifting : 0.00 0.00 0 0 + linesearchdiving : 0.00 0.00 0 0 + localbranching : 0.00 0.00 0 0 + mutation : 0.00 0.00 0 0 + nlpdiving : 0.00 0.00 0 0 + objpscostdiving : 0.00 0.00 0 0 + octane : 0.00 0.00 0 0 + oneopt : 0.00 0.00 0 0 + proximity : 0.00 0.00 0 0 + pscostdiving : 0.00 0.00 0 0 + randrounding : 0.00 0.00 0 0 + rens : 0.00 0.00 0 0 + rins : 0.00 0.00 0 0 + rootsoldiving : 0.00 0.00 0 0 + rounding : 0.00 0.00 0 0 + shiftandpropagate: 0.00 0.00 0 0 + shifting : 0.00 0.00 0 0 + simplerounding : 0.00 0.00 0 0 + subnlp : 0.00 0.00 0 0 + trivial : 0.00 0.00 2 3 + trysol : 0.00 0.00 0 0 + twoopt : 0.00 0.00 0 0 + undercover : 0.00 0.00 0 0 + vbounds : 0.00 0.00 0 0 + veclendiving : 0.00 0.00 0 0 + zeroobj : 0.00 0.00 0 0 + zirounding : 0.00 0.00 0 0 + other solutions : - - - 0 +LP : Time Calls Iterations Iter/call Iter/sec Time-0-It Calls-0-It + primal LP : 0.00 0 0 0.00 - 0.00 0 + dual LP : 0.00 0 0 0.00 - 0.00 0 + lex dual LP : 0.00 0 0 0.00 - + barrier LP : 0.00 0 0 0.00 - 0.00 0 + diving/probing LP: 0.00 0 0 0.00 - + strong branching : 0.00 0 0 0.00 - + (at root node) : - 0 0 0.00 - + conflict analysis: 0.00 0 0 0.00 - +B&B Tree : + number of runs : 1 + nodes : 1 (0 internal, 1 leaves) + nodes (total) : 1 (0 internal, 1 leaves) + nodes left : 0 + max depth : 0 + max depth (total): 0 + backtracks : 0 (0.0%) + delayed cutoffs : 0 + repropagations : 0 (0 domain reductions, 0 cutoffs) + avg switch length: 2.00 + switching time : 0.00 +Root Node : + First LP value : - + First LP Iters : 0 + First LP Time : 0.00 + Final Dual Bound : +3.00000000000000e+00 + Final Root Iters : 0 +Solution : + Solutions found : 3 (2 improvements) + First Solution : -2.00000000000000e+00 (in run 1, after 0 nodes, 0.00 seconds, depth 0, found by ) + Gap First Sol. : infinite + Gap Last Sol. : infinite + Primal Bound : +3.00000000000000e+00 (in run 1, after 1 nodes, 0.00 seconds, depth 0, found by ) + Dual Bound : +3.00000000000000e+00 + Gap : 0.00 % + Avg. Gap : 0.00 % (0.00 primal-dual integral) diff --git a/tests/user_test.php b/tests/user_test.php new file mode 100644 index 00000000..5e892c69 --- /dev/null +++ b/tests/user_test.php @@ -0,0 +1,165 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +class user_test extends basic_testcase { + + private $user = null; + private $group1= null; + private $group2 = null; + private $group3 = null; + private $group4 = null; + + /** + * @covers \mod_ratingallocate\local\user::__construct + */ + protected function setUp() { + $this->group1 = new \mod_ratingallocate\local\group(1); + $this->group2 = new \mod_ratingallocate\local\group(2); + $this->group3 = new \mod_ratingallocate\local\group(3); + $this->group4 = new \mod_ratingallocate\local\group(4); + $this->user = new \mod_ratingallocate\local\user(1, [$this->group1, $this->group2]); + } + + /** + * @covers \mod_ratingallocate\local\user::set_priority + * @covers \mod_ratingallocate\local\user::get_priority + */ + public function test_valid_priority() { + $this->user->set_priority($this->group1, 10); + $this->assertEquals($this->user->get_priority($this->group1), 10); + } + + /** + * @covers \mod_ratingallocate\local\user::set_priority + * @expectedException exception + */ + public function test_invalid_priority() { + $this->user->set_priority($this->group1, 0); + } + + /** + * @covers \mod_ratingallocate\local\user::set_priority + * @expectedException exception + */ + public function test_invalid_priority2() { + $this->user->set_priority($this->group1, -1); + } + + /** + * @covers \mod_ratingallocate\local\user::set_priority + * @expectedException exception + */ + public function test_invalid_priority3() { + $this->user->set_priority($this->group1, 'Test'); + } + + /** + * @depends test_valid_priority + * @covers \mod_ratingallocate\local\user::add_selected_group + * @covers \mod_ratingallocate\local\user::exists_selected_group + */ + public function test_add_one_selected_group() { + $this->user->add_selected_group($this->group3); + $this->assertTrue($this->user->exists_selected_group($this->group3)); + } + + /** + * @covers \mod_ratingallocate\local\user::add_selected_group + * @expectedException exception + */ + public function test_add_already_selected_group() { + $this->user->add_selected_group($this->group1); + $this->user->add_selected_group($this->group1); + } + + /** + * @depends test_add_one_selected_group + * @covers \mod_ratingallocate\local\user::remove_selected_group + * @covers \mod_ratingallocate\local\user::exists_selected_group + */ + public function test_remove_one_selected_group() { + $this->user->remove_selected_group($this->group1); + $this->assertFalse($this->user->exists_selected_group($this->group1)); + } + + /** + * @depends test_add_one_selected_group + * @depends test_valid_priority + * @covers \mod_ratingallocate\local\user::set_selected_groups + * @covers \mod_ratingallocate\local\user::get_selected_groups + */ + public function test_add_multiple_selected_groups() { + $this->user->set_selected_groups([$this->group3, $this->group4]); + $this->assertContains($this->group4, $this->user->get_selected_groups()); + } + + /** + * @covers \mod_ratingallocate\local\user::__construct + * @covers \mod_ratingallocate\local\user::get_assigned_group + */ + public function test_no_assigned_group_initially() { + $this->assertNull($this->user->get_assigned_group()); + } + + /** + * @covers \mod_ratingallocate\local\user::set_assigned_group + * @covers \mod_ratingallocate\local\user::get_assigned_group + */ + public function test_assign_group() { + $this->user->set_assigned_group($this->group4); + $this->assertSame($this->group4, $this->user->get_assigned_group()); + } + + /** + * @covers \mod_ratingallocate\local\user::set_assigned_group + * @covers \mod_ratingallocate\local\user::get_assigned_group + */ + public function test_assign_two_groups() { + $this->user->set_assigned_group($this->group3); + $this->user->set_assigned_group($this->group4); + $this->assertSame($this->group4, $this->user->get_assigned_group()); + } + + /** + * @covers \mod_ratingallocate\local\user::set_assigned_group + * @covers \mod_ratingallocate\local\user::get_assigned_group + */ + public function test_double_assign() { + $this->user->set_assigned_group($this->group1); + $this->assertSame($this->group1, $this->user->get_assigned_group()); + } + + /** + * @covers \mod_ratingallocate\local\user::set_assigned_groups + * @covers \mod_ratingallocate\local\user::is_choice_satisfied + */ + public function test_choice_satisfaction_for_selected_group() { + $this->user->set_assigned_group($this->group1); + $this->assertTrue($this->user->is_choice_satisfied()); + } + + /** + * @covers \mod_ratingallocate\local\user::set_assigned_groups + * @covers \mod_ratingallocate\local\user::is_choice_satisfied + */ + public function test_choice_satisfaction_for_no_selected_group() { + $this->user->set_assigned_group($this->group3); + $this->assertFalse($this->user->is_choice_satisfied()); + } + +} \ No newline at end of file diff --git a/tests/utility_test.php b/tests/utility_test.php new file mode 100644 index 00000000..8d1546a2 --- /dev/null +++ b/tests/utility_test.php @@ -0,0 +1,123 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +class utility_test extends basic_testcase { + + /** + * @covers \mod_ratingallocate\local\utility::transform_to_groups + */ + public static function test_to_group_transformation() { + $choices = []; + + $choices[1] = new stdClass(); + $choices[1]->maxsize = 2; + $choices[1]->id = 1; + + $choices[2] = new stdClass(); + $choices[2]->maxsize = 2; + $choices[2]->id = 2; + + $groups = \mod_ratingallocate\local\utility::transform_to_groups($choices); + + foreach($groups as $group) + self::assertEquals($choices[$group->get_id()]->maxsize, $group->get_limit()); + } + + /** + * @covers \mod_ratingallocate\local\utility::transform_to_users + */ + public static function test_to_user_transformation() { + $ratings = []; + + $ratings = []; + $ratings[1] = new stdClass(); + $ratings[1]->userid = 1; + $ratings[1]->choiceid = 1; + $ratings[1]->rating = 5; + + $ratings[2] = new stdClass(); + $ratings[2]->userid = 1; + $ratings[2]->choiceid = 2; + $ratings[2]->rating = 5; + + $users = \mod_ratingallocate\local\utility::transform_to_users($ratings); + + foreach($ratings as $rating) + self::assertEquals($rating->userid, $users[$rating->userid]->get_id()); + } + + /** + * @depends test_to_user_transformation + * @depends test_to_group_transformation + * @covers \mod_ratingallocate\local\utility::transform_to_users_and_groups + */ + public static function test_to_user_and_group_transformation() { + $choices = []; + + $choices[1] = new stdClass(); + $choices[1]->maxsize = 2; + $choices[1]->id = 1; + + $choices[2] = new stdClass(); + $choices[2]->maxsize = 2; + $choices[2]->id = 2; + + $ratings = []; + + $ratings[1] = new stdClass(); + $ratings[1]->userid = 2; + $ratings[1]->choiceid = 1; + $ratings[1]->rating = 5; + + $ratings[2] = new stdClass(); + $ratings[2]->userid = 1; + $ratings[2]->choiceid = 2; + $ratings[2]->rating = 5; + + list($users, $groups) = \mod_ratingallocate\local\utility::transform_to_users_and_groups($choices, $ratings); + + foreach($users as $user) + foreach($user->get_selected_groups() as $group) + self::assertEquals($group->get_id(), $ratings[$group->get_id()]->choiceid); + } + + /** + * @covers \mod_ratingallocate\local\utility::transform_from_users_and_groups + */ + public static function test_from_transformation() { + $users = []; + $groups = []; + + for($i = 0; $i < 2; ++$i) + $groups[] = new \mod_ratingallocate\local\group($i); + + for($i = 0; $i < 4; ++$i) { + $users[] = new \mod_ratingallocate\local\user($i, $groups); + $users[$i]->set_assigned_group($groups[rand() % count($groups)]); + } + + $allocations = \mod_ratingallocate\local\utility::transform_from_users_and_groups($users, $groups); + + foreach($allocations as $i_key => $i) { + foreach($i as $k) { + self::assertSame($users[$k]->get_assigned_group(), $groups[$i_key]); + } + } + } + +} \ No newline at end of file diff --git a/webservice/config.php b/webservice/config.php new file mode 100644 index 00000000..93ba3899 --- /dev/null +++ b/webservice/config.php @@ -0,0 +1,5 @@ +main(); \ No newline at end of file