<?php
/**
 * The file of monte carlo algorithm of ZenDAS.
 *
 * @copyright   Copyright 2009-2023 禅道软件（青岛）有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
 * @license     ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
 * @author      qixinzhi <qixinzhi@easycorp.ltd>
 * @package     montecarlo
 * @link        http://www.zentao.net
 */
?>

<?php
require_once dirname(dirname(__DIR__)) . '/zendasmath/distribution/triangular.php';
require_once dirname(dirname(__DIR__)) . '/zendasmath/distribution/normal.php';
require_once dirname(dirname(__DIR__)) . '/zendasmath/basic/describe.php';
require_once dirname(dirname(__DIR__)) . '/zendasmath/basic/combination.php';
require_once __DIR__ . '/hypothesis.class.php';
require_once __DIR__ . '/plan.class.php';
require_once __DIR__ . '/mathcheck.php';

/**
 * 蒙特卡洛算法
 * monteCarlo
 */
class monteCarloCalc
{
    /**
     * 假设变量参数。
     * Hypothesis of variables.
     *
     * @var array
     * @access private
     */
    private $hypothesis;

    /**
     * 假设变量模拟结果。
     *
     * @var array
     * @access private
     */
    private $hypothesisData;

    /**
     * 预测变量公式。
     * Forecast of formula.
     *
     * @var array
     * @access private
     */
    private $forecast;

    /**
     * 决策变量参数。
     *
     * @var array
     * @access private
     */
    private $decision = array();

    /**
     * 方案组合穷举
     *
     * @var array
     * @access private
     */
    private $planList;

    /**
     * 目标参数。
     *
     * @var array
     * @access private
     */
    private $objective = array();

    /**
     * 要求参数。
     *
     * @var array
     * @access private
     */
    private $request = array();

    /**
     * 是否进行目标寻优。
     *
     * @var bool
     * @access private
     */
    private $optQuest;

    /**
     * 运行次数。
     * Run times.
     *
     * @var int
     * @access private
     */
    private $times;

    public $mathCheck;
    public $isError;
    public $errorCode;
    public $errorMsg = '';
    public $satisfyRequest;

    public static $functionList = array('MIN', 'MAX', 'CEIL');

    /**
     * Constructor.
     *   'a' => array('type' => 'dist', 'method' => 'normal(1, 2, 0, 10)'),
     *   'b' => array('type' => 'dist', 'method' => 'triangular(1, 2, 3)'),
     *   'c' => array('type' => 'fixed', 'value' => '5'),
     * )
     * @param  int    $hypothesis
     * @param  int    $forecast eg. 'a+b+max(c ,d)'
     * @param  int    $times
     * @param  int    $optQuest
     * @access public
     * @return viod
     */
    public function __construct($hypothesis, $forecast, $times, $optQuest = false)
    {
        $this->hypothesis = $this->initHypothesis($hypothesis);
        $this->forecast   = $forecast;
        $this->times      = $times;
        $this->optQuest   = $optQuest;

        $this->isError        = false;
        $this->satisfyRequest = true;

        MathCheck::initFuncList();

        /* 检查目标寻优相关参数的合法性，更新目标寻优状态。*/
        if($optQuest !== false)
        {
            $this->optQuest = $this->checkOptQuest($optQuest);

            /* 如果设置了目标寻优参数，那么初始化对应的参数变量。*/
            $this->decision  = isset($optQuest->decision)  ? $optQuest->decision  : array();
            $this->objective = isset($optQuest->objective) ? $optQuest->objective : array();
            $this->request   = isset($optQuest->request)   ? $optQuest->request   : array();
        }

        /* 处理所有的假设变量，按照运行次数生成精确值存入数组。*/
        $this->simulateHypothesis();
        /* 检查预测公式是否存在错误。*/
        $this->checkForecast();
        /* 初始化方案组合。*/
        $this->planList = $this->initPlanList();
        /* 处理目标寻优。*/
        $this->handleObjective();
    }

    /**
     * 描述性参数对象。
     *
     * @access public
     * @return void
     */
    public function describe()
    {
        $describe = new stdclass();
        $describe->hypothesis = $this->hypothesis;
        $describe->planList   = $this->planList;
        $describe->objective  = $this->objective;
        $describe->times      = $this->times;

        $describe->optQuest = $this->optQuest;
        if($describe->optQuest) $describe->decision = $this->decision;

        $planIndex      = $this->optQuest ? $this->objective['bestFit'] : 0;
        $describe->plan = $this->planList[$planIndex];

        $describe->request = ($this->optQuest && !empty($this->request));
        if($describe->request)
        {
            $describe->satisfyRequest = $this->satisfyRequest;
            $describe->requestResult = $this->planList[$planIndex]->requestResult;
        }

        return $describe;
    }

    /**
     * 敏感度分析计算.
     *
     * @param  int    $forecastValues
     * @access public
     * @return void
     */
    public function sensitivity($forecastValues)
    {
        $forecast = MathCheck::replaceFormula($this->forecast);
        $r = array();
        foreach($this->hypothesis as $hypoVar => $hypothesis)
        {
            if(strpos($forecast, $hypoVar) === false || $hypothesis->type != 'dist') continue;
            $r[$hypoVar] = $this->correlationR($hypothesis->data, $forecastValues);
        }

        asort($r);

        $result = array();
        $rsq    = array();
        foreach($r as $var => $value)
        {
            $rsq[$var] = $value ** 2;
            $result[$var] = array('r' => round($value, 2));
        }

        $rsqSum = array_sum($rsq);

        foreach($rsq as $var => $value)
        {
            $sign = $r[$var] > 0 ? 1 : -1;

            $result[$var]['rsq']     = round($value, 2);
            $result[$var]['percent'] = $sign * round(($value / $rsqSum) * 100, 2);
        }

        return $result;
    }

    /**
     * 计算相关性系数 r
     *
     * @param  int    $a
     * @param  int    $b
     * @access private
     * @return void
     */
    private function correlationR($a, $b)
    {
        $σa = Describe::standard($a, false);
        $σb = Describe::standard($b, false);

        $meanA   = Describe::mean($a, false);
        $meanB   = Describe::mean($b, false);

        $sum = 0;
        foreach($a as $index => $aValue)
        {
            $bValue = $b[$index];
            $sum += ($aValue - $meanA) * ($bValue - $meanB);
        }
        $r = $sum / (($this->times - 1) * $σa * $σb);

        return $r;
    }

    /**
     * 检查预测公式。
     *
     * @access private
     * @return void
     */
    private function checkForecast()
    {
        if($this->isError) return;

        @trigger_error('init', E_USER_NOTICE);
        $errorLevel = error_reporting(0);
        try
        {
            $forecast = MathCheck::replaceFormula($this->forecast);

            /* 替换假设变量为具体数值。*/
            foreach($this->hypothesis as $hypoVar => $hypothesis)
            {
                $value = $hypothesis->type == 'dist' ? current($hypothesis->data) : $hypothesis->value;
                $forecast = str_replace($hypoVar, $value, $forecast);
            }
            /* 替换决策变量为具体数值。*/
            foreach($this->decision as $decisionVar => $_)
            {
                $forecast = str_replace($decisionVar, '1', $forecast);
            }
            $forecastValue = eval("return {$forecast};");

            if(MathCheck::$hasError)
            {
                $this->isError = true;
                $this->errorCode = '10001';
                return;
            }

            $this->isError = !is_numeric($forecastValue);
        }
        catch(ParseError $e)
        {
            $this->isError = true;
            $this->errorCode = '10002';
        }
        catch(Error $e)
        {
            $this->isError = true;
            $this->errorCode = '10002';
        }
        error_reporting($errorLevel);
        $error = error_get_last();
    }

    /**
     * 检查目标寻优相关参数的合法性，更新目标寻优状态。
     * 进行目标寻优计算需要满足以下条件
     * 1.设置了决策变量
     * 2.设置了目标
     * 3.预测变量公式中包含至少一个决策变量
     *
     * @access private
     * @return bool
     */
    private function checkOptQuest($optQuest)
    {
        if(!isset($optQuest->decision) or empty($optQuest->decision)) return false;
        if(!isset($optQuest->objective) or empty($optQuest->objective)) return false;

        foreach(array_keys($optQuest->decision) as $decision)
        {
            if(strpos($this->forecast, $decision) !== false) return true;
        }

        return false;
    }

    /**
     * 穷举所有方案组合。
     *
     * @access private
     * @return viod
     */
    private function initPlanList()
    {
        if($this->isError) return array();

        $forecasts = array();
        $forecasts['export'] = $this->forecast;
        if($this->optQuest === false) return array(new Plan('default', $forecasts, $this->hypothesis, $this->times, false));

        $decision = unserialize(serialize($this->decision));

        $decisionVars = array_keys($decision);
        $currentVar   = array_shift($decisionVars);

        $planList = array_shift($decision);
        $planList = array_map(function ($value) use ($currentVar) {
            return array($currentVar => $value);
        }, $planList);

        while(!empty($decision))
        {
            $newPlanList = array();
            $currentDecision = array_shift($decision);
            $currentVar      = array_shift($decisionVars);

            foreach($planList as $plan)
            {
                foreach($currentDecision as $variable => $value)
                {
                    $plan[$currentVar] = $value;
                    $newPlanList[] = $plan;
                }
            }

            $planList = $newPlanList;
        }

        foreach($this->request as $y => $request)
        {
            $forecasts[$y] = $request['formula'];
        }
        $planList = array_map(function ($value) use ($forecasts) {
            return new Plan($value, $forecasts, $this->hypothesis, $this->times);
        }, $planList);

        return $planList;
    }

    /**
     * 预处理假设变量，如果是分布变量，初始化为分布函数对象。
     *
     * @param  array   $hypothesis
     * @access private
     * @return array
     */
    private function initHypothesis($hypothesis)
    {
        $readyHypothesisList = array();
        foreach($hypothesis as $hypo => $config)
        {
            $type = $config['type'];
            $method = $type == HypothesisVariable::$distributionType ? $config['method'] : null;
            $value  = $type == HypothesisVariable::$fixedType ? $config['value'] : null;

            $readyHypothesisList[$hypo] = new HypothesisVariable($type, $method, $value);
        }

        return $readyHypothesisList;
    }

    /**
     * 模拟假设变量为具体值。
     *
     * @access private
     * @return viod
     */
    private function simulateHypothesis()
    {
        foreach($this->hypothesis as $hypothesis)
        {
            if($hypothesis->type == 'dist')
            {
                $hypothesis->data = array();
                for($index = 0; $index < $this->times; $index += 1)
                {
                    $hypothesis->data[] = $hypothesis->distribution->rand();
                }
            }
        }
    }

    /**
     * 处理目标寻优。
     *
     * @access private
     * @return void
     */
    private function handleObjective()
    {
        if($this->isError or !$this->optQuest) return;

        $target        = $this->objective['target'];
        $statisticType = $this->objective['stat'];

        /* 计算所有方案的目标值 */
        foreach($this->planList as $index => $plan)
        {
            $plan->$statisticType(false);
        }

        /* 首先根据要求过滤。*/
        $filterPlans = $this->handleRequest();

        $bestFit = null;
        foreach($filterPlans as $index => $plan)
        {
            if($bestFit === null)
            {
                $bestFit = $index;
                continue;
            }

            $bestFitPlan = $this->planList[$bestFit];

            if($target == 'min') $bestFit = $plan->$statisticType(false) > $bestFitPlan->$statisticType(false) ? $bestFit : $index;
            if($target == 'max') $bestFit = $plan->$statisticType(false) < $bestFitPlan->$statisticType(false) ? $bestFit : $index;
        }

        $this->objective['bestFit'] = $bestFit;
    }

    /**
     * 处理要求相关的参数。
     *
     * @access private
     * @return void
     */
    private function handleRequest()
    {
        if(empty($this->request)) return $this->planList;

        $filterPlans = array();
        $judges      = array();

        /* 首先，将所有要求的条件参数解析成统一的格式，并拍平为一维数组。*/
        foreach($this->request as $key => $conditions)
        {
            foreach($conditions['params'] as $condition)
            {
                list($stat, $sign, $rightValue) = $this->parseCondition($condition);

                $judges[] = array($key, $stat, $sign, $rightValue);
            }
        }

        /* 遍历所有的方案，计算条件是否符合，并将条件判断结果反写到方案对象中。*/
        foreach($this->planList as $index => $plan)
        {
            $boolFlag      = true;
            $requestResult = array();
            foreach($judges as $judge)
            {
                list($key, $stat, $sign, $rightValue) = $judge;
                $newSign = $sign;
                if($newSign == '=') $newSign = '==';

                $leftValue       = $plan->$stat(true, true, $key);
                $result          = eval("return $leftValue $newSign $rightValue;");
                $boolFlag        = ($boolFlag && $result);
                $requestResult[] = array($key, $stat, $sign, $rightValue, $result, $leftValue);
            }
            $plan->requestResult    = $requestResult;
            $this->planList[$index] = $plan;

            if($boolFlag) $filterPlans[$index] = $plan;
        }

        /* 如果没有任何方案符合要求，那么直接返回所有的方案并更新要求满足状态。*/
        if(empty($filterPlans))
        {
            $filterPlans = $this->planList;
            $this->satisfyRequest = false;
        }

        return $filterPlans;
    }

    /**
     * 解析条件参数。
     *
     * @param  int    $condition
     * @access private
     * @return void
     */
    private function parseCondition($condition)
    {
        $conditionList = array();
        $conditionList['gt']  = '>';
        $conditionList['lt']  = '<';
        $conditionList['gte'] = '>=';
        $conditionList['lte'] = '<=';
        $conditionList['eq']  = '=';
        $conditionList['ne']  = '!=';

        $stat       = $condition['stat'];
        $sign       = $conditionList[$condition['condition']];
        $rightValue = $condition['value'];

        return array($stat, $sign, $rightValue);
    }
}


