Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/omlt/linear_tree/lt_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def __init__(
self.__model = lt_regressor
self.__scaling_object = scaling_object

is_scaled = True
# Process input bounds to insure scaled input bounds exist for formulations
if scaled_input_bounds is None:
if unscaled_input_bounds is not None and scaling_object is not None:
Expand All @@ -75,12 +76,14 @@ def __init__(
# input bounds = unscaled input bounds
elif unscaled_input_bounds is not None and scaling_object is None:
scaled_input_bounds = unscaled_input_bounds
is_scaled = False
elif unscaled_input_bounds is None:
msg = "Input Bounds needed to represent linear trees as MIPs"
raise ValueError(msg)

self.__unscaled_input_bounds = unscaled_input_bounds
self.__scaled_input_bounds = scaled_input_bounds
self.__is_scaled = is_scaled

self.__splits, self.__leaves, self.__thresholds = _parse_tree_data(
lt_regressor, scaled_input_bounds
Expand All @@ -99,6 +102,16 @@ def scaled_input_bounds(self):
"""Returns dict containing scaled input bounds."""
return self.__scaled_input_bounds

@property
def unscaled_input_bounds(self):
"""Returns dict containing unscaled input bounds."""
return self.__unscaled_input_bounds

@property
def is_scaled(self):
"""Returns bool indicating whether model is scaled."""
return self.__is_scaled

@property
def splits(self):
"""Returns dict containing split information."""
Expand Down
45 changes: 32 additions & 13 deletions src/omlt/linear_tree/lt_formulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,17 @@
self.model_definition.scaled_input_bounds,
)

input_vars = self.block.scaled_inputs
if self.model_definition.is_scaled is True:
output_vars = self.block.scaled_outputs

Check warning on line 104 in src/omlt/linear_tree/lt_formulation.py

View check run for this annotation

Codecov / codecov/patch

src/omlt/linear_tree/lt_formulation.py#L104

Added line #L104 was not covered by tests
else:
output_vars = self.block.outputs

_add_gdp_formulation_to_block(
block=self.block,
model_definition=self.model_definition,
input_vars=self.block.scaled_inputs,
output_vars=self.block.scaled_outputs,
input_vars=input_vars,
output_vars=output_vars,
transformation=self.transformation,
epsilon=self.epsilon,
include_leaf_equalities=True,
Expand Down Expand Up @@ -181,12 +187,16 @@
)

input_vars = self.block.scaled_inputs
if self.model_definition.is_scaled is True:
output_vars = self.block.scaled_outputs
else:
output_vars = self.block.outputs

_add_gdp_formulation_to_block(
block=block,
model_definition=self.model_definition,
input_vars=input_vars,
output_vars=self.block.scaled_outputs,
output_vars=output_vars,
transformation="custom",
epsilon=self.epsilon,
include_leaf_equalities=False,
Expand Down Expand Up @@ -285,7 +295,8 @@
(default: True)
"""
leaves = model_definition.leaves
input_bounds = model_definition.scaled_input_bounds
scaled_input_bounds = model_definition.scaled_input_bounds
unscaled_input_bounds = model_definition.unscaled_input_bounds
n_inputs = model_definition.n_inputs

# The set of leaves and the set of features
Expand All @@ -295,17 +306,25 @@

# Use the input_bounds and the linear models in the leaves to calculate
# the lower and upper bounds on the output variable. Required for Pyomo.GDP
output_bounds = _build_output_bounds(model_definition, input_bounds)
scaled_output_bounds = _build_output_bounds(model_definition, scaled_input_bounds)
unscaled_output_bounds = _build_output_bounds(
model_definition, unscaled_input_bounds
)

# Ouptuts are automatically scaled based on whether inputs are scaled
block.outputs.setub(output_bounds[1])
block.outputs.setlb(output_bounds[0])
block.scaled_outputs.setub(output_bounds[1])
block.scaled_outputs.setlb(output_bounds[0])

block.intermediate_output = pe.Var(
tree_ids, bounds=(output_bounds[0], output_bounds[1])
)
block.outputs.setub(unscaled_output_bounds[1])
block.outputs.setlb(unscaled_output_bounds[0])
block.scaled_outputs.setub(scaled_output_bounds[1])
block.scaled_outputs.setlb(scaled_output_bounds[0])

if model_definition.is_scaled is True:
block.intermediate_output = pe.Var(
tree_ids, bounds=(scaled_output_bounds[0], scaled_output_bounds[1])
)
else:
block.intermediate_output = pe.Var(
tree_ids, bounds=(unscaled_output_bounds[0], unscaled_output_bounds[1])
)

# Create a disjunct for each leaf containing the bound constraints
# and the linear model expression.
Expand Down
31 changes: 31 additions & 0 deletions tests/linear_tree/test_lt_formulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,12 @@ def test_linear_tree_model_single_var(): # noqa: C901
splits = ltmodel_small.splits
leaves = ltmodel_small.leaves
thresholds = ltmodel_small.thresholds
is_scaled = ltmodel_small.is_scaled
unscaled_input_bounds = ltmodel_small.unscaled_input_bounds

assert scaled_input_bounds is not None
assert unscaled_input_bounds is not None
assert not is_scaled
assert n_inputs == 1
assert n_outputs == 1
# test for splits
Expand Down Expand Up @@ -384,6 +388,33 @@ def test_scaling():
):
LinearTreeDefinition(regr)

formulation = LinearTreeHybridBigMFormulation(lt_def2)

model1 = pe.ConcreteModel()
model1.x = pe.Var(initialize=0)
model1.y = pe.Var(initialize=0)
model1.obj = pe.Objective(expr=1)
model1.lt = OmltBlock()
model1.lt.build_formulation(formulation)

@model1.Constraint()
def connect_inputs(mdl):
return mdl.x == mdl.lt.inputs[0]

@model1.Constraint()
def connect_outputs(mdl):
return mdl.y == mdl.lt.outputs[0]

model1.x.fix(0.5)

status_1_bigm = pe.SolverFactory("scip").solve(model1, tee=True)
pe.assert_optimal_termination(status_1_bigm)
solution_1_bigm = (pe.value(model1.x), pe.value(model1.y))
y_pred = regr.predict(
np.array((solution_1_bigm[0] - mean_x_small) / std_x_small).reshape(1, -1)
)
assert y_pred[0] == pytest.approx((solution_1_bigm[1] - mean_y_small) / std_y_small)


#### MULTIVARIATE INPUT TESTING ####

Expand Down
Loading