Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Prediction model representation.
19
 *
20
 * @package   core_analytics
21
 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_analytics;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Prediction model representation.
31
 *
32
 * @package   core_analytics
33
 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
34
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class model {
37
 
38
    /**
39
     * All as expected.
40
     */
41
    const OK = 0;
42
 
43
    /**
44
     * There was a problem.
45
     */
46
    const GENERAL_ERROR = 1;
47
 
48
    /**
49
     * No dataset to analyse.
50
     */
51
    const NO_DATASET = 2;
52
 
53
    /**
54
     * Model with low prediction accuracy.
55
     */
56
    const LOW_SCORE = 4;
57
 
58
    /**
59
     * Not enough data to evaluate the model properly.
60
     */
61
    const NOT_ENOUGH_DATA = 8;
62
 
63
    /**
64
     * Invalid analysable for the time splitting method.
65
     */
66
    const ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD = 4;
67
 
68
    /**
69
     * Invalid analysable for all time splitting methods.
70
     */
71
    const ANALYSABLE_STATUS_INVALID_FOR_RANGEPROCESSORS = 8;
72
 
73
    /**
74
     * Invalid analysable for the target
75
     */
76
    const ANALYSABLE_STATUS_INVALID_FOR_TARGET = 16;
77
 
78
    /**
79
     * Minimum score to consider a non-static prediction model as good.
80
     */
81
    const MIN_SCORE = 0.7;
82
 
83
    /**
84
     * Minimum prediction confidence (from 0 to 1) to accept a prediction as reliable enough.
85
     */
86
    const PREDICTION_MIN_SCORE = 0.6;
87
 
88
    /**
89
     * Maximum standard deviation between different evaluation repetitions to consider that evaluation results are stable.
90
     */
91
    const ACCEPTED_DEVIATION = 0.05;
92
 
93
    /**
94
     * Number of evaluation repetitions.
95
     */
96
    const EVALUATION_ITERATIONS = 10;
97
 
98
    /**
99
     * @var \stdClass
100
     */
101
    protected $model = null;
102
 
103
    /**
104
     * @var \core_analytics\local\analyser\base
105
     */
106
    protected $analyser = null;
107
 
108
    /**
109
     * @var \core_analytics\local\target\base
110
     */
111
    protected $target = null;
112
 
113
    /**
114
     * @var \core_analytics\predictor
115
     */
116
    protected $predictionsprocessor = null;
117
 
118
    /**
119
     * @var \core_analytics\local\indicator\base[]
120
     */
121
    protected $indicators = null;
122
 
123
    /**
124
     * @var \context[]
125
     */
126
    protected $contexts = null;
127
 
128
    /**
129
     * Unique Model id created from site info and last model modification.
130
     *
131
     * @var string
132
     */
133
    protected $uniqueid = null;
134
 
135
    /**
136
     * Constructor.
137
     *
138
     * @param int|\stdClass $model
139
     * @return void
140
     */
141
    public function __construct($model) {
142
        global $DB;
143
 
144
        if (is_scalar($model)) {
145
            $model = $DB->get_record('analytics_models', array('id' => $model), '*', MUST_EXIST);
146
            if (!$model) {
147
                throw new \moodle_exception('errorunexistingmodel', 'analytics', '', $model);
148
            }
149
        }
150
        $this->model = $model;
151
    }
152
 
153
    /**
154
     * Quick safety check to discard site models which required components are not available anymore.
155
     *
156
     * @return bool
157
     */
158
    public function is_available() {
159
        $target = $this->get_target();
160
        if (!$target) {
161
            return false;
162
        }
163
 
164
        $classname = $target->get_analyser_class();
165
        if (!class_exists($classname)) {
166
            return false;
167
        }
168
 
169
        return true;
170
    }
171
 
172
    /**
173
     * Returns the model id.
174
     *
175
     * @return int
176
     */
177
    public function get_id() {
178
        return $this->model->id;
179
    }
180
 
181
    /**
182
     * Returns a plain \stdClass with the model data.
183
     *
184
     * @return \stdClass
185
     */
186
    public function get_model_obj() {
187
        return $this->model;
188
    }
189
 
190
    /**
191
     * Returns the model target.
192
     *
193
     * @return \core_analytics\local\target\base
194
     */
195
    public function get_target() {
196
        if ($this->target !== null) {
197
            return $this->target;
198
        }
199
        $instance = \core_analytics\manager::get_target($this->model->target);
200
        $this->target = $instance;
201
 
202
        return $this->target;
203
    }
204
 
205
    /**
206
     * Returns the model indicators.
207
     *
208
     * @return \core_analytics\local\indicator\base[]
209
     */
210
    public function get_indicators() {
211
        if ($this->indicators !== null) {
212
            return $this->indicators;
213
        }
214
 
215
        $fullclassnames = json_decode($this->model->indicators);
216
 
217
        if (!is_array($fullclassnames)) {
218
            throw new \coding_exception('Model ' . $this->model->id . ' indicators can not be read');
219
        }
220
 
221
        $this->indicators = array();
222
        foreach ($fullclassnames as $fullclassname) {
223
            $instance = \core_analytics\manager::get_indicator($fullclassname);
224
            if ($instance) {
225
                $this->indicators[$fullclassname] = $instance;
226
            } else {
227
                debugging('Can\'t load ' . $fullclassname . ' indicator', DEBUG_DEVELOPER);
228
            }
229
        }
230
 
231
        return $this->indicators;
232
    }
233
 
234
    /**
235
     * Returns the list of indicators that could potentially be used by the model target.
236
     *
237
     * It includes the indicators that are part of the model.
238
     *
239
     * @return \core_analytics\local\indicator\base[]
240
     */
241
    public function get_potential_indicators() {
242
 
243
        $indicators = \core_analytics\manager::get_all_indicators();
244
 
245
        if (empty($this->analyser)) {
246
            $this->init_analyser(array('notimesplitting' => true));
247
        }
248
 
249
        foreach ($indicators as $classname => $indicator) {
250
            if ($this->analyser->check_indicator_requirements($indicator) !== true) {
251
                unset($indicators[$classname]);
252
            }
253
        }
254
        return $indicators;
255
    }
256
 
257
    /**
258
     * Returns the model analyser (defined by the model target).
259
     *
260
     * @param array $options Default initialisation with no options.
261
     * @return \core_analytics\local\analyser\base
262
     */
263
    public function get_analyser($options = array()) {
264
        if ($this->analyser !== null) {
265
            return $this->analyser;
266
        }
267
 
268
        $this->init_analyser($options);
269
 
270
        return $this->analyser;
271
    }
272
 
273
    /**
274
     * Initialises the model analyser.
275
     *
276
     * @throws \coding_exception
277
     * @param array $options
278
     * @return void
279
     */
280
    protected function init_analyser($options = array()) {
281
 
282
        $target = $this->get_target();
283
        $indicators = $this->get_indicators();
284
 
285
        if (empty($target)) {
286
            throw new \moodle_exception('errornotarget', 'analytics');
287
        }
288
 
289
        $potentialtimesplittings = $this->get_potential_timesplittings();
290
 
291
        $timesplittings = array();
292
        if (empty($options['notimesplitting'])) {
293
            if (!empty($options['evaluation'])) {
294
                // The evaluation process will run using all available time splitting methods unless one is specified.
295
                if (!empty($options['timesplitting'])) {
296
                    $timesplitting = \core_analytics\manager::get_time_splitting($options['timesplitting']);
297
 
298
                    if (empty($potentialtimesplittings[$timesplitting->get_id()])) {
299
                        throw new \moodle_exception('errorcannotusetimesplitting', 'analytics');
300
                    }
301
                    $timesplittings = array($timesplitting->get_id() => $timesplitting);
302
                } else {
303
                    $timesplittingsforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation();
304
 
305
                    // They both have the same objects, using $potentialtimesplittings as its items are sorted.
306
                    $timesplittings = array_intersect_key($potentialtimesplittings, $timesplittingsforevaluation);
307
                }
308
            } else {
309
 
310
                if (empty($this->model->timesplitting)) {
311
                    throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
312
                }
313
 
314
                // Returned as an array as all actions (evaluation, training and prediction) go through the same process.
315
                $timesplittings = array($this->model->timesplitting => $this->get_time_splitting());
316
            }
317
 
318
            if (empty($timesplittings)) {
319
                throw new \moodle_exception('errornotimesplittings', 'analytics');
320
            }
321
        }
322
 
323
        $classname = $target->get_analyser_class();
324
        if (!class_exists($classname)) {
325
            throw new \coding_exception($classname . ' class does not exists');
326
        }
327
 
328
        // Returns a \core_analytics\local\analyser\base class.
329
        $this->analyser = new $classname($this->model->id, $target, $indicators, $timesplittings, $options);
330
    }
331
 
332
    /**
333
     * Returns the model time splitting method.
334
     *
335
     * @return \core_analytics\local\time_splitting\base|false Returns false if no time splitting.
336
     */
337
    public function get_time_splitting() {
338
        if (empty($this->model->timesplitting)) {
339
            return false;
340
        }
341
        return \core_analytics\manager::get_time_splitting($this->model->timesplitting);
342
    }
343
 
344
    /**
345
     * Returns the time-splitting methods that can be used by this model.
346
     *
347
     * @return \core_analytics\local\time_splitting\base[]
348
     */
349
    public function get_potential_timesplittings() {
350
 
351
        $timesplittings = \core_analytics\manager::get_all_time_splittings();
352
        uasort($timesplittings, function($a, $b) {
353
            return strcasecmp($a->get_name(), $b->get_name());
354
        });
355
 
356
        foreach ($timesplittings as $key => $timesplitting) {
357
            if (!$this->get_target()->can_use_timesplitting($timesplitting)) {
358
                unset($timesplittings[$key]);
359
                continue;
360
            }
361
        }
362
        return $timesplittings;
363
    }
364
 
365
    /**
366
     * Creates a new model. Enables it if $timesplittingid is specified.
367
     *
368
     * @param \core_analytics\local\target\base $target
369
     * @param \core_analytics\local\indicator\base[] $indicators
370
     * @param string|false $timesplittingid The time splitting method id (its fully qualified class name)
371
     * @param string|null $processor The machine learning backend this model will use.
372
     * @return \core_analytics\model
373
     */
374
    public static function create(\core_analytics\local\target\base $target, array $indicators,
375
                                  $timesplittingid = false, $processor = null) {
376
        global $USER, $DB;
377
 
378
        $indicatorclasses = self::indicator_classes($indicators);
379
 
380
        $now = time();
381
 
382
        $modelobj = new \stdClass();
383
        $modelobj->target = $target->get_id();
384
        $modelobj->indicators = json_encode($indicatorclasses);
385
        $modelobj->version = $now;
386
        $modelobj->timecreated = $now;
387
        $modelobj->timemodified = $now;
388
        $modelobj->usermodified = $USER->id;
389
 
390
        if ($target->based_on_assumptions()) {
391
            $modelobj->trained = 1;
392
        }
393
 
394
        if ($timesplittingid) {
395
            if (!\core_analytics\manager::is_valid($timesplittingid, '\core_analytics\local\time_splitting\base')) {
396
                throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
397
            }
398
            if (substr($timesplittingid, 0, 1) !== '\\') {
399
                throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
400
            }
401
            $modelobj->timesplitting = $timesplittingid;
402
        }
403
 
404
        if ($processor &&
405
                !manager::is_valid($processor, '\core_analytics\classifier') &&
406
                !manager::is_valid($processor, '\core_analytics\regressor')) {
407
            throw new \coding_exception('The provided predictions processor \\' . $processor . '\processor is not valid');
408
        } else {
409
            $modelobj->predictionsprocessor = $processor;
410
        }
411
 
412
        $id = $DB->insert_record('analytics_models', $modelobj);
413
 
414
        // Get db defaults.
415
        $modelobj = $DB->get_record('analytics_models', array('id' => $id), '*', MUST_EXIST);
416
 
417
        $model = new static($modelobj);
418
 
419
        return $model;
420
    }
421
 
422
    /**
423
     * Does this model exist?
424
     *
425
     * If no indicators are provided it considers any model with the provided
426
     * target a match.
427
     *
428
     * @param \core_analytics\local\target\base $target
429
     * @param \core_analytics\local\indicator\base[]|false $indicators
430
     * @return bool
431
     */
432
    public static function exists(\core_analytics\local\target\base $target, $indicators = false) {
433
        global $DB;
434
 
435
        $existingmodels = $DB->get_records('analytics_models', array('target' => $target->get_id()));
436
 
437
        if (!$existingmodels) {
438
            return false;
439
        }
440
 
441
        if (!$indicators && $existingmodels) {
442
            return true;
443
        }
444
 
445
        $indicatorids = array_keys($indicators);
446
        sort($indicatorids);
447
 
448
        foreach ($existingmodels as $modelobj) {
449
            $model = new \core_analytics\model($modelobj);
450
            $modelindicatorids = array_keys($model->get_indicators());
451
            sort($modelindicatorids);
452
 
453
            if ($indicatorids === $modelindicatorids) {
454
                return true;
455
            }
456
        }
457
        return false;
458
    }
459
 
460
    /**
461
     * Updates the model.
462
     *
463
     * @param int|bool $enabled
464
     * @param \core_analytics\local\indicator\base[]|false $indicators False to respect current indicators
465
     * @param string|false $timesplittingid False to respect current time splitting method
466
     * @param string|false $predictionsprocessor False to respect current predictors processor value
467
     * @param int[]|false $contextids List of context ids for this model. False to respect the current list of contexts.
468
     * @return void
469
     */
470
    public function update($enabled, $indicators = false, $timesplittingid = '', $predictionsprocessor = false,
471
            $contextids = false) {
472
        global $USER, $DB;
473
 
474
        \core_analytics\manager::check_can_manage_models();
475
 
476
        $now = time();
477
 
478
        if ($indicators !== false) {
479
            $indicatorclasses = self::indicator_classes($indicators);
480
            $indicatorsstr = json_encode($indicatorclasses);
481
        } else {
482
            // Respect current value.
483
            $indicatorsstr = $this->model->indicators;
484
        }
485
 
486
        if ($timesplittingid === false) {
487
            // Respect current value.
488
            $timesplittingid = $this->model->timesplitting;
489
        }
490
 
491
        if ($predictionsprocessor === false) {
492
            // Respect current value.
493
            $predictionsprocessor = $this->model->predictionsprocessor;
494
        }
495
 
496
        if ($contextids === false) {
497
            $contextsstr = $this->model->contextids;
498
        } else if (!$contextids) {
499
            $contextsstr = null;
500
        } else {
501
            $contextsstr = json_encode($contextids);
502
 
503
            // Reset the internal cache.
504
            $this->contexts = null;
505
        }
506
 
507
        if ($this->model->timesplitting !== $timesplittingid ||
508
                $this->model->indicators !== $indicatorsstr ||
509
                $this->model->predictionsprocessor !== $predictionsprocessor) {
510
 
511
            // Delete generated predictions before changing the model version.
512
            $this->clear();
513
 
514
            // It needs to be reset as the version changes.
515
            $this->uniqueid = null;
516
            $this->indicators = null;
517
 
518
            // We update the version of the model so different time splittings are not mixed up.
519
            $this->model->version = $now;
520
 
521
            // Reset trained flag.
522
            if (!$this->is_static()) {
523
                $this->model->trained = 0;
524
            }
525
 
526
        } else if ($this->model->enabled != $enabled) {
527
            // We purge the cached contexts with insights as some will not be visible anymore.
528
            $this->purge_insights_cache();
529
        }
530
 
531
        $this->model->enabled = intval($enabled);
532
        $this->model->indicators = $indicatorsstr;
533
        $this->model->timesplitting = $timesplittingid;
534
        $this->model->predictionsprocessor = $predictionsprocessor;
535
        $this->model->contextids = $contextsstr;
536
        $this->model->timemodified = $now;
537
        $this->model->usermodified = $USER->id;
538
 
539
        $DB->update_record('analytics_models', $this->model);
540
    }
541
 
542
    /**
543
     * Removes the model.
544
     *
545
     * @return void
546
     */
547
    public function delete() {
548
        global $DB;
549
 
550
        \core_analytics\manager::check_can_manage_models();
551
 
552
        $this->clear();
553
 
554
        // Method self::clear is already clearing the current model version.
555
        $predictor = $this->get_predictions_processor(false);
556
        if ($predictor->is_ready() !== true) {
557
            $predictorname = \core_analytics\manager::get_predictions_processor_name($predictor);
558
            debugging('Prediction processor ' . $predictorname . ' is not ready to be used. Model ' .
559
                $this->model->id . ' could not be deleted.');
560
        } else {
561
            $predictor->delete_output_dir($this->get_output_dir(array(), true), $this->get_unique_id());
562
        }
563
 
564
        $DB->delete_records('analytics_models', array('id' => $this->model->id));
565
        $DB->delete_records('analytics_models_log', array('modelid' => $this->model->id));
566
    }
567
 
568
    /**
569
     * Evaluates the model.
570
     *
571
     * This method gets the site contents (through the analyser) creates a .csv dataset
572
     * with them and evaluates the model prediction accuracy multiple times using the
573
     * machine learning backend. It returns an object where the model score is the average
574
     * prediction accuracy of all executed evaluations.
575
     *
576
     * @param array $options
577
     * @return \stdClass[]
578
     */
579
    public function evaluate($options = array()) {
580
 
581
        \core_analytics\manager::check_can_manage_models();
582
 
583
        if ($this->is_static()) {
584
            $this->get_analyser()->add_log(get_string('noevaluationbasedassumptions', 'analytics'));
585
            $result = new \stdClass();
586
            $result->status = self::NO_DATASET;
587
            return array($result);
588
        }
589
 
590
        $options['evaluation'] = true;
591
 
592
        if (empty($options['mode'])) {
593
            $options['mode'] = 'configuration';
594
        }
595
 
596
        switch ($options['mode']) {
597
            case 'trainedmodel':
598
 
599
                // We are only interested on the time splitting method used by the trained model.
600
                $options['timesplitting'] = $this->model->timesplitting;
601
 
602
                // Provide the trained model directory to the ML backend if that is what we want to evaluate.
603
                $trainedmodeldir = $this->get_output_dir(['execution']);
604
                break;
605
            case 'configuration':
606
 
607
                $trainedmodeldir = false;
608
                break;
609
 
610
            default:
611
                throw new \moodle_exception('errorunknownaction', 'analytics');
612
        }
613
 
614
        $this->init_analyser($options);
615
 
616
        if (empty($this->get_indicators())) {
617
            throw new \moodle_exception('errornoindicators', 'analytics');
618
        }
619
 
620
        $this->heavy_duty_mode();
621
 
622
        // Before get_labelled_data call so we get an early exception if it is not ready.
623
        $predictor = $this->get_predictions_processor();
624
 
625
        $datasets = $this->get_analyser()->get_labelled_data($this->get_contexts());
626
 
627
        // No datasets generated.
628
        if (empty($datasets)) {
629
            $result = new \stdClass();
630
            $result->status = self::NO_DATASET;
631
            $result->info = $this->get_analyser()->get_logs();
632
            return array($result);
633
        }
634
 
635
        if (!PHPUNIT_TEST && CLI_SCRIPT) {
636
            echo PHP_EOL . get_string('processingsitecontents', 'analytics') . PHP_EOL;
637
        }
638
 
639
        $results = array();
640
        foreach ($datasets as $timesplittingid => $dataset) {
641
 
642
            $timesplitting = \core_analytics\manager::get_time_splitting($timesplittingid);
643
 
644
            $result = new \stdClass();
645
 
646
            $dashestimesplittingid = str_replace('\\', '', $timesplittingid);
647
            $outputdir = $this->get_output_dir(array('evaluation', $dashestimesplittingid));
648
 
649
            // Evaluate the dataset, the deviation we accept in the results depends on the amount of iterations.
650
            if ($this->get_target()->is_linear()) {
651
                $predictorresult = $predictor->evaluate_regression($this->get_unique_id(), self::ACCEPTED_DEVIATION,
652
                    self::EVALUATION_ITERATIONS, $dataset, $outputdir, $trainedmodeldir);
653
            } else {
654
                $predictorresult = $predictor->evaluate_classification($this->get_unique_id(), self::ACCEPTED_DEVIATION,
655
                    self::EVALUATION_ITERATIONS, $dataset, $outputdir, $trainedmodeldir);
656
            }
657
 
658
            $result->status = $predictorresult->status;
659
            $result->info = $predictorresult->info;
660
 
661
            if (isset($predictorresult->score)) {
662
                $result->score = $predictorresult->score;
663
            } else {
664
                // Prediction processors may return an error, default to 0 score in that case.
665
                $result->score = 0;
666
            }
667
 
668
            $dir = false;
669
            if (!empty($predictorresult->dir)) {
670
                $dir = $predictorresult->dir;
671
            }
672
 
673
            $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info, $options['mode']);
674
 
675
            $results[$timesplitting->get_id()] = $result;
676
        }
677
 
678
        return $results;
679
    }
680
 
681
    /**
682
     * Trains the model using the site contents.
683
     *
684
     * This method prepares a dataset from the site contents (through the analyser)
685
     * and passes it to the machine learning backends. Static models are skipped as
686
     * they do not require training.
687
     *
688
     * @return \stdClass
689
     */
690
    public function train() {
691
 
692
        \core_analytics\manager::check_can_manage_models();
693
 
694
        if ($this->is_static()) {
695
            $this->get_analyser()->add_log(get_string('notrainingbasedassumptions', 'analytics'));
696
            $result = new \stdClass();
697
            $result->status = self::OK;
698
            return $result;
699
        }
700
 
701
        if (!$this->is_enabled() || empty($this->model->timesplitting)) {
702
            throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
703
        }
704
 
705
        if (empty($this->get_indicators())) {
706
            throw new \moodle_exception('errornoindicators', 'analytics');
707
        }
708
 
709
        $this->heavy_duty_mode();
710
 
711
        // Before get_labelled_data call so we get an early exception if it is not writable.
712
        $outputdir = $this->get_output_dir(array('execution'));
713
 
714
        // Before get_labelled_data call so we get an early exception if it is not ready.
715
        $predictor = $this->get_predictions_processor();
716
 
717
        $datasets = $this->get_analyser()->get_labelled_data($this->get_contexts());
718
 
719
        // No training if no files have been provided.
720
        if (empty($datasets) || empty($datasets[$this->model->timesplitting])) {
721
 
722
            $result = new \stdClass();
723
            $result->status = self::NO_DATASET;
724
            $result->info = $this->get_analyser()->get_logs();
725
            return $result;
726
        }
727
        $samplesfile = $datasets[$this->model->timesplitting];
728
 
729
        // Train using the dataset.
730
        if ($this->get_target()->is_linear()) {
731
            $predictorresult = $predictor->train_regression($this->get_unique_id(), $samplesfile, $outputdir);
732
        } else {
733
            $predictorresult = $predictor->train_classification($this->get_unique_id(), $samplesfile, $outputdir);
734
        }
735
 
736
        $result = new \stdClass();
737
        $result->status = $predictorresult->status;
738
        $result->info = $predictorresult->info;
739
 
740
        if ($result->status !== self::OK) {
741
            return $result;
742
        }
743
 
744
        $this->flag_file_as_used($samplesfile, 'trained');
745
 
746
        // Mark the model as trained if it wasn't.
747
        if ($this->model->trained == false) {
748
            $this->mark_as_trained();
749
        }
750
 
751
        return $result;
752
    }
753
 
754
    /**
755
     * Get predictions from the site contents.
756
     *
757
     * It analyses the site contents (through analyser classes) looking for samples
758
     * ready to receive predictions. It generates a dataset with all samples ready to
759
     * get predictions and it passes it to the machine learning backends or to the
760
     * targets based on assumptions to get the predictions.
761
     *
762
     * @return \stdClass
763
     */
764
    public function predict() {
765
        global $DB;
766
 
767
        \core_analytics\manager::check_can_manage_models();
768
 
769
        if (!$this->is_enabled() || empty($this->model->timesplitting)) {
770
            throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
771
        }
772
 
773
        if (empty($this->get_indicators())) {
774
            throw new \moodle_exception('errornoindicators', 'analytics');
775
        }
776
 
777
        $this->heavy_duty_mode();
778
 
779
        // Before get_unlabelled_data call so we get an early exception if it is not writable.
780
        $outputdir = $this->get_output_dir(array('execution'));
781
 
782
        if (!$this->is_static()) {
783
            // Predictions using a machine learning backend.
784
 
785
            // Before get_unlabelled_data call so we get an early exception if it is not ready.
786
            $predictor = $this->get_predictions_processor();
787
 
788
            $samplesdata = $this->get_analyser()->get_unlabelled_data($this->get_contexts());
789
 
790
            // Get the prediction samples file.
791
            if (empty($samplesdata) || empty($samplesdata[$this->model->timesplitting])) {
792
 
793
                $result = new \stdClass();
794
                $result->status = self::NO_DATASET;
795
                $result->info = $this->get_analyser()->get_logs();
796
                return $result;
797
            }
798
            $samplesfile = $samplesdata[$this->model->timesplitting];
799
 
800
            // We need to throw an exception if we are trying to predict stuff that was already predicted.
801
            $params = array('modelid' => $this->model->id, 'action' => 'predicted', 'fileid' => $samplesfile->get_id());
802
            if ($predicted = $DB->get_record('analytics_used_files', $params)) {
803
                throw new \moodle_exception('erroralreadypredict', 'analytics', '', $samplesfile->get_id());
804
            }
805
 
806
            $indicatorcalculations = \core_analytics\dataset_manager::get_structured_data($samplesfile);
807
 
808
            // Estimation and classification processes run on the machine learning backend side.
809
            if ($this->get_target()->is_linear()) {
810
                $predictorresult = $predictor->estimate($this->get_unique_id(), $samplesfile, $outputdir);
811
            } else {
812
                $predictorresult = $predictor->classify($this->get_unique_id(), $samplesfile, $outputdir);
813
            }
814
 
815
            // Prepare the results object.
816
            $result = new \stdClass();
817
            $result->status = $predictorresult->status;
818
            $result->info = $predictorresult->info;
819
            $result->predictions = $this->format_predictor_predictions($predictorresult);
820
 
821
        } else {
822
            // Predictions based on assumptions.
823
 
824
            $indicatorcalculations = $this->get_analyser()->get_static_data($this->get_contexts());
825
            // Get the prediction samples file.
826
            if (empty($indicatorcalculations) || empty($indicatorcalculations[$this->model->timesplitting])) {
827
 
828
                $result = new \stdClass();
829
                $result->status = self::NO_DATASET;
830
                $result->info = $this->get_analyser()->get_logs();
831
                return $result;
832
            }
833
 
834
            // Same as reset($indicatorcalculations) as models based on assumptions only analyse 1 single
835
            // time-splitting method.
836
            $indicatorcalculations = $indicatorcalculations[$this->model->timesplitting];
837
 
838
            // Prepare the results object.
839
            $result = new \stdClass();
840
            $result->status = self::OK;
841
            $result->info = [];
842
            $result->predictions = $this->get_static_predictions($indicatorcalculations);
843
        }
844
 
845
        if ($result->status !== self::OK) {
846
            return $result;
847
        }
848
 
849
        if ($result->predictions) {
850
            list($samplecontexts, $predictionrecords) = $this->execute_prediction_callbacks($result->predictions,
851
                $indicatorcalculations);
852
        }
853
 
854
        if (!empty($samplecontexts) && $this->uses_insights()) {
855
            $this->trigger_insights($samplecontexts, $predictionrecords);
856
        }
857
 
858
        if (!$this->is_static()) {
859
            $this->flag_file_as_used($samplesfile, 'predicted');
860
        }
861
 
862
        return $result;
863
    }
864
 
865
    /**
866
     * Returns the model predictions processor.
867
     *
868
     * @param bool $checkisready
869
     * @return \core_analytics\predictor
870
     */
871
    public function get_predictions_processor($checkisready = true) {
872
        return manager::get_predictions_processor($this->model->predictionsprocessor, $checkisready);
873
    }
874
 
875
    /**
876
     * Formats the predictor results.
877
     *
878
     * @param array $predictorresult
879
     * @return array
880
     */
881
    private function format_predictor_predictions($predictorresult) {
882
 
883
        $predictions = array();
884
        if (!empty($predictorresult->predictions)) {
885
            foreach ($predictorresult->predictions as $sampleinfo) {
886
 
887
                // We parse each prediction.
888
                switch (count($sampleinfo)) {
889
                    case 1:
890
                        // For whatever reason the predictions processor could not process this sample, we
891
                        // skip it and do nothing with it.
892
                        debugging($this->model->id . ' model predictions processor could not process the sample with id ' .
893
                            $sampleinfo[0], DEBUG_DEVELOPER);
894
                        continue 2;
895
                    case 2:
896
                        // Prediction processors that do not return a prediction score will have the maximum prediction
897
                        // score.
898
                        list($uniquesampleid, $prediction) = $sampleinfo;
899
                        $predictionscore = 1;
900
                        break;
901
                    case 3:
902
                        list($uniquesampleid, $prediction, $predictionscore) = $sampleinfo;
903
                        break;
904
                    default:
905
                        break;
906
                }
907
                $predictiondata = (object)['prediction' => $prediction, 'predictionscore' => $predictionscore];
908
                $predictions[$uniquesampleid] = $predictiondata;
909
            }
910
        }
911
        return $predictions;
912
    }
913
 
914
    /**
915
     * Execute the prediction callbacks defined by the target.
916
     *
917
     * @param \stdClass[] $predictions
918
     * @param array $indicatorcalculations
919
     * @return array
920
     */
921
    protected function execute_prediction_callbacks(&$predictions, $indicatorcalculations) {
922
 
923
        // Here we will store all predictions' contexts, this will be used to limit which users will see those predictions.
924
        $samplecontexts = array();
925
        $records = array();
926
 
927
        foreach ($predictions as $uniquesampleid => $prediction) {
928
 
929
            // The unique sample id contains both the sampleid and the rangeindex.
930
            list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
931
            if ($this->get_target()->triggers_callback($prediction->prediction, $prediction->predictionscore)) {
932
 
933
                // Prepare the record to store the predicted values.
934
                list($record, $samplecontext) = $this->prepare_prediction_record($sampleid, $rangeindex, $prediction->prediction,
935
                    $prediction->predictionscore, json_encode($indicatorcalculations[$uniquesampleid]));
936
 
937
                // We will later bulk-insert them all.
938
                $records[$uniquesampleid] = $record;
939
 
940
                // Also store all samples context to later generate insights or whatever action the target wants to perform.
941
                $samplecontexts[$samplecontext->id] = $samplecontext;
942
 
943
                $this->get_target()->prediction_callback($this->model->id, $sampleid, $rangeindex, $samplecontext,
944
                    $prediction->prediction, $prediction->predictionscore);
945
            }
946
        }
947
 
948
        if (!empty($records)) {
949
            $this->save_predictions($records);
950
        }
951
 
952
        return [$samplecontexts, $records];
953
    }
954
 
955
    /**
956
     * Generates insights and updates the cache.
957
     *
958
     * @param \context[] $samplecontexts
959
     * @param  \stdClass[] $predictionrecords
960
     * @return void
961
     */
962
    protected function trigger_insights($samplecontexts, $predictionrecords) {
963
 
964
        // Notify the target that all predictions have been processed.
965
        if ($this->get_analyser()::one_sample_per_analysable()) {
966
 
967
            // We need to do something unusual here. self::save_predictions uses the bulk-insert function (insert_records()) for
968
            // performance reasons and that function does not return us the inserted ids. We need to retrieve them from
969
            // the database, and we need to do it using one single database query (for performance reasons as well).
970
            $predictionrecords = $this->add_prediction_ids($predictionrecords);
971
 
972
            $samplesdata = $this->predictions_sample_data($predictionrecords);
973
            $samplesdata = $this->append_calculations_info($predictionrecords, $samplesdata);
974
 
975
            $predictions = array_map(function($predictionobj) use ($samplesdata) {
976
                $prediction = new \core_analytics\prediction($predictionobj, $samplesdata[$predictionobj->sampleid]);
977
                return $prediction;
978
            }, $predictionrecords);
979
        } else {
980
            $predictions = [];
981
        }
982
 
983
        $this->get_target()->generate_insight_notifications($this->model->id, $samplecontexts, $predictions);
984
 
985
        if ($this->get_target()->link_insights_report()) {
986
 
987
            // Update cache.
988
            foreach ($samplecontexts as $context) {
989
                \core_analytics\manager::cached_models_with_insights($context, $this->get_id());
990
            }
991
        }
992
    }
993
 
994
    /**
995
     * Get predictions from a static model.
996
     *
997
     * @param array $indicatorcalculations
998
     * @return \stdClass[]
999
     */
1000
    protected function get_static_predictions(&$indicatorcalculations) {
1001
 
1002
        $headers = array_shift($indicatorcalculations);
1003
 
1004
        // Get rid of the sampleid header.
1005
        array_shift($headers);
1006
 
1007
        // Group samples by analysable for \core_analytics\local\target::calculate.
1008
        $analysables = array();
1009
        // List all sampleids together.
1010
        $sampleids = array();
1011
 
1012
        foreach ($indicatorcalculations as $uniquesampleid => $indicators) {
1013
 
1014
            // Get rid of the sampleid column.
1015
            unset($indicators[0]);
1016
            $indicators = array_combine($headers, $indicators);
1017
            $indicatorcalculations[$uniquesampleid] = $indicators;
1018
 
1019
            list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
1020
 
1021
            $analysable = $this->get_analyser()->get_sample_analysable($sampleid);
1022
            $analysableclass = get_class($analysable);
1023
            if (empty($analysables[$analysableclass])) {
1024
                $analysables[$analysableclass] = array();
1025
            }
1026
            if (empty($analysables[$analysableclass][$rangeindex])) {
1027
                $analysables[$analysableclass][$rangeindex] = (object)[
1028
                    'analysable' => $analysable,
1029
                    'indicatorsdata' => array(),
1030
                    'sampleids' => array()
1031
                ];
1032
            }
1033
 
1034
            // Using the sampleid as a key so we can easily merge indicators data later.
1035
            $analysables[$analysableclass][$rangeindex]->indicatorsdata[$sampleid] = $indicators;
1036
            // We could use indicatorsdata keys but the amount of redundant data is not that big and leaves code below cleaner.
1037
            $analysables[$analysableclass][$rangeindex]->sampleids[$sampleid] = $sampleid;
1038
 
1039
            // Accumulate sample ids to get all their associated data in 1 single db query (analyser::get_samples).
1040
            $sampleids[$sampleid] = $sampleid;
1041
        }
1042
 
1043
        // Get all samples data.
1044
        list($sampleids, $samplesdata) = $this->get_samples($sampleids);
1045
 
1046
        // Calculate the targets.
1047
        $predictions = array();
1048
        foreach ($analysables as $analysableclass => $rangedata) {
1049
            foreach ($rangedata as $rangeindex => $data) {
1050
 
1051
                // Attach samples data and calculated indicators data.
1052
                $this->get_target()->clear_sample_data();
1053
                $this->get_target()->add_sample_data($samplesdata);
1054
                $this->get_target()->add_sample_data($data->indicatorsdata);
1055
 
1056
                // Append new elements (we can not get duplicates because sample-analysable relation is N-1).
1057
                $timesplitting = $this->get_time_splitting();
1058
                $timesplitting->set_modelid($this->get_id());
1059
                $timesplitting->set_analysable($data->analysable);
1060
                $range = $timesplitting->get_range_by_index($rangeindex);
1061
 
1062
                $this->get_target()->filter_out_invalid_samples($data->sampleids, $data->analysable, false);
1063
                $calculations = $this->get_target()->calculate($data->sampleids, $data->analysable, $range['start'], $range['end']);
1064
 
1065
                // Missing $indicatorcalculations values in $calculations are caused by is_valid_sample. We need to remove
1066
                // these $uniquesampleid from $indicatorcalculations because otherwise they will be stored as calculated
1067
                // by self::save_prediction.
1068
                $indicatorcalculations = array_filter($indicatorcalculations, function($indicators, $uniquesampleid)
1069
                        use ($calculations, $rangeindex) {
1070
                    list($sampleid, $indicatorsrangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
1071
                    if ($rangeindex == $indicatorsrangeindex && !isset($calculations[$sampleid])) {
1072
                        return false;
1073
                    }
1074
                    return true;
1075
                }, ARRAY_FILTER_USE_BOTH);
1076
 
1077
                foreach ($calculations as $sampleid => $value) {
1078
 
1079
                    $uniquesampleid = $this->get_time_splitting()->append_rangeindex($sampleid, $rangeindex);
1080
 
1081
                    // Null means that the target couldn't calculate the sample, we also remove them from $indicatorcalculations.
1082
                    if (is_null($calculations[$sampleid])) {
1083
                        unset($indicatorcalculations[$uniquesampleid]);
1084
                        continue;
1085
                    }
1086
 
1087
                    // Even if static predictions are based on assumptions we flag them as 100% because they are 100%
1088
                    // true according to what the developer defined.
1089
                    $predictions[$uniquesampleid] = (object)['prediction' => $value, 'predictionscore' => 1];
1090
                }
1091
            }
1092
        }
1093
        return $predictions;
1094
    }
1095
 
1096
    /**
1097
     * Stores the prediction in the database.
1098
     *
1099
     * @param int $sampleid
1100
     * @param int $rangeindex
1101
     * @param int $prediction
1102
     * @param float $predictionscore
1103
     * @param string $calculations
1104
     * @return \context
1105
     */
1106
    protected function prepare_prediction_record($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
1107
        $context = $this->get_analyser()->sample_access_context($sampleid);
1108
 
1109
        $record = new \stdClass();
1110
        $record->modelid = $this->model->id;
1111
        $record->contextid = $context->id;
1112
        $record->sampleid = $sampleid;
1113
        $record->rangeindex = $rangeindex;
1114
        $record->prediction = $prediction;
1115
        $record->predictionscore = $predictionscore;
1116
        $record->calculations = $calculations;
1117
        $record->timecreated = time();
1118
 
1119
        $analysable = $this->get_analyser()->get_sample_analysable($sampleid);
1120
        $timesplitting = $this->get_time_splitting();
1121
        $timesplitting->set_modelid($this->get_id());
1122
        $timesplitting->set_analysable($analysable);
1123
        $range = $timesplitting->get_range_by_index($rangeindex);
1124
        if ($range) {
1125
            $record->timestart = $range['start'];
1126
            $record->timeend = $range['end'];
1127
        }
1128
 
1129
        return array($record, $context);
1130
    }
1131
 
1132
    /**
1133
     * Save the prediction objects.
1134
     *
1135
     * @param \stdClass[] $records
1136
     */
1137
    protected function save_predictions($records) {
1138
        global $DB;
1139
        $DB->insert_records('analytics_predictions', $records);
1140
    }
1141
 
1142
    /**
1143
     * Enabled the model using the provided time splitting method.
1144
     *
1145
     * @param string|false $timesplittingid False to respect the current time splitting method.
1146
     * @return void
1147
     */
1148
    public function enable($timesplittingid = false) {
1149
        global $DB, $USER;
1150
 
1151
        $now = time();
1152
 
1153
        if ($timesplittingid && $timesplittingid !== $this->model->timesplitting) {
1154
 
1155
            if (!\core_analytics\manager::is_valid($timesplittingid, '\core_analytics\local\time_splitting\base')) {
1156
                throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
1157
            }
1158
 
1159
            if (substr($timesplittingid, 0, 1) !== '\\') {
1160
                throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
1161
            }
1162
 
1163
            // Delete generated predictions before changing the model version.
1164
            $this->clear();
1165
 
1166
            // It needs to be reset as the version changes.
1167
            $this->uniqueid = null;
1168
 
1169
            $this->model->timesplitting = $timesplittingid;
1170
            $this->model->version = $now;
1171
 
1172
            // Reset trained flag.
1173
            if (!$this->is_static()) {
1174
                $this->model->trained = 0;
1175
            }
1176
        } else if (empty($this->model->timesplitting)) {
1177
            // A valid timesplitting method needs to be supplied before a model can be enabled.
1178
            throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
1179
 
1180
        }
1181
 
1182
        // Purge pages with insights as this may change things.
1183
        if ($this->model->enabled != 1) {
1184
            $this->purge_insights_cache();
1185
        }
1186
 
1187
        $this->model->enabled = 1;
1188
        $this->model->timemodified = $now;
1189
        $this->model->usermodified = $USER->id;
1190
 
1191
        // We don't always update timemodified intentionally as we reserve it for target, indicators or timesplitting updates.
1192
        $DB->update_record('analytics_models', $this->model);
1193
    }
1194
 
1195
    /**
1196
     * Is this a static model (as defined by the target)?.
1197
     *
1198
     * Static models are based on assumptions instead of in machine learning
1199
     * backends results.
1200
     *
1201
     * @return bool
1202
     */
1203
    public function is_static() {
1204
        return (bool)$this->get_target()->based_on_assumptions();
1205
    }
1206
 
1207
    /**
1208
     * Is this model enabled?
1209
     *
1210
     * @return bool
1211
     */
1212
    public function is_enabled() {
1213
        return (bool)$this->model->enabled;
1214
    }
1215
 
1216
    /**
1217
     * Is this model already trained?
1218
     *
1219
     * @return bool
1220
     */
1221
    public function is_trained() {
1222
        // Models which targets are based on assumptions do not need training.
1223
        return (bool)$this->model->trained || $this->is_static();
1224
    }
1225
 
1226
    /**
1227
     * Marks the model as trained
1228
     *
1229
     * @return void
1230
     */
1231
    public function mark_as_trained() {
1232
        global $DB;
1233
 
1234
        \core_analytics\manager::check_can_manage_models();
1235
 
1236
        $this->model->trained = 1;
1237
        $DB->update_record('analytics_models', $this->model);
1238
    }
1239
 
1240
    /**
1241
     * Get the contexts with predictions.
1242
     *
1243
     * @param bool $skiphidden Skip hidden predictions
1244
     * @return \stdClass[]
1245
     */
1246
    public function get_predictions_contexts($skiphidden = true) {
1247
        global $DB, $USER;
1248
 
1249
        $sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
1250
                  JOIN {context} ctx ON ctx.id = ap.contextid
1251
                 WHERE ap.modelid = :modelid";
1252
        $params = array('modelid' => $this->model->id);
1253
 
1254
        if ($skiphidden) {
1255
            $sql .= " AND NOT EXISTS (
1256
              SELECT 1
1257
                FROM {analytics_prediction_actions} apa
1258
               WHERE apa.predictionid = ap.id AND apa.userid = :userid AND
1259
                     (apa.actionname = :fixed OR apa.actionname = :notuseful OR
1260
                     apa.actionname = :useful OR apa.actionname = :notapplicable OR
1261
                     apa.actionname = :incorrectlyflagged)
1262
            )";
1263
            $params['userid'] = $USER->id;
1264
            $params['fixed'] = \core_analytics\prediction::ACTION_FIXED;
1265
            $params['notuseful'] = \core_analytics\prediction::ACTION_NOT_USEFUL;
1266
            $params['useful'] = \core_analytics\prediction::ACTION_USEFUL;
1267
            $params['notapplicable'] = \core_analytics\prediction::ACTION_NOT_APPLICABLE;
1268
            $params['incorrectlyflagged'] = \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED;
1269
        }
1270
 
1271
        return $DB->get_records_sql($sql, $params);
1272
    }
1273
 
1274
    /**
1275
     * Has this model generated predictions?
1276
     *
1277
     * We don't check analytics_predictions table because targets have the ability to
1278
     * ignore some predicted values, if that is the case predictions are not even stored
1279
     * in db.
1280
     *
1281
     * @return bool
1282
     */
1283
    public function any_prediction_obtained() {
1284
        global $DB;
1285
        return $DB->record_exists('analytics_predict_samples',
1286
            array('modelid' => $this->model->id, 'timesplitting' => $this->model->timesplitting));
1287
    }
1288
 
1289
    /**
1290
     * Whether this model generates insights or not (defined by the model's target).
1291
     *
1292
     * @return bool
1293
     */
1294
    public function uses_insights() {
1295
        $target = $this->get_target();
1296
        return $target::uses_insights();
1297
    }
1298
 
1299
    /**
1300
     * Whether predictions exist for this context.
1301
     *
1302
     * @param \context $context
1303
     * @return bool
1304
     */
1305
    public function predictions_exist(\context $context) {
1306
        global $DB;
1307
 
1308
        // Filters out previous predictions keeping only the last time range one.
1309
        $select = "modelid = :modelid AND contextid = :contextid";
1310
        $params = array('modelid' => $this->model->id, 'contextid' => $context->id);
1311
        return $DB->record_exists_select('analytics_predictions', $select, $params);
1312
    }
1313
 
1314
    /**
1315
     * Gets the predictions for this context.
1316
     *
1317
     * @param \context $context
1318
     * @param bool $skiphidden Skip hidden predictions
1319
     * @param int $page The page of results to fetch. False for all results.
1320
     * @param int $perpage The max number of results to fetch. Ignored if $page is false.
1321
     * @return array($total, \core_analytics\prediction[])
1322
     */
1323
    public function get_predictions(\context $context, $skiphidden = true, $page = false, $perpage = 100) {
1324
        global $DB, $USER;
1325
 
1326
        \core_analytics\manager::check_can_list_insights($context);
1327
 
1328
        // Filters out previous predictions keeping only the last time range one.
1329
        $sql = "SELECT ap.*
1330
                  FROM {analytics_predictions} ap
1331
                  JOIN (
1332
                    SELECT sampleid, max(rangeindex) AS rangeindex
1333
                      FROM {analytics_predictions}
1334
                     WHERE modelid = :modelidsubap and contextid = :contextidsubap
1335
                    GROUP BY sampleid
1336
                  ) apsub
1337
                  ON ap.sampleid = apsub.sampleid AND ap.rangeindex = apsub.rangeindex
1338
                WHERE ap.modelid = :modelid and ap.contextid = :contextid";
1339
 
1340
        $params = array('modelid' => $this->model->id, 'contextid' => $context->id,
1341
            'modelidsubap' => $this->model->id, 'contextidsubap' => $context->id);
1342
 
1343
        if ($skiphidden) {
1344
            $sql .= " AND NOT EXISTS (
1345
              SELECT 1
1346
                FROM {analytics_prediction_actions} apa
1347
               WHERE apa.predictionid = ap.id AND apa.userid = :userid AND
1348
                     (apa.actionname = :fixed OR apa.actionname = :notuseful OR
1349
                     apa.actionname = :useful OR apa.actionname = :notapplicable OR
1350
                     apa.actionname = :incorrectlyflagged)
1351
            )";
1352
            $params['userid'] = $USER->id;
1353
            $params['fixed'] = \core_analytics\prediction::ACTION_FIXED;
1354
            $params['notuseful'] = \core_analytics\prediction::ACTION_NOT_USEFUL;
1355
            $params['useful'] = \core_analytics\prediction::ACTION_USEFUL;
1356
            $params['notapplicable'] = \core_analytics\prediction::ACTION_NOT_APPLICABLE;
1357
            $params['incorrectlyflagged'] = \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED;
1358
        }
1359
 
1360
        $sql .= " ORDER BY ap.timecreated DESC";
1361
        if (!$predictions = $DB->get_records_sql($sql, $params)) {
1362
            return array();
1363
        }
1364
 
1365
        // Get predicted samples' ids.
1366
        $sampleids = array_map(function($prediction) {
1367
            return $prediction->sampleid;
1368
        }, $predictions);
1369
 
1370
        list($unused, $samplesdata) = $this->get_samples($sampleids);
1371
 
1372
        $current = 0;
1373
 
1374
        if ($page !== false) {
1375
            $offset = $page * $perpage;
1376
            $limit = $offset + $perpage;
1377
        }
1378
 
1379
        foreach ($predictions as $predictionid => $predictiondata) {
1380
 
1381
            $sampleid = $predictiondata->sampleid;
1382
 
1383
            // Filter out predictions which samples are not available anymore.
1384
            if (empty($samplesdata[$sampleid])) {
1385
                unset($predictions[$predictionid]);
1386
                continue;
1387
            }
1388
 
1389
            // Return paginated dataset - we cannot paginate in the DB because we post filter the list.
1390
            if ($page === false || ($current >= $offset && $current < $limit)) {
1391
                // Replace \stdClass object by \core_analytics\prediction objects.
1392
                $prediction = new \core_analytics\prediction($predictiondata, $samplesdata[$sampleid]);
1393
                $predictions[$predictionid] = $prediction;
1394
            } else {
1395
                unset($predictions[$predictionid]);
1396
            }
1397
 
1398
            $current++;
1399
        }
1400
 
1401
        if (empty($predictions)) {
1402
            return array();
1403
        }
1404
 
1405
        return [$current, $predictions];
1406
    }
1407
 
1408
    /**
1409
     * Returns the actions executed by users on the predictions.
1410
     *
1411
     * @param  \context|null $context
1412
     * @return \moodle_recordset
1413
     */
1414
    public function get_prediction_actions(?\context $context): \moodle_recordset {
1415
        global $DB;
1416
 
1417
        $sql = "SELECT apa.id, apa.predictionid, apa.userid, apa.actionname, apa.timecreated,
1418
                       ap.contextid, ap.sampleid, ap.rangeindex, ap.prediction, ap.predictionscore
1419
                  FROM {analytics_prediction_actions} apa
1420
                  JOIN {analytics_predictions} ap ON ap.id = apa.predictionid
1421
                 WHERE ap.modelid = :modelid";
1422
        $params = ['modelid' => $this->model->id];
1423
 
1424
        if ($context) {
1425
            $sql .= " AND ap.contextid = :contextid";
1426
            $params['contextid'] = $context->id;
1427
        }
1428
 
1429
        return $DB->get_recordset_sql($sql, $params);
1430
    }
1431
 
1432
    /**
1433
     * Returns the sample data of a prediction.
1434
     *
1435
     * @param \stdClass $predictionobj
1436
     * @return array
1437
     */
1438
    public function prediction_sample_data($predictionobj) {
1439
 
1440
        list($unused, $samplesdata) = $this->get_samples(array($predictionobj->sampleid));
1441
 
1442
        if (empty($samplesdata[$predictionobj->sampleid])) {
1443
            throw new \moodle_exception('errorsamplenotavailable', 'analytics');
1444
        }
1445
 
1446
        return $samplesdata[$predictionobj->sampleid];
1447
    }
1448
 
1449
    /**
1450
     * Returns the samples data of the provided predictions.
1451
     *
1452
     * @param \stdClass[] $predictionrecords
1453
     * @return array
1454
     */
1455
    public function predictions_sample_data(array $predictionrecords): array {
1456
 
1457
        $sampleids = [];
1458
        foreach ($predictionrecords as $predictionobj) {
1459
            $sampleids[] = $predictionobj->sampleid;
1460
        }
1461
        list($sampleids, $samplesdata) = $this->get_analyser()->get_samples($sampleids);
1462
 
1463
        return $samplesdata;
1464
    }
1465
 
1466
    /**
1467
     * Appends the calculation info to the samples data.
1468
     *
1469
     * @param   \stdClass[] $predictionrecords
1470
     * @param   array $samplesdata
1471
     * @return  array
1472
     */
1473
    public function append_calculations_info(array $predictionrecords, array $samplesdata): array {
1474
 
1475
        if ($extrainfo = calculation_info::pull_info($predictionrecords)) {
1476
            foreach ($samplesdata as $sampleid => $data) {
1477
                // The extra info come prefixed by extra: so we will not have overwrites here.
1478
                $samplesdata[$sampleid] = $samplesdata[$sampleid] + $extrainfo[$sampleid];
1479
            }
1480
        }
1481
        return $samplesdata;
1482
    }
1483
 
1484
    /**
1485
     * Returns the description of a sample
1486
     *
1487
     * @param \core_analytics\prediction $prediction
1488
     * @return array 2 elements: list(string, \renderable)
1489
     */
1490
    public function prediction_sample_description(\core_analytics\prediction $prediction) {
1491
        return $this->get_analyser()->sample_description($prediction->get_prediction_data()->sampleid,
1492
            $prediction->get_prediction_data()->contextid, $prediction->get_sample_data());
1493
    }
1494
 
1495
    /**
1496
     * Returns the default output directory for prediction processors
1497
     *
1498
     * @return string
1499
     */
1500
    public static function default_output_dir(): string {
1501
        global $CFG;
1502
 
1503
        return $CFG->dataroot . DIRECTORY_SEPARATOR . 'models';
1504
    }
1505
 
1506
    /**
1507
     * Returns the output directory for prediction processors.
1508
     *
1509
     * Directory structure as follows:
1510
     * - Evaluation runs:
1511
     *   models/$model->id/$model->version/evaluation/$model->timesplitting
1512
     * - Training  & prediction runs:
1513
     *   models/$model->id/$model->version/execution
1514
     *
1515
     * @param array $subdirs
1516
     * @param bool $onlymodelid Preference over $subdirs
1517
     * @return string
1518
     */
1519
    public function get_output_dir($subdirs = array(), $onlymodelid = false) {
1520
        $subdirstr = '';
1521
        foreach ($subdirs as $subdir) {
1522
            $subdirstr .= DIRECTORY_SEPARATOR . $subdir;
1523
        }
1524
 
1525
        $outputdir = get_config('analytics', 'modeloutputdir');
1526
        if (empty($outputdir)) {
1527
            // Apply default value.
1528
            $outputdir = self::default_output_dir();
1529
        }
1530
 
1531
        // Append model id.
1532
        $outputdir .= DIRECTORY_SEPARATOR . $this->model->id;
1533
        if (!$onlymodelid) {
1534
            // Append version + subdirs.
1535
            $outputdir .= DIRECTORY_SEPARATOR . $this->model->version . $subdirstr;
1536
        }
1537
 
1538
        make_writable_directory($outputdir);
1539
 
1540
        return $outputdir;
1541
    }
1542
 
1543
    /**
1544
     * Returns a unique id for this model.
1545
     *
1546
     * This id should be unique for this site.
1547
     *
1548
     * @return string
1549
     */
1550
    public function get_unique_id() {
1551
        global $CFG;
1552
 
1553
        if (!is_null($this->uniqueid)) {
1554
            return $this->uniqueid;
1555
        }
1556
 
1557
        // Generate a unique id for this site, this model and this time splitting method, considering the last time
1558
        // that the model target and indicators were updated.
1559
        $ids = array($CFG->wwwroot, $CFG->prefix, $this->model->id, $this->model->version);
1560
        $this->uniqueid = sha1(implode('$$', $ids));
1561
 
1562
        return $this->uniqueid;
1563
    }
1564
 
1565
    /**
1566
     * Exports the model data for displaying it in a template.
1567
     *
1568
     * @param \renderer_base $output The renderer to use for exporting
1569
     * @return \stdClass
1570
     */
1571
    public function export(\renderer_base $output) {
1572
 
1573
        \core_analytics\manager::check_can_manage_models();
1574
 
1575
        $data = clone $this->model;
1576
 
1577
        $data->modelname = format_string($this->get_name());
1578
        $data->name = $this->inplace_editable_name()->export_for_template($output);
1579
        $data->target = $this->get_target()->get_name();
1580
        $data->targetclass = $this->get_target()->get_id();
1581
 
1582
        if ($timesplitting = $this->get_time_splitting()) {
1583
            $data->timesplitting = $timesplitting->get_name();
1584
        }
1585
 
1586
        $data->indicators = array();
1587
        foreach ($this->get_indicators() as $indicator) {
1588
            $data->indicators[] = $indicator->get_name();
1589
        }
1590
        return $data;
1591
    }
1592
 
1593
    /**
1594
     * Exports the model data to a zip file.
1595
     *
1596
     * @param string $zipfilename
1597
     * @param bool $includeweights Include the model weights if available
1598
     * @return string Zip file path
1599
     */
1600
    public function export_model(string $zipfilename, bool $includeweights = true): string {
1601
 
1602
        \core_analytics\manager::check_can_manage_models();
1603
 
1604
        $modelconfig = new model_config($this);
1605
        return $modelconfig->export($zipfilename, $includeweights);
1606
    }
1607
 
1608
    /**
1609
     * Imports the provided model.
1610
     *
1611
     * Note that this method assumes that model_config::check_dependencies has already been called.
1612
     *
1613
     * @throws \moodle_exception
1614
     * @param  string $zipfilepath Zip file path
1615
     * @return \core_analytics\model
1616
     */
1617
    public static function import_model(string $zipfilepath): \core_analytics\model {
1618
 
1619
        \core_analytics\manager::check_can_manage_models();
1620
 
1621
        $modelconfig = new \core_analytics\model_config();
1622
        return $modelconfig->import($zipfilepath);
1623
    }
1624
 
1625
    /**
1626
     * Can this model be exported?
1627
     *
1628
     * @return bool
1629
     */
1630
    public function can_export_configuration(): bool {
1631
 
1632
        if (empty($this->model->timesplitting)) {
1633
            return false;
1634
        }
1635
        if (!$this->get_indicators()) {
1636
            return false;
1637
        }
1638
 
1639
        if ($this->is_static()) {
1640
            return false;
1641
        }
1642
 
1643
        return true;
1644
    }
1645
 
1646
    /**
1647
     * Returns the model logs data.
1648
     *
1649
     * @param int $limitfrom
1650
     * @param int $limitnum
1651
     * @return \stdClass[]
1652
     */
1653
    public function get_logs($limitfrom = 0, $limitnum = 0) {
1654
        global $DB;
1655
 
1656
        \core_analytics\manager::check_can_manage_models();
1657
 
1658
        return $DB->get_records('analytics_models_log', array('modelid' => $this->get_id()), 'timecreated DESC', '*',
1659
            $limitfrom, $limitnum);
1660
    }
1661
 
1662
    /**
1663
     * Merges all training data files into one and returns it.
1664
     *
1665
     * @return \stored_file|false
1666
     */
1667
    public function get_training_data() {
1668
 
1669
        \core_analytics\manager::check_can_manage_models();
1670
 
1671
        $timesplittingid = $this->get_time_splitting()->get_id();
1672
        return \core_analytics\dataset_manager::export_training_data($this->get_id(), $timesplittingid);
1673
    }
1674
 
1675
    /**
1676
     * Has the model been trained using data from this site?
1677
     *
1678
     * This method is useful to determine if a trained model can be evaluated as
1679
     * we can not use the same data for training and for evaluation.
1680
     *
1681
     * @return bool
1682
     */
1683
    public function trained_locally(): bool {
1684
        global $DB;
1685
 
1686
        if (!$this->is_trained() || $this->is_static()) {
1687
            // Early exit.
1688
            return false;
1689
        }
1690
 
1691
        if ($DB->record_exists('analytics_train_samples', ['modelid' => $this->model->id])) {
1692
            return true;
1693
        }
1694
 
1695
        return false;
1696
    }
1697
 
1698
    /**
1699
     * Flag the provided file as used for training or prediction.
1700
     *
1701
     * @param \stored_file $file
1702
     * @param string $action
1703
     * @return void
1704
     */
1705
    protected function flag_file_as_used(\stored_file $file, $action) {
1706
        global $DB;
1707
 
1708
        $usedfile = new \stdClass();
1709
        $usedfile->modelid = $this->model->id;
1710
        $usedfile->fileid = $file->get_id();
1711
        $usedfile->action = $action;
1712
        $usedfile->time = time();
1713
        $DB->insert_record('analytics_used_files', $usedfile);
1714
    }
1715
 
1716
    /**
1717
     * Log the evaluation results in the database.
1718
     *
1719
     * @param string $timesplittingid
1720
     * @param float $score
1721
     * @param string $dir
1722
     * @param array $info
1723
     * @param string $evaluationmode
1724
     * @return int The inserted log id
1725
     */
1726
    protected function log_result($timesplittingid, $score, $dir = false, $info = false, $evaluationmode = 'configuration') {
1727
        global $DB, $USER;
1728
 
1729
        $log = new \stdClass();
1730
        $log->modelid = $this->get_id();
1731
        $log->version = $this->model->version;
1732
        $log->evaluationmode = $evaluationmode;
1733
        $log->target = $this->model->target;
1734
        $log->indicators = $this->model->indicators;
1735
        $log->timesplitting = $timesplittingid;
1736
        $log->dir = $dir;
1737
        if ($info) {
1738
            // Ensure it is not an associative array.
1739
            $log->info = json_encode(array_values($info));
1740
        }
1741
        $log->score = $score;
1742
        $log->timecreated = time();
1743
        $log->usermodified = $USER->id;
1744
 
1745
        return $DB->insert_record('analytics_models_log', $log);
1746
    }
1747
 
1748
    /**
1749
     * Utility method to return indicator class names from a list of indicator objects
1750
     *
1751
     * @param \core_analytics\local\indicator\base[] $indicators
1752
     * @return string[]
1753
     */
1754
    private static function indicator_classes($indicators) {
1755
 
1756
        // What we want to check and store are the indicator classes not the keys.
1757
        $indicatorclasses = array();
1758
        foreach ($indicators as $indicator) {
1759
            if (!\core_analytics\manager::is_valid($indicator, '\core_analytics\local\indicator\base')) {
1760
                if (!is_object($indicator) && !is_scalar($indicator)) {
1761
                    $indicator = strval($indicator);
1762
                } else if (is_object($indicator)) {
1763
                    $indicator = '\\' . get_class($indicator);
1764
                }
1765
                throw new \moodle_exception('errorinvalidindicator', 'analytics', '', $indicator);
1766
            }
1767
            $indicatorclasses[] = $indicator->get_id();
1768
        }
1769
 
1770
        return $indicatorclasses;
1771
    }
1772
 
1773
    /**
1774
     * Clears the model training and prediction data.
1775
     *
1776
     * Executed after updating model critical elements like the time splitting method
1777
     * or the indicators.
1778
     *
1779
     * @return void
1780
     */
1781
    public function clear() {
1782
        global $DB, $USER;
1783
 
1784
        \core_analytics\manager::check_can_manage_models();
1785
 
1786
        // Delete current model version stored stuff.
1787
        $predictor = $this->get_predictions_processor(false);
1788
        if ($predictor->is_ready() !== true) {
1789
            $predictorname = \core_analytics\manager::get_predictions_processor_name($predictor);
1790
            debugging('Prediction processor ' . $predictorname . ' is not ready to be used. Model ' .
1791
                $this->model->id . ' could not be cleared.');
1792
        } else {
1793
            $predictor->clear_model($this->get_unique_id(), $this->get_output_dir());
1794
        }
1795
 
1796
        $DB->delete_records_select('analytics_prediction_actions', "predictionid IN
1797
            (SELECT id FROM {analytics_predictions} WHERE modelid = :modelid)", ['modelid' => $this->get_id()]);
1798
 
1799
        $DB->delete_records('analytics_predictions', array('modelid' => $this->model->id));
1800
        $DB->delete_records('analytics_predict_samples', array('modelid' => $this->model->id));
1801
        $DB->delete_records('analytics_train_samples', array('modelid' => $this->model->id));
1802
        $DB->delete_records('analytics_used_files', array('modelid' => $this->model->id));
1803
        $DB->delete_records('analytics_used_analysables', array('modelid' => $this->model->id));
1804
 
1805
        // Purge all generated files.
1806
        \core_analytics\dataset_manager::clear_model_files($this->model->id);
1807
 
1808
        // We don't expect people to clear models regularly and the cost of filling the cache is
1809
        // 1 db read per context.
1810
        $this->purge_insights_cache();
1811
 
1812
        if (!$this->is_static()) {
1813
            $this->model->trained = 0;
1814
        }
1815
 
1816
        $this->model->timemodified = time();
1817
        $this->model->usermodified = $USER->id;
1818
        $DB->update_record('analytics_models', $this->model);
1819
    }
1820
 
1821
    /**
1822
     * Returns the name of the model.
1823
     *
1824
     * By default, models use their target's name as their own name. They can have their explicit name, too. In which
1825
     * case, the explicit name is used instead of the default one.
1826
     *
1827
     * @return string|lang_string
1828
     */
1829
    public function get_name() {
1830
 
1831
        if (trim($this->model->name ?? '') === '') {
1832
            return $this->get_target()->get_name();
1833
 
1834
        } else {
1835
            return $this->model->name;
1836
        }
1837
    }
1838
 
1839
    /**
1840
     * Renames the model to the given name.
1841
     *
1842
     * When given an empty string, the model falls back to using the associated target's name as its name.
1843
     *
1844
     * @param string $name The new name for the model, empty string for using the default name.
1845
     */
1846
    public function rename(string $name) {
1847
        global $DB, $USER;
1848
 
1849
        $this->model->name = $name;
1850
        $this->model->timemodified = time();
1851
        $this->model->usermodified = $USER->id;
1852
 
1853
        $DB->update_record('analytics_models', $this->model);
1854
    }
1855
 
1856
    /**
1857
     * Returns an inplace editable element with the model's name.
1858
     *
1859
     * @return \core\output\inplace_editable
1860
     */
1861
    public function inplace_editable_name() {
1862
 
1863
        $displayname = format_string($this->get_name());
1864
 
1865
        return new \core\output\inplace_editable('core_analytics', 'modelname', $this->model->id,
1866
            has_capability('moodle/analytics:managemodels', \context_system::instance()), $displayname, $this->model->name);
1867
    }
1868
 
1869
    /**
1870
     * Returns true if the time-splitting method used by this model is invalid for this model.
1871
     * @return  bool
1872
     */
1873
    public function invalid_timesplitting_selected(): bool {
1874
        $currenttimesplitting = $this->model->timesplitting;
1875
        if (empty($currenttimesplitting)) {
1876
            // Not set is different from invalid. This function is used to identify invalid
1877
            // time-splittings.
1878
            return false;
1879
        }
1880
 
1881
        $potentialtimesplittings = $this->get_potential_timesplittings();
1882
        if ($currenttimesplitting && empty($potentialtimesplittings[$currenttimesplitting])) {
1883
            return true;
1884
        }
1885
 
1886
        return false;
1887
    }
1888
 
1889
    /**
1890
     * Adds the id from {analytics_predictions} db table to the prediction \stdClass objects.
1891
     *
1892
     * @param  \stdClass[] $predictionrecords
1893
     * @return \stdClass[] The prediction records including their ids in {analytics_predictions} db table.
1894
     */
1895
    private function add_prediction_ids($predictionrecords) {
1896
        global $DB;
1897
 
1898
        $firstprediction = reset($predictionrecords);
1899
 
1900
        $contextids = array_map(function($predictionobj) {
1901
            return $predictionobj->contextid;
1902
        }, $predictionrecords);
1903
 
1904
        // Limited to 30000 records as a middle point between the ~65000 params limit in pgsql and the size limit for mysql which
1905
        // can be increased if required up to a reasonable point.
1906
        $chunks = array_chunk($contextids, 30000);
1907
        foreach ($chunks as $contextidschunk) {
1908
            list($contextsql, $contextparams) = $DB->get_in_or_equal($contextidschunk, SQL_PARAMS_NAMED);
1909
 
1910
            // We select the fields that will allow us to map ids to $predictionrecords. Given that we already filter by modelid
1911
            // we have enough with sampleid and rangeindex. The reason is that the sampleid relation to a site is N - 1.
1912
            $fields = 'id, sampleid, rangeindex';
1913
 
1914
            // We include the contextid and the timecreated filter to reduce the number of records in $dbpredictions. We can not
1915
            // add as many OR conditions as records in $predictionrecords.
1916
            $sql = "SELECT $fields
1917
                      FROM {analytics_predictions}
1918
                     WHERE modelid = :modelid
1919
                           AND contextid $contextsql
1920
                           AND timecreated >= :firsttimecreated";
1921
            $params = $contextparams + ['modelid' => $this->model->id, 'firsttimecreated' => $firstprediction->timecreated];
1922
            $dbpredictions = $DB->get_recordset_sql($sql, $params);
1923
            foreach ($dbpredictions as $id => $dbprediction) {
1924
                // The append_rangeindex implementation is the same regardless of the time splitting method in use.
1925
                $uniqueid = $this->get_time_splitting()->append_rangeindex($dbprediction->sampleid, $dbprediction->rangeindex);
1926
                $predictionrecords[$uniqueid]->id = $dbprediction->id;
1927
            }
1928
        }
1929
 
1930
        return $predictionrecords;
1931
    }
1932
 
1933
    /**
1934
     * Wrapper around analyser's get_samples to skip DB's max-number-of-params exception.
1935
     *
1936
     * @param  array  $sampleids
1937
     * @return array
1938
     */
1939
    public function get_samples(array $sampleids): array {
1940
 
1941
        if (empty($sampleids)) {
1942
            throw new \coding_exception('No sample ids provided');
1943
        }
1944
 
1945
        $chunksize = count($sampleids);
1946
 
1947
        // We start with just 1 chunk, if it is too large for the db we split the list of sampleids in 2 and we
1948
        // try again. We repeat this process until the chunk is small enough for the db engine to process. The
1949
        // >= has been added in case there are other \dml_read_exceptions unrelated to the max number of params.
1950
        while (empty($done) && $chunksize >= 1) {
1951
 
1952
            $chunks = array_chunk($sampleids, $chunksize);
1953
            $allsampleids = [];
1954
            $allsamplesdata = [];
1955
 
1956
            foreach ($chunks as $index => $chunk) {
1957
 
1958
                try {
1959
                    list($chunksampleids, $chunksamplesdata) = $this->get_analyser()->get_samples($chunk);
1960
                } catch (\dml_read_exception $e) {
1961
 
1962
                    // Reduce the chunksize, we use floor() so the $chunksize is always less than the previous $chunksize value.
1963
                    $chunksize = floor($chunksize / 2);
1964
                    break;
1965
                }
1966
 
1967
                // We can sum as these two arrays are indexed by sampleid and there are no collisions.
1968
                $allsampleids = $allsampleids + $chunksampleids;
1969
                $allsamplesdata = $allsamplesdata + $chunksamplesdata;
1970
 
1971
                if ($index === count($chunks) - 1) {
1972
                    // We successfully processed all the samples in all chunks, we are done.
1973
                    $done = true;
1974
                }
1975
            }
1976
        }
1977
 
1978
        if (empty($done)) {
1979
            if (!empty($e)) {
1980
                // Throw the last exception we caught, the \dml_read_exception we have been catching is unrelated to the max number
1981
                // of param's exception.
1982
                throw new \dml_read_exception($e);
1983
            } else {
1984
                throw new \coding_exception('We should never reach this point, there is a bug in ' .
1985
                    'core_analytics\\model::get_samples\'s code');
1986
            }
1987
        }
1988
        return [$allsampleids, $allsamplesdata];
1989
    }
1990
 
1991
    /**
1992
     * Contexts where this model should be active.
1993
     *
1994
     * @return \context[] Empty array if there are no context restrictions.
1995
     */
1996
    public function get_contexts() {
1997
        if ($this->contexts !== null) {
1998
            return $this->contexts;
1999
        }
2000
 
2001
        if (!$this->model->contextids) {
2002
            $this->contexts = [];
2003
            return $this->contexts;
2004
        }
2005
        $contextids = json_decode($this->model->contextids);
2006
 
2007
        // We don't expect this list to be massive as contexts need to be selected manually using the edit model form.
2008
        $this->contexts = array_map(function($contextid) {
2009
            return \context::instance_by_id($contextid, IGNORE_MISSING);
2010
        }, $contextids);
2011
 
2012
        return $this->contexts;
2013
    }
2014
 
2015
    /**
2016
     * Purges the insights cache.
2017
     */
2018
    private function purge_insights_cache() {
2019
        $cache = \cache::make('core', 'contextwithinsights');
2020
        $cache->purge();
2021
    }
2022
 
2023
    /**
2024
     * Increases system memory and time limits.
2025
     *
2026
     * @return void
2027
     */
2028
    private function heavy_duty_mode() {
2029
        if (ini_get('memory_limit') != -1) {
2030
            raise_memory_limit(MEMORY_HUGE);
2031
        }
2032
        \core_php_time_limit::raise();
2033
    }
2034
}