use std::hash::Hasher;
use hashbrown::HashTable;
use rustc_hash::FxHasher;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
conformance::alignments::{
sync_prod_net::{SyncProdNetConstructionError, SyncProdNetTransition, SyncProductNet},
AlignmentMove, AlignmentResult,
},
utils::dijkstra_search::{search, NodeID, SearchError, SearchProblem, SearchState},
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum AlignmentError {
SearchError(SearchError),
SyncProdNetConstructionFailed(SyncProdNetConstructionError),
}
impl std::fmt::Display for AlignmentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for AlignmentError {}
impl From<SyncProdNetConstructionError> for AlignmentError {
fn from(value: SyncProdNetConstructionError) -> Self {
Self::SyncProdNetConstructionFailed(value)
}
}
impl From<SearchError> for AlignmentError {
fn from(value: SearchError) -> Self {
Self::SearchError(value)
}
}
pub type TokenCount = u8;
pub type TracePos = u16;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub(crate) struct PetriNetStep {
transition: u32,
log_move: bool,
}
#[derive(Debug, Default)]
pub(crate) struct PetriNetAlignmentSpace {
num_places: usize,
markings: Vec<TokenCount>,
trace_pos: Vec<TracePos>,
seen: HashTable<NodeID>,
current: Vec<TokenCount>,
next: Vec<TokenCount>,
}
impl PetriNetAlignmentSpace {
fn reset(&mut self, net: &SyncProductNet) {
self.num_places = net.num_model_places;
self.markings.clear();
self.trace_pos.clear();
self.seen.clear();
self.current.resize(net.num_model_places, 0);
self.next.resize(net.num_model_places, 0);
self.markings
.extend_from_slice(&net.initial_marking[..net.num_model_places]);
self.trace_pos.push(0);
self.add_seen(0);
}
#[inline]
fn add_seen(&mut self, node: NodeID) {
let off = node as usize * self.num_places;
let hash = hash_state(
&self.markings[off..off + self.num_places],
self.trace_pos[node as usize],
);
let markings = &self.markings;
let trace_pos = &self.trace_pos;
let num_places = self.num_places;
self.seen.insert_unique(hash, node, |other| {
let off = *other as usize * num_places;
hash_state(&markings[off..off + num_places], trace_pos[*other as usize])
});
}
#[inline]
fn find_seen(&self, marking: &[TokenCount], trace_position: TracePos) -> Option<NodeID> {
let hash = hash_state(marking, trace_position);
let num_places = self.num_places;
self.seen
.find(hash, |node| {
let off = *node as usize * num_places;
&self.markings[off..off + num_places] == marking
&& self.trace_pos[*node as usize] == trace_position
})
.copied()
}
}
#[derive(Debug)]
struct PetriNetAlignment<'a> {
net: &'a SyncProductNet,
space: &'a mut PetriNetAlignmentSpace,
}
impl<'a> PetriNetAlignment<'a> {
fn new(net: &'a SyncProductNet, space: &'a mut PetriNetAlignmentSpace) -> Self {
Self { net, space }
}
}
impl SearchProblem for PetriNetAlignment<'_> {
type Step = PetriNetStep;
type Cost = u32;
fn initial(&mut self) -> NodeID {
self.space.reset(self.net);
0
}
fn max_edge_cost(&self) -> u32 {
self.net.max_edge_cost
}
#[inline]
fn is_goal(&self, node: NodeID) -> bool {
let np = self.net.num_model_places;
let off = node as usize * np;
self.space.trace_pos[node as usize] == self.net.trace_length
&& self.space.markings[off..off + np] == self.net.final_marking[..np]
}
#[inline]
fn expand<F: FnMut(NodeID, bool, u32, PetriNetStep)>(
&mut self,
node: NodeID,
via: Option<PetriNetStep>,
mut emit: F,
) {
let net = self.net;
let space = &mut *self.space;
let np = space.num_places;
let off = node as usize * np;
let trace_pos = space.trace_pos[node as usize];
let last_move_was_log = via.is_some_and(|s| s.log_move);
space
.current
.copy_from_slice(&space.markings[off..off + np]);
let log_or_sync = net
.transitions_by_trace_pos
.get(trace_pos as usize)
.map(|v| v.as_slice())
.unwrap_or_default();
let model_end = if last_move_was_log {
0
} else {
net.num_model_trans
};
for trans_idx in log_or_sync.iter().copied().chain(0..model_end) {
let trans = &net.transitions[trans_idx];
if !is_enabled(&space.current, trans) {
continue;
}
if fire_transition(&space.current, &mut space.next, trans).is_none() {
continue;
}
let is_model_move = matches!(trans.move_type, AlignmentMove::ModelMove { .. });
let new_trace_pos = if is_model_move {
trace_pos
} else {
trace_pos + 1
};
let step = PetriNetStep {
transition: trans_idx as u32,
log_move: matches!(trans.move_type, AlignmentMove::LogMove { .. }),
};
let cost = trans.cost;
match space.find_seen(&space.next, new_trace_pos) {
Some(existing) => emit(existing, false, cost, step),
None => {
let new_id = space.trace_pos.len() as NodeID;
space.markings.extend_from_slice(&space.next);
space.trace_pos.push(new_trace_pos);
space.add_seen(new_id);
emit(new_id, true, cost, step);
}
}
}
}
}
pub(crate) fn align(
net: &SyncProductNet,
space: &mut PetriNetAlignmentSpace,
state: &mut SearchState<PetriNetStep>,
max_states: Option<usize>,
) -> Result<AlignmentResult, AlignmentError> {
let mut problem = PetriNetAlignment::new(net, space);
let res = search(&mut problem, state, max_states)?;
Ok(AlignmentResult {
moves: res
.path
.iter()
.map(|s| net.transitions[s.transition as usize].move_type.clone())
.collect(),
cost: res.cost,
states_visited: res.states_visited,
})
}
#[inline]
fn is_enabled(marking: &[TokenCount], trans: &SyncProdNetTransition) -> bool {
trans
.inputs
.iter()
.all(|(place, weight)| &marking[*place] >= weight)
}
#[inline]
#[must_use]
fn fire_transition(
current: &[TokenCount],
reached: &mut [TokenCount],
trans: &SyncProdNetTransition,
) -> Option<()> {
reached.copy_from_slice(current);
for (place, weight) in &trans.inputs {
reached[*place] -= weight;
}
for (place, weight) in &trans.outputs {
reached[*place] = reached[*place].checked_add(*weight)?;
}
Some(())
}
#[inline]
fn hash_state(marking: &[TokenCount], trace_pos: TracePos) -> u64 {
let mut h = FxHasher::default();
h.write(marking);
h.write_u16(trace_pos);
h.finish()
}