use std::fmt;
use std::str::FromStr;
use serde::de::{Deserializer, Error as DeError};
use serde::{Deserialize, Serialize, Serializer};
use sha2::{Digest, Sha256};
use super::graph::SynapticGraph;
use crate::types::Polarity;
pub const TOPOLOGY_DIGEST_SCHEMA_VERSION: u16 = 1;
pub const TOPOLOGY_DIGEST_DOMAIN: &str = "synaptic-wiring.topology.digest.v1";
pub const TOPOLOGY_DIGEST_ALGORITHM: &str = "sha256";
const DOMAIN_TERMINATOR: u8 = 0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TopologyDigest {
schema_version: u16,
hash: [u8; 32],
}
impl TopologyDigest {
#[must_use]
pub fn from_graph(graph: &SynapticGraph) -> Self {
let edges = canonical_edges(graph);
Self {
schema_version: TOPOLOGY_DIGEST_SCHEMA_VERSION,
hash: hash_v1(graph.neuron_count() as u64, &edges),
}
}
#[must_use]
pub fn schema_version(&self) -> u16 {
self.schema_version
}
#[must_use]
pub fn algorithm(&self) -> &'static str {
TOPOLOGY_DIGEST_ALGORITHM
}
#[must_use]
pub fn domain(&self) -> &'static str {
TOPOLOGY_DIGEST_DOMAIN
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.hash
}
#[must_use]
pub fn to_hex(&self) -> String {
hex_encode(&self.hash)
}
}
impl fmt::Display for TopologyDigest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{TOPOLOGY_DIGEST_DOMAIN}:{TOPOLOGY_DIGEST_ALGORITHM}:{}",
self.to_hex()
)
}
}
impl FromStr for TopologyDigest {
type Err = TopologyDigestParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (domain, algorithm, hex) = split_manifest(s)?;
if domain != TOPOLOGY_DIGEST_DOMAIN {
return Err(TopologyDigestParseError(format!(
"unsupported digest domain {domain:?}"
)));
}
if algorithm != TOPOLOGY_DIGEST_ALGORITHM {
return Err(TopologyDigestParseError(format!(
"unsupported digest algorithm {algorithm:?}"
)));
}
Ok(Self {
schema_version: TOPOLOGY_DIGEST_SCHEMA_VERSION,
hash: hex_decode_sha256(hex)?,
})
}
}
impl Serialize for TopologyDigest {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for TopologyDigest {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(DeError::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TopologyDigestParseError(String);
impl TopologyDigestParseError {
fn format() -> Self {
Self(format!(
"expected {TOPOLOGY_DIGEST_DOMAIN}:{TOPOLOGY_DIGEST_ALGORITHM}:<64 hex chars>"
))
}
}
impl fmt::Display for TopologyDigestParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for TopologyDigestParseError {}
impl SynapticGraph {
#[must_use]
pub fn topology_digest(&self) -> TopologyDigest {
TopologyDigest::from_graph(self)
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct CanonicalEdge {
source: u64,
target: u64,
delay: u16,
polarity: u8,
weight_bits: u32,
}
fn canonical_edges(graph: &SynapticGraph) -> Vec<CanonicalEdge> {
let mut edges = Vec::with_capacity(graph.synapse_count());
for src in 0..graph.neuron_count() {
let source = src as u64;
for (target, weight, delay, polarity) in graph.outgoing(src) {
debug_assert!(
weight.is_finite(),
"valid graphs reject non-finite weights before digest"
);
edges.push(CanonicalEdge {
source,
target: u64::from(target),
delay,
polarity: polarity_tag(polarity),
weight_bits: weight.to_bits(),
});
}
}
edges.sort_unstable();
edges
}
fn hash_v1(neuron_count: u64, edges: &[CanonicalEdge]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(TOPOLOGY_DIGEST_DOMAIN.as_bytes());
hasher.update([DOMAIN_TERMINATOR]);
hasher.update(TOPOLOGY_DIGEST_SCHEMA_VERSION.to_le_bytes());
hasher.update(neuron_count.to_le_bytes());
hasher.update((edges.len() as u64).to_le_bytes());
for edge in edges {
hasher.update(edge.source.to_le_bytes());
hasher.update(edge.target.to_le_bytes());
hasher.update(edge.weight_bits.to_le_bytes());
hasher.update(edge.delay.to_le_bytes());
hasher.update([edge.polarity]);
}
hasher.finalize().into()
}
fn polarity_tag(polarity: Polarity) -> u8 {
match polarity {
Polarity::Excitatory => 0,
Polarity::Inhibitory => 1,
}
}
fn split_manifest(s: &str) -> Result<(&str, &str, &str), TopologyDigestParseError> {
let (domain, rest) = s
.split_once(':')
.ok_or_else(TopologyDigestParseError::format)?;
let (algorithm, hex) = rest
.split_once(':')
.ok_or_else(TopologyDigestParseError::format)?;
Ok((domain, algorithm, hex))
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
out
}
fn hex_decode_sha256(hex: &str) -> Result<[u8; 32], TopologyDigestParseError> {
if hex.len() != 64 {
return Err(TopologyDigestParseError(format!(
"digest hex must be 64 characters, got {}",
hex.len()
)));
}
let mut out = [0u8; 32];
let bytes = hex.as_bytes();
for (i, slot) in out.iter_mut().enumerate() {
let hi = hex_nibble(bytes[i * 2])?;
let lo = hex_nibble(bytes[i * 2 + 1])?;
*slot = (hi << 4) | lo;
}
Ok(out)
}
fn hex_nibble(b: u8) -> Result<u8, TopologyDigestParseError> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(TopologyDigestParseError(format!(
"invalid hex digit {:?}",
b as char
))),
}
}