haal-0.7.0.0: src/Haal/Statistics.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
#ifdef LIQUID
-- GHC unboxes the strict Int fields of 'Tally' when optimising, which
-- LiquidHaskell cannot match against the data refinement, so verification
-- builds keep them boxed. Remove once this is fixed upstream:
-- https://github.com/ucsd-progsys/liquidhaskell/issues/2629
{-# OPTIONS_GHC -fplugin=LiquidHaskell
-fplugin-opt=LiquidHaskell:--prune-unsorted
-fno-unbox-small-strict-fields #-}
#endif
{- | Statistics for learning experiments.
An experiment emits an 'Event' at every step that matters for statistics: when
it changes 'Phase', for every query sent to the SUL, for every hypothesis and for
every counterexample. A statistic is a fold over these events, a
'Control.Foldl.Fold' from the @foldl@ package. Folds combine with their
'Applicative' instance into a single fold that still runs in one pass, so any
number of statistics can be measured at once:
> (model, stats) = runExperiment (measuredExperiment statistics learner oracle) sul
'statistics' measures what most experiments report: the membership queries and
symbols sent while constructing hypotheses and while validating them, and every
hypothesis and counterexample. Combine it with folds of your own:
> runExperiment (measuredExperiment ((,) <$> statistics <*> myFold) learner oracle) sul
See 'Haal.Experiment.measuredExperiment' and 'Haal.Experiment.experimentWith'.
Users define their own statistics as folds, e.g. with 'Control.Foldl.premap'
and 'Control.Foldl.prefilter' over the folds of @foldl@, and can test one by
running it over a list of events with 'Control.Foldl.fold'.
-}
module Haal.Statistics (
-- * Events
Phase (..),
Event (..),
-- * Statistics
Statistics (..),
Tally (..),
statistics,
total,
rounds,
)
where
import qualified Control.Foldl as L
-- | The phase of a learning experiment.
data Phase
= -- | The learner is constructing or refining a hypothesis.
Learning
| -- | The oracle is validating a hypothesis. This is the equivalence query,
-- approximated by conformance testing, which sends test cases to the SUL
-- as membership queries.
Testing
deriving (Show, Eq)
-- | Something that happened during an experiment.
data Event aut i o
= -- | The experiment entered a phase.
PhaseChanged Phase
| -- | A query was sent to the SUL, with its inputs and outputs.
Queried [i] [o]
| -- | The learner produced a hypothesis (every one, including the final one).
Hypothesis (aut Int i o)
| -- | The oracle found a counterexample.
Counterexample [i]
{- | The number of queries and symbols sent to a SUL. The fields are strict,
because an experiment sends a great number of queries.
-}
data Tally = Tally {queries :: !Int, symbols :: !Int} deriving (Show, Eq)
{-@ data Tally = Tally {queries :: Nat, symbols :: Nat} @-}
{- | The statistics of an experiment, measured by 'statistics'. The number of
equivalence queries is the number of hypotheses: each hypothesis is validated
once.
-}
data Statistics aut i o = Statistics
{ learning :: !Tally
-- ^ Membership queries during hypothesis construction
, testing :: !Tally
-- ^ Membership queries during hypothesis validation
, hypotheses :: [aut Int i o]
-- ^ Every hypothesis, including the final one, most recent first
, counterexamples :: [[i]]
-- ^ Every counterexample, most recent first
}
deriving instance (Show (aut Int i o), Show i) => Show (Statistics aut i o)
deriving instance (Eq (aut Int i o), Eq i) => Eq (Statistics aut i o)
{-@ data Statistics aut i o = Statistics
{ learning :: Tally
, testing :: Tally
, hypotheses :: [aut Int i o]
, counterexamples :: [[i]]
} @-}
-- | Queries and symbols of both phases together.
{-@ total :: s:Statistics aut i o -> {t:Tally | queries t == queries (learning s) + queries (testing s)
&& symbols t == symbols (learning s) + symbols (testing s)} @-}
total :: Statistics aut i o -> Tally
total s = Tally (queries l + queries t) (symbols l + symbols t)
where
l = learning s
t = testing s
-- | The number of rounds, i.e. of counterexamples found.
rounds :: Statistics aut i o -> Int
rounds = length . counterexamples
{- | Add queries and symbols to the tally of a phase. Verified by LiquidHaskell:
the total grows by exactly the given amounts, and the tally of the other phase
is untouched.
-}
{-@ tick :: p:Phase -> q:Nat -> n:Nat -> s:Statistics aut i o
-> {r:Statistics aut i o | queries (learning r) + queries (testing r) == queries (learning s) + queries (testing s) + q
&& symbols (learning r) + symbols (testing r) == symbols (learning s) + symbols (testing s) + n
&& (p == Learning => (queries (testing r) == queries (testing s)
&& symbols (testing r) == symbols (testing s)))
&& (p == Testing => (queries (learning r) == queries (learning s)
&& symbols (learning r) == symbols (learning s)))} @-}
tick :: Phase -> Int -> Int -> Statistics aut i o -> Statistics aut i o
tick p q n s = case p of
Learning -> s{learning = add (learning s)}
Testing -> s{testing = add (testing s)}
where
add (Tally q0 n0) = Tally (q0 + q) (n0 + n)
-- | The state of 'statistics': the current phase and the statistics so far.
data Acc aut i o = Acc !Phase !(Statistics aut i o)
{- | Measure the 'Statistics' of an experiment: the membership queries and
symbols sent in each phase, and every hypothesis and counterexample.
-}
statistics :: L.Fold (Event aut i o) (Statistics aut i o)
statistics = L.Fold step (Acc Learning (Statistics (Tally 0 0) (Tally 0 0) [] [])) (\(Acc _ s) -> s)
where
step (Acc _ s) (PhaseChanged p) = Acc p s
step (Acc p s) (Queried is _) = Acc p (tick p 1 (length is) s)
step (Acc p s) (Hypothesis aut) = Acc p s{hypotheses = aut : hypotheses s}
step (Acc p s) (Counterexample cex) = Acc p s{counterexamples = cex : counterexamples s}