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 all commits
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
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
"""
SCiarella marked this conversation as resolved.
Show resolved Hide resolved

# 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,
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(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,
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.
Loading
Loading