Skip to content

Commit

Permalink
Deprecate dir in favor of direction
Browse files Browse the repository at this point in the history
  • Loading branch information
dhadka committed Sep 10, 2024
1 parent 177e8a8 commit d7e91f2
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 38 deletions.
29 changes: 25 additions & 4 deletions rhodium/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import scipy.stats as stats
from collections import OrderedDict
from abc import ABCMeta, abstractmethod
from enum import Enum
from platypus import Real, Integer, Permutation, Subset
from .expr import _evaluate_all

Expand Down Expand Up @@ -56,6 +57,12 @@ def __init__(self, name, default_value=None, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)

class Direction(Enum):
MINIMIZE = -1
MAXIMIZE = 1
INFO = 2
IGNORE = 0

class Response(NamedObject):
"""Defines a model response (i.e., output).
Expand All @@ -66,17 +73,31 @@ class Response(NamedObject):
participate in optimization.
"""

# These constants are deprecated. Use the Direction enum instead.
MINIMIZE = -1
MAXIMIZE = 1
MAXIMIZE = 1
INFO = 2
IGNORE = 0

def __init__(self, name, dir=INFO, **kwargs):
def __init__(self, name, direction=Direction.INFO, dir=None, **kwargs):
super().__init__(name)
self.dir = dir

self.direction = Direction(direction)

if dir is not None:
warnings.warn(f"'dir' is deprecated, use 'direction' instead", DeprecationWarning, stacklevel=2)
if isinstance(dir, Direction):
self.direction = dir
else:
self.direction = Direction(dir)

for k, v in kwargs.items():
setattr(self, k, v)

def __getattr__(self, name):
if name == 'dir':
warnings.warn(f"'dir' is deprecated, use 'direction' instead", DeprecationWarning, stacklevel=2)
return self.direction.value
raise AttributeError(name=name, obj=self)

_eval_env = {}
module = __import__("math", fromlist=[''])
Expand Down
30 changes: 15 additions & 15 deletions rhodium/optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import functools
from collections import OrderedDict
from platypus import Job, Problem, unique, nondominated
from .model import Response, DataSet
from .model import Direction, Response, DataSet

def generate_jobs(model, samples):
if isinstance(samples, dict):
Expand Down Expand Up @@ -55,10 +55,10 @@ def run(self):
offset = 0

for response in self.model.responses:
if response.dir is not Response.IGNORE:
if response.direction is not Direction.IGNORE:
args[response.name] = raw_output[offset]
offset += 1
elif len(self.model.responses) == 1 and self.model.responses[0].dir != Response.IGNORE:
elif len(self.model.responses) == 1 and self.model.responses[0].direction != Direction.IGNORE:
args[self.model.responses[0].name] = raw_output

self.output = args
Expand Down Expand Up @@ -90,7 +90,7 @@ def _evaluation_function(vars, model, nvars, nobjs, nconstrs, levers):
job = EvaluateJob(model, env)
job.run()

objectives = [job.output[r.name] for r in model.responses if r.dir in [Response.MINIMIZE, Response.MAXIMIZE]]
objectives = [job.output[r.name] for r in model.responses if r.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]]
constraints = [constraint.distance(job.output) for constraint in model.constraints]

if nconstrs > 0:
Expand All @@ -108,7 +108,7 @@ def _to_problem(model):
levers.append((lever, len(vars)))

nvars = len(variables)
nobjs = sum([1 if r.dir == Response.MINIMIZE or r.dir == Response.MAXIMIZE else 0 for r in model.responses])
nobjs = sum([1 if r.direction in [Direction.MINIMIZE, Direction.MAXIMIZE] else 0 for r in model.responses])
nconstrs = len(model.constraints)

function = functools.partial(_evaluation_function,
Expand All @@ -120,7 +120,7 @@ def _to_problem(model):

problem = Problem(nvars, nobjs, nconstrs, function)
problem.types[:] = variables
problem.directions[:] = [Problem.MINIMIZE if r.dir == Response.MINIMIZE else Problem.MAXIMIZE for r in model.responses if r.dir == Response.MINIMIZE or r.dir == Response.MAXIMIZE]
problem.directions[:] = [r.direction.value for r in model.responses if r.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]]
problem.constraints[:] = "==0"
return (problem, levers)

Expand Down Expand Up @@ -153,7 +153,7 @@ def optimize(model, algorithm="NSGAII", NFE=10000, module="platypus", **kwargs):
env[lever.name] = lever.from_variables(vars[offset:(offset+length)])
offset += length

if any([r.dir not in [Response.MINIMIZE, Response.MAXIMIZE] for r in model.responses]):
if any([r.direction not in [Direction.MINIMIZE, Direction.MAXIMIZE] for r in model.responses]):
# if there are any responses not included in the optimization, we must
# re-evaluate the model to get all responses
env = evaluate(model, env)
Expand All @@ -170,7 +170,7 @@ def _robust_evaluation_function(vars, model, SOWs, nvars, nobjs, nconstrs, lever
constraints = {}

for response in model.responses:
if response.dir in [Response.MINIMIZE, Response.MAXIMIZE]:
if response.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]:
objectives[response] = []

for constraint in model.constraints:
Expand All @@ -188,16 +188,16 @@ def _robust_evaluation_function(vars, model, SOWs, nvars, nobjs, nconstrs, lever
job.run()

for response in model.responses:
if response.dir in [Response.MINIMIZE, Response.MAXIMIZE]:
if response.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]:
objectives[response].append(job.output[response.name])

for constraint in model.constraints:
constraints[constraint].append(constraint.distance(job.output))

if isinstance(obj_aggregate, dict):
objective_values = [obj_aggregate[r.name](objectives[r]) for r in model.responses if r.dir in [Response.MINIMIZE, Response.MAXIMIZE]]
objective_values = [obj_aggregate[r.name](objectives[r]) for r in model.responses if r.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]]
else:
objective_values = [obj_aggregate(objectives[r]) for r in model.responses if r.dir in [Response.MINIMIZE, Response.MAXIMIZE]]
objective_values = [obj_aggregate(objectives[r]) for r in model.responses if r.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]]

if isinstance(constr_aggregate, dict):
constraint_values = [constr_aggregate[c](constraints[c]) for c in model.constraints]
Expand All @@ -219,7 +219,7 @@ def _to_robust_problem(model, SOWs, obj_aggregate, constr_aggregate):
levers.append((lever, len(vars)))

nvars = len(variables)
nobjs = sum([1 if r.dir == Response.MINIMIZE or r.dir == Response.MAXIMIZE else 0 for r in model.responses])
nobjs = sum([1 if r.direction in [Direction.MINIMIZE, Direction.MAXIMIZE] else 0 for r in model.responses])
nconstrs = len(model.constraints)

function = functools.partial(_robust_evaluation_function,
Expand All @@ -234,7 +234,7 @@ def _to_robust_problem(model, SOWs, obj_aggregate, constr_aggregate):

problem = Problem(nvars, nobjs, nconstrs, function)
problem.types[:] = variables
problem.directions[:] = [Problem.MINIMIZE if r.dir == Response.MINIMIZE else Problem.MAXIMIZE for r in model.responses if r.dir == Response.MINIMIZE or r.dir == Response.MAXIMIZE]
problem.directions[:] = [r.direction.value for r in model.responses if r.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]]
problem.constraints[:] = "==0"
return (problem, levers)

Expand Down Expand Up @@ -271,13 +271,13 @@ def robust_optimize(model, SOWs, algorithm="NSGAII", NFE=10000, obj_aggregate=No
env[lever.name] = lever.from_variables(vars[offset:(offset+length)])
offset += length

if any([r.dir not in [Response.MINIMIZE, Response.MAXIMIZE] for r in model.responses]):
if any([r.direction not in [Direction.MINIMIZE, Direction.MAXIMIZE] for r in model.responses]):
# if there are any responses not included in the optimization, we must
# re-evaluate the model to get all responses
env = evaluate(model, env)

# here we copy over the objectives from the evaluated solution, which has been aggregated over all SOWs
for i, response in enumerate([r for r in model.responses if r.dir in [Response.MINIMIZE, Response.MAXIMIZE]]):
for i, response in enumerate([r for r in model.responses if r.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]]):
env[response.name] = solution.objectives[i]

result.append(env)
Expand Down
22 changes: 11 additions & 11 deletions rhodium/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from scipy.interpolate import griddata
from matplotlib.legend_handler import HandlerPatch
from .config import RhodiumConfig
from .model import Response
from .model import Direction, Response
from .brush import BrushSet, apply_brush, color_brush, brush_color_map, color_indices

# When set, override the plt.show() method to save. Intended for CI
Expand Down Expand Up @@ -765,10 +765,10 @@ def parallel_coordinates(model, data, c=None, cols=None, ax=None, colors=None,

for i in range(ncols):
if target == "top":
if model.responses[df.columns.values[i]].dir == Response.MINIMIZE:
if model.responses[df.columns.values[i]].direction == Direction.MINIMIZE:
df.iloc[:, i] = 1-df.iloc[:, i]
elif target == "bottom":
if model.responses[df.columns.values[i]].dir == Response.MAXIMIZE:
if model.responses[df.columns.values[i]].direction == Direction.MAXIMIZE:
df.iloc[:, i] = 1-df.iloc[:, i]

# determine values to use for xticks
Expand Down Expand Up @@ -833,32 +833,32 @@ def parallel_coordinates(model, data, c=None, cols=None, ax=None, colors=None,
format = "%.2f"

if target == "top":
value = df_min.iloc[i] if model.responses[df.columns.values[i]].dir == Response.MINIMIZE else df_max.iloc[i]
value = df_min.iloc[i] if model.responses[df.columns.values[i]].direction == Direction.MINIMIZE else df_max.iloc[i]

if model.responses[df.columns.values[i]].dir != Response.INFO:
if model.responses[df.columns.values[i]].direction != Direction.INFO:
format = format + "*"
elif target == "bottom":
value = df_max.iloc[i] if model.responses[df.columns.values[i]].dir == Response.MINIMIZE else df_min.iloc[i]
value = df_max.iloc[i] if model.responses[df.columns.values[i]].direction == Direction.MINIMIZE else df_min.iloc[i]
else:
value = df_max.iloc[i]

if model.responses[df.columns.values[i]].dir == Response.MAXIMIZE:
if model.responses[df.columns.values[i]].direction == Direction.MAXIMIZE:
format = format + "*"

ax.text(i, 1.001, format % value, ha="center", fontsize=10)
format = "%.2f"

if target == "top":
value = df_max.iloc[i] if model.responses[df.columns.values[i]].dir == Response.MINIMIZE else df_min.iloc[i]
value = df_max.iloc[i] if model.responses[df.columns.values[i]].direction == Direction.MINIMIZE else df_min.iloc[i]
elif target == "bottom":
value = df_min.iloc[i] if model.responses[df.columns.values[i]].dir == Response.MINIMIZE else df_max.iloc[i]
value = df_min.iloc[i] if model.responses[df.columns.values[i]].direction == Direction.MINIMIZE else df_max.iloc[i]

if model.responses[df.columns.values[i]].dir != Response.INFO:
if model.responses[df.columns.values[i]].direction != Direction.INFO:
format = format + "*"
else:
value = df_min.iloc[i]

if model.responses[df.columns.values[i]].dir == Response.MINIMIZE:
if model.responses[df.columns.values[i]].direction == Direction.MINIMIZE:
format = format + "*"

ax.text(i, -0.001, format % value, ha="center", va="top", fontsize=10)
Expand Down
10 changes: 5 additions & 5 deletions rhodium/robustness.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# along with Rhodium. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import scipy.spatial as sp
from .model import DataSet, Response, populate_defaults, update
from .model import DataSet, Direction, Response, populate_defaults, update
from .sampling import sample_lhs
from .optimization import evaluate

Expand Down Expand Up @@ -45,7 +45,7 @@ def regret_type1(model, results, baseline, percentile=90):
quantiles = []

for response in model.responses:
if response.dir == Response.MINIMIZE or response.dir == Response.MAXIMIZE:
if response.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]:
values = [abs((result[response.name] - baseline[response.name]) / baseline[response.name]) for result in results]
quantiles.append(np.percentile(values, percentile))

Expand All @@ -59,9 +59,9 @@ def regret_type2(model, all_results, baseline_results, percentile=90):
entry = {}

for response in model.responses:
if response.dir == Response.MINIMIZE:
if response.direction == Direction.MINIMIZE:
entry[response.name] = min([result[i][response.name] for result in all_results])
elif response.dir == Response.MAXIMIZE:
elif response.direction == Direction.MAXIMIZE:
entry[response.name] = max([result[i][response.name] for result in all_results])

best.append(entry)
Expand All @@ -70,7 +70,7 @@ def regret_type2(model, all_results, baseline_results, percentile=90):
quantiles = []

for response in model.responses:
if response.dir == Response.MINIMIZE or response.dir == Response.MAXIMIZE:
if response.direction in [Direction.MINIMIZE, Direction.MAXIMIZE]:
values = []

for i in range(len(all_results[0])):
Expand Down
29 changes: 26 additions & 3 deletions rhodium/test/model_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import unittest
import warnings
import numpy as np
from rhodium.model import Constraint, Model, Parameter, Response, \
from rhodium.model import Constraint, Direction, Model, Parameter, Response, \
CategoricalUncertainty, IntegerUncertainty, PointUncertainty, \
TriangularUncertainty, UniformUncertainty

Expand All @@ -42,6 +42,30 @@ def testComplexConstraint(self):
self.assertNotEqual(0, c.distance({"x": 0, "y": 1}))
self.assertNotEqual(0, c.distance({"x": 1, "y": 1}))

class TestResponse(unittest.TestCase):

def testInvalidName(self):
with warnings.catch_warnings(record=True) as w:
Response("f-1")
self.assertEqual(1, len(w))
self.assertTrue(issubclass(w[0].category, DeprecationWarning))

def testDeprecatedDir(self):
for d in [Response.MINIMIZE, Response.MAXIMIZE, Response.INFO, Response.IGNORE]:
# Using dir argument issues a warning
with warnings.catch_warnings(record=True) as w:
r = Response("f", dir=d)
self.assertEqual(1, len(w))
self.assertTrue(issubclass(w[0].category, DeprecationWarning))
self.assertEqual(Direction(d), r.direction)

# Reading dir attribute issues a warning
with warnings.catch_warnings(record=True) as w:
r = Response("f", direction=Direction(d))
self.assertEqual(d, r.dir)
self.assertEqual(1, len(w))
self.assertTrue(issubclass(w[0].category, DeprecationWarning))

class TestModelParameters(unittest.TestCase):

def test(self):
Expand Down Expand Up @@ -69,9 +93,8 @@ def testOrder(self):
self.assertEqual(p3, m.parameters[2])

def testInvalidName(self):
m = Model("foo")
with warnings.catch_warnings(record=True) as w:
p = Parameter("x-1")
Parameter("x-1")
self.assertEqual(1, len(w))
self.assertTrue(issubclass(w[0].category, DeprecationWarning))

Expand Down

0 comments on commit d7e91f2

Please sign in to comment.