From ed12ba6ba81b83301f1c7ce6d6a2350e80afaa93 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Wed, 20 Dec 2017 07:18:34 +0100 Subject: [PATCH] MDL-60520 analytics: Per-model ml backend --- admin/settings/analytics.php | 4 +- .../analytics/classes/output/form/edit_model.php | 13 +++++ admin/tool/analytics/model.php | 7 ++- analytics/classes/manager.php | 60 ++++++++++++++++++++-- analytics/classes/model.php | 47 ++++++++++++++--- lang/en/analytics.php | 4 +- lib/classes/plugininfo/mlbackend.php | 3 +- lib/db/install.xml | 5 +- lib/db/upgrade.php | 14 +++++ version.php | 2 +- 10 files changed, 138 insertions(+), 21 deletions(-) diff --git a/admin/settings/analytics.php b/admin/settings/analytics.php index b42a2523e07..d58124c8005 100644 --- a/admin/settings/analytics.php +++ b/admin/settings/analytics.php @@ -37,8 +37,8 @@ if ($hassiteconfig) { $predictors[$fullclassname] = new lang_string('pluginname', $pluginname); } $settings->add(new \core_analytics\admin_setting_predictor('analytics/predictionsprocessor', - new lang_string('predictionsprocessor', 'analytics'), new lang_string('predictionsprocessor_help', 'analytics'), - '\mlbackend_php\processor', $predictors) + new lang_string('defaultpredictionsprocessor', 'analytics'), new lang_string('predictionsprocessor_help', 'analytics'), + \core_analytics\manager::default_mlbackend(), $predictors) ); // Log store. diff --git a/admin/tool/analytics/classes/output/form/edit_model.php b/admin/tool/analytics/classes/output/form/edit_model.php index 66270b25cbe..af384c3791a 100644 --- a/admin/tool/analytics/classes/output/form/edit_model.php +++ b/admin/tool/analytics/classes/output/form/edit_model.php @@ -72,6 +72,19 @@ class edit_model extends \moodleform { $mform->addElement('select', 'timesplitting', get_string('timesplittingmethod', 'analytics'), $timesplittings); $mform->addHelpButton('timesplitting', 'timesplittingmethod', 'analytics'); + $defaultprocessor = \core_analytics\manager::get_predictions_processor_name( + \core_analytics\manager::get_predictions_processor() + ); + $predictionprocessors = ['' => get_string('defaultpredictoroption', 'analytics', $defaultprocessor)]; + foreach ($this->_customdata['predictionprocessors'] as $classname => $predictionsprocessor) { + $optionname = \tool_analytics\output\helper::class_to_option($classname); + $predictionprocessors[$optionname] = \core_analytics\manager::get_predictions_processor_name($predictionsprocessor); + } + + $mform->addElement('select', 'predictionsprocessor', get_string('predictionsprocessor', 'analytics'), + $predictionprocessors); + $mform->addHelpButton('predictionsprocessor', 'predictionsprocessor', 'analytics'); + $mform->addElement('hidden', 'id', $this->_customdata['id']); $mform->setType('id', PARAM_INT); diff --git a/admin/tool/analytics/model.php b/admin/tool/analytics/model.php index 58f11292bd4..b70fc24070a 100644 --- a/admin/tool/analytics/model.php +++ b/admin/tool/analytics/model.php @@ -110,7 +110,8 @@ switch ($action) { 'id' => $model->get_id(), 'model' => $model, 'indicators' => $model->get_potential_indicators(), - 'timesplittings' => \core_analytics\manager::get_enabled_time_splitting_methods() + 'timesplittings' => \core_analytics\manager::get_enabled_time_splitting_methods(), + 'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors() ); $mform = new \tool_analytics\output\form\edit_model(null, $customdata); @@ -126,7 +127,8 @@ switch ($action) { $indicators[] = \core_analytics\manager::get_indicator($indicatorclass); } $timesplitting = \tool_analytics\output\helper::option_to_class($data->timesplitting); - $model->update($data->enabled, $indicators, $timesplitting); + $predictionsprocessor = \tool_analytics\output\helper::option_to_class($data->predictionsprocessor); + $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor); redirect(new \moodle_url('/admin/tool/analytics/index.php')); } @@ -137,6 +139,7 @@ switch ($action) { $callable = array('\tool_analytics\output\helper', 'class_to_option'); $modelobj->indicators = array_map($callable, json_decode($modelobj->indicators)); $modelobj->timesplitting = \tool_analytics\output\helper::class_to_option($modelobj->timesplitting); + $modelobj->predictionsprocessor = \tool_analytics\output\helper::class_to_option($modelobj->predictionsprocessor); $mform->set_data($modelobj); $mform->display(); break; diff --git a/analytics/classes/manager.php b/analytics/classes/manager.php index cdf787e2853..ddc70e0dd06 100644 --- a/analytics/classes/manager.php +++ b/analytics/classes/manager.php @@ -36,6 +36,11 @@ defined('MOODLE_INTERNAL') || die(); class manager { /** + * Default mlbackend + */ + const DEFAULT_MLBACKEND = '\mlbackend_php\processor'; + + /** * @var \core_analytics\predictor[] */ protected static $predictionprocessors = null; @@ -117,9 +122,9 @@ class manager { } /** - * Returns the site selected predictions processor. + * Returns the provided predictions processor class. * - * @param string $predictionclass + * @param false|string $predictionclass Returns the system default processor if false * @param bool $checkisready * @return \core_analytics\predictor */ @@ -128,13 +133,13 @@ class manager { // We want 0 or 1 so we can use it as an array key for caching. $checkisready = intval($checkisready); - if ($predictionclass === false) { + if (!$predictionclass) { $predictionclass = get_config('analytics', 'predictionsprocessor'); } if (empty($predictionclass)) { // Use the default one if nothing set. - $predictionclass = '\mlbackend_php\processor'; + $predictionclass = self::default_mlbackend(); } if (!class_exists($predictionclass)) { @@ -180,6 +185,44 @@ class manager { } /** + * Returns the name of the provided predictions processor. + * + * @param \core_analytics\predictor $predictionsprocessor + * @return string + */ + public static function get_predictions_processor_name(\core_analytics\predictor $predictionsprocessor) { + $component = substr(get_class($predictionsprocessor), 0, strpos(get_class($predictionsprocessor), '\\', 1)); + return get_string('pluginname', $component); + } + + /** + * Whether the provided plugin is used by any model. + * + * @param string $plugin + * @return bool + */ + public static function is_mlbackend_used($plugin) { + $models = self::get_all_models(); + foreach ($models as $model) { + $processor = $model->get_predictions_processor(); + $noprefixnamespace = ltrim(get_class($processor), '\\'); + $processorplugin = substr($noprefixnamespace, 0, strpos($noprefixnamespace, '\\')); + if ($processorplugin == $plugin) { + return true; + } + } + + // Default predictions processor. + $defaultprocessorclass = get_config('analytics', 'predictionsprocessor'); + $pluginclass = '\\' . $plugin . '\\processor'; + if ($pluginclass === $defaultprocessorclass) { + return true; + } + + return false; + } + + /** * Get all available time splitting methods. * * @return \core_analytics\local\time_splitting\base[] @@ -547,6 +590,15 @@ class manager { } /** + * Default system backend. + * + * @return string + */ + public static function default_mlbackend() { + return self::DEFAULT_MLBACKEND; + } + + /** * Returns the provided element classes in the site. * * @param string $element diff --git a/analytics/classes/model.php b/analytics/classes/model.php index 0bee65002ca..26abea13609 100644 --- a/analytics/classes/model.php +++ b/analytics/classes/model.php @@ -111,6 +111,11 @@ class model { protected $target = null; /** + * @var \core_analytics\predictor + */ + protected $predictionsprocessor = null; + + /** * @var \core_analytics\local\indicator\base[] */ protected $indicators = null; @@ -336,7 +341,8 @@ class model { * @param string $timesplittingid The time splitting method id (its fully qualified class name) * @return \core_analytics\model */ - public static function create(\core_analytics\local\target\base $target, array $indicators, $timesplittingid = false) { + public static function create(\core_analytics\local\target\base $target, array $indicators, + $timesplittingid = false, $processor = false) { global $USER, $DB; \core_analytics\manager::check_can_manage_models(); @@ -353,6 +359,14 @@ class model { $modelobj->timemodified = $now; $modelobj->usermodified = $USER->id; + if ($processor && + !self::is_valid($processor, '\core_analytics\classifier') && + !self::is_valid($processor, '\core_analytics\regressor')) { + throw new \coding_exception('The provided predictions processor \\' . $processor . '\processor is not valid'); + } else { + $modelobj->predictionsprocessor = $processor; + } + $id = $DB->insert_record('analytics_models', $modelobj); // Get db defaults. @@ -411,9 +425,10 @@ class model { * @param int|bool $enabled * @param \core_analytics\local\indicator\base[]|false $indicators False to respect current indicators * @param string|false $timesplittingid False to respect current time splitting method + * @param string|false $predictionsprocessor False to respect current predictors processor value * @return void */ - public function update($enabled, $indicators = false, $timesplittingid = '') { + public function update($enabled, $indicators = false, $timesplittingid = '', $predictionsprocessor = false) { global $USER, $DB; \core_analytics\manager::check_can_manage_models(); @@ -433,8 +448,14 @@ class model { $timesplittingid = $this->model->timesplitting; } + if ($predictionsprocessor === false) { + // Respect current value. + $predictionsprocessor = $this->model->predictionsprocessor; + } + if ($this->model->timesplitting !== $timesplittingid || - $this->model->indicators !== $indicatorsstr) { + $this->model->indicators !== $indicatorsstr || + $this->model->predictionsprocessor !== $predictionsprocessor) { // Delete generated predictions before changing the model version. $this->clear(); @@ -458,6 +479,7 @@ class model { $this->model->enabled = intval($enabled); $this->model->indicators = $indicatorsstr; $this->model->timesplitting = $timesplittingid; + $this->model->predictionsprocessor = $predictionsprocessor; $this->model->timemodified = $now; $this->model->usermodified = $USER->id; @@ -477,7 +499,7 @@ class model { $this->clear(); // Method self::clear is already clearing the current model version. - $predictor = \core_analytics\manager::get_predictions_processor(); + $predictor = $this->get_predictions_processor(); $predictor->delete_output_dir($this->get_output_dir(array(), true)); $DB->delete_records('analytics_models', array('id' => $this->model->id)); @@ -516,7 +538,7 @@ class model { $this->heavy_duty_mode(); // Before get_labelled_data call so we get an early exception if it is not ready. - $predictor = \core_analytics\manager::get_predictions_processor(); + $predictor = $this->get_predictions_processor(); $datasets = $this->get_analyser()->get_labelled_data(); @@ -608,7 +630,7 @@ class model { $outputdir = $this->get_output_dir(array('execution')); // Before get_labelled_data call so we get an early exception if it is not ready. - $predictor = \core_analytics\manager::get_predictions_processor(); + $predictor = $this->get_predictions_processor(); $datasets = $this->get_analyser()->get_labelled_data(); @@ -677,7 +699,7 @@ class model { // Before get_unlabelled_data call so we get an early exception if it is not ready. if (!$this->is_static()) { - $predictor = \core_analytics\manager::get_predictions_processor(); + $predictor = $this->get_predictions_processor(); } $samplesdata = $this->get_analyser()->get_unlabelled_data(); @@ -739,6 +761,15 @@ class model { } /** + * Returns the model predictions processor. + * + * @return \core_analytics\predictor + */ + public function get_predictions_processor() { + return manager::get_predictions_processor($this->model->predictionsprocessor); + } + + /** * Formats the predictor results. * * @param array $predictorresult @@ -1457,7 +1488,7 @@ class model { \core_analytics\manager::check_can_manage_models(); // Delete current model version stored stuff. - $predictor = \core_analytics\manager::get_predictions_processor(); + $predictor = $this->get_predictions_processor(); $predictor->clear_model($this->get_unique_id(), $this->get_output_dir()); $predictionids = $DB->get_fieldset_select('analytics_predictions', 'id', 'modelid = :modelid', diff --git a/lang/en/analytics.php b/lang/en/analytics.php index 9d4715e07dd..75e6adfa6c0 100644 --- a/lang/en/analytics.php +++ b/lang/en/analytics.php @@ -31,6 +31,8 @@ $string['analyticslogstore_help'] = 'The log store that will be used by the anal $string['analyticssettings'] = 'Analytics settings'; $string['coursetoolong'] = 'The course is too long'; $string['enabledtimesplittings'] = 'Time splitting methods'; +$string['defaultpredictionsprocessor'] = 'Default predictions processor'; +$string['defaultpredictoroption'] = 'Default processor ({$a})'; $string['disabledmodel'] = 'Disabled model'; $string['erroralreadypredict'] = 'File {$a} has already been used to generate predictions.'; $string['errorcannotreaddataset'] = 'Dataset file {$a} can not be read'; @@ -82,7 +84,7 @@ $string['novalidsamples'] = 'No valid samples available'; $string['onlycli'] = 'Analytics processes execution via command line only'; $string['onlycliinfo'] = 'Analytics processes like evaluating models, training machine learning algorithms or getting predictions can take some time, they will run as cron tasks and they can be forced via command line. Disable this setting if you want your site managers to be able to run these processes manually via web interface'; $string['predictionsprocessor'] = 'Predictions processor'; -$string['predictionsprocessor_help'] = 'A predictions processor is the machine-learning backend that processes the datasets generated by calculating models\' indicators and targets. All trained algorithms and predictions will be deleted if you change to another predictions processor.'; +$string['predictionsprocessor_help'] = 'A predictions processor is the machine-learning backend that processes the datasets generated by calculating models\' indicators and targets. Each model can use a different processor, the one specified here will be the default value.'; $string['privacy:metadata:analytics:indicatorcalc'] = 'Indicator calculations'; $string['privacy:metadata:analytics:indicatorcalc:starttime'] = 'Calculation start time'; $string['privacy:metadata:analytics:indicatorcalc:endtime'] = 'Calculation end time'; diff --git a/lib/classes/plugininfo/mlbackend.php b/lib/classes/plugininfo/mlbackend.php index 1e1d0235fdc..884b0fe1c11 100644 --- a/lib/classes/plugininfo/mlbackend.php +++ b/lib/classes/plugininfo/mlbackend.php @@ -40,7 +40,8 @@ class mlbackend extends base { * @return bool */ public function is_uninstall_allowed() { - return true; + + return !\core_analytics\manager::is_mlbackend_used('mlbackend_' . $this->name); } /** diff --git a/lib/db/install.xml b/lib/db/install.xml index 60aa82ef74b..43188505c91 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3718,6 +3718,7 @@ + @@ -3920,4 +3921,4 @@ - \ No newline at end of file + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 6ec46a8b503..519066694d8 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2565,5 +2565,19 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2018101800.00); } + if ($oldversion < 2018102200.00) { + // Define field predictionsprocessor to be added to analytics_models. + $table = new xmldb_table('analytics_models'); + $field = new xmldb_field('predictionsprocessor', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'timesplitting'); + + // Conditionally launch add field predictionsprocessor. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2018102200.00); + } + return true; } diff --git a/version.php b/version.php index d10fd0eef92..af76cef02d9 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018101900.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018102200.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. -- 2.11.4.GIT