Skip to content

Commit 5371277

Browse files
Merge pull request #1 from optuna/add-simple-auto-matplotlib-example
Add a simple matplotlib example
2 parents 65cbd10 + ff72907 commit 5371277

File tree

4 files changed

+147
-0
lines changed

4 files changed

+147
-0
lines changed

examples/auto-matplotlib/README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Configure Matplotlib with Optuna MCP
2+
3+
This example aims to show the combination of qualitative evaluation by an LLM agent and the black-box optimization by Optuna MCP.
4+
As a simple example, we use Matplotlib.
5+
6+
## Overview
7+
8+
Simply put, an LLM agent qualitatively evaluates the legend position of images generated by `target_generator` in the self-implemented Matplotlib MCP ([matplotlib_server.py](./matplotlib_server.py)) based on the instruction detailed later and searches for the optimal legend position using Optuna based on the qualitative judge.
9+
To keep the example simple, we consider only one parameter.
10+
11+
> [!NOTE]
12+
> There is room for improvement in the qualitative evaluation scheme and the optimization itself, but such enhancement is not the scope of this example.
13+
14+
<table>
15+
<caption>Default and optimized figures. <b>Left</b>: The default figure. The legend overlaps with the x-label, making it hard to recognize the label. <b>Right</b>: The optimized figure. The overlap is successfully removed by the optimization using Optuna, making the x-label visible.</caption>
16+
<tr>
17+
<td><img src="./images/first-plot.png" alt=""></td>
18+
<td><img src="./images/best-plot.png" alt=""></td>
19+
</tr>
20+
</table>
21+
22+
## Workflow
23+
24+
The example works as follows:
25+
1. Suggest the position controlling parameter, i.e., `bbox_to_anchor_y`, using Optuna MCP.
26+
2. Generate an image file using Matplotlib MCP.
27+
3. Upload the image file manually and let the LLM agent qualitatively evaluate the image file based on the instruction described in `Prompt`.
28+
4. Report the qualitative score using Optuna MCP.
29+
5. Repeat 1. -- 4.
30+
31+
## How to Reproduce Demo
32+
33+
### Setups
34+
35+
Please first install the dependencies for this example:
36+
37+
```shell
38+
$ uv pip install matplotlib numpy "mcp[cli]>=1.5.0"
39+
```
40+
41+
The directory structure of this example should look like:
42+
43+
```shell
44+
$ tree ./auto-matplotlib
45+
auto-matplotlib/
46+
└── matplotlib_server.py
47+
```
48+
49+
To enable the MCP server for Matplotlib in Claude Desktop, go to `Claude > Settings > Developer > Edit Config > claude_desktop_config.json` and add the following inside `mcpServers`:
50+
51+
52+
```json
53+
"AutoMatplotlib": {
54+
"command": "/path/to/uv",
55+
"args": [
56+
"--directory",
57+
"/path/to/auto-matplotlib",
58+
"run",
59+
"matplotlib_server.py"
60+
]
61+
}
62+
```
63+
64+
### Prompt
65+
66+
Use the following prompt to iterate the routine described in `Workflow`:
67+
68+
```txt
69+
Create a study named `auto-matplotlib-demo` to maximize the qualitative score of plot figures.
70+
71+
Our task is to optimize the legend position in the figure.
72+
The legend MUST be located below the `xlabel`.
73+
We will optimize the position by controlling `bbox_to_anchor_y` in range of `(-0.1, 0.1)`.
74+
75+
After that, repeat the following five times:
76+
1. Sample a trial from Optuna MCP.
77+
2. Generate a plot given the trial using `target_generator`.
78+
3. Ask me to upload the generated image.
79+
4. Look at the generated image and evaluate `bbox_to_anchor_y` qualitatively from 1 (worst) to 9 (best).
80+
5. Tell the qualitative assessment to Optuna MCP.
81+
82+
Note that `bbox_to_anchor_y` is not good if:
83+
- the legend hides the `xlabel` of the figure,
84+
- the legend hides the main plots or is located above `xlabel`,
85+
- there is an insufficient margin between the upper part of the legend and the lower part of the `xlabel`.
86+
87+
Please evaluate each criterion qualitatively from 1 (worst) to 3 (best).
88+
```
49.6 KB
Loading
49.3 KB
Loading
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from __future__ import annotations
2+
3+
import os
4+
5+
import matplotlib.pyplot as plt
6+
from mcp.server.fastmcp import FastMCP
7+
import numpy as np
8+
9+
10+
mcp = FastMCP("AutoMatplotlib")
11+
plt.rcParams["font.family"] = "Times New Roman"
12+
plt.rcParams["font.size"] = 16
13+
rng = np.random.RandomState(42)
14+
X1 = rng.random(size=(3, 100, 40)) * 10 - 5
15+
X2 = np.clip(rng.normal(size=(3, 100, 40)) * 2.5, -5, 5)
16+
os.makedirs("figs/", exist_ok=True)
17+
18+
19+
@mcp.tool()
20+
def target_generator(trial_number: int, bbox_to_anchor_y: float) -> str:
21+
"""
22+
Generate a plot figure based on the trial suggested by Optuna MCP.
23+
24+
Args:
25+
trial_number: The trial number.
26+
bbox_to_anchor_y:
27+
The `bbox_to_anchor_y` stored in `params` of a `trial` suggested by Optuna MCP.
28+
"""
29+
fig, axes = plt.subplots(ncols=2, nrows=2, figsize=(10, 5), sharex=True)
30+
dx = np.arange(100) + 1
31+
for i, d in enumerate([5, 10, 20, 40]):
32+
ax = axes[i // 2][i % 2]
33+
34+
def _subplot(ax: plt.Axes, values: list[list[float]]) -> plt.Line2D:
35+
cum_values = np.minimum.accumulate(values, axis=-1)
36+
mean = np.mean(cum_values, axis=0)
37+
stderr = np.std(cum_values, axis=0) / np.sqrt(len(values))
38+
(line,) = ax.plot(dx, mean)
39+
ax.fill_between(dx, mean - stderr, mean + stderr, alpha=0.2)
40+
return line
41+
42+
lines = []
43+
ax.set_title(f"{d}D")
44+
lines.append(_subplot(ax, np.sum((X1[..., :d] - 2) ** 2, axis=-1)))
45+
lines.append(_subplot(ax, np.sum((X2[..., :d] - 2) ** 2, axis=-1)))
46+
47+
fig.supxlabel("Number of Trials")
48+
fig.supylabel("Objective Values")
49+
labels = ["Uniform", "Gaussian"]
50+
loc = "lower center"
51+
bbox_to_anchor = (0.5, bbox_to_anchor_y)
52+
fig.legend(handles=lines, labels=labels, loc=loc, ncols=2, bbox_to_anchor=bbox_to_anchor)
53+
fig_path = f"figs/fig{trial_number}.png"
54+
plt.savefig(fig_path, bbox_inches="tight")
55+
return f"{fig_path} generated for Trial {trial_number} with {bbox_to_anchor_y=}"
56+
57+
58+
if __name__ == "__main__":
59+
mcp.run()

0 commit comments

Comments
 (0)