#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DfgActivityId(pub &'static str);
impl DfgActivityId {
#[must_use]
pub const fn new(name: &'static str) -> Self {
DfgActivityId(name)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.0
}
#[inline]
#[must_use]
pub const fn into_inner(self) -> &'static str {
self.0
}
#[inline]
#[must_use]
pub const fn as_inner(&self) -> &'static str {
self.0
}
}
impl From<&'static str> for DfgActivityId {
#[inline]
fn from(s: &'static str) -> Self {
DfgActivityId(s)
}
}
impl AsRef<str> for DfgActivityId {
#[inline]
fn as_ref(&self) -> &str {
self.0
}
}
impl core::fmt::Display for DfgActivityId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.0)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct DfgSourceMarker;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct DfgTargetMarker;
mod dfg_endpoint_seal {
pub trait SourceSeal {}
pub trait TargetSeal {}
impl SourceSeal for super::DfgSourceMarker {}
impl TargetSeal for super::DfgTargetMarker {}
}
pub trait IsDfgSource: dfg_endpoint_seal::SourceSeal {}
impl IsDfgSource for DfgSourceMarker {}
pub trait IsDfgTarget: dfg_endpoint_seal::TargetSeal {}
impl IsDfgTarget for DfgTargetMarker {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct DfgWeight(pub u64);
impl DfgWeight {
pub fn count(self) -> u64 {
self.0
}
#[must_use]
pub fn into_frequency(self) -> DfgFrequency {
DfgFrequency(self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct DfgNode {
activity: String,
}
impl DfgNode {
pub fn new(activity: impl Into<String>) -> Self {
DfgNode {
activity: activity.into(),
}
}
pub fn activity(&self) -> &str {
&self.activity
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DfgEdge {
from: String,
to: String,
weight: DfgWeight,
}
impl DfgEdge {
pub fn new(from: impl Into<String>, to: impl Into<String>, weight: u64) -> Self {
DfgEdge {
from: from.into(),
to: to.into(),
weight: DfgWeight(weight),
}
}
pub fn from(&self) -> &str {
&self.from
}
pub fn to(&self) -> &str {
&self.to
}
pub fn weight(&self) -> DfgWeight {
self.weight
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Dfg {
nodes: Vec<DfgNode>,
edges: Vec<DfgEdge>,
}
impl Dfg {
pub fn new(
nodes: impl IntoIterator<Item = DfgNode>,
edges: impl IntoIterator<Item = DfgEdge>,
) -> Self {
Dfg {
nodes: nodes.into_iter().collect(),
edges: edges.into_iter().collect(),
}
}
pub fn nodes(&self) -> &[DfgNode] {
&self.nodes
}
pub fn edges(&self) -> &[DfgEdge] {
&self.edges
}
#[must_use = "check the shape-check result"]
pub fn validate(&self) -> Result<(), DfgRefusal> {
use std::collections::HashSet;
if self.nodes.is_empty() {
return Err(DfgRefusal::EmptyGraph);
}
let mut acts: HashSet<&str> = HashSet::new();
for n in &self.nodes {
if n.activity().is_empty() {
return Err(DfgRefusal::MissingActivity);
}
acts.insert(n.activity());
}
for e in &self.edges {
if !acts.contains(e.from()) || !acts.contains(e.to()) {
return Err(DfgRefusal::DanglingEdge);
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DfgRefusal {
MissingActivity,
NegativeWeight,
DanglingEdge,
EmptyGraph,
DiscoveryRequired,
InconsistentObjectType,
}
impl core::fmt::Display for DfgRefusal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let law = match self {
DfgRefusal::MissingActivity => "MissingActivity",
DfgRefusal::NegativeWeight => "NegativeWeight",
DfgRefusal::DanglingEdge => "DanglingEdge",
DfgRefusal::EmptyGraph => "EmptyGraph",
DfgRefusal::DiscoveryRequired => "DiscoveryRequired",
DfgRefusal::InconsistentObjectType => "InconsistentObjectType",
};
write!(f, "DFG refused by law: {law}")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct DfgFrequency(pub u64);
impl DfgFrequency {
#[must_use]
pub fn count(self) -> u64 {
self.0
}
#[must_use]
pub fn from_weight(w: DfgWeight) -> Self {
DfgFrequency(w.0)
}
#[must_use]
pub fn into_weight(self) -> DfgWeight {
DfgWeight(self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct DfgDuration(pub i64);
impl DfgDuration {
#[must_use]
pub fn ns(self) -> i64 {
self.0
}
#[must_use]
pub fn is_negative(self) -> bool {
self.0 < 0
}
#[must_use]
pub fn from_ns(ns: i64) -> Option<Self> {
if ns < 0 {
None
} else {
Some(DfgDuration(ns))
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DfgEdgeFull {
from: String,
to: String,
frequency: DfgFrequency,
duration_ns: Option<DfgDuration>,
}
impl DfgEdgeFull {
pub fn new(from: impl Into<String>, to: impl Into<String>, freq: u64) -> Self {
DfgEdgeFull {
from: from.into(),
to: to.into(),
frequency: DfgFrequency(freq),
duration_ns: None,
}
}
pub fn with_duration_ns(mut self, ns: i64) -> Self {
self.duration_ns = Some(DfgDuration(ns));
self
}
pub fn from(&self) -> &str {
&self.from
}
pub fn to(&self) -> &str {
&self.to
}
pub fn frequency(&self) -> DfgFrequency {
self.frequency
}
#[must_use]
pub fn duration_ns(&self) -> Option<DfgDuration> {
self.duration_ns
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DfgTypedEdge<S: IsDfgSource, T: IsDfgTarget> {
from: String,
to: String,
weight: DfgWeight,
_src: core::marker::PhantomData<S>,
_tgt: core::marker::PhantomData<T>,
}
impl<S: IsDfgSource, T: IsDfgTarget> DfgTypedEdge<S, T> {
pub fn new(from: impl Into<String>, to: impl Into<String>, weight: u64) -> Self {
DfgTypedEdge {
from: from.into(),
to: to.into(),
weight: DfgWeight(weight),
_src: core::marker::PhantomData,
_tgt: core::marker::PhantomData,
}
}
pub fn from(&self) -> &str {
&self.from
}
pub fn to(&self) -> &str {
&self.to
}
pub fn weight(&self) -> DfgWeight {
self.weight
}
pub fn into_edge(self) -> DfgEdge {
DfgEdge {
from: self.from,
to: self.to,
weight: self.weight,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DfgObjectType(pub &'static str);
impl DfgObjectType {
#[must_use]
pub const fn new(name: &'static str) -> Self {
DfgObjectType(name)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.0
}
#[inline]
#[must_use]
pub const fn into_inner(self) -> &'static str {
self.0
}
#[inline]
#[must_use]
pub const fn as_inner(&self) -> &'static str {
self.0
}
}
impl From<&'static str> for DfgObjectType {
#[inline]
fn from(s: &'static str) -> Self {
DfgObjectType(s)
}
}
impl AsRef<str> for DfgObjectType {
#[inline]
fn as_ref(&self) -> &str {
self.0
}
}
impl core::fmt::Display for DfgObjectType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.0)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ObjectCentricDfg {
pub per_type: Vec<(String, Dfg)>,
}
impl ObjectCentricDfg {
pub fn new() -> Self {
ObjectCentricDfg::default()
}
pub fn with_type_dfg(mut self, object_type: impl Into<String>, dfg: Dfg) -> Self {
self.per_type.push((object_type.into(), dfg));
self
}
#[must_use]
pub fn get(&self, object_type: &str) -> Option<&Dfg> {
self.per_type
.iter()
.find(|(t, _)| t == object_type)
.map(|(_, d)| d)
}
pub fn object_types(&self) -> impl Iterator<Item = &str> {
self.per_type.iter().map(|(t, _)| t.as_str())
}
#[must_use = "check the shape-check result"]
pub fn validate_all(&self) -> Result<(), DfgRefusal> {
use std::collections::HashSet;
let mut seen: HashSet<&str> = HashSet::new();
for (object_type, dfg) in &self.per_type {
if !seen.insert(object_type.as_str()) {
return Err(DfgRefusal::InconsistentObjectType);
}
dfg.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct DfgEdgeKey {
from: String,
to: String,
}
impl DfgEdgeKey {
pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
DfgEdgeKey {
from: from.into(),
to: to.into(),
}
}
pub fn from(&self) -> &str {
&self.from
}
pub fn to(&self) -> &str {
&self.to
}
pub fn into_edge(self, weight: u64) -> DfgEdge {
DfgEdge {
from: self.from,
to: self.to,
weight: DfgWeight(weight),
}
}
}
use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DirectlyFollowsGraph {
pub activities: HashMap<String, u64>,
pub arcs: HashMap<(String, String), u64>,
pub start_activities: HashMap<String, u64>,
pub end_activities: HashMap<String, u64>,
pub trace_count: u64,
}
impl DirectlyFollowsGraph {
pub fn empty() -> Self {
Self::default()
}
pub fn activity_count(&self, activity: &str) -> u64 {
self.activities.get(activity).copied().unwrap_or(0)
}
pub fn arc_count(&self, from: &str, to: &str) -> u64 {
self.arcs
.get(&(from.to_string(), to.to_string()))
.copied()
.unwrap_or(0)
}
pub fn start_count(&self, activity: &str) -> u64 {
self.start_activities.get(activity).copied().unwrap_or(0)
}
pub fn end_count(&self, activity: &str) -> u64 {
self.end_activities.get(activity).copied().unwrap_or(0)
}
pub fn follows(&self, from: &str, to: &str) -> bool {
self.arc_count(from, to) > 0
}
pub fn causes(&self, a: &str, b: &str) -> bool {
self.follows(a, b) && !self.follows(b, a)
}
pub fn parallel(&self, a: &str, b: &str) -> bool {
self.follows(a, b) && self.follows(b, a)
}
pub fn exclusive(&self, a: &str, b: &str) -> bool {
!self.follows(a, b) && !self.follows(b, a)
}
pub fn activities_sorted(&self) -> Vec<&str> {
let mut v: Vec<&str> = self.activities.keys().map(String::as_str).collect();
v.sort_unstable();
v
}
pub fn to_structural_dfg(&self) -> Option<Dfg> {
if self.activities.is_empty() {
return None;
}
let nodes: Vec<DfgNode> = {
let mut names: Vec<&str> = self.activities.keys().map(String::as_str).collect();
names.sort_unstable();
names.iter().map(|a| DfgNode::new(*a)).collect()
};
let edges: Vec<DfgEdge> = {
let mut arcs: Vec<_> = self.arcs.iter().collect();
arcs.sort_unstable_by_key(|((f, t), _)| (f.as_str(), t.as_str()));
arcs.iter()
.map(|((f, t), &w)| DfgEdge::new(f.as_str(), t.as_str(), w))
.collect()
};
Some(Dfg::new(nodes, edges))
}
}
#[derive(Debug, Default)]
pub struct DfgMiner {
dfg: DirectlyFollowsGraph,
}
impl DfgMiner {
pub fn new() -> Self {
Self::default()
}
pub fn record_trace(&mut self, activities: &[impl AsRef<str>]) {
if activities.is_empty() {
return;
}
self.dfg.trace_count += 1;
*self
.dfg
.start_activities
.entry(activities[0].as_ref().to_string())
.or_insert(0) += 1;
*self
.dfg
.end_activities
.entry(activities[activities.len() - 1].as_ref().to_string())
.or_insert(0) += 1;
for i in 0..activities.len() {
let a = activities[i].as_ref();
*self.dfg.activities.entry(a.to_string()).or_insert(0) += 1;
if i + 1 < activities.len() {
let b = activities[i + 1].as_ref();
*self
.dfg
.arcs
.entry((a.to_string(), b.to_string()))
.or_insert(0) += 1;
}
}
}
pub fn trace_count(&self) -> u64 {
self.dfg.trace_count
}
pub fn as_dfg(&self) -> &DirectlyFollowsGraph {
&self.dfg
}
pub fn build(self) -> DirectlyFollowsGraph {
self.dfg
}
pub fn from_traces<I, T, S>(traces: I) -> DirectlyFollowsGraph
where
I: IntoIterator<Item = T>,
T: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut miner = Self::new();
for trace in traces {
let acts: Vec<S> = trace.into_iter().collect();
miner.record_trace(&acts);
}
miner.build()
}
}