# © 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