Skip to content
Closed
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
12 changes: 11 additions & 1 deletion ax/analysis/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ def _create_analysis_card(

def _create_analysis_card_group(
self,
title: str,
subtitle: str,
children: Sequence[AnalysisCardBase],
) -> AnalysisCardGroup:
"""
Expand All @@ -133,11 +135,15 @@ def _create_analysis_card_group(
"""
return AnalysisCardGroup(
name=self.__class__.__name__,
title=title,
subtitle=subtitle,
children=children,
)

def _create_analysis_card_group_or_card(
self,
title: str,
subtitle: str,
children: Sequence[AnalysisCardBase],
) -> AnalysisCardBase:
"""
Expand All @@ -148,7 +154,11 @@ def _create_analysis_card_group_or_card(
if len(children) == 1:
return children[0]

return self._create_analysis_card_group(children=children)
return self._create_analysis_card_group(
title=title,
subtitle=subtitle,
children=children,
)


class AnalysisE(ExceptionE):
Expand Down
225 changes: 155 additions & 70 deletions ax/analysis/analysis_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,37 @@ class AnalysisCardBase(SortableBase, ABC):

When rendering in an IPython environment (ex. Jupyter), use AnalyisCardBase.flatten
to produce an ordered list of cards to render.

Args:
name: The class name of the Analysis that produced this card (ex. "Summary",
"ArmEffects", etc.).
"""

name: str
# Timestamp is especially useful when querying the database for the most recently
# produced artifacts.

title: str
subtitle: str

_timestamp: datetime

def __init__(self, name: str, timestamp: datetime | None = None) -> None:
def __init__(
self,
name: str,
title: str,
subtitle: str,
timestamp: datetime | None = None,
) -> None:
"""
Args:
name: The class name of the Analysis that produced this card (ex. "Summary",
"ArmEffects", etc.).
title: Human-readable title which describes the card's contents. This
appears in all user facing interfaces as the title of the card.
subtitle: Human-readable subtitle which provides additional information or
context to improve the usability of the analysis for users.
timestamp: The time at which the Analysis was computed. This can be
especially useful when querying the database for the most recently
produced artifacts.
"""
self.name = name
self.title = title
self.subtitle = subtitle
self._timestamp = timestamp if timestamp is not None else datetime.now()

@abstractmethod
Expand Down Expand Up @@ -107,27 +125,93 @@ def hierarchy_str(self, level: int = 0) -> str:
def _unique_id(self) -> str:
return str(hash(str(self.__dict__)))

def _ipython_display_(self) -> None:
"""
IPython display hook. This is called when the AnalysisCard is rendered in an
IPython environment (ex. Jupyter). This method should not be implemented by
subclasses; instead they should implement the representation-specific helpers
such as _body_html_ and _body_papermill_.
"""

if is_running_in_papermill():
for card in self.flatten():
display(Markdown(f"**{card.title}**\n\n{card.subtitle}"))
display(card._body_papermill())
return

display(HTML(self._repr_html_()))

@abstractmethod
def _body_html(self, depth: int) -> str:
"""
Return the HTML body of the card (the dataframe, plot, grid, etc.). This is
used by the AnalysisCardBase._repr_html_ method to render the card in an
IPython environment (ex. Jupyter).

This, not _repr_html_ or _to_html, should be implemented by subclasses of
AnalysisCardBase in most cases in order to keep treatment of titles and
subtitles consistent.

Since this method can sometimes be called recursively a "depth" parameter can
be passed in as well.
"""
pass

def _repr_html_(self) -> str:
"""
IPython HTML representation hook. This is called when the AnalysisCard is
rendered in an IPython environment (ex. Jupyter). This method should be
implemented by subclasses of Analysis to display the AnalysisCard in a useful
way.
"""

return self._to_html(depth=0)

def _to_html(self, depth: int) -> str:
return html_card_template.format(
title_str=self.title,
subtitle_str=self.subtitle if depth < 2 else "",
body_html=self._body_html(depth=depth),
)


class AnalysisCardGroup(AnalysisCardBase):
"""
An ordered collection of AnalysisCards. This is useful for grouping related
analyses together.

This is analogous to a "branch node" in a tree structure.

Args:
name: The name of the Analysis that produced this card.
"""

children: list[AnalysisCardBase]

def __init__(
self,
name: str,
title: str,
subtitle: str,
children: Sequence[AnalysisCardBase],
timestamp: datetime | None = None,
) -> None:
super().__init__(name=name, timestamp=timestamp)
"""
Args:
name: The class name of the Analysis that produced this card (ex. "Summary",
"ArmEffects", etc.).
title: Human-readable title which describes the card's contents. This
appears in all user facing interfaces as the title of the card.
subtitle: Human-readable subtitle which provides additional information or
context to improve the usability of the analysis for users.
timestamp: The time at which the Analysis was computed. This can be
especially useful when querying the database for the most recently
produced artifacts.
"""
super().__init__(
name=name,
title=title,
subtitle=subtitle,
timestamp=timestamp,
)

self.children = [
child
for child in children
Expand All @@ -147,27 +231,51 @@ def hierarchy_str(self, level: int = 0) -> str:
child.hierarchy_str(level=level + 1) for child in self.children
)

def _ipython_display_(self) -> None:
def _body_html(self, depth: int) -> str:
"""
IPython display hook. This is called when the AnalysisCard is rendered in an
IPython environment (ex. Jupyter). This method should not be implemented by
subclasses; instead they should implement the representation-specific helpers
such as _body_html_ and _body_papermill_.
When rendering an AnalysisCardGroup as HTML use the following rules when
constructing the card's body:

* Render children in order
* Render AnalysisCards (leaves) in a 2xN grid when adjacent to each other
* Do not render AnalysisCardGroups in a grid
* Do not render subtitles below depth == 2 (this is handled in
AnalysisCardBase._to_html, not this method).
"""

if is_running_in_papermill():
for card in self.flatten():
display(Markdown(f"**{card.title}**\n\n{card.subtitle}"))
display(card._body_papermill())
return
res = []
leaf_cards = []
for child in self.children:
# Accumulate adjacent AnalysisCard leaves so they can be inserted into an
# HTML grid later.
if isinstance(child, AnalysisCard):
leaf_cards.append(child)
continue

# If there are leaves accumulated, collect them into a grid and empty
# the accumulator before appending the current child AnalysisCardGroup
# underneath.
if len(leaf_cards) > 0:
leaves_grid = html_grid_template.format(
card_divs="".join(
[card._to_html(depth=depth + 1) for card in leaf_cards]
)
)
res.append(leaves_grid)
leaf_cards = []

display(
HTML(
html_grid_template.format(
card_divs="".join([card._repr_html_() for card in self.flatten()])
res.append(child._to_html(depth=depth + 1))

# Collect the accumulated leaves a final time to append to the result.
if len(leaf_cards) > 0:
leaves_grid = html_grid_template.format(
card_divs="".join(
[card._to_html(depth=depth + 1) for card in leaf_cards]
)
)
)
res.append(leaves_grid)

return "\n".join(res)


class AnalysisCard(AnalysisCardBase):
Expand All @@ -183,9 +291,6 @@ class AnalysisCard(AnalysisCardBase):
This is analogous to a "leaf node" in a tree structure.
"""

title: str
subtitle: str

df: pd.DataFrame # Raw data produced by the Analysis

# Blob is the data processed for end-user consumption, encoded as a string,
Expand All @@ -203,10 +308,26 @@ def __init__(
blob: str,
timestamp: datetime | None = None,
) -> None:
super().__init__(name=name, timestamp=timestamp)
"""
Args:
name: The name of the Analysis that produced this card.
title: A human-readable title which describes the card's contents.
subtitle: A human-readable subtitle which elaborates on the card's title if
necessary.
df: The raw data produced by the Analysis.
blob: The data processed for end-user consumption, encoded as a string,
typically JSON.
timestamp: The time at which the Analysis was computed. This can be
especially useful when querying the database for the most recently
produced artifacts.
"""
super().__init__(
name=name,
title=title,
subtitle=subtitle,
timestamp=timestamp,
)

self.title = title
self.subtitle = subtitle
self.df = df
self.blob = blob

Expand All @@ -220,50 +341,14 @@ def flatten(self) -> list[AnalysisCard]:
def hierarchy_str(self, level: int = 0) -> str:
return f"{' ' * level}{self.title}"

def _ipython_display_(self) -> None:
"""
IPython display hook. This is called when the AnalysisCard is rendered in an
IPython environment (ex. Jupyter). This method should not be implemented by
subclasses; instead they should implement the representation-specific helpers
such as _body_html_ and _body_papermill_.
"""

if is_running_in_papermill():
display(Markdown(f"**{self.title}**\n\n{self.subtitle}"))
display(self._body_papermill())
return

display(HTML(self._repr_html_()))

def _repr_html_(self) -> str:
def _body_html(self, depth: int) -> str:
"""
IPython HTML representation hook. This is called when the AnalysisCard is
rendered in an IPython environment (ex. Jupyter). This method should be
implemented by subclasses of Analysis to display the AnalysisCard in a useful
way.
"""

return html_card_template.format(
title_str=self.title,
subtitle_str=self.subtitle,
body_html=self._body_html(),
)

def _body_html(self) -> str:
"""
Return the HTML body of the AnalysisCard (the dataframe, plot, etc.). This is
used by the AnalysisCard._repr_html_ method to render the AnalysisCard in an
IPython environment (ex. Jupyter).

This, not _repr_html_, should be implemented by subclasses of AnalysisCard in
most cases.

By default, this method displays the raw data in a pandas DataFrame.
"""

return f"<div class='content'>{self.df.to_html()}</div>"

def _body_papermill(self) -> Any: # pyre-ignore[3]
def _body_papermill(self) -> Any:
"""
Return the body of the AnalysisCard in a simplified format for when html is
undesirable (ex. when rendering the Ax website).
Expand Down
4 changes: 3 additions & 1 deletion ax/analysis/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def compute(
metric_names = [*none_throws(experiment.optimization_config).metrics.keys()]

return self._create_analysis_card_group(
title="T230247379",
subtitle="T230247379",
children=[
*CrossValidationPlot(metric_names=metric_names)
.compute_or_error_card(
Expand All @@ -44,5 +46,5 @@ def compute(
adapter=adapter,
)
.flatten()
]
],
)
4 changes: 2 additions & 2 deletions ax/analysis/healthcheck/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
ConstraintsFeasibilityAnalysis,
)
from ax.analysis.healthcheck.healthcheck_analysis import (
HealthcheckAnalysis,
create_healthcheck_analysis_card,
HealthcheckAnalysisCard,
HealthcheckStatus,
)
Expand All @@ -23,9 +23,9 @@
from ax.analysis.healthcheck.should_generate_candidates import ShouldGenerateCandidates

__all__ = [
"create_healthcheck_analysis_card",
"ConstraintsFeasibilityAnalysis",
"CanGenerateCandidatesAnalysis",
"HealthcheckAnalysis",
"HealthcheckAnalysisCard",
"HealthcheckStatus",
"ShouldGenerateCandidates",
Expand Down
Loading