97 lines
3.3 KiB
Python
97 lines
3.3 KiB
Python
# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK
|
|
"""Mathematical methods used in PrimAITE."""
|
|
from random import random
|
|
from typing import Any, Iterable, Mapping
|
|
|
|
|
|
def simulate_trial(p_of_success: float) -> bool:
|
|
"""
|
|
Simulates the outcome of a single trial in a Bernoulli process.
|
|
|
|
This function returns True with a probability 'p_of_success', simulating a success outcome in a single
|
|
trial of a Bernoulli process. When this function is executed multiple times, the set of outcomes follows
|
|
a binomial distribution. This is useful in scenarios where one needs to model or simulate events that
|
|
have two possible outcomes (success or failure) with a fixed probability of success.
|
|
|
|
:param p_of_success: The probability of success in a single trial, ranging from 0 to 1.
|
|
:returns: True if the trial is successful (with probability 'p_of_success'); otherwise, False.
|
|
"""
|
|
return random() < p_of_success
|
|
|
|
|
|
def graph_has_cycle(graph: Mapping[Any, Iterable[Any]]) -> bool:
|
|
"""Detect cycles in a directed graph.
|
|
|
|
Provide the graph as a dictionary that describes which nodes are linked. For example:
|
|
{0: {1,2}, 1:{2,3}, 3:{0}} here there's a cycle 0 -> 1 -> 3 -> 0
|
|
{'a': ('b','c'), c:('b')} here there is no cycle
|
|
|
|
:param graph: a mapping from node to a set of nodes to which it is connected.
|
|
:type graph: Mapping[Any, Iterable[Any]]
|
|
:return: Whether the graph has any cycles
|
|
:rtype: bool
|
|
"""
|
|
visited = set()
|
|
currently_visiting = set()
|
|
|
|
def depth_first_search(node: Any) -> bool:
|
|
"""Perform depth-first search (DFS) traversal to detect cycles starting from a given node."""
|
|
if node in currently_visiting:
|
|
return True # Cycle detected
|
|
if node in visited:
|
|
return False # Already visited, no need to explore further
|
|
|
|
visited.add(node)
|
|
currently_visiting.add(node)
|
|
|
|
for neighbour in graph.get(node, []):
|
|
if depth_first_search(neighbour):
|
|
return True # Cycle detected
|
|
|
|
currently_visiting.remove(node)
|
|
return False
|
|
|
|
# Start DFS traversal from each node
|
|
for node in graph:
|
|
if depth_first_search(node):
|
|
return True # Cycle detected
|
|
|
|
return False # No cycles found
|
|
|
|
|
|
def topological_sort(graph: Mapping[Any, Iterable[Any]]) -> Iterable[Any]:
|
|
"""
|
|
Perform topological sorting on a directed graph.
|
|
|
|
This guarantees that if there's a directed edge from node A to node B, then A appears before B.
|
|
|
|
:param graph: A dictionary representing the directed graph, where keys are node identifiers
|
|
and values are lists of outgoing edges from each node.
|
|
:type graph: dict[int, list[Any]]
|
|
|
|
:return: A topologically sorted list of node identifiers.
|
|
:rtype: list[Any]
|
|
"""
|
|
visited: set[Any] = set()
|
|
stack: list[Any] = []
|
|
|
|
def dfs(node: Any) -> None:
|
|
"""
|
|
Depth-first search traversal to visit nodes and their neighbors.
|
|
|
|
:param node: The current node to visit.
|
|
:type node: Any
|
|
"""
|
|
if node in visited:
|
|
return
|
|
visited.add(node)
|
|
for neighbour in graph.get(node, []):
|
|
dfs(neighbour)
|
|
stack.append(node)
|
|
|
|
# Perform DFS traversal from each node
|
|
for node in graph:
|
|
dfs(node)
|
|
|
|
return stack
|