File size: 15,721 Bytes
9425aed | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | {-# 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 }
|