Skip to content

Commit

Permalink
Merge pull request #258 from GEMDAT-repos/multiple_paths
Browse files Browse the repository at this point in the history
Multiple paths
  • Loading branch information
SCiarella authored Feb 12, 2024
2 parents 82816c1 + 260a996 commit efd7def
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 44 deletions.
158 changes: 138 additions & 20 deletions src/gemdat/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ def start_site(self) -> tuple[int, int, int]:
return self.sites[0]

@property
def end_site(self) -> tuple[int, int, int]:
"""Return end site."""
def stop_site(self) -> tuple[int, int, int]:
"""Return stop site."""
if self.sites is None:
raise ValueError('Voxel coordinates of the path are required.')
return self.sites[-1]
Expand Down Expand Up @@ -187,15 +187,16 @@ 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])
weight = 0.5 * (F[node] + F[neighbor])
exp_n_energy = np.exp(weight)
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=weight,
weight_exp=weight_exp)

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


def calculate_path_difference(path1: list, path2: list) -> float:
"""Calculate the difference between two paths. This difference is defined
as the percentage of sites that are not shared between the two paths.
Parameters
----------
path1 : list
List of sites defining the first path
path2 : list
List of sites defining the second path
Returns
-------
difference : float
Difference between the two paths
"""

# Find the shortest and longest paths
shortest, longest = sorted((path1, path2), key=len)

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

return 1 - (shared_nodes / len(shortest))


def _paths_too_similar(path: list, list_of_paths: list,
min_diff: float) -> bool:
"""Check if the path is too similar to the other paths.
Parameters
----------
path : list
List of sites defining the path
list_of_paths : list
List of Pathway objects defining the other paths
min_diff : float
Minimum difference between the paths
Returns
-------
too_similar : bool
True if the path is too similar to the other paths
"""

for good_path in list_of_paths:
if calculate_path_difference(path, good_path.sites) < min_diff:
return True
return False


def multiple_paths(
*,
F_graph: nx.Graph,
start: tuple,
stop: tuple,
method: _PATHFINDING_METHODS = 'dijkstra',
n_paths: int = 3,
min_diff: float = 0.15,
) -> 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
stop: tuple
Coordinates of the stopping 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
Returns
-------
list_of_paths: list[Pathway]
List of the n_paths shortest paths between the start and stop sites
"""

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

list_of_paths = [best_path]

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

# Attempt to find the Np shortest paths
for idx, path in enumerate(all_paths):
if _paths_too_similar(path, list_of_paths, min_diff):
continue

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) == n_paths:
break

return list_of_paths


def optimal_path(
F_graph: nx.Graph,
start: tuple,
end: tuple,
stop: tuple,
method: _PATHFINDING_METHODS = 'dijkstra',
) -> Pathway:
"""Calculate the shortest cost-effective path using the desired method.
Expand All @@ -219,8 +337,8 @@ def optimal_path(
Graph of the free energy
start : tuple
Coordinates of the starting point
end: tuple
Coordinates of the ending point
stop: tuple
Coordinates of the stoping point
method : str
Method used to calculate the shortest path. Options are:
- 'dijkstra': Dijkstra's algorithm
Expand All @@ -231,19 +349,19 @@ def optimal_path(
Returns
-------
path: Pathway
Optimal path on the graph between start and end
Optimal path on the graph between start and stop
"""

optimal_path = nx.shortest_path(
F_graph,
source=start,
target=end,
target=stop,
weight='weight_exp' if method == 'dijkstra-exp' else 'weight',
method='dijkstra' if method in ('dijkstra-exp',
'minmax-energy') else method)

if method == 'minmax-energy':
optimal_path = _optimal_path_minmax_energy(F_graph, start, end,
optimal_path = _optimal_path_minmax_energy(F_graph, start, stop,
optimal_path)
elif method not in ('dijkstra', 'bellman-ford', 'dijkstra-exp'):
raise ValueError(f'Unknown method {method}')
Expand All @@ -253,7 +371,7 @@ def optimal_path(
return path


def _optimal_path_minmax_energy(F_graph: nx.Graph, start: tuple, end: tuple,
def _optimal_path_minmax_energy(F_graph: nx.Graph, start: tuple, stop: tuple,
optimal_path: list) -> list:
"""Find the optimal path that has the minimum maximum-energy.
Expand All @@ -263,15 +381,15 @@ def _optimal_path_minmax_energy(F_graph: nx.Graph, start: tuple, end: tuple,
Graph of the free energy
start : tuple
Coordinates of the starting point
end: tuple
Coordinates of the ending point
stop: tuple
Coordinates of the stoping point
optimal_path : list
List of the nodes of the optimal path
Returns
-------
optimal_path: list
Optimal path on the graph between start and end
Optimal path on the graph between start and stop
"""

max_energy = max([F_graph.nodes[node]['energy'] for node in optimal_path])
Expand All @@ -287,7 +405,7 @@ def _optimal_path_minmax_energy(F_graph: nx.Graph, start: tuple, end: tuple,
pruned_path = nx.shortest_path(
pruned_F_graph,
source=start,
target=end,
target=stop,
weight='weight',
)
minmax_energy = max(
Expand Down Expand Up @@ -351,17 +469,17 @@ def find_best_perc_path(F: np.ndarray,
best_path = Pathway()

peaks = vol.find_peaks()
for starting_point in peaks:
for start_point in peaks:

# Get the end point which is a periodic image of the peak
end_point = starting_point + image
# Get the stop point which is a periodic image of the peak
stop_point = start_point + image

# Find the shortest percolating path through this peak
try:
path = optimal_path(
F_graph,
tuple(starting_point),
tuple(end_point),
tuple(start_point),
tuple(stop_point),
)
except nx.NetworkXNoPath:
continue
Expand Down
28 changes: 24 additions & 4 deletions src/gemdat/plots/matplotlib/_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,38 @@
from gemdat.volume import Volume


def energy_along_path(*, path: Pathway, structure: Structure,
def energy_along_path(*, paths: Pathway | list[Pathway], structure: Structure,
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(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 +87,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,
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.
Loading

0 comments on commit efd7def

Please sign in to comment.