Skip to content

Commit 9708d33

Browse files
committed
experiment.py: improve typing on to/from json and shift to helper function
1 parent c036a6c commit 9708d33

2 files changed

Lines changed: 135 additions & 107 deletions

File tree

src/axtreme/experiment.py

Lines changed: 1 addition & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Helper functions for ax Experiments."""
22

33
from collections.abc import Iterable, Mapping
4-
from typing import Any, cast
4+
from typing import Any
55

66
import numpy as np
77
import pandas as pd
@@ -232,109 +232,3 @@ def add_simulation_data_to_experiment(
232232

233233
# Use the helper function to add the metric data to the experiment
234234
return add_metric_data_to_experiment(experiment, parameterizations, distribution_metric_results)
235-
236-
237-
def extract_data_from_experiment_as_json(
238-
experiment: Experiment,
239-
) -> dict[int, dict[str, dict[str, dict[str, float | dict[str, float]]]]]:
240-
"""Extracts the data from an ax Experiment.
241-
242-
Args:
243-
experiment: The Ax experiment to extract the data from.
244-
245-
Returns:
246-
A dictionary with the following structure:
247-
248-
.. code-block:: json
249-
250-
{
251-
"trial_index": {
252-
"arm_name": {
253-
"parameters": {
254-
"parameter_name": "parameter_value"
255-
},
256-
"metrics": {
257-
"metric_name": {
258-
"mean": 0.0,
259-
"sem": 0.0
260-
}
261-
}
262-
}
263-
}
264-
}
265-
"""
266-
metric_data = experiment.fetch_data().df
267-
trial_indexes = metric_data[["trial_index"]].drop_duplicates()
268-
trials = experiment.get_trials_by_indices(trial_indexes["trial_index"].values)
269-
270-
trial_data: dict[int, dict[str, dict[str, dict[str, float | dict[str, float]]]]] = {}
271-
272-
metric_data_by_arm = metric_data.groupby("arm_name")
273-
274-
for trial in trials:
275-
if trial.index not in trial_data:
276-
trial_data[trial.index] = {}
277-
for arm in trial.arms:
278-
if arm.name not in trial_data[trial.index]:
279-
trial_data[trial.index][arm.name] = {
280-
"parameters": {},
281-
"metrics": {},
282-
}
283-
284-
for parameter_name, parameter_value in arm.parameters.items():
285-
trial_data[trial.index][arm.name]["parameters"][parameter_name] = parameter_value
286-
287-
arm_metric_data = metric_data_by_arm.get_group(arm.name)[["metric_name", "mean", "sem"]]
288-
for metric_name, mean, sem in arm_metric_data.to_numpy():
289-
trial_data[trial.index][arm.name]["metrics"][metric_name] = {"mean": mean, "sem": sem}
290-
291-
return trial_data
292-
293-
294-
def add_json_data_to_experiment(
295-
experiment: Experiment, json_data: dict[int, dict[str, dict[str, dict[str, float | dict[str, float]]]]]
296-
) -> None:
297-
"""Adds the data from a dictionary to an ax Experiment.
298-
299-
Args:
300-
experiment: The ax Experiment to add the data to.
301-
json_data: The data to add to the ax Experiment. The structure should be the same as the output of
302-
extract_data_from_experiment.
303-
304-
Example:
305-
.. code-block:: json
306-
307-
{
308-
"trial_index": {
309-
"arm_name": {
310-
"parameters": {
311-
"parameter_name": "parameter_value"
312-
},
313-
"metrics": {
314-
"metric_name": {
315-
"mean": 0.0,
316-
"sem": 0.0
317-
}
318-
}
319-
}
320-
}
321-
}
322-
"""
323-
# For each arm in the dictionary, extract the parameters and metrics for all arms in the trial
324-
for trial_arms in json_data.values():
325-
# The parameters should always be of type dict[str, float]
326-
# So we use cast to tell mypy that we know this is the case
327-
trial_parameters: list[dict[str, float]] = [
328-
cast("dict[str, float]", arm_data["parameters"]) for arm_data in trial_arms.values()
329-
]
330-
331-
# The metrics should always be of type dict[str, dict[str, float]]
332-
# So we use cast to tell mypy that we know this is the case
333-
trial_metrics: list[dict[str, dict[str, float | None]]] = [
334-
cast("dict[str, dict[str, float | None]]", arm_data["metrics"]) for arm_data in trial_arms.values()
335-
]
336-
_ = add_metric_data_to_experiment(
337-
experiment=experiment,
338-
parameterizations=trial_parameters,
339-
metric_data=trial_metrics,
340-
)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,145 @@
11
"""Helpers for working with the Ax Experiment class."""
22

3+
from typing import TypeAlias, TypedDict, cast
4+
35
import pandas as pd
46
from ax import Experiment
57

8+
from axtreme.experiment import add_metric_data_to_experiment
9+
610

711
def input_out_df_from_experiment(exp: Experiment) -> pd.DataFrame:
812
"""Shows the x and y data in one dataframe."""
913
df_y = exp.fetch_data().df
1014
df_x = pd.DataFrame([{**trial.arm.parameters, "arm_name": trial.arm.name} for trial in exp.trials.values()]) # pyright: ignore[reportAttributeAccessIssue]
1115
return df_y.merge(df_x, on="arm_name", how="inner")
16+
17+
18+
class _MetricValueData(TypedDict):
19+
mean: float | None
20+
sem: float | None
21+
22+
23+
# Key is the parameter name
24+
_ParametersData: TypeAlias = dict[str, None | str | bool | float | int]
25+
# Key is the metric name
26+
_MetricData: TypeAlias = dict[str, _MetricValueData]
27+
28+
29+
class _ArmData(TypedDict):
30+
parameters: _ParametersData
31+
metrics: _MetricData
32+
33+
34+
# ExperimentRunsData stores the details (x location, result) of all the completed trials an experiment has run.
35+
# Structure: dict[<trial_index_str>, dict[<arm_name>, _ArmData]]
36+
# NOTE: We have created this data structure for our own convenience. It is not part of or related to the ax library.
37+
# TODO(sw 2026-06-14): This could be upgraded to use Ax types (e.g. axtreme.core.types.TParameterization)
38+
ExperimentRunsData: TypeAlias = dict[str, dict[str, _ArmData]]
39+
40+
41+
def extract_data_from_experiment_as_json(
42+
experiment: Experiment,
43+
) -> ExperimentRunsData:
44+
"""Extracts the details (x location, results) of the completed trials of an Ax Experiment.
45+
46+
This does not serialize the full Experiment. The information extracted is the minimum necessary for these trial runs
47+
to be added to another experiment.
48+
49+
Args:
50+
experiment: The Ax experiment to extract the data from.
51+
52+
Returns:
53+
A dictionary with the following structure:
54+
55+
.. code-block:: json
56+
57+
{
58+
<trial_index_str>: {
59+
"<arm_name>": {
60+
"parameters": {
61+
"<parameter_name>": <parameter_value>
62+
},
63+
"metrics": {
64+
"<metric_name>": {
65+
"mean": 0.0,
66+
"sem": 0.0
67+
}
68+
}
69+
}
70+
}
71+
}
72+
"""
73+
metric_data = experiment.fetch_data().df
74+
trial_indexes = metric_data[["trial_index"]].drop_duplicates()
75+
trials = experiment.get_trials_by_indices(trial_indexes["trial_index"].values)
76+
77+
trial_data: ExperimentRunsData = {}
78+
79+
metric_data_by_arm = metric_data.groupby("arm_name")
80+
81+
for trial in trials:
82+
trial_index = str(trial.index)
83+
if trial_index not in trial_data:
84+
# Keys in json dictionaries must be strings.
85+
trial_data[trial_index] = {}
86+
# BatchTrails can have multiple arms
87+
for arm in trial.arms:
88+
if arm.name not in trial_data[trial_index]:
89+
trial_data[trial_index][arm.name] = {
90+
"parameters": {},
91+
"metrics": {},
92+
}
93+
94+
for parameter_name, parameter_value in arm.parameters.items():
95+
trial_data[trial_index][arm.name]["parameters"][parameter_name] = parameter_value
96+
97+
arm_metric_data = metric_data_by_arm.get_group(arm.name)[["metric_name", "mean", "sem"]]
98+
for metric_name, mean, sem in arm_metric_data.to_numpy():
99+
trial_data[trial_index][arm.name]["metrics"][metric_name] = {"mean": mean, "sem": sem}
100+
101+
return trial_data
102+
103+
104+
def add_json_data_to_experiment(experiment: Experiment, json_data: ExperimentRunsData) -> None:
105+
"""Adds the data from a dictionary to an ax Experiment.
106+
107+
Args:
108+
experiment: The ax Experiment to add the data to.
109+
json_data: The data to add to the ax Experiment. The structure should be the same as the output of
110+
extract_data_from_experiment.
111+
112+
Example:
113+
.. code-block:: json
114+
115+
{
116+
<trial_index_str>: {
117+
"<arm_name>": {
118+
"parameters": {
119+
"<parameter_name>": <parameter_value>
120+
},
121+
"metrics": {
122+
"<metric_name>": {
123+
"mean": 0.0,
124+
"sem": 0.0
125+
}
126+
}
127+
}
128+
}
129+
}
130+
"""
131+
# For each arm in the dictionary, extract the parameters and metrics for all arms in the trial
132+
for trial_arms in json_data.values():
133+
# If a single trial has multiple arms, a batch trial is created.
134+
trial_parameters: list[_ParametersData] = [arm_data["parameters"] for arm_data in trial_arms.values()]
135+
136+
# add_metric_data_to_experiment's metric_data param is a broader union (it also accepts float/tuple
137+
# forms from other callers) than the _MetricValueData TypedDict -- cast to bridge the two.
138+
trial_metrics: list[dict[str, dict[str, float | None]]] = [
139+
cast("dict[str, dict[str, float | None]]", arm_data["metrics"]) for arm_data in trial_arms.values()
140+
]
141+
_ = add_metric_data_to_experiment(
142+
experiment=experiment,
143+
parameterizations=trial_parameters,
144+
metric_data=trial_metrics,
145+
)

0 commit comments

Comments
 (0)