Source code for chama.optimize

"""
The optimize module contains high-level solvers for sensor placement
optimization.

.. rubric:: Contents

.. autosummary::

    ImpactFormulation
    CoverageFormulation
"""

from __future__ import print_function, division
import pyomo.environ as pe
import chama.utils as cu
import numpy as np
import pandas as pd
from pyomo.opt import SolverStatus, TerminationCondition
import pyomo.environ as aml
import itertools

dummy_sensor_name = '__DUMMY_SENSOR_UNDETECTED__'

# ToDo: lookup how to reference a method in rst.


[docs]class ImpactFormulation(object): """ Sensor placement based on minimizing average impact across a set of scenarios. """ def __init__(self): self._model = None self._impact = None self._sensor = None self._scenario = None self._impact_col_name = 'Impact' self._use_scenario_probability = False self._use_sensor_cost = False self._solved = False
[docs] def solve(self, impact=None, sensor=None, scenario=None, sensor_budget=None, use_sensor_cost=False, use_scenario_probability=False, impact_col_name='Impact', mip_solver_name='glpk', pyomo_options=None, solver_options=None): """ Solves the sensor placement optimization by minimizing impact. Parameters ---------- impact : pandas DataFrame Impact assessment. Impact is stored as a pandas DataFrame with columns **Scenario**, **Sensor**, and **Impact**. Each row contains a single detection time (or other measure of impact/damage) for a sensor that detects a scenario. The column name for Impact can also be specified by the user using the argument 'impact_col_name'. sensor : pandas DataFrame Sensor characteristics. Contains sensor cost for each sensor. Sensor characteristics are stored as a pandas DataFrame with columns **Sensor** and **Cost**. Cost is used in the sensor placement optimization if the 'use_sensor_cost' flag is set to True. scenario : pandas DataFrame Scenario characteristics. Contains scenario probability and the impact for undetected scenarios. Scenario characteristics are stored as a pandas DataFrame with columns **Scenario**, **Undetected Impact**, and **Probability**. Undetected Impact is required for each scenario. Probability is used if the 'use_scenario_probability' flag is set to True. sensor_budget : float The total budget available for purchase/installation of sensors. Solution will select a family of sensors whose combined cost is below the sensor_budget. For a simple sensor budget of N sensors, set this to N and the 'use_sensor_cost' to False. use_sensor_cost : bool Boolean indicating if sensor cost should be used in the optimization. If False, sensors have equal cost of 1. use_scenario_probability : bool Boolean indicating if scenario probability should be used in the optimization. If False, scenarios have equal probability. impact_col_name : str The name of the column containing the impact data to be used in the objective function. mip_solver_name : str Optimization solver name passed to Pyomo. The solver must be supported by Pyomo and support solution of mixed-integer programming problems. pyomo_options : dict Keyword arguments to be passed to the Pyomo solver .solve method Defaults to an empty dictionary. solver_options : dict Solver specific options to pass through Pyomo to the underlying solver. Defaults to an empty dictionary. Returns ------- A dictionary with the following keys: * Sensors: A list of the selected sensors * Objective: The mean impact based on the selected sensors * FractionDetected: The fraction of scenarios that were detected * TotalSensorCost: Total cost of the selected sensors * Assessment: The impact value for each sensor-scenario pair. The assessment is stored as a pandas DataFrame with columns **Scenario**, **Sensor**, and **Impact** (same format as the input Impact assessment). If the selected sensors did not detect a particular scenario, the impact is set to the Undetected Impact. """ self.create_pyomo_model(impact=impact, sensor=sensor, scenario=scenario, sensor_budget=sensor_budget, use_sensor_cost=use_sensor_cost, use_scenario_probability=use_scenario_probability, impact_col_name=impact_col_name) self.solve_pyomo_model(sensor_budget=sensor_budget, mip_solver_name=mip_solver_name, pyomo_options=pyomo_options, solver_options=solver_options) results_dict = self.create_solution_summary() return results_dict
[docs] def create_pyomo_model(self, impact=None, sensor=None, scenario=None, sensor_budget=None, use_sensor_cost=False, use_scenario_probability=False, impact_col_name='Impact'): """ Returns the Pyomo model. See :py:meth:`ImpactFormulation.solve` for more information on parameters. Returns ------- Pyomo ConcreteModel ready to be solved """ # reset the internal model and data attributes # BLN: Why do we reset these when they will be overwritten at the # end of this method anyway? self._model = None self._impact = None self._sensor = None self._scenario = None # validate the pandas dataframe input cu._df_columns_required('impact', impact, {'Scenario': object, 'Sensor': object, impact_col_name: [float, int]}) cu._df_nans_not_allowed('impact', impact) if sensor is not None: cu._df_columns_required('sensor', sensor, {'Sensor': object}) cu._df_nans_not_allowed('sensor', sensor) sensor = sensor.set_index('Sensor') assert(sensor.index.names[0] == 'Sensor') cu._df_columns_required('scenario', scenario, {'Scenario': object, 'Undetected Impact': [float, int]}) cu._df_nans_not_allowed('scenario', scenario) # validate optional columns in pandas dataframe input if use_scenario_probability: cu._df_columns_required('scenario', scenario, {'Probability': float}) if use_sensor_cost: if sensor is None: raise ValueError( 'ImpactFormulation: use_sensor_cost cannot be ' 'True if "sensor" DataFrame is not provided.') cu._df_columns_required('sensor', sensor, {'Cost': [float, int]}) # Notice, setting the index here impact = impact.set_index(['Scenario', 'Sensor']) assert(impact.index.names[0] == 'Scenario') assert(impact.index.names[1] == 'Sensor') # Python set will extract the unique Scenario and Sensor values scenario_list = sorted(scenario['Scenario'].unique()) # Always get sensor list from impact DataFrame in case there are # sensors in the sensor DataFrame that didn't detect anything and # therefore do not appear in the impact DataFrame sensor_list = sorted(set(impact.index.get_level_values('Sensor'))) if use_sensor_cost: sensor_cost = sensor['Cost'] else: sensor_cost = pd.Series(data=[1.0]*len(sensor_list), index=sensor_list) # Add in the data for the dummy sensor to account for a scenario that # is undetected sensor_list.append(dummy_sensor_name) df_dummy = pd.DataFrame(scenario_list, columns=['Scenario']) df_dummy = df_dummy.set_index(['Scenario']) scenario = scenario.set_index(['Scenario']) df_dummy[impact_col_name] = scenario['Undetected Impact'] scenario.reset_index(level=[0], inplace=True) df_dummy['Sensor'] = dummy_sensor_name df_dummy = df_dummy.reset_index().set_index(['Scenario', 'Sensor']) impact = pd.concat([impact, df_dummy]) sensor_cost[dummy_sensor_name] = 0.0 # Create a list of tuples for all the scenario/sensor pairs where # detection has occurred scenario_sensor_pairs = impact.index.tolist() # Create the (jagged) index set of sensors that were able to detect a # particular scenario scenario_sensors = dict() for (a, i) in scenario_sensor_pairs: if a not in scenario_sensors: scenario_sensors[a] = list() scenario_sensors[a].append(i) # create the model container model = pe.ConcreteModel() model.scenario_sensors = scenario_sensors # Pyomo does not create an ordered dummy set when passed a list - do # this for now as a workaround model.scenario_set = pe.Set(initialize=scenario_list, ordered=True) model.sensor_set = pe.Set(initialize=sensor_list, ordered=True) model.scenario_sensor_pairs_set = \ pe.Set(initialize=scenario_sensor_pairs, ordered=True) # create mutable parameter that may be changed model.sensor_budget = pe.Param(initialize=sensor_budget, mutable=True, within=aml.Any) # x_{a,i} variable indicates which sensor is the first to detect # scenario a model.x = pe.Var(model.scenario_sensor_pairs_set, bounds=(0, 1)) # y_i variable indicates if a sensor is installed or not model.y = pe.Var(model.sensor_set, within=pe.Binary) # objective function minimize the sum impact across all scenarios if use_scenario_probability: scenario.set_index(['Scenario'], inplace=True) model.obj = pe.Objective(expr= \ sum(float(scenario.at[a, 'Probability']) * float(impact[impact_col_name].loc[a, i]) * model.x[a, i] for (a, i) in scenario_sensor_pairs)) else: model.obj = pe.Objective(expr= \ 1.0 / float(len(scenario_list)) * sum(float(impact[impact_col_name].loc[a, i]) * model.x[a, i] for (a, i) in scenario_sensor_pairs)) # constrain the problem to have only one x value for each scenario def limit_x_rule(m, a): return sum(m.x[a, i] for i in scenario_sensors[a]) == 1 model.limit_x = pe.Constraint(model.scenario_set, rule=limit_x_rule) # can only detect scenario a with location i if location i is selected def detect_only_if_sensor_rule(m, a, i): return m.x[a, i] <= model.y[i] model.detect_only_if_sensor = \ pe.Constraint(model.scenario_sensor_pairs_set, rule=detect_only_if_sensor_rule) # limit the number of sensors model.total_sensor_cost = pe.Expression(expr=sum(float(sensor_cost[i]) * model.y[i] for i in sensor_list)) model.sensor_budget_con = pe.Constraint(expr= model.total_sensor_cost <= model.sensor_budget) self._model = model impact.reset_index(inplace=True) self._impact = impact self._sensor = sensor scenario.reset_index(inplace=True) self._scenario = scenario self._impact_col_name = impact_col_name self._use_sensor_cost = use_sensor_cost self._use_scenario_probability = use_scenario_probability # Any changes to the model require re-solving self._solved = False return model
[docs] def add_grouping_constraint(self, sensor_list, select=None, min_select=None, max_select=None): """ Adds a sensor grouping constraint to the sensor placement model. This constraint forces a certain number of sensors to be selected from a particular subset of all the possible sensors. The keyword argument 'select' enforces an equality constraint, while 'min_select' and 'max_select' correspond to lower and upper bounds on the grouping constraints, respectively. You can specify one or both of 'min_select' and 'max_select' OR use 'select' Parameters ---------- sensor_list : list of strings List containing the string names of a subset of the sensors select : positive integer or None The exact number of sensors from the sensor_list that should be selected min_select : positive integer or None The minimum number of sensors from the sensor_list that should be selected max_select : positive integer or None The maximum number of sensors from the sensor_list that should be selected """ _add_grouping_constraint(self, sensor_list=sensor_list, select=select, min_select=min_select, max_select=max_select)
[docs] def solve_pyomo_model(self, sensor_budget=None, mip_solver_name='glpk', pyomo_options=None, solver_options=None): """ Solves the Pyomo model created to perform the sensor placement. See :py:meth:`ImpactFormulation.solve` for more information on parameters. """ if self._model is None: raise RuntimeError('Cannot call solve_pyomo_model before the model' ' is created with create_pyomo_model' ) # change the sensor budget if necessary if sensor_budget is not None: self._model.sensor_budget = sensor_budget (solved, results) = _solve_pyomo_model(self._model, mip_solver_name=mip_solver_name, pyomo_options=pyomo_options, solver_options=solver_options) self._solved = solved
[docs] def create_solution_summary(self): """ Creates a dictionary representing common summary information about the solution from a Pyomo model object that has already been solved. See :py:meth:`ImpactFormulation.solve` for more information on the solution summary. Returns ------- Dictionary containing a summary of results. """ if self._model is None: raise RuntimeError('Cannot call create_solution_summary before ' 'the model is created and solved.' ) if not self._solved: return {'Solved': self._solved, 'Objective': None, 'Sensors': None, 'ImpactAssessment': None} model = self._model impact_df = self._impact scenario_df = self._scenario selected_sensors = [] for key in model.y: if pe.value(model.y[key]) > 0.5: if key != dummy_sensor_name: selected_sensors.append(key) obj_value = pe.value(model.obj) selected_impact = {'Scenario': [], 'Sensor': [], 'Impact': []} for key in model.x: scenario = key[0] sensor = key[1] if pe.value(model.x[(scenario, sensor)]) > 0.5: if sensor == dummy_sensor_name: sensor = None impact_val = \ scenario_df[scenario_df['Scenario'] == scenario]['Undetected Impact'].values[0] else: impact_val = \ impact_df[(impact_df['Scenario'] == scenario) & (impact_df['Sensor'] == sensor)][ self._impact_col_name].values[0] selected_impact['Scenario'].append(scenario) selected_impact['Sensor'].append(sensor) selected_impact['Impact'].append(impact_val) selected_impact = pd.DataFrame(selected_impact) selected_impact = selected_impact[['Scenario', 'Sensor', 'Impact']] frac_detected = 0 for a in model.scenario_set: detected = sum(pe.value(model.x[a,i]) for i in model.scenario_sensors[a] if i != dummy_sensor_name) eps = 1e-6 assert(detected >= 0-eps and detected <= 1+eps) if detected > 0.5: frac_detected += 1 frac_detected = float(frac_detected)/float(len(model.scenario_set)) return {'Solved': self._solved, 'Objective': obj_value, 'Sensors': selected_sensors, 'FractionDetected': frac_detected, 'TotalSensorCost': pe.value(model.total_sensor_cost), 'Assessment': selected_impact}
[docs]class CoverageFormulation(object): """ Sensor placement based on maximizing coverage of a set of entities. An 'entity' can represent geographic areas, scenarios, or scenario-time pairs. """ def __init__(self): self._model = None
[docs] def solve(self, coverage, sensor=None, entity=None, sensor_budget=None, use_sensor_cost=None, use_entity_weight=False, redundancy=0, coverage_col_name='Coverage', mip_solver_name='glpk', pyomo_options=None, solver_options=None): """ Solves the sensor placement optimization by maximizing coverage. Parameters ---------- coverage : pandas DataFrame Coverage data. Coverage is stored as a pandas DataFrame with columns **Sensor** and **Coverage**. Each row contains a list of entities that are covered by single sensor. The column name for Coverage can also be specified by the user using the argument 'coverage_col_name'. sensor : pandas DataFrame Sensor characteristics. Contains sensor cost for each sensor. Sensor characteristics are stored as a pandas DataFrame with columns **Sensor** and **Cost**. Cost is used in the sensor placement optimization if the 'use_sensor_cost' flag is set to True. entity : pandas DataFrame Entity characteristics. Contains entity weights. Entity characteristics are stored as a pandas DataFrame with columns **Entity** and **Weight**. Weight is used if the 'use_entity_weight' flag is set to True. sensor_budget : float The total budget available for purchase/installation of sensors. Solution will select a family of sensors whose combined cost is below the sensor_budget. For a simple sensor budget of N sensors, set this to N and the 'use_sensor_cost' to False. use_sensor_cost : bool Boolean indicating if sensor cost should be used in the optimization. If False, sensors have equal cost of 1. use_entity_weight : bool Boolean indicating if entity weights should be used in the optimization. If False, entities have equal weight. redundancy : int Redundancy level. A value of 0 means only one sensor is required to covered an entity, whereas a value of 1 means two sensors must cover an entity before it considered covered. coverage_col_name : str The name of the column containing the coverage data to be used in the objective function. mip_solver_name : str Optimization solver name passed to Pyomo. The solver must be supported by Pyomo and support solution of mixed-integer programming problems. pyomo_options : dict Keyword arguments to be passed to the Pyomo solver .solve method. Defaults to an empty dictionary. solver_options : dict Solver specific options to pass through Pyomo to the underlying solver. Defaults to an empty dictionary. Returns ------- A dictionary with the following keys: * Sensors: A list of the selected sensors * Objective: The mean coverage based on the selected sensors * FractionDetected: the fraction of entities that are detected * TotalSensorCost: Total cost of the selected sensors * EntityAssessment: a dictionary whose keys are the entity names, and values are a list of sensors that detect that entity * SensorAssessment: a dictionary whose keys are the sensor names, and values are the list of entities that are detected by that sensor """ self.create_pyomo_model(coverage=coverage, sensor=sensor, entity=entity, sensor_budget=sensor_budget, use_sensor_cost=use_sensor_cost, use_entity_weight=use_entity_weight, redundancy=redundancy, coverage_col_name=coverage_col_name) self.solve_pyomo_model(sensor_budget=sensor_budget, mip_solver_name=mip_solver_name, pyomo_options=pyomo_options, solver_options=solver_options) # might want to throw this exception, might want to pass this through to the results object if not self._model.solved: raise RuntimeError("Optimization failed to solve. Please set pyomo_options={'tee': True}" " and check solver logs.") results_dict = self.create_solution_summary() return results_dict
[docs] def create_pyomo_model(self, coverage, sensor=None, entity=None, sensor_budget=None, use_sensor_cost=False, use_entity_weight=False, redundancy=0, coverage_col_name='Coverage'): """ Returns the Pyomo model. See :py:meth:`CoverageFormulation.solve` for more information on parameters. Returns ------- Pyomo ConcreteModel ready to be solved """ self._model = None self._model= model = pe.ConcreteModel() # build the list of entities from the coverage DataFrame covered_items = coverage['Coverage'].tolist() entity_list = cu._unique_items_from_list_of_lists(covered_items) if entity is None: if use_entity_weight: raise ValueError('CoverageFormulation: use_entity_weight cannot be True if' '"entity" DataFrame is not provided.') else: # add potential additional entities from the entity DataFrame additional_entities = set(entity['Entity'].unique()) entity_list = entity_list.union(additional_entities) entity_list = sorted(entity_list) # TODO: Add DataFrame column checks like in the ImpactFormulation if sensor is None and use_sensor_cost: raise ValueError('CoverageFormulation: use_sensor_cost cannot be True if "sensor" DataFrame is not provided.') # Always get sensor list from coverage DataFrame in case there are # sensors in the sensor DataFrame that didn't detect anything and # therefore do not appear in the coverage DataFrame sensor_list = sorted(coverage['Sensor'].unique()) # make a series of the coverage column (for faster access) coverage_series = coverage.set_index('Sensor')[coverage_col_name] # create a dictionary of sets where the key is the entity, and the # value is the set of sensors that covers that entity entity_sensors = {e:set() for e in entity_list} for s in sensor_list: s_entities = coverage_series[s] for e in s_entities: entity_sensors[e].add(s) for e in entity_sensors.keys(): entity_sensors[e] = list(sorted(entity_sensors[e])) model.entity_list = pe.Set(initialize=entity_list, ordered=True) model.sensor_list = pe.Set(initialize=sensor_list, ordered=True) if redundancy > 0: model.x = pe.Var(model.entity_list, within=pe.Binary) else: model.x = pe.Var(model.entity_list, bounds=(0,1)) model.y = pe.Var(model.sensor_list, within=pe.Binary) if use_entity_weight: entity_weights = entity.set_index('Entity')['Weight'] # Check for missing entity weights, set to 0 missing_entities = set(entity_list) - set(entity_weights.index) assert len(missing_entities) == 0, "Missing entity weights , " + str(missing_entities) model.obj = pe.Objective(expr=sum(float(entity_weights[e])*model.x[e] for e in entity_list), sense=pe.maximize) else: model.obj = pe.Objective(expr=sum(model.x[e] for e in entity_list), sense=pe.maximize) def entity_covered_rule(m, e): if redundancy > 0: return (redundancy + 1.0)*m.x[e] <= sum(m.y[b] for b in entity_sensors[e]) return m.x[e] <= sum(m.y[b] for b in entity_sensors[e]) model.entity_covered = pe.Constraint(model.entity_list, rule=entity_covered_rule) if sensor_budget is None: if use_sensor_cost: raise ValueError('CoverageFormulation: sensor_budget must be specified if use_sensor_cost is set to True.') sensor_budget = len(sensor_list) # no sensor budget provided - allow all sensors model.sensor_budget = pe.Param(initialize=sensor_budget, mutable=True, within=aml.Any) if use_sensor_cost: sensor_cost = sensor.set_index('Sensor')['Cost'] model.total_sensor_cost = pe.Expression(expr=sum(sensor_cost[s]*model.y[s] for s in sensor_list)) else: model.total_sensor_cost = pe.Expression(expr=sum(model.y[s] for s in sensor_list)) model.sensor_upper_limit = pe.Constraint(expr= model.total_sensor_cost <= model.sensor_budget) model.entity_sensors = entity_sensors model.solved = False self._model = model return model
[docs] def add_grouping_constraint(self, sensor_list, select=None, min_select=None, max_select=None): """ Adds a sensor grouping constraint to the sensor placement model. This constraint forces a certain number of sensors to be selected from a particular subset of all the possible sensors. The keyword argument 'select' enforces an equality constraint, while 'min_select' and 'max_select' correspond to lower and upper bounds on the grouping constraints, respectively. You can specify one or both of 'min_select' and 'max_select' OR use 'select' Parameters ---------- sensor_list : list of strings List containing the string names of a subset of the sensors select : positive integer or None The exact number of sensors from the sensor_list that should be selected min_select : positive integer or None The minimum number of sensors from the sensor_list that should be selected max_select : positive integer or None The maximum number of sensors from the sensor_list that should be selected """ _add_grouping_constraint(self, sensor_list=sensor_list, select=select, min_select=min_select, max_select=max_select)
[docs] def solve_pyomo_model(self, sensor_budget=None, mip_solver_name='glpk', pyomo_options=None, solver_options=None): """ Solves the Pyomo model created to perform the sensor placement. See :py:meth:`CoverageFormulation.solve` for more information on parameters. """ if self._model is None: raise RuntimeError('Cannot call solve_pyomo_model before the model' ' is created with create_pyomo_model' ) self._model.solved = False # change the sensor budget if necessary if sensor_budget is not None: self._model.sensor_budget = sensor_budget (solved, results) = _solve_pyomo_model(self._model, mip_solver_name=mip_solver_name, pyomo_options=pyomo_options, solver_options=solver_options) self._model.solved = solved
[docs] def create_solution_summary(self): """ Creates a dictionary representing common summary information about the solution from a Pyomo model object that has already been solved. See :py:meth:`CoverageFormulation.solve` for more information on the solution summary. Returns ------- Dictionary containing a summary of results. """ if self._model is None: raise RuntimeError('Cannot call create_solution_summary before ' 'the model is created and solved.' ) model = self._model if not model.solved: return {'Solved': model.solved, 'Objective': None, 'Sensors': None, 'FractionDetected': None, 'EntityAssessment': None, 'SensorAssessment': None} selected_sensors = [] for key in model.y: if pe.value(model.y[key]) > 0.5: selected_sensors.append(key) obj_value = pe.value(model.obj) frac_detected = sum(pe.value(model.x[e]) for e in model.x)/(len(model.x)) entity_assessment = {e:[] for e in model.entity_list} for e in model.entity_list: for s in model.entity_sensors[e]: if pe.value(model.y[s]) > 0.5: entity_assessment[e].append(s) sensor_assessment = dict() for s in model.sensor_list: if pe.value(model.y[s]) > 0.5: sensor_assessment[s] = [e for e in model.entity_list if s in model.entity_sensors[e]] return {'Solved': model.solved, 'Objective': obj_value, 'Sensors': selected_sensors, 'FractionDetected': frac_detected, 'TotalSensorCost': pe.value(model.total_sensor_cost), 'EntityAssessment': entity_assessment, 'SensorAssessment': sensor_assessment}
def _add_grouping_constraint(self, sensor_list, select=None, min_select=None, max_select=None): #TODO: Should we make this easier by just allowing lower bound and #upper bound and do an equality if they are the same? if self._model is None: raise RuntimeError('Cannot add a grouping constraint to a' 'nonexistent model. Please call the ' 'create_pyomo_model function before trying to ' 'add grouping constraints') if select is not None and min_select is not None: raise ValueError('Invalid keyword arguments for adding grouping ' 'constraint. Cannot specify both a "select" ' 'value and a "min_select" value') if select is not None and max_select is not None: raise ValueError('Invalid keyword arguments for adding grouping ' 'constraint. Cannot specify both a "select" ' 'value and a "max_select" value') if select is None and max_select is None and min_select is None: raise ValueError('Must specify a sensor selection limit for the ' 'grouping constraint.') gconlist = self._model.find_component('_groupingconlist') if gconlist is None: self._model._groupingconlist = pe.ConstraintList() gconlist = self._model._groupingconlist # Check to make sure all sensors are valid and build sum expression sensor_sum = sum(self._model.y[i] for i in sensor_list) if select is not None: # Select exactly 'select' sensors from sensor_list if select < 0: raise ValueError('Cannot select a negative number of sensors') gconlist.add(sensor_sum == select) elif min_select is not None and max_select is not None: # Select between min_select and max_select sensors from # sensor_list if (min_select < 0) or (max_select < 0): raise ValueError('Cannot select a negative number of sensors') if min_select > max_select: raise ValueError('min_select must be less than max_select') #gconlist.add(min_select <= sensor_sum <= max_select) # Chained inequalities are deprecated gconlist.add(aml.inequality(min_select, sensor_sum, max_select)) elif min_select is not None: # Select at least min_select sensors from sensor list if min_select < 0: raise ValueError('Cannot select a negative number of sensors') gconlist.add(min_select <= sensor_sum) else: # Select at most max_select sensors from sensor list if max_select < 0: raise ValueError('Cannot select a negative number of sensors') gconlist.add(sensor_sum <= max_select) # Any changes to the model require re-solving self._solved = False def _solve_pyomo_model(model, mip_solver_name='glpk', pyomo_options=None, solver_options=None): """ Internal method to solve the Pyomo model and check the optimization status """ if pyomo_options is None: pyomo_options = {} if solver_options is None: solver_options = {} if model is None: raise RuntimeError('Cannot call solve_pyomo_model before the model' ' is created with create_pyomo_model' ) # create the solver opt = pe.SolverFactory(mip_solver_name) results = opt.solve(model, options=solver_options, **pyomo_options) # Check solver status solved = None if (results.solver.status == SolverStatus.ok) and \ (results.solver.termination_condition == TerminationCondition.optimal): solved = True else: solved = False print('The solver was unable to find an optimal solution') return (solved, results)