Source code for skcriteria.madm.simus

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2016-2017, Cabral, Juan; Luczywo, Nadia
# All rights reserved.

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:

# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.

# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.

# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.


# =============================================================================
# DOCS
# =============================================================================

"""SIMUS (Sequential Interactive Model for Urban Systems) Method"""

__all__ = [
    "SIMUS"]


# =============================================================================
# IMPORTS
# =============================================================================

import operator

import numpy as np

import joblib

from .. import norm, rank
from ..validate import MAX, MIN
from ..utils import lp
from ..utils.doc_inherit import doc_inherit

from ._dmaker import DecisionMaker


# =============================================================================
# FUNCTIONS
# =============================================================================

# ==============
# STAGES
# ==============

def _make_and_run_stage(mtx, b, senses, zindex, solver):
    # retrieve the problem class
    problem = lp.Minimize if senses[zindex] == MIN else lp.Maximize

    # create the variables
    xs = tuple(
        lp.Float("x{}".format(idx), low=0) for idx in range(mtx.shape[1]))

    # create the objective function
    z_coef = mtx[zindex]
    z = sum(c * x for c, x in zip(z_coef, xs))

    # the conditions
    conditions = []
    for idx in range(mtx.shape[0]):
        if idx == zindex:
            continue
        coef = mtx[idx]

        left = sum(c * x for c, x in zip(coef, xs))
        op = operator.le if senses[idx] == MAX else operator.ge
        right = b[idx]

        condition = op(left, right)
        conditions.append(condition)
    stage = problem(z=z, solver=solver).sa(*conditions)
    return stage, stage.solve()


def solve_stages(t_nmtx, b, ncriteria, solver, jobs):
    stages_results = jobs(
        joblib.delayed(_make_and_run_stage)(
            mtx=t_nmtx, b=b, senses=ncriteria, zindex=idx, solver=solver)
        for idx in range(t_nmtx.shape[0]))

    stages, results = [], []
    for s, r in stages_results:
        stages.append(s)
        results.append(r)

    # create the results mtx
    arr_result = np.vstack([np.asarray(r.values) for r in results])
    with np.errstate(invalid='ignore'):
        norm_result = norm.sum(arr_result, axis=1)
    norm_result[np.isnan(norm_result)] = 0

    return stages, norm_result


# ==============
# FIRST METHOD
# ==============

def first_method(stage_results):
    # project sum value
    sp = np.sum(stage_results, axis=0)

    # times that $v_{ij} > 0$ ($q$)
    q = np.sum(stage_results > 0, axis=0).astype(float)

    # participation factor
    fp = q / len(stage_results)

    # first method points
    vp = sp * fp

    return vp


# ==============
# SECOND METHOD
# ==============

def _dom_by_crit(crit):
    shape = len(crit), 1
    crit_B = np.tile(crit, shape)
    crit_A = crit_B.T
    dom = crit_A - crit_B
    dom[dom < 0] = 0
    return dom


def second_method(stage_results, jobs):
    # dominances by criteria
    dom_by_crit = jobs(
        joblib.delayed(_dom_by_crit)(crit)
        for crit in stage_results)

    # dominance
    doms = np.sum(dom_by_crit, axis=0)

    # domination
    tita_j_p = np.sum(doms, axis=1)

    # subordination
    tita_j_d = np.sum(doms, axis=0)

    # second method points
    points = tita_j_p - tita_j_d

    return points, tita_j_p, tita_j_d, doms, tuple(dom_by_crit)


# ===============
# SIMUS FUNCTION
# ===============

def simus(
    nmtx, ncriteria, nweights,
    rank_by=1, b=None, solver="pulp", njobs=None
):
    # determine the njobs
    njobs = njobs or joblib.cpu_count()

    t_nmtx = nmtx.T

    # check the b array and complete the missing values
    b = np.asarray(b)
    if None in b:
        mins = np.min(t_nmtx, axis=1)
        maxs = np.max(t_nmtx, axis=1)

        auto_b = np.where(ncriteria == MAX, maxs, mins)
        b = np.where(b.astype(bool), b, auto_b)

    # multiprocessing environment
    with joblib.Parallel(n_jobs=njobs) as jobs:

        # create and execute the stages
        stages, stage_results = solve_stages(
            t_nmtx=t_nmtx, b=b, ncriteria=ncriteria,
            solver=solver, jobs=jobs)

        # first methods points
        points1 = first_method(stage_results)
        points2, tita_j_p, tita_j_d, doms, dom_by_crit = second_method(
            stage_results, jobs)

    points = [points1, points2][rank_by - 1]
    ranking = rank.rankdata(points, reverse=True)

    return (
        ranking, stages, stage_results, points1,
        points2, tita_j_p, tita_j_d, doms, dom_by_crit)


# =============================================================================
# OO
# =============================================================================

[docs]class SIMUS(DecisionMaker): r"""SIMUS (Sequential Interactive Model for Urban Systems) developed by Nolberto Munier (2011) is a tool to aid decision-making problems with multiple objectives. The method solves successive scenarios formulated as linear programs. For each scenario, the decision-maker must choose the criterion to be considered objective while the remaining restrictions constitute the constrains system that the projects are subject to. In each case, if there is a feasible solution that is optimum, it is recorded in a matrix of efficient results. Then, from this matrix two rankings allow the decision maker to compare results obtained by different procedures. The first ranking is obtained through a linear weighting of each column by a factor - equivalent of establishing a weight - and that measures the participation of the corresponding project. In the second ranking, the method uses dominance and subordinate relationships between projects, concepts from the French school of MCDM. Parameters ---------- mnorm : string, callable, optional (default="none") Normalization method for the alternative matrix. wnorm : string, callable, optional (default="none") Normalization method for the weights array. rank_by : 1 or 2 (default=1) Wich of the two methods are used to calculate the ranking. The two methods are executed always. solver : str, default="pulp" Which solver to use to solve the undelying linear programs. The full list are available in `skcriteria.utils.lp.SOLVERS` njobs : int, default=None How many cores to use to solve the linear programs and the second method. By default all the availables cores are used. Returns ------- Decision : :py:class:`skcriteria.madm.Decision` With values: - **kernel_**: None - **rank_**: A ranking (start at 1) where the i-nth element represent the position of the i-nth alternative. - **best_alternative_**: The index of the best alternative. - **alpha_solution_**: True - **beta_solution_**: False - **gamma_solution_**: True - **e_**: Particular data created by this method. - **rank_by**: 1 or 2. Wich of the two methods are used to calculate the ranking. Esentialy if the rank is calculated with ``e_.points1`` or ``e_points2`` - **solver**: With solver was used for the underlying linear problems. - **stages**: The underlying linear problems. - **stage_results**: The values of the variables of the linear problems as a n-dimensional array. When th `n-th` row represent the result values of the variables for the `n-th` stage. - **points1**: The points of every alternative obtained by the first method. - **points2**: The points of every alternative obtained by the first method. - **tita_j_p**: 2nd. method domination. - **tita_j_d**: 2nd. method subordination. - **doms**: Total dominance matrix of the 2nd. method. - **dom_by_crit**: Dominance by criteria of the 2nd method. References ---------- .. [1] Munier, N. (2011). A strategy for using multicriteria analysis in decision-making: a guide for simple and complex environmental projects. Springer Science & Business Media. .. [2] Munier, N., Carignano, C., & Alberto, C. UN MÉTODO DE PROGRAMACIÓN MULTIOBJETIVO. Revista de la Escuela de Perfeccionamiento en Investigación Operativa, 24(39). """ def __init__(self, mnorm="none", wnorm="none", rank_by=1, solver="pulp", njobs=None): super(SIMUS, self).__init__(mnorm=mnorm, wnorm=wnorm) self._solver = solver self._njobs = njobs self._rank_by = rank_by
[docs] @doc_inherit def solve(self, ndata, b): nmtx, ncriteria, nweights = ndata.mtx, ndata.criteria, ndata.weights data = simus( nmtx, ncriteria, nweights, rank_by=self._rank_by, b=b, solver=self._solver, njobs=self._njobs) (ranking, stages, stage_results, points1, points2, tita_j_p, tita_j_d, doms, dom_by_crit) = data ranking = data[0] extra = { "rank_by": self._rank_by, "solver": self._solver, "stages": data[1], "stage_results": data[2], "points1": data[3], "points2": data[4], "tita_j_p": data[5], "tita_j_d": data[6], "doms": data[7], "dom_by_crit": data[8]} return None, ranking, extra
@property def solver(self): """Which solver to use to solve the undelying linear programs. The full list are available in `skcriteria.utils.lp.SOLVERS` """ return self._solver @property def njobs(self): """How many cores to use to solve the linear programs and the second method. By default all the availables cores are used. """ return self._njobs
# ============================================================================= # MAIN # ============================================================================= if __name__ == "__main__": print(__doc__)