Skip to content

Commit

Permalink
Add plot_parameters() method, bugfix pytest flags, updt. examples
Browse files Browse the repository at this point in the history
  • Loading branch information
BradyPlanden committed Nov 24, 2023
1 parent e0cd5b5 commit 42325aa
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 18 deletions.
24 changes: 11 additions & 13 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,16 @@ def pytest_configure(config):


def pytest_collection_modifyitems(config, items):
def skip_marker(marker_name, reason):
skip = pytest.mark.skip(reason=reason)
if config.getoption("--unit") and not config.getoption("--examples"):
skip_examples = pytest.mark.skip(
reason="need --examples option to run examples tests"
)
for item in items:
if marker_name in item.keywords:
item.add_marker(skip)
if "examples" in item.keywords:
item.add_marker(skip_examples)

if config.getoption("--unit"):
skip_marker("examples", "need --examples option to run")
return

if config.getoption("--examples"):
skip_marker("unit", "need --unit option to run")
return

skip_marker("unit", "need --unit option to run")
if config.getoption("--examples") and not config.getoption("--unit"):
skip_unit = pytest.mark.skip(reason="need --unit option to run unit tests")
for item in items:
if "unit" in item.keywords:
item.add_marker(skip_unit)
3 changes: 3 additions & 0 deletions examples/scripts/spm_CMAES.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)

Expand Down
3 changes: 3 additions & 0 deletions examples/scripts/spm_IRPropMin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)

Expand Down
3 changes: 3 additions & 0 deletions examples/scripts/spm_SNES.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)

Expand Down
3 changes: 3 additions & 0 deletions examples/scripts/spm_XNES.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)

Expand Down
3 changes: 3 additions & 0 deletions examples/scripts/spm_adam.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)

Expand Down
3 changes: 3 additions & 0 deletions examples/scripts/spm_descent.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)

Expand Down
3 changes: 3 additions & 0 deletions examples/scripts/spm_nlopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)

Expand Down
3 changes: 3 additions & 0 deletions examples/scripts/spm_pso.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)

Expand Down
1 change: 1 addition & 0 deletions pybop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
from .plotting.plot_cost2d import plot_cost2d
from .plotting.quick_plot import StandardPlot, quick_plot
from .plotting.plot_convergence import plot_convergence
from .plotting.plot_parameters import plot_parameters

#
# Remove any imported modules, so we don't expose them as part of pybop
Expand Down
4 changes: 2 additions & 2 deletions pybop/optimisers/pints_optimisers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class GradientDescent(pints.GradientDescent):

def __init__(self, x0, sigma0=0.1, bounds=None):
if bounds is not None:
print("Boundaries ignored by GradientDescent")
print("NOTE: Boundaries ignored by Gradient Descent")

self.boundaries = None # Bounds ignored in pints.GradDesc
super().__init__(x0, sigma0, self.boundaries)
Expand All @@ -23,7 +23,7 @@ class Adam(pints.Adam):

def __init__(self, x0, sigma0=0.1, bounds=None):
if bounds is not None:
print("Boundaries ignored by Adam")
print("NOTE: Boundaries ignored by Adam")

self.boundaries = None # Bounds ignored in pints.Adam
super().__init__(x0, sigma0, self.boundaries)
Expand Down
164 changes: 164 additions & 0 deletions pybop/plotting/plot_parameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import pybop
import math
import plotly.graph_objects as go
from plotly.subplots import make_subplots


def plot_parameters(
optim, xaxis_titles="Iteration", yaxis_titles=None, title="Convergence"
):
"""
Plot the evolution of the parameters during the optimisation process.
Parameters:
----------
optim : optimisation object
An object representing the optimisation process, which should contain
information about the cost function, optimiser, and the history of the
parameter values throughout the iterations.
xaxis_title : str, optional
Title for the x-axis, representing the iteration number or a similar
discrete time step in the optimisation process (default is "Iteration").
yaxis_title : str, optional
Title for the y-axis, which typically represents the metric being
optimised, such as cost or loss (default is "Cost").
title : str, optional
Title of the plot, which provides an overall description of what the
plot represents (default is "Convergence").
Returns:
-------
fig : plotly.graph_objs.Figure
The Plotly figure object for the plot depicting how the parameters of
the optimisation algorithm evolve over its course. This can be useful
for diagnosing the behaviour of the optimisation algorithm.
Notes:
-----
The function assumes that the 'optim' object has a 'cost.problem.parameters'
attribute containing the parameters of the optimisation algorithm and a 'log'
attribute containing a history of the iterations.
"""

if optim.optimiser.name() in ["NLoptOptimize", "SciPyMinimize"]:
print("Parameter plot not yet supported for this optimiser.")
return

# Extract parameters from the optimisation object
params = optim.cost.problem.parameters

# Create the traces from the optimisation log
traces = create_traces(params, optim.log)

# Create the axis titles
axis_titles = []
for param in params:
axis_titles.append(("Function Call", param.name))

# Create the figure
fig = create_subplots_with_traces(traces, axis_titles=axis_titles)

# Display the figure
fig.show()

return fig


def create_traces(params, trace_data, x_values=None):
"""
Generate a list of Plotly Scatter trace objects from provided trace data.
This function assumes that each column in the `trace_data` represents a separate trace to be plotted,
and that the `params` list contains objects with a `name` attribute used for trace names.
Text wrapping for trace names is performed by `pybop.StandardPlot.wrap_text`.
Parameters:
- params (list): A list of objects, where each object has a `name` attribute used as the trace name.
The list should have the same length as the number of traces in `trace_data`.
- trace_data (list of lists): A 2D list where each inner list represents y-values for a trace.
- x_values (list, optional): A list of x-values to be used for all traces. If not provided, a
range of integers starting from 0 will be used.
Returns:
- list: A list of Plotly `go.Scatter` objects, each representing a trace to be plotted.
Notes:
- The function depends on `pybop.StandardPlot.wrap_text` for text wrapping, which needs to be available
in the execution context.
- The function assumes that `go` from `plotly.graph_objs` is already imported as `go`.
"""

traces = []

# If x_values are not provided:
if x_values is None:
x_values = list(range(len(trace_data[0]) * len(trace_data)))

# Determine the number of elements in the smallest arrays
num_elements = len(trace_data[0][0])

# Initialize a list of lists to store our columns
columns = [[] for _ in range(num_elements)]

# Loop through each numpy array in trace_data
for array in trace_data:
# Loop through each item (which is a n-element array) in the numpy array
for item in array:
# Loop through each element in the item and append to the corresponding column
for i in range(num_elements):
columns[i].append(item[i])

# Create a trace for each column
for i in range(len(columns)):
wrap_param = pybop.StandardPlot.wrap_text(params[i].name, width=50)
traces.append(go.Scatter(x=x_values, y=columns[i], name=wrap_param))

return traces


def create_subplots_with_traces(
traces,
plot_size=(1024, 576),
title="Parameter Convergence",
axis_titles=None,
**layout_kwargs,
):
"""
Creates a subplot figure with the given traces.
:param traces: List of plotly.graph_objs traces that will be added to the subplots.
:param plot_size: Tuple (width, height) representing the desired size of the plot.
:param title: The main title of the subplot figure.
:param axis_titles: List of tuples for axis titles in the form [(x_title, y_title), ...] for each subplot.
:param layout_kwargs: Additional keyword arguments to be passed to fig.update_layout for custom layout.
:return: A plotly figure object with the subplots.
"""
num_traces = len(traces)
num_cols = int(math.ceil(math.sqrt(num_traces)))
num_rows = int(math.ceil(num_traces / num_cols))

fig = make_subplots(rows=num_rows, cols=num_cols, start_cell="bottom-left")

for idx, trace in enumerate(traces):
row = (idx // num_cols) + 1
col = (idx % num_cols) + 1
fig.add_trace(trace, row=row, col=col)

if axis_titles and idx < len(axis_titles):
x_title, y_title = axis_titles[idx]
fig.update_xaxes(title_text=x_title, row=row, col=col)
fig.update_yaxes(title_text=y_title, row=row, col=col)

if plot_size:
layout_kwargs["width"], layout_kwargs["height"] = plot_size

if title:
layout_kwargs["title_text"] = title

# Set the legend above the subplots
fig.update_layout(
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
**layout_kwargs,
)

return fig
6 changes: 3 additions & 3 deletions pybop/plotting/quick_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ def __init__(
xaxis_title=None,
yaxis_title=None,
trace_name=None,
width=720,
height=540,
width=1024,
height=576,
):
self.x = x if isinstance(x, list) else x.tolist()
self.y = y
Expand Down Expand Up @@ -176,7 +176,7 @@ def __call__(self):
return fig


def quick_plot(params, cost, title="Scatter Plot", width=720, height=540):
def quick_plot(params, cost, title="Scatter Plot", width=1024, height=576):
"""
Plot the target dataset against the minimised model output.
Expand Down

0 comments on commit 42325aa

Please sign in to comment.