Source code for ax.service.utils.analysis_base
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
# pyre-strict
import traceback
from typing import Iterable
import pandas as pd
from ax.analysis.analysis import Analysis, AnalysisCard, AnalysisCardLevel, AnalysisE
from ax.analysis.markdown.markdown_analysis import MarkdownAnalysisCard
from ax.analysis.plotly.parallel_coordinates import ParallelCoordinatesPlot
from ax.core.experiment import Experiment
from ax.core.generation_strategy_interface import GenerationStrategyInterface
from ax.service.utils.with_db_settings_base import WithDBSettingsBase
from ax.utils.common.typeutils import checked_cast
[docs]
class AnalysisBase(WithDBSettingsBase):
"""
Base class for analysis functionality shared between AxClient and Scheduler.
"""
# pyre-fixme[13]: Attribute `experiment` is declared in class
# `AnalysisBase` to have type `Experiment` but is never initialized
experiment: Experiment
# pyre-fixme[13]: Attribute `generation_strategy` is declared in class
# `AnalysisBase` to have type `GenerationStrategyInterface` but
# is never initialized
generation_strategy: GenerationStrategyInterface
def _choose_analyses(self) -> list[Analysis]:
"""
Choose Analyses to compute based on the Experiment, GenerationStrategy, etc.
"""
# TODO Create a useful heuristic for choosing analyses
return [ParallelCoordinatesPlot()]
[docs]
def compute_analyses(
self, analyses: Iterable[Analysis] | None = None
) -> list[AnalysisCard]:
"""
Compute Analyses for the Experiment and GenerationStrategy associated with this
Scheduler instance and save them to the DB if possible. If an Analysis fails to
compute (e.g. due to a missing metric), it will be skipped and a warning will
be logged.
Args:
analyses: Analyses to compute. If None, the Scheduler will choose a set of
Analyses to compute based on the Experiment and GenerationStrategy.
"""
analyses = analyses if analyses is not None else self._choose_analyses()
results = [
analysis.compute_result(
experiment=self.experiment,
generation_strategy=self.generation_strategy,
)
for analysis in analyses
]
# TODO Accumulate Es into their own card, perhaps via unwrap_or_else
cards = [result.unwrap() for result in results if result.is_ok()]
for result in results:
if result.is_err():
e = checked_cast(AnalysisE, result.err)
traceback_str = "".join(
traceback.format_exception(
type(result.err.exception),
e.exception,
e.exception.__traceback__,
)
)
cards.append(
MarkdownAnalysisCard(
name=e.analysis.name,
# It would be better if we could reliably compute the title
# without risking another error
title=f"{e.analysis.name} Error",
subtitle=f"An error occurred while computing {e.analysis}",
attributes=e.analysis.attributes,
blob=traceback_str,
df=pd.DataFrame(),
level=AnalysisCardLevel.DEBUG,
)
)
self._save_analysis_cards_to_db_if_possible(
analysis_cards=cards,
experiment=self.experiment,
)
return cards