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
10 changes: 6 additions & 4 deletions mpisppy/opt/fwph.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,16 +931,18 @@ def _options_checks_fw(self):
# proximal terms (no binary variables allowed in FWPH QPs)
if ('linearize_binary_proximal_terms' in self.options
and self.options['linearize_binary_proximal_terms']):
print('Warning: linearize_binary_proximal_terms cannot be used '
'with the FWPH algorithm. Ignoring...')
if self.cylinder_rank == 0:
print('Warning: linearize_binary_proximal_terms cannot be used '
'with the FWPH algorithm. Ignoring...')
self.options['linearize_binary_proximal_terms'] = False

# 3b. Check that the user did not specify the linearization of all
# proximal terms (FWPH QPs should be QPs)
if ('linearize_proximal_terms' in self.options
and self.options['linearize_proximal_terms']):
print('Warning: linearize_proximal_terms cannot be used '
'with the FWPH algorithm. Ignoring...')
if self.cylinder_rank == 0:
print('Warning: linearize_proximal_terms cannot be used '
'with the FWPH algorithm. Ignoring...')
self.options['linearize_proximal_terms'] = False

# 4. Provide a time limit of inf if the user did not specify
Expand Down
108 changes: 108 additions & 0 deletions mpisppy/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,5 +548,113 @@ def test_coeff_rho_args_added(self):
self.assertIn("coeff_rho", cfg)


class TestFWPHLinearizeForwarding(unittest.TestCase):
"""The fwph_hub / fwph_spoke factories must forward
cfg.linearize_proximal_terms and cfg.linearize_binary_proximal_terms
into the options dict so FWPH._options_checks_fw can warn that
FWPH cannot honor them (issue #274)."""

def _make_cfg(self, lin_prox=True, lin_bin=True):
cfg = Config()
cfg.popular_args()
cfg.ph_args()
cfg.fwph_args()
cfg.two_sided_args()
cfg.solver_name = "gurobi"
cfg.linearize_proximal_terms = lin_prox
cfg.linearize_binary_proximal_terms = lin_bin
return cfg

def _beans(self, cfg):
# placeholder values for the positional args of the factories;
# nothing here is actually called because the factories just
# stuff them into the returned dict.
return dict(
scenario_creator=lambda *a, **kw: None,
scenario_denouement=None,
all_scenario_names=["Scenario1"],
)

def test_fwph_hub_forwards_linearize_options(self):
import mpisppy.utils.cfg_vanilla as vanilla
cfg = self._make_cfg(lin_prox=True, lin_bin=True)
hub = vanilla.fwph_hub(cfg, **self._beans(cfg))
opts = hub["opt_kwargs"]["options"]
self.assertTrue(opts["linearize_proximal_terms"])
self.assertTrue(opts["linearize_binary_proximal_terms"])

def test_fwph_spoke_forwards_linearize_options(self):
import mpisppy.utils.cfg_vanilla as vanilla
cfg = self._make_cfg(lin_prox=True, lin_bin=True)
spoke = vanilla.fwph_spoke(cfg, **self._beans(cfg))
opts = spoke["opt_kwargs"]["options"]
self.assertTrue(opts["linearize_proximal_terms"])
self.assertTrue(opts["linearize_binary_proximal_terms"])

def test_fwph_hub_forwards_false_when_off(self):
import mpisppy.utils.cfg_vanilla as vanilla
cfg = self._make_cfg(lin_prox=False, lin_bin=False)
hub = vanilla.fwph_hub(cfg, **self._beans(cfg))
opts = hub["opt_kwargs"]["options"]
self.assertFalse(opts["linearize_proximal_terms"])
self.assertFalse(opts["linearize_binary_proximal_terms"])


class TestFWPHOptionsChecksWarnings(unittest.TestCase):
"""FWPH._options_checks_fw must print a warning (rank 0 only) when
linearize_proximal_terms or linearize_binary_proximal_terms is on,
and must clear the flag so the QP solve proceeds."""

def _stub(self, lin_prox=False, lin_bin=False, rank=0):
import types
stub = types.SimpleNamespace()
stub.cylinder_rank = rank
stub.options = {
"linearize_proximal_terms": lin_prox,
"linearize_binary_proximal_terms": lin_bin,
}
stub.FW_options = {
"FW_iter_limit": 1,
"FW_weight": 0.0,
"FW_conv_thresh": 1e-4,
"solver_name": "gurobi",
}
return stub

def _run(self, stub):
import io
import contextlib
from mpisppy.opt.fwph import FWPH
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
FWPH._options_checks_fw(stub)
return buf.getvalue()

def test_warn_and_clear_linearize_proximal_on_rank0(self):
stub = self._stub(lin_prox=True, rank=0)
out = self._run(stub)
self.assertIn("linearize_proximal_terms cannot be used", out)
self.assertFalse(stub.options["linearize_proximal_terms"])

def test_warn_and_clear_linearize_binary_proximal_on_rank0(self):
stub = self._stub(lin_bin=True, rank=0)
out = self._run(stub)
self.assertIn("linearize_binary_proximal_terms cannot be used", out)
self.assertFalse(stub.options["linearize_binary_proximal_terms"])

def test_no_warning_on_nonzero_rank(self):
stub = self._stub(lin_prox=True, lin_bin=True, rank=1)
out = self._run(stub)
self.assertEqual(out, "")
# flag is still cleared regardless of rank
self.assertFalse(stub.options["linearize_proximal_terms"])
self.assertFalse(stub.options["linearize_binary_proximal_terms"])

def test_no_warning_when_flags_off(self):
stub = self._stub(lin_prox=False, lin_bin=False, rank=0)
out = self._run(stub)
self.assertEqual(out, "")


if __name__ == "__main__":
unittest.main()
13 changes: 13 additions & 0 deletions mpisppy/utils/cfg_vanilla.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,14 @@ def fwph_hub(cfg,

options.update(_fwph_options(cfg))

# Forward linearize_* options so FWPH._options_checks_fw can warn that
# FWPH cannot honor them. Without this forwarding, the user sets
# --linearize-proximal-terms, FWPH never sees it in self.options, and
# there is no warning when FWPH proceeds with its (intended) quadratic
# objective.
options["linearize_proximal_terms"] = cfg.linearize_proximal_terms
options["linearize_binary_proximal_terms"] = cfg.linearize_binary_proximal_terms

hub_dict = {
"hub_class": FWPHHub,
"hub_kwargs": {"options": {"rel_gap": cfg.rel_gap,
Expand Down Expand Up @@ -773,6 +781,11 @@ def fwph_spoke(

options.update(_fwph_options(cfg))

# Match fwph_hub: forward linearize_* so FWPH._options_checks_fw can
# warn that FWPH cannot honor them. See the comment in fwph_hub.
options["linearize_proximal_terms"] = cfg.linearize_proximal_terms
options["linearize_binary_proximal_terms"] = cfg.linearize_binary_proximal_terms

fw_dict = {
"spoke_class": FrankWolfeOuterBound,
"opt_class": FWPH,
Expand Down
Loading