sov-kernel-monster / haskell /ConsensusVoting.hs
SNAPKITTYWEST's picture
chore: push full sov-kernel-monster content from local build
9425aed verified
Raw
History Blame Contribute Delete
15.7 kB
{-# LANGUAGE DeriveGeneric #-}
module ConsensusVoting
( castVote
, consensusRound
, detectConflict
, resolveConflict
, syncAgentStates
, multiAgentVote
, getConsensusObservations
, aggregateVotesForObservation
, updateWorldModelWithConsensus
, anomalyScoring
, conflictThreshold
) where
import ConsensusTypes
import qualified Data.Map as Map
import Data.Map (Map)
import Data.List (sortBy, groupBy, maximumBy, nub)
import Data.Ord (comparing, Down(..))
import GHC.Generics (Generic)
-- ─────────────────────────────────────────────────────────────────────────────
-- Consensus Parameters
-- ─────────────────────────────────────────────────────────────────────────────
-- | Threshold for declaring observations in conflict (normalized difference)
conflictThreshold :: Double
conflictThreshold = 0.25
-- | Minimum votes needed to form consensus
minVotesForConsensus :: Int
minVotesForConsensus = 2
-- | Consensus threshold (66%+)
consensusThreshold :: Double
consensusThreshold = 0.66
-- | Anomaly severity threshold (1-10 scale)
anomalySeverityThreshold :: Double
anomalySeverityThreshold = 0.5
-- ─────────────────────────────────────────────────────────────────────────────
-- Voting: Cast, Aggregate, Tally
-- ─────────────────────────────────────────────────────────────────────────────
-- | Cast a vote: voter agrees/disagrees with observation
castVote :: ConsensusState -> AgentId -> ObservationId -> Double -> Int -> ConsensusState
castVote state voterId obsId agreeScore round =
let vote = makeVote voterId obsId agreeScore round 0
newVotes = votes state ++ [vote]
in state { votes = newVotes }
-- | Aggregate votes for single observation into consensus score
aggregateVotesForObservation :: ConsensusState -> ObservationId -> (Double, Int)
aggregateVotesForObservation state obsId =
let obsVotes = votesForObservation (votes state) obsId
voteCount = length obsVotes
in if voteCount < minVotesForConsensus
then (0.0, 0)
else (consensusScore obsVotes, voteCount)
-- | Get all observations that reached consensus (66%+)
getConsensusObservations :: ConsensusState -> [Observation]
getConsensusObservations state =
let allObs = observations state
in filter (\obs -> let (score, count) = aggregateVotesForObservation state (obsId obs)
in count >= minVotesForConsensus && score > 0.33) allObs
-- ─────────────────────────────────────────────────────────────────────────────
-- Consensus Round: Observation β†’ Voting β†’ World Model Update
-- ─────────────────────────────────────────────────────────────────────────────
-- | Execute full consensus round: tally votes, detect conflicts, update world model
consensusRound :: ConsensusState -> [AgentId] -> Int -> ConsensusState
consensusRound state participatingAgents roundNum =
let -- Step 1: Group observations by ObservationId
obsGroups = groupObservationsById state
-- Step 2: Compute consensus for each group
consensusResults = map (\(obsId, obs) ->
let (score, voteCount) = aggregateVotesForObservation state obsId
vots = votesForObservation (votes state) obsId
in (obsId, obs, score, voteCount, vots)
) obsGroups
-- Step 3: Separate consensed vs. conflicted
(consensusObs, conflictedObs) = partitionConsensus consensusResults
-- Step 4: Detect conflicts
detectedConflicts = detectConflictsBatch state conflictedObs
-- Step 5: Resolve conflicts via majority vote
resolvedObs = map (\(o, c) -> resolveConflictVia state o c) (zip conflictedObs detectedConflicts)
-- Step 6: All observations that passed consensus filter
finalConsensusObs = consensusObs ++ resolvedObs
-- Step 7: Update world model
updatedModel = updateWorldModelWithConsensus (worldModel state) finalConsensusObs state
-- Step 8: Compute global confidence
globalConf = if null finalConsensusObs
then confidence state
else averageDouble (map (\(_, _, s, _, _) -> s) consensusResults)
-- Step 9: Create vote round record
voteRound = VoteRound roundNum 0 (votes state) (observations state)
-- Step 10: Increment generation
newGen = generation state + 1
in state
{ worldModel = updatedModel
, confidence = globalConf
, voteRounds = voteRounds state ++ [voteRound]
, conflicts = conflicts state ++ detectedConflicts
, generation = newGen
}
-- | Group observations by ObservationId
groupObservationsById :: ConsensusState -> [(ObservationId, [Observation])]
groupObservationsById state =
let grouped = groupBy (\o1 o2 -> obsId o1 == obsId o2)
(sortBy (comparing obsId) (observations state))
in map (\g -> (obsId (head g), g)) grouped
-- | Partition consensus results into consensed vs. conflicted
partitionConsensus :: [(ObservationId, [Observation], Double, Int, [Vote])]
-> ([(ObservationId, [Observation], Double, Int, [Vote])],
[(ObservationId, [Observation], Double, Int, [Vote])])
partitionConsensus results =
let isConflicted (_, _, score, count, _) =
count >= minVotesForConsensus && score <= 0.33
in (filter (not . isConflicted) results, filter isConflicted results)
-- ─────────────────────────────────────────────────────────────────────────────
-- Conflict Detection + Resolution
-- ─────────────────────────────────────────────────────────────────────────────
-- | Detect conflicts: two agents' measurements differ by > threshold
detectConflictsBatch :: ConsensusState
-> [(ObservationId, [Observation], Double, Int, [Vote])]
-> [Conflict]
detectConflictsBatch state conflictedGroups =
map (\(obsId, obs, _, _, vots) ->
let agents = map agentId obs
measDiff = if length obs >= 2
then
let m1 = measurements (head obs)
m2 = measurements (obs !! 1)
in measurementDifference m1 m2
else 0.0
in Conflict obsId (RegionId 0) agents measDiff False
) conflictedGroups
-- | Detect conflict between two observations
detectConflict :: Observation -> Observation -> Bool
detectConflict obs1 obs2 =
let dist = measurementDifference (measurements obs1) (measurements obs2)
in dist > conflictThreshold && vectorDistance (coordinates obs1) (coordinates obs2) < 0.1
-- | Resolve conflict by majority vote
resolveConflict :: ConsensusState -> Conflict -> Observation
resolveConflict state conflict =
case filter (\o -> obsId o == conflictObsId conflict) (observations state) of
[] -> error "Conflict references non-existent observation"
(o:_) -> o
-- | Resolve conflict in batch
resolveConflictVia :: ConsensusState -> (ObservationId, [Observation], Double, Int, [Vote])
-> Conflict -> Observation
resolveConflictVia state (obsId, obs, _, _, votes) conflict =
if null obs then error "resolveConflictVia: no observations in conflict group"
else if null votes
then head obs -- No votes: return first observation
else
-- Find observation with highest average agreement
let obsWithScores = [(o, averageDouble [agreement v | v <- votes, votedObsId v == obsId o]) | o <- obs]
in if null obsWithScores
then head obs -- Fallback: no matching votes found
else let (winningObs, _) = maximumBy (comparing snd) obsWithScores
in winningObs
-- ─────────────────────────────────────────────────────────────────────────────
-- Multi-Agent Voting
-- ─────────────────────────────────────────────────────────────────────────────
-- | All agents vote on a new observation from one agent
multiAgentVote :: ConsensusState -> Observation -> [AgentId] -> Int -> ConsensusState
multiAgentVote state newObs voterIds round =
let -- Observation already added to state
obsIdToVoteOn = obsId newObs
-- Each agent compares against their own measurements in same region
votes = concatMap (\voterId ->
let otherObs = observationsByAgent (observations state) voterId
-- If agent has measurements in same region, vote on similarity
similarObs = filter (\o -> case (regionType newObs, regionType o) of
(Just r1, Just r2) -> r1 == r2
_ -> False) otherObs
in if null similarObs
then [makeVote voterId obsIdToVoteOn 0.0 round 0] -- Uncertain
else let avgDiff = averageDouble [measurementDifference (measurements newObs) (measurements o) | o <- similarObs]
agreeScore = 1.0 - min 1.0 (avgDiff / 0.5) -- normalize to [-1, 1]
in [makeVote voterId obsIdToVoteOn agreeScore round 0]
) voterIds
newVotes = votes state ++ votes
in state { votes = newVotes }
-- ─────────────────────────────────────────────────────────────────────────────
-- World Model Update
-- ─────────────────────────────────────────────────────────────────────────────
-- | Update world model with newly consensed observations
updateWorldModelWithConsensus :: WorldModel -> [Observation] -> ConsensusState -> WorldModel
updateWorldModelWithConsensus model consensusObs state =
let -- Extract regions
regionsFromObs = [(RegionId i, regionType o) | (i, o) <- zip [0..] consensusObs, regionType o /= Nothing]
newRegionMap = Map.fromList [(rId, rt) | (rId, Just rt) <- regionsFromObs]
-- Update agent positions (latest from consensus observations)
newAgentPositions = Map.fromList [(agentId o, coordinates o) | o <- consensusObs]
-- Detect anomalies
detectedAnomalies = anomalyScoring state consensusObs
-- Frontier regions (low confidence, high anomaly)
frontierIds = [RegionId i | (i, _) <- zip [0..] consensusObs, any (\a -> anomalySeverity a > 0.6) detectedAnomalies]
-- Extract confidence from observations
obsConfidenceValues = map (\o -> o.confidence) consensusObs
in WorldModel
{ regionTypes = Map.union newRegionMap (regionTypes model)
, agentPositions = Map.union newAgentPositions (agentPositions model)
, anomalies = anomalies model ++ detectedAnomalies
, frontierRegions = nub (frontierRegions model ++ frontierIds)
, modelConfidence = averageDouble obsConfidenceValues
, modelGeneration = modelGeneration model + 1
}
-- ─────────────────────────────────────────────────────────────────────────────
-- Anomaly Detection
-- ─────────────────────────────────────────────────────────────────────────────
-- | Score anomalies from consensus observations
anomalyScoring :: ConsensusState -> [Observation] -> [Anomaly]
anomalyScoring state consensusObs =
let -- Find observations with high measurement variance
highVarianceObs = filter (\o -> confidence o < 0.5) consensusObs
-- Group by location
locGroups = groupBy (\o1 o2 -> vectorDistance (coordinates o1) (coordinates o2) < 0.05)
(sortBy (comparing coordinates) highVarianceObs)
-- Create anomalies
anomalies = concatMap (\group ->
if length group > 0
then let avgLoc = Vector
(averageDouble (map (\o -> vx (coordinates o)) group))
(averageDouble (map (\o -> vy (coordinates o)) group))
(averageDouble (map (\o -> vz (coordinates o)) group))
agentSet = map agentId group
severity = 1.0 - averageDouble (map confidence group)
in [Anomaly (length (anomalies state)) avgLoc severity 0 agentSet 0.5]
else []
) locGroups
in anomalies
-- ─────────────────────────────────────────────────────────────────────────────
-- State Synchronization
-- ─────────────────────────────────────────────────────────────────────────────
-- | Synchronize all agents to shared world model and confidence
syncAgentStates :: ConsensusState -> ConsensusState
syncAgentStates state =
-- All agents converge to world model + global confidence
-- This is a no-op in ConsensusState (agents are external)
-- but signifies: all agents should now use state's worldModel + confidence
state
-- ─────────────────────────────────────────────────────────────────────────────
-- Utility: Observation as a data type that can be updated
-- ─────────────────────────────────────────────────────────────────────────────
-- | Mark observation as WORM-sealed
sealObservation :: Observation -> Int -> Observation
sealObservation obs round = obs { wormSealed = True, sealRound = Just round }
-- | Seal all observations in consensus state
sealObservationsInRound :: ConsensusState -> Int -> ConsensusState
sealObservationsInRound state round =
let sealedObs = map (\o -> sealObservation o round) (observations state)
in state { observations = sealedObs }