Skip to content
Open
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: 8 additions & 2 deletions ax/analysis/graphviz/generation_strategy_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,16 @@ def validate_applicable_state(
GenerationStrategyGraph requires a GenerationStrategy to be provided.
"""
if generation_strategy is None:
return "GenerationStrategyGraph requires a GenerationStrategy"
return (
"A GenerationStrategy must be provided to visualize the "
"experiment's optimization workflow."
)

if len(generation_strategy._nodes) == 0:
return "GenerationStrategy has no nodes to visualize"
return (
"The GenerationStrategy has no nodes to visualize. Ensure the "
"generation strategy is fully configured."
)

return None

Expand Down
18 changes: 17 additions & 1 deletion ax/analysis/graphviz/tests/test_generation_strategy_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,23 @@ def test_validate_applicable_state_no_gs(self) -> None:
generation_strategy=None,
)
self.assertIsNotNone(result)
self.assertIn("requires a GenerationStrategy", result)
self.assertIn("GenerationStrategy must be provided", result)

def test_validate_applicable_state_empty_nodes(self) -> None:
"""Test that validation fails when GenerationStrategy has no nodes."""
analysis = GenerationStrategyGraph()
gs = GenerationStrategy(
steps=[
GenerationStep(generator=Generators.SOBOL, num_trials=5),
]
)
gs._nodes = []
result = analysis.validate_applicable_state(
experiment=None,
generation_strategy=gs,
)
self.assertIsNotNone(result)
self.assertIn("no nodes to visualize", result)

def test_validate_applicable_state_valid(self) -> None:
"""Test that validation passes with a valid GenerationStrategy."""
Expand Down
5 changes: 2 additions & 3 deletions ax/analysis/healthcheck/baseline_improvement.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,8 @@ def validate_applicable_state(

if experiment.optimization_config is None:
return (
"Experiment does not have an `OptimizationConfig`. "
"BaselineImprovementAnalysis requires defined objectives to compare "
"against for proper evaluation."
"The experiment must have an OptimizationConfig with defined "
"objectives to evaluate baseline improvement."
)

try:
Expand Down
10 changes: 7 additions & 3 deletions ax/analysis/healthcheck/predictable_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ def validate_applicable_state(
return experiment_validation

if generation_strategy is None:
return "PredictableMetricsAnalysis requires a GenerationStrategy."
return (
"A GenerationStrategy must be provided to evaluate metric "
"predictability."
)

# Resolve adapter from generation strategy if not provided
resolved_adapter = adapter
Expand All @@ -129,8 +132,9 @@ def validate_applicable_state(
# RandomAdapter has no model to evaluate
if isinstance(resolved_adapter, RandomAdapter):
return (
"PredictableMetricsAnalysis is not applicable when using a "
"RandomAdapter because there is no model to evaluate."
"This analysis is not applicable when using a random exploration "
"strategy (RandomAdapter) because there is no predictive model to "
"evaluate."
)

return None
Expand Down
2 changes: 1 addition & 1 deletion ax/analysis/healthcheck/tests/test_predictable_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def test_random_adapter_not_applicable(self) -> None:

self.assertIsNotNone(error)
self.assertIn("RandomAdapter", error)
self.assertIn("no model to evaluate", error)
self.assertIn("no predictive model to evaluate", error)

def test_validate_applicable_state_no_experiment(self) -> None:
"""Test that validation fails when experiment is None."""
Expand Down
20 changes: 16 additions & 4 deletions ax/analysis/insights.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ def validate_applicable_state(

experiment = none_throws(experiment)
if experiment.optimization_config is None:
return "Experiment must have an OptimizationConfig to compute insights."
return (
"The experiment must have an OptimizationConfig (with defined "
"objectives) to compute insights."
)

@override
def compute(
Expand Down Expand Up @@ -191,14 +194,23 @@ def validate_applicable_state(

experiment = none_throws(experiment)
if experiment.optimization_config is None:
return "Experiment must have an OptimizationConfig."
return (
"The experiment must have an OptimizationConfig (with defined "
"objectives and constraints)."
)

optimization_config = none_throws(experiment.optimization_config)
if len(optimization_config.objective.metric_names) > 1:
return "Experiment may not have more than one Objective."
return (
"This analysis only supports single-objective optimization. The "
"experiment has more than one objective metric."
)

if len(optimization_config.outcome_constraints) == 0:
return "Experiment must have at least one OutcomeConstraint."
return (
"The experiment must have at least one OutcomeConstraint (a metric "
"constraint defined in the optimization config)."
)

@override
def compute(
Expand Down
5 changes: 3 additions & 2 deletions ax/analysis/plotly/marginal_effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ def validate_applicable_state(
]
if len(self.parameters) == 0:
return (
"MarginalEffectsPlot is only for `ChoiceParameter`s, "
"but no ChoiceParameters were found in the experiment."
"MarginalEffectsPlot is only applicable to experiments with "
"ChoiceParameters (discrete, enumerated parameters), but none "
"were found."
)
else:
for param_name in none_throws(self.parameters):
Expand Down
5 changes: 4 additions & 1 deletion ax/analysis/plotly/p_feasible.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ def validate_applicable_state(

optimization_config = experiment.optimization_config
if optimization_config is None:
return "Experiment must have an OptimizationConfig."
return (
"The experiment must have an OptimizationConfig (with defined "
"objectives and constraints) to compute probability of feasibility."
)

if optimization_config.objective.is_multi_objective:
return "Only single-objective optimization is supported."
Expand Down
2 changes: 1 addition & 1 deletion ax/analysis/plotly/tests/test_bandit_rollout.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def setUp(self) -> None:

def test_validate_applicable_state(self) -> None:
self.assertIn(
"Requires an Experiment",
"An Experiment must be provided",
none_throws(BanditRollout().validate_applicable_state()),
)

Expand Down
2 changes: 1 addition & 1 deletion ax/analysis/plotly/tests/test_constraint_feasibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
class TestConstraintFeasibilityPlot(TestCase):
def test_validate_applicable_state(self) -> None:
self.assertIn(
"Requires an Experiment",
"An Experiment must be provided",
none_throws(ConstraintFeasibilityPlot().validate_applicable_state()),
)

Expand Down
2 changes: 1 addition & 1 deletion ax/analysis/plotly/tests/test_marginal_effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def setUp(self) -> None:

def test_validate_applicable_state(self) -> None:
self.assertIn(
"Requires an Experiment",
"An Experiment must be provided",
none_throws(
MarginalEffectsPlot(metric_name="foo").validate_applicable_state()
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def test_compute(self) -> None:

def test_validate_applicable_state(self) -> None:
self.assertIn(
"Requires an Experiment.",
"An Experiment must be provided",
none_throws(ObjectivePFeasibleFrontierPlot().validate_applicable_state()),
)

Expand Down
15 changes: 14 additions & 1 deletion ax/analysis/plotly/tests/test_p_feasible.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,23 @@
class TestPFeasiblePlot(TestCase):
def test_validate_applicable_state(self) -> None:
self.assertIn(
"Requires an Experiment",
"An Experiment must be provided",
none_throws(PFeasiblePlot().validate_applicable_state()),
)

self.assertIn(
"must have an OptimizationConfig",
none_throws(
PFeasiblePlot().validate_applicable_state(
experiment=get_branin_experiment(
with_trial=True,
with_completed_trial=True,
has_optimization_config=False,
)
)
),
)

self.assertIn(
"must have at least one OutcomeConstraint",
none_throws(
Expand Down
2 changes: 1 addition & 1 deletion ax/analysis/plotly/tests/test_parallel_coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
class TestParallelCoordinatesPlot(TestCase):
def test_validate_applicable_state(self) -> None:
self.assertIn(
"Requires an Experiment",
"An Experiment must be provided",
none_throws(ParallelCoordinatesPlot("branin").validate_applicable_state()),
)

Expand Down
2 changes: 1 addition & 1 deletion ax/analysis/plotly/tests/test_progression.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
class TestProgression(TestCase):
def test_validate_applicable_state(self) -> None:
self.assertIn(
"Requires an Experiment",
"An Experiment must be provided",
none_throws(
ProgressionPlot(metric_name="branin_map").validate_applicable_state()
),
Expand Down
2 changes: 1 addition & 1 deletion ax/analysis/plotly/tests/test_top_surfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
class TestTopSurfacesAnalysis(TestCase):
def test_validate_applicable_state(self) -> None:
self.assertIn(
"Requires an Experiment",
"An Experiment must be provided",
none_throws(TopSurfacesAnalysis().validate_applicable_state()),
)

Expand Down
2 changes: 1 addition & 1 deletion ax/analysis/tests/test_metric_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def test_compute(self) -> None:

def test_validate_applicable_state(self) -> None:
self.assertIn(
"Requires an Experiment",
"An Experiment must be provided",
none_throws(MetricSummary().validate_applicable_state()),
)

Expand Down
55 changes: 54 additions & 1 deletion ax/analysis/tests/test_overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
import pandas as pd
from ax.adapter.base import Adapter
from ax.adapter.registry import Generators
from ax.analysis.insights import _MAX_NUM_PARAMS_FOR_SECOND_ORDER, InsightsAnalysis
from ax.analysis.insights import (
_MAX_NUM_PARAMS_FOR_SECOND_ORDER,
InsightsAnalysis,
OutcomeConstraintsAnalysis,
)
from ax.analysis.overview import OverviewAnalysis
from ax.analysis.plotly.arm_effects import ArmEffectsPlot
from ax.analysis.plotly.scatter import ScatterPlot
Expand Down Expand Up @@ -40,6 +44,8 @@
from ax.utils.common.constants import Keys
from ax.utils.common.testutils import TestCase
from ax.utils.testing.core_stubs import (
get_branin_experiment,
get_branin_experiment_with_multi_objective,
get_data,
get_experiment_with_scalarized_objective_and_outcome_constraint,
get_offline_experiments_subset,
Expand Down Expand Up @@ -500,3 +506,50 @@ def patched_init(self_inner: TopSurfacesAnalysis, **kwargs: object) -> None:
self.assertGreater(len(captured_orders), 0)
for order in captured_orders:
self.assertEqual(order, "total")

def test_insights_validate_no_optimization_config(self) -> None:
"""Test InsightsAnalysis validation when experiment has no
OptimizationConfig."""
experiment = get_branin_experiment(has_optimization_config=False)
result = InsightsAnalysis().validate_applicable_state(
experiment=experiment,
)
self.assertIsNotNone(result)
self.assertIn("must have an OptimizationConfig", result)

def test_outcome_constraints_validate_applicable_state(self) -> None:
"""Test OutcomeConstraintsAnalysis validation for various invalid
experiment configurations."""
analysis = OutcomeConstraintsAnalysis()

with self.subTest("no optimization config"):
result = analysis.validate_applicable_state(
experiment=get_branin_experiment(
with_trial=True,
with_completed_trial=True,
has_optimization_config=False,
),
)
self.assertIsNotNone(result)
self.assertIn("must have an OptimizationConfig", result)

with self.subTest("multi-objective"):
result = analysis.validate_applicable_state(
experiment=get_branin_experiment_with_multi_objective(
with_trial=True,
with_completed_trial=True,
),
)
self.assertIsNotNone(result)
self.assertIn("only supports single-objective", result)

with self.subTest("no outcome constraints"):
result = analysis.validate_applicable_state(
experiment=get_branin_experiment(
with_trial=True,
with_completed_trial=True,
has_optimization_config=True,
),
)
self.assertIsNotNone(result)
self.assertIn("must have at least one OutcomeConstraint", result)
2 changes: 1 addition & 1 deletion ax/analysis/tests/test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_validate_applicable_state(self) -> None:
with self.subTest("requires_experiment"):
error_message = analysis.validate_applicable_state()
self.assertIsNotNone(error_message)
self.assertIn("Requires an Experiment", none_throws(error_message))
self.assertIn("An Experiment must be provided", none_throws(error_message))

with self.subTest("requires_trials"):
experiment = get_branin_experiment()
Expand Down
12 changes: 12 additions & 0 deletions ax/analysis/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
_prepare_p_feasible,
_relativize_df_with_sq,
prepare_arm_data,
validate_outcome_constraints,
)
from ax.api.client import Client
from ax.api.configs import RangeParameterConfig
Expand Down Expand Up @@ -881,6 +882,17 @@ def test_offline(self) -> None:
additional_arms=additional_arms,
)

def test_validate_outcome_constraints_no_optimization_config(self) -> None:
"""Test validate_outcome_constraints returns error when experiment has
no OptimizationConfig."""
experiment = Experiment(
name="no_opt_config",
search_space=self.client._experiment.search_space.clone(),
)
result = validate_outcome_constraints(experiment=experiment)
self.assertIsNotNone(result)
self.assertIn("must have an OptimizationConfig", result)

def test_scalarized_constraints(self) -> None:
df = pd.DataFrame(
{
Expand Down
12 changes: 8 additions & 4 deletions ax/analysis/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,7 @@ def validate_experiment(
"""

if experiment is None:
return "Requires an Experiment."
return "An Experiment must be provided to compute this analysis."

if require_trials:
if len(experiment.trials) == 0:
Expand Down Expand Up @@ -1172,16 +1172,20 @@ def validate_outcome_constraints(
"""
optimization_config = experiment.optimization_config
if optimization_config is None:
return "Experiment must have an OptimizationConfig."
return (
"The experiment must have an OptimizationConfig (with defined objectives "
"and constraints) to compute outcome constraints."
)

outcome_constraint_metrics = [
outcome_constraint.metric_names[0]
for outcome_constraint in optimization_config.outcome_constraints
]
if len(outcome_constraint_metrics) == 0:
return (
"Experiment must have at least one OutcomeConstraint to calculate "
"probability of feasibility."
"The experiment must have at least one OutcomeConstraint (a metric "
"constraint defined in the optimization config) to calculate probability "
"of feasibility."
)

return None
Loading