Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple paths #258

Merged
merged 8 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
99 changes: 97 additions & 2 deletions src/gemdat/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,15 @@ def free_energy_graph(F: np.ndarray,
for move in movements:
neighbor = tuple((node + move) % F.shape)
if neighbor in G.nodes:
exp_n_energy = np.exp(F[neighbor])
exp_n_energy = np.exp(0.5 * (F[node] + F[neighbor]))
SCiarella marked this conversation as resolved.
Show resolved Hide resolved
if exp_n_energy < max_energy_threshold:
weight_exp = exp_n_energy
else:
weight_exp = max_energy_threshold

G.add_edge(node,
neighbor,
weight=F[neighbor],
weight=0.5 * (F[node] + F[neighbor]),
SCiarella marked this conversation as resolved.
Show resolved Hide resolved
weight_exp=weight_exp)

return G
Expand All @@ -205,6 +205,101 @@ def free_energy_graph(F: np.ndarray,
'dijkstra-exp']


def calculate_path_difference(path_sites1: list, path_sites2: list) -> float:
SCiarella marked this conversation as resolved.
Show resolved Hide resolved
"""Calculate the difference between two paths. This difference is defined
as the percentage of sites that are not shared between the two paths.

Parameters
----------
path_sites1 : list
List of sites defining the first path
path_sites2 : list
List of sites defining the second path

Returns
-------
difference : float
Difference between the two paths
"""

# Find the shortest and longest paths
shortest_path = path_sites1 if len(path_sites1) <= len(
path_sites2) else path_sites2
longest_path = path_sites2 if shortest_path is path_sites1 else path_sites1
SCiarella marked this conversation as resolved.
Show resolved Hide resolved

# Calculate the number of nodes shared between the shortest and longest paths
shared_nodes = 0
for node in shortest_path:
if node in longest_path:
shared_nodes += 1

return 1 - (shared_nodes / len(shortest_path))


def multiple_paths(
F_graph: nx.Graph,
start: tuple,
end: tuple,
method: _PATHFINDING_METHODS = 'dijkstra',
Npaths: int = 3,
min_diff: float = 0.15,
SCiarella marked this conversation as resolved.
Show resolved Hide resolved
) -> list[Pathway]:
""" Calculate the Np shortest paths between two sites on the graph.
This procedure is based the algorithm by Jin Y. Yen (https://doi.org/10.1287/mnsc.17.11.712)
and its implementation in NetworkX. Only paths that are different by at least min_diff are considered.

Parameters
----------
F_graph : nx.Graph
Graph of the free energy
start : tuple
Coordinates of the starting point
end: tuple
Coordinates of the ending point
method : str
Method used to calculate the shortest path. Options are:
- 'dijkstra': Dijkstra's algorithm
- 'bellman-ford': Bellman-Ford algorithm
- 'minmax-energy': Minmax energy algorithm
- 'dijkstra-exp': Dijkstra's algorithm with exponential weights
Npaths : int
Number of paths to be calculated
min_diff : float
Minimum difference between the paths
"""
SCiarella marked this conversation as resolved.
Show resolved Hide resolved

# First compute the optimal path
best_path = optimal_path(F_graph, start, end, method)

list_of_paths_sites = [best_path.sites]
list_of_paths = [best_path]

# Compute the iterator over all the short paths
all_paths = nx.shortest_simple_paths(F_graph,
source=start,
target=end,
weight='weight')

# Attempt to find the Np shortest paths
for idx, path in enumerate(all_paths):
try:
for good_path in list_of_paths_sites:
if good_path and calculate_path_difference(
path, good_path) < min_diff:
raise ValueError('Path too similar to another')
except ValueError:
continue

list_of_paths_sites.append(path)
path_energy = [F_graph.nodes[node]['energy'] for node in path]
list_of_paths.append(Pathway(sites=path, energy=path_energy))

if len(list_of_paths) == Npaths:
break
SCiarella marked this conversation as resolved.
Show resolved Hide resolved

return list_of_paths


def optimal_path(
F_graph: nx.Graph,
start: tuple,
Expand Down
32 changes: 28 additions & 4 deletions src/gemdat/plots/matplotlib/_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,42 @@
from gemdat.volume import Volume


def energy_along_path(*, path: Pathway, structure: Structure,
def energy_along_path(*, paths: Pathway | list[Pathway], structure: Structure,
stefsmeets marked this conversation as resolved.
Show resolved Hide resolved
volume: Volume) -> plt.Figure:
"""Plot energy along specified path.

Parameters
----------
path : Pathway
Pathway object containing the energy along the path
paths : Pathway | list[Pathway]
Pathway object containing the energy along the path, or list of Pathways
structure : Structure
Structure object to get the site information
volume : Volume
Volume object to get the site information

Returns
-------
fig : matplotlib.figure.Figure
Output figure
"""
# The first Pathway in paths is assumed to be the optimal one
if isinstance(paths, Pathway):
path = paths
else:
path = paths[0]

if path.energy is None:
raise ValueError('Pathway does not contain energy data')
if path.sites is None:
raise ValueError('Pathway does not contain site data')

fig, ax = plt.subplots(figsize=(8, 4))

ax.plot(range(len(path.energy)), path.energy, marker='o', color='r')
ax.plot(range(len(path.energy)),
path.energy,
marker='o',
color='r',
label='Optimal path')
ax.set(ylabel='Free energy [eV]')

nearest_structure_label, nearest_structure_coord = path.path_over_structure(
Expand Down Expand Up @@ -77,6 +91,16 @@ def energy_along_path(*, path: Pathway, structure: Structure,
rotation=45)
ax_up.get_yaxis().set_visible(False)

# If available, plot the other pathways
if isinstance(paths, list):
for idx, path in enumerate(paths[1:]):
if path.energy is None:
raise ValueError('Pathway does not contain energy data')
ax.plot(range(len(path.energy)),
path.energy,
SCiarella marked this conversation as resolved.
Show resolved Hide resolved
label=f'Alternative {idx+1}')
ax.legend(fontsize=8)

return fig


Expand Down
48 changes: 35 additions & 13 deletions src/gemdat/plots/plotly/_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@


def path_on_landscape(
paths: Pathway | list[Pathway],
volume: Volume,
path: Pathway,
structure: Structure,
) -> go.Figure:
"""Ploth path over free energy.
"""Ploth paths over free energy.

Uses plotly as plotting backend.

Arguments
---------
paths : Pathway | list[Pathway]
Pathway object containing the energy along the path, or list of Pathways
volume : Volume
Input volume to create the landscape
path : Pathway
Pathway to plot
structure : Structure
Input structure

Expand All @@ -32,17 +32,17 @@ def path_on_landscape(
fig : go.Figure
Output as plotly figure
"""
# The first Pathway in paths is assumed to be the optimal one
if isinstance(paths, Pathway):
path = paths
else:
path = paths[0]

fig = density(volume.to_probability(), structure)

path = np.asarray(path.cartesian_path(volume))
x_path, y_path, z_path = path.T

# Color path and endpoints differently
color = ['red' for _ in x_path]
color[0] = 'blue'
color[-1] = 'blue'
x_path, y_path, z_path = np.asarray(path.cartesian_path(volume)).T

# Plot the optimal path
fig.add_trace(
go.Scatter3d(
x=x_path,
Expand All @@ -52,11 +52,33 @@ def path_on_landscape(
line={'width': 3},
marker={
'size': 6,
'color': color,
'color': 'teal',
'symbol': 'circle',
'opacity': 0.9
},
name='Path',
name='Optimal path',
))

# If available, plot the other pathways
if isinstance(paths, list):
for idx, path in enumerate(paths[1:]):

x_path, y_path, z_path = np.asarray(path.cartesian_path(volume)).T

fig.add_trace(
go.Scatter3d(
x=x_path,
y=y_path,
z=z_path,
mode='markers+lines',
line={'width': 3},
marker={
'size': 5,
#'color': color,
'symbol': 'circle',
'opacity': 0.9
},
name=f'Alternative {idx+1}',
))

return fig
Binary file modified tests/integration/baseline_images/plot_test/path_energy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 19 additions & 6 deletions tests/integration/path_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
import pytest

from gemdat.io import load_known_material
from gemdat.path import optimal_path
from gemdat.path import multiple_paths, optimal_path


@pytest.vaspxml_available # type: ignore
def test_fractional_coordinates(vasp_path_vol, vasp_full_path):
frac_sites = vasp_full_path.fractional_path(vasp_path_vol)

assert isclose(frac_sites[-1][0], 0.39285714285714285)
assert isclose(frac_sites[19][1], 0.7142857142857143)
assert isclose(frac_sites[10][2], 0.5714285714285714)
assert isclose(frac_sites[19][1], 0.6428571428571429)
assert isclose(frac_sites[10][2], 0.42857142857142855)

assert all(element < 1 for element in max(frac_sites))
assert all(element > 0 for element in max(frac_sites))
Expand All @@ -24,7 +24,7 @@ def test_fractional_coordinates(vasp_path_vol, vasp_full_path):
# start, stop, method, expected
((10, 4, 13), (21, 3, 10), 'dijkstra', 5.210130709736149),
((7, 9, 2), (20, 4, 2), 'bellman-ford', 5.5363545176821),
((18, 7, 12), (25, 3, 13), 'dijkstra-exp', 5.114620231293609),
((18, 7, 12), (25, 3, 13), 'dijkstra-exp', 5.029753032964493),
((3, 3, 6), (0, 9, 4), 'minmax-energy', 2.708267913357463),
)

Expand All @@ -38,7 +38,7 @@ def test_optimal_path(vasp_F_graph, start, stop, method, expected):

@pytest.vaspxml_available # type: ignore
def test_find_best_perc_path(vasp_full_path):
assert isclose(vasp_full_path.cost, 11.490420312258468)
assert isclose(vasp_full_path.cost, 11.488013690080908)
assert vasp_full_path.start_site == (11, 9, 6)


Expand All @@ -54,5 +54,18 @@ def test_nearest_structure_reference(vasp_full_vol, vasp_full_path):

assert isclose(nearest_structure_coord[0][0], 0.2381760000000003)
assert isclose(nearest_structure_coord[0][1], 1.8160919999999998)
assert isclose(nearest_structure_coord[20][2], 1.8160920000000003)
assert isclose(nearest_structure_coord[20][2], 3.145908)
assert isclose(nearest_structure_coord[-1][-1], 1.816092)


@pytest.vaspxml_available # type: ignore
def test_multiple_paths(vasp_F_graph):
paths = multiple_paths(vasp_F_graph,
start=(10, 4, 13),
end=(21, 3, 10),
Npaths=3,
min_diff=0.1)

assert len(paths) == 3
assert isclose(sum(paths[-1].energy), 5.351758190646607)
assert sum(paths[-1].energy) > sum(paths[-2].energy)
2 changes: 1 addition & 1 deletion tests/integration/plot_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ def test_msd_per_element(vasp_traj):
@image_comparison2(baseline_images=['path_energy'])
def test_path_energy(vasp_full_vol, vasp_full_path):
structure = load_known_material('argyrodite')
plots.energy_along_path(path=vasp_full_path,
plots.energy_along_path(paths=vasp_full_path,
volume=vasp_full_vol,
structure=structure)
Loading