class MultiplierModelBase(ModelBase):
    ''' This class is a base class for different multiplier models.
        It implements general structure of all multiplier models.
        Concrete variations of multiplier models are passed in the
        class constructor.

        Attributes:
            tolerance (double): lower bound of the variables of multilier
                model.
            _concrete_model: concrete implementation of the multiplier
                model.
            _input_variables (dict of str to pulp.LpVariable): dictionary of
                pulp variables than maps variable names to pulp variables
                corresponding to input categories.
            _output_variables (dict of str to pulp.LpVariable): dictionary of
                pulp variables than maps variable names to pulp variables
                corresponding to output categories.
            _dmu_constraint_names (dict of str to str): dictionary that maps
                constraint names to DMU code.

        Args:
            input_data (InputData): object that stores all data of
                a DEA instance.
            tolerance (double): lower bound of the variables of multilier
                model.
            concrete_model: concrete implementation of the multiplier
                model.
    '''
    def __init__(self, input_data, tolerance, concrete_model):
        super(MultiplierModelBase, self).__init__(input_data)
        self.tolerance = tolerance
        self._concrete_model = concrete_model
        self._input_variables = dict()
        self._output_variables = dict()
        self._dmu_constraint_names = dict()

    def _create_lp(self):
        ''' Creates initial LP model.
        '''
        assert len(self.input_data.DMU_codes) != 0
        # create LP for the first DMU - it is the easiest way to
        # adapt current code
        for elem in self.input_data.DMU_codes:
            dmu_code = elem
            break

        self.lp_model = pulp.LpProblem(
            'Multiplier model: {orientation}-oriented'.format(
            orientation=self._concrete_model.get_orientation()),
            self._concrete_model.get_objective_type())

        self._output_variables = pulp.LpVariable.dicts(
            'mu', self.input_data.output_categories, self.tolerance, None,
            pulp.LpContinuous)
        self._input_variables = pulp.LpVariable.dicts(
            'eta', self.input_data.input_categories, self.tolerance, None,
            pulp.LpContinuous)

        self.lp_model += (self._concrete_model.get_objective_function(
            self.input_data, dmu_code, self._input_variables,
            self._output_variables),
            'Efficiency score or inverse of efficiency score')

        self._dmu_constraint_names.clear()
        for dmu in self.input_data.DMU_codes:
            output_sum = pulp.lpSum([self.input_data.coefficients[
                dmu, category] * self._output_variables[category]
                for category in self.input_data.output_categories])
            input_sum = pulp.lpSum([self.input_data.coefficients[
                dmu, category] * self._input_variables[category]
                for category in self.input_data.input_categories])
            name = 'DMU_constraint_{count}'.format(count=dmu)
            self.lp_model += (output_sum - input_sum <= 0, name)
            self._dmu_constraint_names[name] = dmu

        self.lp_model += (self._concrete_model.get_equality_constraint(
            self.input_data, dmu_code, self._input_variables,
            self._output_variables), 'equality_constraint')

    def _update_lp(self, dmu_code):
        ''' Updates existing linear program with coefficients corresponding
            to a given DMU.

            Args:
                dmu_code (str): DMU code.
        '''
        self._concrete_model.update_objective(self.input_data, dmu_code,
                                              self._input_variables,
                                              self._output_variables,
                                              self.lp_model)
        self._concrete_model.update_equality_constraint(self.input_data,
                                                        dmu_code,
                                                        self._input_variables,
                                                        self._output_variables,
                                                        self.lp_model)

    def _fill_solution(self, dmu_code, model_solution):
        ''' Fills given solution with data calculated for one DMU.

            Args:
                dmu_code (str): DMU code for which the LP was solved.
                model_solution (Solution): object where solution for one DMU
                    will be written.
        '''
        model_solution.orientation = self._concrete_model.get_orientation()
        model_solution.add_lp_status(dmu_code, self.lp_model.status)
        if self.lp_model.status == pulp.LpStatusOptimal:
            for input_category in self.input_data.input_categories:
                model_solution.add_input_dual(
                    dmu_code, input_category,
                    self._input_variables[input_category].varValue)

            for output_category in self.input_data.output_categories:
                model_solution.add_output_dual(
                    dmu_code, output_category,
                    self._output_variables[output_category].varValue)
            lambda_variables = dict()
            for dmu_constraint, dmu in self._dmu_constraint_names.items():
                if self.lp_model.constraints[dmu_constraint].pi is None:
                    self.lp_model.constraints[dmu_constraint].pi = 0
                # else:
                #     print(self.lp_model.constraints[dmu_constraint].pi)
                if (abs(self.lp_model.constraints[dmu_constraint].pi) >
                        ZERO_TOLERANCE):
                    lambda_variables[dmu] = self._concrete_model.process_dual_value(
                        self.lp_model.constraints[dmu_constraint].pi)
            lambda_var = lambda_variables.get(dmu_code, 0)
            model_solution.add_efficiency_score(
                dmu_code, self._get_efficiency_score(lambda_var))
            # print('lambda_variables: {0}'.format(lambda_variables))
            model_solution.add_lambda_variables(dmu_code, lambda_variables)

    def _get_efficiency_score(self, lambda_variable):
        ''' Returns efficiency score based on a given lambda variable.

            Args:
                lambda_variable (double): value of the lambda variable.

            Returns:
                double: efficiency spyDEA.core.
        '''
        eff_score = self._concrete_model.process_obj_var(pulp.value(
            self.lp_model.objective))
        # None is possible when objective function is zero
        if eff_score is None:
            eff_score = 0
        if is_efficient(eff_score, lambda_variable):
            eff_score = 1
        return eff_score class MultiplierInputOrientedModel(InputOrientedModel):
    ''' Implements input-oriented multiplier model.
    '''
    def get_objective_type(self):
        ''' Returns pulp.LpMaximize - we maximize objective function in case
            of input-oriented multiplier model.

            Returns:
                pulp.LpMaximize.
        '''
        return pulp.LpMaximize

    def get_objective_function(self, input_data, dmu_code, input_variables,
                               output_variables):
        ''' Generates objective function of input-oriented multiplier model.

            Args:
                input_data (InputData): object that stores input data.
                dmu_code (str): DMU code.
                input_variables (dict of str to pulp.LpVariable): dictionary
                    that maps variable name to pulp variable corresponding
                    to input categories.
                output_variables (dict of str to pulp.LpVariable): dictionary
                    that maps variable name to pulp variable corresponding
                    to output categories.

            Returns:
                pulp.LpSum: objective function.
        '''
        return pulp.lpSum([input_data.coefficients[dmu_code, category] *
                          output_variables[category]
                          for category in input_data.output_categories])

    def update_objective(self, input_data, dmu_code, input_variables,
                         output_variables, lp_model):
        ''' Updates coefficients of the objective function of a given model.

            Args:
                input_data (InputData): object that stores input data.
                dmu_code (str): DMU code.
                input_variables (dict of str to pulp.LpVariable): dictionary
                    that maps variable name to pulp variable corresponding
                    to input categories.
                output_variables (dict of str to pulp.LpVariable): dictionary
                    that maps variable name to pulp variable corresponding
                    to output categories.
                lp_model (pulp.LpProblem): linear programming model.
        '''
        for category in input_data.output_categories:
            lp_model.objective[output_variables[category]] = input_data.coefficients[
                dmu_code, category]

    def get_equality_constraint(self, input_data, dmu_code, input_variables,
                                output_variables):
        ''' Generates equality constraint of input-oriented multiplier model.

            Args:
                input_data (InputData): object that stores input data.
                dmu_code (str): DMU code.
                input_variables (dict of str to pulp.LpVariable): dictionary
                    that maps variable name to pulp variable corresponding
                    to input categories.
                output_variables (dict of str to pulp.LpVariable): dictionary
                    that maps variable name to pulp variable corresponding
                    to output categories.

            Returns:
                pulp.LpConstraint: equality constraint.
        '''
        return pulp.lpSum([input_data.coefficients[dmu_code, category] *
                          input_variables[category]
                          for category in input_data.input_categories]) == 1

    def update_equality_constraint(self, input_data, dmu_code, input_variables,
                                   output_variables, lp_model):
        ''' Updates coefficients of the equality constraint of a given model.

            Args:
                input_data (InputData): object that stores input data.
                dmu_code (str): DMU code.
                input_variables (dict of str to pulp.LpVariable): dictionary
                    that maps variable name to pulp variable corresponding
                    to input categories.
                output_variables (dict of str to pulp.LpVariable): dictionary
                    that maps variable name to pulp variable corresponding
                    to output categories.
                lp_model (pulp.LpProblem): linear programming model.
        '''
        for category, var in input_variables.items():
            lp_model.constraints['equality_constraint'][var] = input_data.coefficients[
                dmu_code, category] class MultiplierModelWithAbsoluteWeightRestrictions(
        MultiplierModelWithWeigthRestrictionsBase):
    ''' Implements multiplier model with absolute weight restrictions.
    '''
    def _get_multiplier(self, dmu_code, category):
        ''' See base class.
        '''
        return 1

    def _get_constraint_prefix_name(self):
        ''' See base class.
        '''
        return 'Absolute' class MultiplierModelWithVirtualWeightRestrictions(
        MultiplierModelWithWeigthRestrictionsBase):
    ''' Implements multiplier model with virtual weight restrictions.

        Attributes:
            lb_weight_rest_variables (dict of str to tuple of pulp.LpVariable,
                str): dictionary that maps category name to a tuple of pulp
                variable and constraint name.
            ub_weight_rest_variables (dict of str to tuple of pulp.LpVariable,
                str): dictionary that maps category name to a tuple of pulp
                variable and constraint name.

        Args:
            model_to_decorate (MultiplierModelBase): multiplier model.
            bounds (dict of str to tuple of double,
                double or dict of tuple of str, str to tuple of double, double:
                dictionary with parsed values of constraints):
                dictionary with parsed values of constraints.
    '''
    def __init__(self, model_to_decorate, bounds):
        super().__init__(model_to_decorate, bounds)
        self.lb_weight_rest_variables = dict()
        self.ub_weight_rest_variables = dict()

    def _create_lp(self):
        ''' See base class.
        '''
        self.lb_weight_rest_variables.clear()
        self.ub_weight_rest_variables.clear()
        super()._create_lp()

    def _get_multiplier(self, dmu_code, category):
        ''' See base class.
        '''
        return self._model_to_decorate.input_data.coefficients[
            dmu_code, category]

    def _get_constraint_prefix_name(self):
        ''' See base class.
        '''
        return 'Virtual'

    def _update_lp(self, dmu_code):
        ''' See base class.
        '''
        self._model_to_decorate._update_lp(dmu_code)
        for category, (lower_bound, upper_bound) in self.bounds.items():

            multiplier = self._get_multiplier(dmu_code, category)

            if lower_bound:
                variable, constraint_name = self.lb_weight_rest_variables[
                    category]
                self.lp_model.constraints[constraint_name][variable] = multiplier

            if upper_bound:
                variable, constraint_name = self.ub_weight_rest_variables[
                    category]
                self.lp_model.constraints[constraint_name][variable] = multiplier

    def _store_vars_lb(self, category, constraint_name, variable):
        ''' See base class.
        '''
        self.lb_weight_rest_variables[category] = (variable, constraint_name)

    def _store_vars_ub(self, category, constraint_name, variable):
        ''' See base class.
        '''
        self.ub_weight_rest_variables[category] = (variable, constraint_name) class MultiplierModelWithPriceRatioConstraints(MultiplierModelBase):
    ''' Implements multiplier model with price ratio restrictions.

        Attributes:
            _model_to_decorate (MultiplierModelBase): multiplier model.
            bounds (dict of str to tuple of double,
                double or dict of tuple of str, str to tuple of double, double:
                dictionary with parsed values of constraints):
                dictionary with parsed values of constraints.

        Args:
            model_to_decorate (MultiplierModelBase): multiplier model.
            bounds (dict of str to tuple of double,
                double or dict of tuple of str, str to tuple of double, double:
                dictionary with parsed values of constraints):
                dictionary with parsed values of constraints.
    '''
    def __init__(self, model_to_decorate, bounds):
        self._model_to_decorate = model_to_decorate
        self.bounds = bounds

    def __getattr__(self, name):
        return getattr(self._model_to_decorate, name)

    def _create_lp(self):
        ''' Creates initial LP.
        '''
        self._model_to_decorate._create_lp()
        self.lp_model = self._model_to_decorate.lp_model
        for (category_in_nom, category_in_denom), (lower_bound, upper_bound) in self.bounds.items():

            variable_in_nom = self._model_to_decorate._input_variables.get(
                category_in_nom, None)
            if variable_in_nom is None:
                variable_in_nom = self._model_to_decorate._output_variables[
                    category_in_nom]

            variable_in_denom = self._model_to_decorate._input_variables.get(
                category_in_denom, None)
            if variable_in_denom is None:
                variable_in_denom = self._model_to_decorate._output_variables[
                    category_in_denom]

            if lower_bound:
                self.lp_model += (variable_in_nom >= variable_in_denom *
                                  lower_bound,
                                  'Price ratio lower bound constraint'
                                  ' on categories {0} and {1}'.format(
                                  category_in_nom,
                                  category_in_denom))
            if upper_bound:
                self.lp_model += (variable_in_nom <= variable_in_denom *
                                  upper_bound,
                                  'Price ratio upper bound constraint on'
                                  ' category {0} and {1}'.format(
                                  category_in_nom,
                                  category_in_denom))

    def _update_lp(self, dmu_code):
        ''' Updates existing linear program with coefficients corresponding
            to a given DMU.

            Args:
                dmu_code (str): DMU code.
        '''
        self._model_to_decorate._update_lp(dmu_code)

    def _create_solution(self):
        ''' Creates solution object.

            Returns:
                Solution: allocated solution.
        '''
        return self._model_to_decorate._create_solution()

    def _fill_solution(self, dmu_code, model_solution):
        ''' Fills given solution with data calculated for one DMU.

            Args:
                dmu_code (str): DMU code for which the LP was solved.
                model_solution (Solution): object where solution for one DMU
                    will be written.
        '''
        self._model_to_decorate._fill_solution(dmu_code, model_solution)

    def _get_efficiency_score(self, lambda_variable):
        ''' Returns efficiency score based on a given lambda variable.

            Args:
                lambda_variable (double): value of the lambda variable.

            Returns:
                double: efficiency spyDEA.core.
        '''
        return self._model_to_decorate._get_efficiency_score(lambda_variable) class EnvelopmentModelWithAbsoluteWeightRestrictions(EnvelopmentModelBase):
    ''' This class implements envelopment model with absolute weight
        restrictions.

        Attributes:
            model (EnvelopmentModelBase): envelopment model.
            bounds (dict of str to tuple of double,
                double or dict of tuple of str, str to tuple of double, double:
                dictionary with parsed values of constraints):
                dictionary with parsed values of constraints.
            new_vars_lb (dict of str to pulp.LpVariable): dictionary that maps
                category name to a pulp variable.
            new_vars_ub (dict of str to pulp.LpVariable): dictionary that maps
                category name to a pulp variable.

        Args:
            model_to_decorate (EnvelopmentModelBase): envelopment model.
            bounds (dict of str to tuple of double,
                double or dict of tuple of str, str to tuple of double, double:
                dictionary with parsed values of constraints):
                dictionary with parsed values of constraints.
    '''
    def __init__(self, model_to_decorate, bounds):
        self.model = model_to_decorate
        self.bounds = bounds
        self.new_vars_lb = dict()
        self.new_vars_ub = dict()
        self._multiplier = None

    def __getattr__(self, name):
        return getattr(self.model, name)

    def _create_lp(self):
        ''' See base class.
        '''
        self.model._create_lp()
        self.lp_model = self.model.lp_model
        self.new_vars_lb.clear()
        self.new_vars_ub.clear()
        for elem in self.input_data.DMU_codes:
            dmu_code = elem
            break

        if self._concrete_model.get_orientation() == 'input': 
            self._multiplier = 1
        else:
            self._multiplier = -1

        # make objective variable unrestricted 
        self._variables['obj_var'].lowBound = None
        self._variables['obj_var'].upBound = None

        for category, (lower_bound, upper_bound) in self.bounds.items():
            if lower_bound:
                variable = pulp.LpVariable(
                    '{0}_category_{1}_lb'.format(
                        self._get_var_name(), category),
                    None, 0, pulp.LpContinuous)
                self.new_vars_lb[category] = variable
                name = self.model._constraints[category]
                self.lp_model.constraints[name].addterm(variable, 1)
                self.lp_model.objective += (self._get_multiplier(
                                            dmu_code, category) *
                                            lower_bound * variable)

            if upper_bound:
                variable = pulp.LpVariable(
                    '{0}_category_{1}_ub'.format(self._get_var_name(),
                                                 category),
                    0, None, pulp.LpContinuous)
                self.new_vars_ub[category] = variable
                name = self.model._constraints[category]
                self.lp_model.constraints[name].addterm(variable, 1)
                self.lp_model.objective += (self._get_multiplier(
                                            dmu_code, category) *
                                            upper_bound * variable)
        #if self._multiplier == -1:
            # add constraint that objective function is >= 1 for
            # output-oriented model
         #   self.lp_model += self.lp_model.objective >= 1


    def _update_lp(self, dmu_code):
        ''' See base class.
        '''
        self.model._update_lp(dmu_code)
        for category, (lower_bound, upper_bound) in self.bounds.items():
            if lower_bound:
                variable = self.new_vars_lb[category]
                self.lp_model.objective[variable] = (self._get_multiplier(
                                                     dmu_code, category) *
                                                     lower_bound)

            if upper_bound:
                variable = self.new_vars_ub[category]
                self.lp_model.objective[variable] = (self._get_multiplier(
                                                     dmu_code, category) *
                                                     upper_bound)

    def _create_solution(self):
        ''' See base class.
        '''
        return self.model._create_solution()

    def _fill_solution(self, dmu_code, model_solution):
        ''' See base class.
        '''
        self.model._fill_solution(dmu_code, model_solution)

    def _get_multiplier(self, dmu_code, category):
        ''' Returns a multiplier used in the model objective function.

            Args:
                dmu_code (str): DMU code.
                category (str): category name.
        '''
        return self._multiplier

    def _get_var_name(self):
        ''' Returns base for a variable name.

            Returns:
                str: base for a variable name.
        '''
        return 'abs'

    def _add_constraints_for_outputs(self, variables, dmu_code,
                                     obj_variable):
        ''' See base class.
        '''
        self.model._add_constraints_for_outputs(variables, dmu_code,
                                                obj_variable)

    def _add_constraints_for_inputs(self, variables,
                                    dmu_code, obj_variable):
        ''' See base class.
        '''
        self.model._add_constraints_for_inputs(variables, dmu_code,
                                               obj_variable)

    def _process_duals(self, dmu_code, categories, func):
        ''' See base class.
        '''
        self.model._process_duals(dmu_code, categories, func) class EnvelopmentModelWithVirtualWeightRestrictions(
        EnvelopmentModelWithAbsoluteWeightRestrictions):
    ''' This class implements envelopment model with virtual weight
        restrictions.
    '''
    def _get_multiplier(self, dmu_code, category):
        ''' Returns a multiplier used in the model objective function.

            Args:
                dmu_code (str): DMU code.
                category (str): category name.
        '''
        return self._multiplier / self.model.input_data.coefficients[dmu_code, category]

    def _get_var_name(self):
        ''' Returns base for a variable name.

            Returns:
                str: base for a variable name.
        '''
        return 'virtual' class EnvelopmentModelWithPriceRatioConstraints(EnvelopmentModelBase):
    ''' This class implements envelopment model with price ratio weight
        restrictions.

        Attributes:
            model (EnvelopmentModelBase): envelopment model.
            bounds (dict of str to tuple of double,
                double or dict of tuple of str, str to tuple of double, double:
                dictionary with parsed values of constraints):
                dictionary with parsed values of constraints.
        Args:
            model_to_decorate (EnvelopmentModelBase): envelopment model.
            bounds (dict of str to tuple of double,
                double or dict of tuple of str, str to tuple of double, double:
                dictionary with parsed values of constraints):
                dictionary with parsed values of constraints.
    '''
    def __init__(self, model_to_decorate, bounds):
        self.model = model_to_decorate
        self.bounds = bounds

    def __getattr__(self, name):
        return getattr(self.model, name)

    def _create_lp(self):
        ''' See base class.
        '''
        self.model._create_lp()
        self.lp_model = self.model.lp_model

        # create varibales
        for (category_in_nom, category_in_denom), (lower_bound, upper_bound) in self.bounds.items():
            if lower_bound:
                variable = pulp.LpVariable(
                    'price_ratio_category_{0}_{1}_lb'.format(
                        category_in_nom, category_in_denom),
                    0, None, pulp.LpContinuous)
                name = self.model._constraints[category_in_nom]
                self.lp_model.constraints[name].addterm(variable, -1)
                name = self.model._constraints[category_in_denom]
                self.lp_model.constraints[name].addterm(variable, lower_bound)

            if upper_bound:
                variable = pulp.LpVariable(
                    'price_ratio_category_{0}_{1}_ub'.format(
                        category_in_nom, category_in_denom),
                    0, None, pulp.LpContinuous)
                name = self.model._constraints[category_in_nom]
                self.lp_model.constraints[name].addterm(variable, 1)
                name = self.model._constraints[category_in_denom]
                self.lp_model.constraints[name].addterm(variable, -upper_bound)

    def _create_solution(self):
        ''' See base class.
        '''
        return self.model._create_solution()

    def _fill_solution(self, dmu_code, model_solution):
        ''' See base class.
        '''
        self.model._fill_solution(dmu_code, model_solution)

    def _update_lp(self, dmu_code):
        ''' See base class.
        '''
        self.model._update_lp(dmu_code)

    def _add_constraints_for_outputs(self, variables, dmu_code,
                                     obj_variable):
        ''' See base class.
        '''
        self.model._add_constraints_for_outputs(variables, dmu_code,
                                                obj_variable)

    def _add_constraints_for_inputs(self, variables,
                                    dmu_code, obj_variable):
        ''' See base class.
        '''
        self.model._add_constraints_for_inputs(variables, dmu_code,
                                               obj_variable)

    def _process_duals(self, dmu_code, categories, func):
        ''' See base class.
        '''
        self.model._process_duals(dmu_code, categories, func) def generate_upper_bound_for_efficiency_score():
    ''' Returns upper bound for efficiency score for usual envelopment model.

        Note:
            This function only works with input-oriented envelopment models.

        Returns:
            double: 1, since efficiency score must be <= 1 for usual
                input-oriented envelopment model.
    '''
    return 1 class Parameters:
    ''' This class stores and manipulates all parameters needed to run
        DEA models. After object creation all parameter values are set
        to empty string.

        Attributes:
            params (dict of str to str): dictionary of parameter names
                and values.

                Example:
                    >> params = {'DEAform': 'env', 'ReturnsToScale': 'VRS'}
    '''
    def __init__(self):
        self.params = dict()
        for key in VALID_PARAM_NAMES:
            self.params[key] = ''

    def update_parameter(self, param_name, param_value):
        ''' Updates existing parameter in the dictionary. If parameter does not
            exist in the dictionary, throws exception.
            Parameter name and value must satisfy conditions specified by
            regular expressions
            VALID_PARAM_NAME and VALID_PARAM_VALUE.

            Example:
                >>> p = Parameters()
                >>> p.get_parameter_value('DEA_FORM')
                ''
                >>> p.update_parameter('DEA_FORM', 'new value')
                >>> p.get_parameter_value('DEA_FORM')
                'new value'
                >>> p.update_parameter('param2', 'value2')
                KeyError
                >>> p.update_parameter('DEA_FORM', '   ')
                ValueError

            Args:
                param_name (str): parameter name.
                param_value (str): parameter value.

            Raises:
                KeyError: if a given parameter name does not belong to
                    VALID_PARAM_NAMES.
                ValueError: if a given parameter value does not satisfy
                    regular expression given in VALID_PARAM_VALUE.
        '''
        if param_name not in self.params:
            raise KeyError('Parameter {0} does not exists in'
                           ' the dictionary'.format(param_name))
        if not validate_string(param_value, VALID_PARAM_VALUE):
            raise ValueError('Trying to update parameter <{0}> with '
                             'not valid value <{1}>'.
                             format(param_name, param_value))
        self.params[param_name] = param_value

    def get_parameter_value(self, param_name):
        ''' Returns a value that corresponds to a given parameter name.
            If there is no such parameter name, throws KeyError exception.

            Example:
                >>> allParams = Parameters()
                >>> allParams.update_parameter('DEA_FORM', 'value1')
                >>> allParams.get_parameter_value('DEA_FORM')
                value1
                >>> allParams.get_parameter_value('Param2')
                KeyError

            Args:
                param_name (str): parameter name.

            Returns:
                str: parameter value.
        '''
        return self.params[param_name]

    def print_all_parameters(self):
        ''' Prints all parameters on screen.
        '''
        for param in self.params.items():
            print("<{0}> = {1}".format(param[0], param[1]))

    def get_all_params_as_string(self):
        ''' Returns all parameters with values as a string. 

            Returns:
                str: all paremeters with values.
        '''
        all_params = []
        for param in self.params.items():
            all_params.append("<{0}> = '{1}'".format(param[0], param[1]))
        return ', '.join(all_params)

    def get_set_of_parameters(self, param_name, delimiter=';'):
        ''' Some of the parameters contain a set of values.
            Returns a set of strings with these values instead of
            one string.

            Args:
                param_name (str): parameter name.
                delimiter (str, optional): delimiter. Defaults to semicolon.

            Returns:
                set of str: set of parsed parameter values.
        '''
        params_as_str = self.get_parameter_value(param_name)
        return set([elem.strip() for elem in params_as_str.split(delimiter)
                    if elem.strip()])

    def copy_all_params(self, params_from):
        ''' Copies all parameter values from a given parameter object.

            Args:
                params_from (Parameters): parameter object from which
                    parameter values will be copied.
        '''
        for name, value in params_from.params.items():
            self.update_parameter(name, value)

    def remove_category_from_params(self, category_type, category_name):
        ''' Removes a given parameter value from parameters.

            Args:
                category_type (str): parameter name.
                category_name (str): value that should be removed.
        '''
        categories = self.get_set_of_parameters(category_type)
        if category_name in categories:
            categories.remove(category_name)
            self.update_parameter(category_type, '; '.join(categories))

    def clear_all_categorical_and_data_fields(self):
        ''' Sets values of parameters from CATEGORICAL_AND_DATA_FIELDS
            to empty strings.
        '''
        for param_name in CATEGORICAL_AND_DATA_FIELDS:
            self.update_parameter(param_name, '')

    def change_category_name(self, old_name, new_name):
        ''' Changes a given category name to a new name in all parameter
            values that contain this category.

            Args:
                old_name (str): previous category name.
                new_name (str): new category name.
        '''
        for param_name in CATEGORICAL_AND_DATA_FIELDS:
            if param_name != 'DATA_FILE' and param_name != 'OUTPUT_FILE':
                param_values = self.get_set_of_parameters(param_name)
                new_param_value = ''
                for val in param_values:

                    value_to_compare = val
                    index = 0
                    name_to_add = new_name
                    price_ratio = False
                    if (param_name == 'ABS_WEIGHT_RESTRICTIONS' or
                            param_name == 'VIRTUAL_WEIGHT_RESTRICTIONS'):
                        value_to_compare, index = dea_utils.find_category_name_in_restrictions(val)
                    elif param_name == 'PRICE_RATIO_RESTRICTIONS':
                        value_to_compare, index = dea_utils.find_category_name_in_restrictions(val)
                        category1, category2 = dea_utils.get_price_ratio_categories(value_to_compare)
                        if old_name == category1:
                            name_to_add = new_name + '/' + category2
                            price_ratio = True
                        elif old_name == category2:
                            name_to_add = category1 + '/' + new_name
                            price_ratio = True

                    if old_name == value_to_compare or price_ratio:
                        new_param_value = (new_param_value + val[:index] +
                                           name_to_add +
                                           val[index + len(value_to_compare):])
                    else:
                        new_param_value = new_param_value + val
                    if new_param_value and param_name != 'CATEGORICAL_CATEGORY':
                        new_param_value = new_param_value + ';'

                if new_param_value:
                    self.update_parameter(param_name, new_param_value) def clean_up_pickled_files():
    ''' Removes mps-files from current folder and p-files from temporary folder.
    '''
    if os.path.exists(TMP_FOLDER):
        filelist = [f for f in os.listdir(TMP_FOLDER) if f.endswith('.p')]
        for f in filelist:
            os.remove(os.path.join(TMP_FOLDER, f))
    # remove mps file if any
    filelist = [f for f in os.listdir('.') if f.endswith('.mps')]
    for f in filelist:
        os.remove(f) def parse_parameters_from_file(filename):
    ''' Reads parameters from a given file. If file does not exist, the
        exception will be thrown. Returns Parameters object with
        all parameters. Does not validate parameters.
        Throws an exception if there are repeating
        parameters (all parameters must be unique).
        All parameters must follow convention: <param_name> {param_value}.
        Supports one line comments:
        <param_name> {param_value} # this parameter is important.
        If some parameters are missing, their value will be set to empty
        string.

        Args:
            filename (str): path to file with parameters. Only text files
                are accepted, but actual file extension does not matter.

        Returns:
            Parameters: parameter object with parsed parameters.

        Raises:
            FileNotFound: if given file does not exist.
            ValueError: if file does not contain any valid parameters.
    '''
    pattern = ''.join(['<(', VALID_PARAM_NAME, r')>\s*{(',
                       VALID_PARAM_VALUE, ')}'])
    params = Parameters()
    # will raise IOError exception if fileName
    # does not exists, 'rU' - r - read mode,
    # U - Universal for encoding
    nb_parsed_params = 0
    with open(filename, 'rU') as file_with_params:
        for line in file_with_params:
            line = extract_comment(line)
            matched_params = re.findall(pattern, line)
            for match in matched_params:
                if match[0] in VALID_PARAM_NAMES:
                    params.update_parameter(match[0], match[1])
                    nb_parsed_params += 1
    if nb_parsed_params == 0:
        raise ValueError('File {filename} does not contain any valid'
                         ' parameter values'.format(filename=filename))
    return params def build_models(params, model_input):
    ''' Creates several models and parameters in the case when
        RETURN_TO_SCALE or ORIENTATION is set to both. If neither
        RETURN_TO_SCALE
        nor ORIENTATION is set to both, then no new models will be created,
        a copy of the given model and parameters will be returned.

        Args:
            params (Parameters): parameters.
            model_input (InputData): data instance.

        Returns:
            tuple of list of ModelBase, list of Parameters: tuple with two
                lists. The first list contains all created models, the
                second list contains corresponding parameters.
    '''
    model_factory.add_input_and_output_categories(params, model_input)
    rts_type = params.get_parameter_value('RETURN_TO_SCALE')
    list_of_param_objects = []
    original_params = Parameters()
    original_params.copy_all_params(params)
    list_of_param_objects.append(original_params)
    if rts_type == 'both':
        original_params.update_parameter('RETURN_TO_SCALE', 'VRS')
        params_with_crs = Parameters()
        params_with_crs.copy_all_params(original_params)
        params_with_crs.update_parameter('RETURN_TO_SCALE', 'CRS')
        list_of_param_objects.append(params_with_crs)
    orientation_type = original_params.get_parameter_value('ORIENTATION')
    nb_param_objs = len(list_of_param_objects)
    if orientation_type == 'both':
        possible_orientation = ['input', 'output']
        for count in range(nb_param_objs):
            assert(count < 2)
            param_obj = list_of_param_objects[count]
            param_obj.update_parameter('ORIENTATION',
                                       possible_orientation[count])
            new_param_obj = Parameters()
            new_param_obj.copy_all_params(param_obj)
            new_param_obj.update_parameter('ORIENTATION',
                                           possible_orientation[1 - count])
            list_of_param_objects.append(new_param_obj)

    models = []
    for param_object in list_of_param_objects:
        models.append(model_factory.create_model(param_object, model_input))
    return models, list_of_param_objects class EnvelopmentModelBase(ModelBase):
    ''' This class is a base class for different envelopment models.
        It implements general structure of all envelopment models.
        Concrete variations of envelopment models are passed in the
        class constructor.

        Attributes:
            _concrete_model: concrete implementation of the envelopment
                model.
            _constraint_creator: object that creates a proper
                constraint depending on presence of
                disposable variables.
            _variables (dict of str to pulp.LpVariable): dictionary of pulp
                variables than maps variable names to pulp variables.
            _constraints (dict of str to str): dictionary that maps name of
                the category to the name of the corresponding constraint.
            _should_add_efficiency (bool): if set to False, previously
                stored efficiency score is used. This variable is changed
                in two phase model.

        Args:
            input_data (InputData): object that stores all data of
                a DEA instance.
            concrete_model: concrete implementation of the envelopment
                model.
            constraint_creator: object that creates a proper
                constraint depending on presence of disposable variables.
    '''
    def __init__(self, input_data, concrete_model, constraint_creator):
        super(EnvelopmentModelBase, self).__init__(input_data)
        self._concrete_model = concrete_model
        self._constraint_creator = constraint_creator
        self._variables = dict()
        self._constraints = dict()
        self._should_add_efficiency = True

    def _create_lp(self):
        ''' Creates initial linear program.
        '''
        assert len(self.input_data.DMU_codes) != 0
        self._variables.clear()
        self._constraints.clear()
        # create LP for the first DMU - it is the easiest way to
        # adapt current code
        for elem in self.input_data.DMU_codes:
            dmu_code = elem
            break

        orientation = self._concrete_model.get_orientation()
        obj_type = self._concrete_model.get_objective_type()
        self.lp_model = pulp.LpProblem('Envelopment model: {0}-'
                                       'oriented'.format(orientation),
                                       obj_type)
        ub_obj = self._concrete_model.get_upper_bound_for_objective_variable()
        obj_variable = pulp.LpVariable('Variable in objective function',
                                       self._concrete_model.
                                       get_lower_bound_for_objective_variable(),
                                       ub_obj,
                                       pulp.LpContinuous)
        self._variables['obj_var'] = obj_variable

        self.lp_model += (obj_variable,
                          'Efficiency score or inverse of efficiency score')

        lambda_variables = pulp.LpVariable.dicts('lambda',
                                                 self.input_data.DMU_codes,
                                                 0, None, pulp.LpContinuous)
        self._variables.update(lambda_variables)
        self._add_constraints_for_outputs(lambda_variables, dmu_code,
                                          obj_variable)
        self._add_constraints_for_inputs(lambda_variables, dmu_code,
                                         obj_variable)

    def _update_lp(self, dmu_code):
        ''' Updates existing linear program with coefficients corresponding
            to a given DMU.

            Args:
                dmu_code (str): DMU code.
        '''
        for output_category in self.input_data.output_categories:
            constraint_name = self._constraints[output_category]
            current_output = self.input_data.coefficients[(dmu_code,
                                                          output_category)]
            constraint = self.lp_model.constraints[constraint_name]
            self._concrete_model.update_output_category_coefficient(
                current_output, constraint, self._variables['obj_var'],
                output_category)
        for input_category in self.input_data.input_categories:
            current_input = self.input_data.coefficients[(dmu_code,
                                                         input_category)]
            constraint_name = self._constraints[input_category]
            constraint = self.lp_model.constraints[constraint_name]
            self._concrete_model.update_input_category_coefficient(
                current_input, constraint, self._variables['obj_var'],
                input_category)

    def _add_constraints_for_outputs(self, variables, dmu_code,
                                     obj_variable):
        ''' Adds constraints for outputs to linear program.

            Args:
                variables (dict of str to pulp.LpVariable): a dictionary
                    that maps DMU codes to pulp.LpVariable, created with
                    pulp.LpVariable.dicts.
                dmu_code (str): DMU code for which LP is being created.
                obj_variable (pulp.LpVariable): LP variable that is optimised
                    (either efficiency score or inverse of efficiency score).
        '''
        for (count, output_category) in enumerate(
                self.input_data.output_categories):
            current_output = self.input_data.coefficients[(dmu_code,
                                                          output_category)]
            output_coeff = self._concrete_model.get_output_variable_coefficient(
                obj_variable, output_category)
            sum_all_outputs = pulp.lpSum([variables[dmu] *
                                         self.input_data.coefficients
                                         [(dmu, output_category)]
                                         for dmu in self.input_data.DMU_codes])
            name = 'constraint_output_{count}'.format(count=count)
            self.lp_model += (self._constraint_creator.create(
                              -output_coeff * current_output +
                              sum_all_outputs, 0, output_category), name)
            self._constraints[output_category] = name

    def _add_constraints_for_inputs(self, variables,
                                    dmu_code, obj_variable):
        ''' Adds constraints for inputs to LP.

            Args:
                variables (dict of {str: pulp.LpVariable}): a dictionary that
                    maps DMU codes to pulp.LpVariable, created with
                    pulp.LpVariable.dicts.
                dmu_code (str): DMU code for which LP is being created.
                obj_variable (pulp.LpVariable): LP variable that is optimised
                    (either efficiency score or inverse of efficiency score).
        '''
        for (count, input_category) in enumerate(
                self.input_data.input_categories):
            current_input = self.input_data.coefficients[(dmu_code,
                                                         input_category)]
            input_coeff = self._concrete_model.get_input_variable_coefficient(
                obj_variable, input_category)
            sum_all_inputs = pulp.lpSum([variables[dmu] *
                                        self.input_data.coefficients
                                        [(dmu, input_category)]
                                        for dmu in
                                        self.input_data.DMU_codes])
            name = 'constraint_input_{count}'.format(count=count)
            self.lp_model += (self._constraint_creator.create(
                              input_coeff * current_input
                              - sum_all_inputs, 0, input_category), name)
            self._constraints[input_category] = name

    def _fill_solution(self, dmu_code, model_solution):
        ''' Fills given solution with data calculated for one DMU.

            Args:
                dmu_code (str): DMU code for which the LP was solved.
                model_solution (Solution): object where solution for one DMU
                    will be written.
        '''
        model_solution.orientation = self._concrete_model.get_orientation()
        model_solution.add_lp_status(dmu_code, self.lp_model.status)

        if self.lp_model.status == pulp.LpStatusOptimal:
            lambda_variables = dict()
            for dmu in self.input_data.DMU_codes:
                var = self._variables.get(dmu, None)
                if (var is not None and var.varValue is not None and
                        abs(var.varValue) > ZERO_TOLERANCE):
                    lambda_variables[dmu] = var.varValue

            if self._should_add_efficiency:
                model_solution.add_efficiency_score(
                    dmu_code, self._concrete_model.process_obj_var
                    (pulp.value(self.lp_model.objective)))
                
            model_solution.add_lambda_variables(dmu_code, lambda_variables)
            self._process_duals(dmu_code, self.input_data.input_categories,
                                model_solution.add_input_dual)
            self._process_duals(dmu_code, self.input_data.output_categories,
                                model_solution.add_output_dual)

    def _process_duals(self, dmu_code, categories, func):
        ''' Helper function that adds duals to solution using given method func.
            Helps to avoid code duplication.

            Args:
                dmu_code (str): DMU code under consideration.
                categories (list of str): list of either input or output
                    categories.
                func (function): a function that accepts DMU code, category and
                    dual variable value and adds it to solution.
        '''
        for category in categories:
            constraint_name = self._constraints[category]
            dual = self._concrete_model.process_dual_value(
                self.lp_model.constraints[constraint_name].pi)
            func(dmu_code, category, dual) class EnvelopmentModelInputOriented(InputOrientedModel):
    ''' This class defines methods specific to input-oriented envelopment
        model.

        Attributes:
            upper_bound_generator (function): function that
                generates an upper bound on efficiency scores for
                envelopment model, see :mod:`refactored.bound_generators`

        Args:
            upper_bound_generator (function): function that
                generates an upper bound on efficiency scores for
                envelopment
                model, see :mod:`refactored.bound_generators`.
    '''
    def __init__(self, upper_bound_generator):
        self.upper_bound_generator = upper_bound_generator

    def get_upper_bound_for_objective_variable(self):
        ''' Returns a proper upper bound on efficiency score which
            is minimized in the case of input-oriented envelopment model.

            Returns:
                double: upper bound on efficiency spyDEA.core.
        '''
        return self.upper_bound_generator()

    def get_lower_bound_for_objective_variable(self):
        ''' Returns 0 which is the lower bound on efficiency score which
            is minimized in the case of input-oriented envelopment model.

            Returns:
                double: zero.
        '''
        return 0

    def get_objective_type(self):
        ''' Returns pulp.LpMinimize - we minimize objective function in case
            of input-oriented envelopment model.

            Returns:
                pulp.LpMaximize: type of objective function.
        '''
        return pulp.LpMinimize

    def get_output_variable_coefficient(self, obj_variable, output_category):
        ''' Returns 1, since in input-oriented model we do not multiply
            current output by anything.

            Args:
                obj_variable (pulp.LpVariable): pulp variable that corresponds
                    to output category of the current DMU.
                output_category (str): output category for which current
                    constraint is being created.

            Returns:
                double: output variable coefficient.
        '''
        return 1

    def get_input_variable_coefficient(self, obj_variable, input_category):
        ''' Returns obj_variable, since in input-oriented model we multiply
            current input by efficiency spyDEA.core.

            Args:
                obj_variable (pulp.LpVariable): pulp variable that corresponds
                    to input category of current DMU.
                input_category (str): input category for which current
                    constraint is being created.

            Returns:
                pulp.LpVariable: input variable coefficient.
        '''
        return obj_variable

    def update_output_category_coefficient(self, current_output, constraint,
                                           obj_var, output_category):
        ''' Updates coefficient of a given output category with a new
            value.

            Args:
                current_output (double): new value for the coefficient.
                constraint (pulp.LpConstraint): constraint whose coefficient
                    should be updated.
                obj_var (pulp.LpVariable): variable of the envelopment
                    model that is optimised in the objective function.
                output_category (str): output category name.
        '''
        constraint.changeRHS(current_output)

    def update_input_category_coefficient(self, current_input, constraint,
                                          obj_var, input_category):
        ''' Updates coefficient of a given input category with a new
            value.

            Args:
                current_output (double): new value for the coefficient.
                constraint (pulp.LpConstraint): constraint whose coefficient
                    should be updated.
                obj_var (pulp.LpVariable): variable of the envelopment
                    model that is optimised in the objective function.
                output_category (str): input category name.
        '''
        constraint[obj_var] = current_input def generate_upper_bound_for_efficiency_score():
    ''' Returns upper bound for efficiency score for usual envelopment model.

        Note:
            This function only works with input-oriented envelopment models.

        Returns:
            double: 1, since efficiency score must be <= 1 for usual
                input-oriented envelopment model.
    '''
    return 1 class DefaultConstraintCreator(object):
    ''' This is a helper class that creates constraints for envelopment model
        with strongly disposable categories.
    '''
    def create(self, lhs, rhs, category):
        ''' By default all categories are strongly disposable.
            Hence all constraints have the form lhs >= rhs.

            Args:
                lhs (pulp.LpAffineExpression): left hand side of the constraint.
                rhs (pulp.LpAffineExpression): right hand side of the
                    constraint.
                category(str): input or output category (it is not used by
                    this class).
        '''
        return lhs >= rhs @pytest.fixture
def DEA_example_data(request):
    categories, xls_data, dmu_name, sheet_name = read_data(
        'tests/DEA_example_data.xls')
    coefficients, has_same_dmus = convert_to_dictionary(xls_data)
    assert has_same_dmus is False
    assert validate_data(categories, coefficients) is True
    request.addfinalizer(clean_up_pickled_files)
    return construct_input_data_instance(categories, coefficients)
import pytest
import datetime
import tests.utils_for_tests as utils_for_tests
from openpyxl import Workbook
from pyDEA.core.data_processing.read_data_from_xls import read_data
from pyDEA.core.data_processing.read_data_from_xls import construct_input_data_instance
from pyDEA.core.data_processing.read_data_from_xls import validate_data, convert_to_dictionary
from pyDEA.core.models.multiplier_model_base import MultiplierModelBase
from pyDEA.core.models.multiplier_model import MultiplierInputOrientedModel
from pyDEA.core.models.multiplier_model_decorators import MultiplierModelWithAbsoluteWeightRestrictions
from pyDEA.core.models.multiplier_model_decorators import MultiplierModelWithVirtualWeightRestrictions
from pyDEA.core.models.multiplier_model_decorators import MultiplierModelWithPriceRatioConstraints
from pyDEA.core.models.envelopment_model_decorators import EnvelopmentModelWithAbsoluteWeightRestrictions
from pyDEA.core.models.envelopment_model_decorators import EnvelopmentModelWithVirtualWeightRestrictions
from pyDEA.core.models.envelopment_model_decorators import EnvelopmentModelWithPriceRatioConstraints
from pyDEA.core.models.bound_generators import generate_upper_bound_for_efficiency_score
from pyDEA.core.data_processing.write_data_to_xls import XLSWriter
from pyDEA.core.data_processing.parameters import Parameters
from pyDEA.core.utils.dea_utils import clean_up_pickled_files
from pyDEA.core.data_processing.parameters import parse_parameters_from_file
from pyDEA.core.utils.model_builder import build_models
from pyDEA.core.models.envelopment_model_base import EnvelopmentModelBase
from pyDEA.core.models.envelopment_model import EnvelopmentModelInputOriented
from pyDEA.core.models.bound_generators import generate_upper_bound_for_efficiency_score
from pyDEA.core.models.envelopment_model_decorators import DefaultConstraintCreator
from tests.utils_for_tests import DEA_example_data

def test_price_ratio_multiplier_model(data):

    model = MultiplierModelBase(data, 0,
                                MultiplierInputOrientedModel())

    bounds = {('I1', 'I2'): (None, 0.4), ('O1', 'O2'): (0.01, None)}
    model = MultiplierModelWithPriceRatioConstraints(model, bounds)
    start_time = datetime.datetime.now()
    model_solution = model.run()
    end_time = datetime.datetime.now()
    utils_for_tests.check_if_category_is_within_price_ratio_constraints(
        model_solution, bounds)

    work_book = Workbook()
    writer = XLSWriter(Parameters(), work_book, datetime.datetime.today(),
                       (end_time - start_time).total_seconds())
    writer.write_data(model_solution)
    work_book.save('tests/test_price_ratio_multi_output.xls')


def test_all_constraints_multiplier_model(data):

    model = MultiplierModelBase(data, 0,
                                MultiplierInputOrientedModel())

    bounds = {'I1': (None, 0.4)}
    model = MultiplierModelWithVirtualWeightRestrictions(model, bounds)

    abs_bounds = {'I2': (None, 0.2)}
    model = MultiplierModelWithAbsoluteWeightRestrictions(model, abs_bounds)

    ratio_bounds = {('I1', 'I2'): (None, 0.4), ('O1', 'O2'): (0.01, None)}
    model = MultiplierModelWithPriceRatioConstraints(model, ratio_bounds)
    start_time = datetime.datetime.now()
    model_solution = model.run()
    end_time = datetime.datetime.now()
    utils_for_tests.check_if_category_is_within_abs_limits(
        model_solution, abs_bounds)
    utils_for_tests.check_if_category_is_within_virtual_limits(
        model_solution, bounds)
    utils_for_tests.check_if_category_is_within_price_ratio_constraints(
        model_solution, ratio_bounds)

    work_book = Workbook()
    writer = XLSWriter(Parameters(), work_book, datetime.datetime.today(),
                       (end_time - start_time).total_seconds())
    writer.write_data(model_solution)
    work_book.save('tests/test_all_constraints_multi_output.xls')


def test_abs_restrictions_env_model(data):
    model = EnvelopmentModelBase(data,
                                 EnvelopmentModelInputOriented(
                                     generate_upper_bound_for_efficiency_score),
                                 DefaultConstraintCreator())

    bounds = {'I2': (0.01, 0.5)}
    model = EnvelopmentModelWithAbsoluteWeightRestrictions(model, bounds)
    start_time = datetime.datetime.now()
    model_solution = model.run()
    end_time = datetime.datetime.now()
    utils_for_tests.check_if_category_is_within_abs_limits(
        model_solution, bounds)

    work_book = Workbook()
    writer = XLSWriter(Parameters(), work_book, datetime.datetime.today(),
                       (end_time - start_time).total_seconds())
    writer.write_data(model_solution)
    work_book.save('tests/test_abs_constraints_env_output.xls')


def test_abs_restrictions_env_model_output(data):
    filename = 'tests/DEA_Harish_parameters.txt'
    params = parse_parameters_from_file(filename)
    categories, data, dmu_name, sheet_name = read_data(
        params.get_parameter_value('DATA_FILE'))
    coefficients, has_same_dmus = convert_to_dictionary(data)
    model_input = construct_input_data_instance(categories, coefficients)
    models, all_params = build_models(params, model_input)
    assert len(models) == 1 and len(all_params) == 1
    model = models[0]

    start_time = datetime.datetime.now()
    model_solution = model.run()
    end_time = datetime.datetime.now()
    bounds = {'Urban Roads (%)': (None, 0.003)}
    utils_for_tests.check_if_category_is_within_abs_limits(
        model_solution, bounds)

    work_book = Workbook()
    writer = XLSWriter(Parameters(), work_book, datetime.datetime.today(),
                       (end_time - start_time).total_seconds())
    writer.write_data(model_solution)
    work_book.save('tests/test_abs_constraints_env_outoriented_output.xls')


def test_virual_restrictions_env_model(data):
    model = EnvelopmentModelBase(data,
                                 EnvelopmentModelInputOriented(
                                     generate_upper_bound_for_efficiency_score),
                                 DefaultConstraintCreator())

    bounds = {'I2': (0.01, 0.5)}
    model = EnvelopmentModelWithVirtualWeightRestrictions(model, bounds)
    start_time = datetime.datetime.now()
    model_solution = model.run()
    end_time = datetime.datetime.now()
    utils_for_tests.check_if_category_is_within_virtual_limits(
        model_solution, bounds)

    work_book = Workbook()
    writer = XLSWriter(Parameters(), work_book, datetime.datetime.today(),
                       (end_time - start_time).total_seconds())
    writer.write_data(model_solution)
    work_book.save('tests/test_virtual_constraints_env_output.xls')


def test_price_ratio_restrictions_env_model(data):
    model = EnvelopmentModelBase(data,

                                 EnvelopmentModelInputOriented(