stim-parser 0.1.0.0 → 0.2.0.0
raw patch · 11 files changed
+763/−41 lines, 11 files
Files
- CHANGELOG.md +33/−2
- README.md +48/−0
- src/StimParser/DEM/Expr.hs +110/−0
- src/StimParser/DEM/Parse.hs +70/−0
- src/StimParser/Expr.hs +4/−4
- src/StimParser/Parse.hs +2/−32
- src/StimParser/ParseUtils.hs +31/−1
- stim-parser.cabal +7/−2
- test/Main.hs +4/−0
- test/Test/DEM/Expr.hs +187/−0
- test/Test/DEM/Parse.hs +267/−0
CHANGELOG.md view
@@ -1,5 +1,36 @@ # Revision history for stim-parser -## 0.1.0.0 -- YYYY-mm-dd+## 0.2.0.0 -- 2026-04-01 -* First version. Released on an unsuspecting world.+* Add DEM (Detector Error Model) parsing support.+ New modules: `StimParser.DEM.Expr`, `StimParser.DEM.Parse`.+* Add `flattenDEM` utility to expand `repeat` blocks and apply+ `shift_detectors` coordinate/ID shifts.+* Migrate all floating-point types from `Float` to `Double` for+ higher precision probability representation.+* Move `parseExhaust`, `parseTuple`, and `parseTupleFloat` to+ `StimParser.ParseUtils` as shared lexer utilities.+* Add `parseNumber` for parsing bare integers or floats as `Double`.+* Expand test suite with DEM parser and flattening tests.++## 0.1.0.0 -- 2026-03-31++* Initial release.+* Parse STIM quantum circuit files into a Haskell AST.+* Supported circuit elements:+ * Gates (Pauli, single-qubit Clifford, two-qubit Clifford,+ collapsing gates)+ * Measurements (M, MXX, MYY, MZZ, MRX, MRY, MRZ, MX, MY, MZ, MR)+ * Generalized Pauli Product gates (MPP, SPP, SPP_DAG)+ * Noise channels (DEPOLARIZE, PAULI_CHANNEL, X/Y/Z_ERROR,+ CORRELATED_ERROR, etc.)+ * Annotations (DETECTOR, OBSERVABLE_INCLUDE, QUBIT_COORDS,+ SHIFT_COORDS, TICK, MPAD)+ * REPEAT blocks+ * Tags on instructions+* `StimParser.Trans.flattenQ` transform to resolve `rec[]` references+ into absolute qubit indices, with coordinate shift support.+* Two example executables: `stim-parser-example`,+ `stim-parser-unit-example`.+* Test suite covering expression types, parsers, parse utilities,+ and transformations.
+ README.md view
@@ -0,0 +1,48 @@+# stim-parser ++parser combinator to parse `stim`'s `.stim` file into `AST`.++## Usage+stim-parser is expected to be used as a `haskell` library, for example:+```haskell+import StimParser.Expr+import StimParser.Parse+import StimParser.ParseUtils (run)++-- Parse a stim gate+main :: IO ()+main = do+ -- Parse individual elements+ print $ run parseGate "CNOT 0 1 2 3"+ -- => Gate CNOT [Q 0,Q 1,Q 2,Q 3]+ + print $ run parseMeasure "MXX(0.01) 2 3"+ -- => Measure MXX (Just 0.01) [Q 2,Q 3]+ + -- Parse a full stim file+ s <- readFile "example.stim"+ print $ run parseStim ("!!!Start " ++ s)+ -- => StimList [...]+```++See `data/example.stim` for a sample input file.++## Executables & Tests++### stim-parser-example+Main example from the README usage section. Parses a sample stim file(`data/example.stim`):+```bash+cabal run stim-parser-example+```++### stim-parser-unit-example+Multiple unit examples demonstrating individual parsers (gates, measurements, noise channels, annotations, etc.):+```bash+cabal run stim-parser-unit-example+```++### Test Suite+Unit tests for all modules (Expr, ParseUtils, Parse, Trans):+```bash+cabal test+```
+ src/StimParser/DEM/Expr.hs view
@@ -0,0 +1,110 @@+{-# LANGUAGE InstanceSigs #-}+module StimParser.DEM.Expr where++-- | A detector index (check node in the bipartite graph).+newtype DetectorId = DetectorId { unDetectorId :: Int }+ deriving (Show, Eq, Ord)++-- | A logical observable index.+newtype ObservableId = ObservableId { unObservableId :: Int }+ deriving (Show, Eq, Ord)++-- | Targets of an error instruction: detectors and/or observables.+data DEMTarget+ = TargetDetector !DetectorId+ | TargetObservable !ObservableId+ deriving (Show, Eq)++-- | A single error instruction: error(p) D0 D1 L0+--+-- Probability is stored as raw Double (not LLR). Downstream packages+-- compute LLRs if needed.+data DEMError = DEMError+ { deProbability :: !Double+ , deTargets :: ![DEMTarget]+ } deriving (Show, Eq)++-- | A detector declaration: detector(0, 0) D0+--+-- Coordinates are stored as [Double] to support arbitrary dimensionality.+data DEMDetector = DEMDetector+ { ddId :: !DetectorId+ , ddCoords :: ![Double]+ } deriving (Show, Eq)++-- | A logical observable declaration: logical_observable L0+data DEMObservable = DEMObservable+ { doId :: !ObservableId+ } deriving (Show, Eq)++-- | Shift detectors instruction: shift_detectors(1, 0) 0+--+-- First argument is coordinate shift, second is detector ID shift.+data DEMShift = DEMShift+ { dsCoordShift :: ![Double]+ , dsDetShift :: !Int+ } deriving (Show, Eq)++-- | A DEM instruction (top-level line).+data DEMInstruction+ = DEMInstrError !DEMError+ | DEMInstrDetector !DEMDetector+ | DEMInstrObservable !DEMObservable+ | DEMInstrShift !DEMShift+ | DEMInstrRepeat !Int ![DEMInstruction]+ deriving (Show, Eq)++-- | A complete Detector Error Model.+newtype DEM = DEM { unDEM :: [DEMInstruction] }+ deriving (Show, Eq)++-- ---------------------------------------------------------------------------+-- Flattening utilities+-- ---------------------------------------------------------------------------++-- | Flatten a DEM by expanding all repeat blocks and applying shift_detectors.+--+-- After flattening, the resulting DEM contains only:+-- - 'DEMInstrError'+-- - 'DEMInstrDetector'+-- - 'DEMInstrObservable'+--+-- All 'DEMInstrRepeat' are expanded into their body repeated N times.+-- All 'DEMInstrShift' are applied to subsequent instructions' coordinates+-- and detector IDs.+--+-- This mirrors 'StimParser.Trans.flattenQ' for circuits.+flattenDEM :: DEM -> DEM+flattenDEM (DEM instrs) = DEM (flattenInstrs [] 0 instrs)++-- | Flatten a list of instructions with accumulated coordinate shift+-- and detector ID shift.+flattenInstrs :: [Double] -> Int -> [DEMInstruction] -> [DEMInstruction]+flattenInstrs _ _ [] = []+flattenInstrs coordShift detShift (i : is) =+ case i of+ DEMInstrShift (DEMShift cs ds) ->+ let newCoordShift = zipWith (+) (padCoords (length cs) coordShift) cs+ newDetShift = detShift + ds+ in flattenInstrs newCoordShift newDetShift is++ DEMInstrRepeat n body ->+ flattenInstrs coordShift detShift (concat (replicate n body) ++ is)++ DEMInstrDetector (DEMDetector (DetectorId did) coords) ->+ let shiftPadded = padCoords (length coords) coordShift+ shiftedCoords = zipWith (+) shiftPadded coords+ shiftedId = DetectorId (did + detShift)+ in DEMInstrDetector (DEMDetector shiftedId shiftedCoords) : flattenInstrs coordShift detShift is++ DEMInstrError err ->+ DEMInstrError err : flattenInstrs coordShift detShift is++ DEMInstrObservable obs ->+ DEMInstrObservable obs : flattenInstrs coordShift detShift is++-- | Pad a coordinate list to a given length with zeros.+-- This handles cases where shift_detectors has fewer coordinates than+-- the detectors being shifted.+padCoords :: Int -> [Double] -> [Double]+padCoords n cs = take n (cs ++ repeat 0)
+ src/StimParser/DEM/Parse.hs view
@@ -0,0 +1,70 @@+{-# OPTIONS_GHC -Wno-unused-do-bind #-}+module StimParser.DEM.Parse where++import Text.Megaparsec+import Text.Megaparsec.Char++import StimParser.ParseUtils+import StimParser.DEM.Expr++-- | Parse a complete DEM string.+parseDEM :: Parser DEM+parseDEM = DEM <$> many parseDEMInstruction++-- | Parse a single DEM instruction.+parseDEMInstruction :: Parser DEMInstruction+parseDEMInstruction =+ DEMInstrError <$> parseDEMError+ <|> DEMInstrDetector <$> parseDEMDetector+ <|> DEMInstrObservable <$> parseDEMObservable+ <|> DEMInstrShift <$> parseDEMShift+ <|> uncurry DEMInstrRepeat <$> parseDEMRepeat++-- | Parse an error instruction: error(0.01) D0 D1 L0+parseDEMError :: Parser DEMError+parseDEMError = do+ lstring "error"+ lstring "("+ p <- parseNumber+ lstring ")"+ targets <- many parseDEMTarget+ return $ DEMError p targets++-- | Parse a single target: D0 or L0+parseDEMTarget :: Parser DEMTarget+parseDEMTarget =+ TargetDetector . DetectorId <$> (lstring "D" *> parseInt)+ <|> TargetObservable . ObservableId <$> (lstring "L" *> parseInt)++-- | Parse a detector declaration: detector(0, 0) D0+parseDEMDetector :: Parser DEMDetector+parseDEMDetector = do+ lstring "detector"+ coords <- parseTupleNumber+ did <- DetectorId <$> (lstring "D" *> parseInt)+ return $ DEMDetector did coords++-- | Parse a logical observable declaration: logical_observable L0+parseDEMObservable :: Parser DEMObservable+parseDEMObservable = do+ lstring "logical_observable"+ oid <- ObservableId <$> (lstring "L" *> parseInt)+ return $ DEMObservable oid++-- | Parse a shift_detectors instruction: shift_detectors(1, 0) 0+parseDEMShift :: Parser DEMShift+parseDEMShift = do+ lstring "shift_detectors"+ coords <- parseTupleNumber+ shift <- parseInt+ return $ DEMShift coords shift++-- | Parse a repeat block: repeat 100 { ... }+parseDEMRepeat :: Parser (Int, [DEMInstruction])+parseDEMRepeat = do+ lstring "repeat"+ n <- parseInt+ lstring "{"+ instrs <- many parseDEMInstruction+ lstring "}"+ return (n, instrs)
src/StimParser/Expr.hs view
@@ -2,7 +2,7 @@ module StimParser.Expr where type Ind = Int-type Ph = Float+type Ph = Double newtype Rec = Rec Int deriving (Show) newtype Sweep = Sweep Int@@ -140,12 +140,12 @@ errorTagTyList = [TWO_QUBIT_LEAKAGE_NOISE_FOR_AN_ADVANCED_SIMULATOR ..] data ErrorTag = ErrorTag ErrorTagTy- | ErrorTagCoef ErrorTagTy Float + | ErrorTagCoef ErrorTagTy Double deriving (Show) data Noise = NoiseNormal NoiseTy (Maybe Tag) (Maybe ErrorTag) [Ph] [Q]- | NoiseE NoiseTy (Maybe Tag) Float [PauliInd]+ | NoiseE NoiseTy (Maybe Tag) Double [PauliInd] deriving (Show) -- General Instruction Tag (v1.15+)@@ -174,7 +174,7 @@ -- TICK[100ns] -- seperate Integer and Float cases-data FInd = In Ind | Fl Float+data FInd = In Ind | Fl Double deriving (Show) instance Num FInd where
src/StimParser/Parse.hs view
@@ -24,9 +24,6 @@ i <- L.decimal return $ PauliInd p i -parseExhaust :: Parser a -> Parser [a]-parseExhaust parseElm = safeManyTill parseElm (notFollowedBy parseElm)- parsePauliChain :: Parser PauliChain parsePauliChain = do let@@ -120,7 +117,7 @@ parseMeasureTy :: Parser MeasureTy parseMeasureTy = parseEnum measureTyList -parsePh :: Parser Float +parsePh :: Parser Double parsePh = do lstring "(" ph <- parseFloat@@ -189,33 +186,6 @@ -- the order is tricky try pm2 <|> pm1 -parseTuple :: Parser a -> Parser [a]-parseTuple pm = do - lstring "("- let - parseE = do - f <- pm- lstring ","- return f- parseTuple_ = (++) <$> parseExhaust parseE <*> ((: []) <$> pm)- phs <- parseTuple_- lstring ")"- return phs--parseTupleFloat :: Parser [Float]-parseTupleFloat = parseTuple parseFloat- -- lstring "("- -- let - -- parseE = do - -- f <- parseFloat- -- lstring ","- -- return f- -- parseTuple = (++) <$> parseExhaust parseE <*> ((: []) <$> parseFloat)- -- phs <- parseTuple- -- lstring ")"- -- return phs-- parseNoise :: Parser Noise parseNoise = do let @@ -273,7 +243,7 @@ pm1 = do i <- parseInt return $ In i - -- Fl Float+ -- Fl Double pm2 = do f <- parseFloat return $ Fl f
src/StimParser/ParseUtils.hs view
@@ -51,6 +51,9 @@ where go = try ([] <$ end) <|> liftA2 (:) p go +parseExhaust :: Parser a -> Parser [a]+parseExhaust parseElm = safeManyTill parseElm (notFollowedBy parseElm)+ manyBetween :: Parser a -> Parser a -> Parser String manyBetween s e = s *> safeManyTill L.charLiteral e @@ -64,8 +67,35 @@ parseInt :: Parser Int parseInt = lexeme $ L.signed sc L.decimal -parseFloat :: Parser Float +parseFloat :: Parser Double parseFloat = lexeme $ L.signed sc L.float++-- | Parse a number that may be an integer or a float.+-- This is useful for coordinates and other contexts where integers+-- like @0@ or @1@ should be accepted as @0.0@ or @1.0@.+parseNumber :: Parser Double+parseNumber = lexeme $ L.signed sc (try L.float <|> fromIntegral <$> L.decimal)++parseTuple :: Parser a -> Parser [a]+parseTuple pm = do+ lstring "("+ let+ parseE = do+ f <- pm+ lstring ","+ return f+ parseTuple_ = (++) <$> parseExhaust parseE <*> ((: []) <$> pm)+ phs <- parseTuple_+ lstring ")"+ return phs++parseTupleFloat :: Parser [Double]+parseTupleFloat = parseTuple parseFloat++-- | Parse a tuple of numbers (integers or floats).+-- Used for DEM coordinates where bare integers like @(0, 0)@ are common.+parseTupleNumber :: Parser [Double]+parseTupleNumber = parseTuple parseNumber parseVar :: Parser String parseVar = do
stim-parser.cabal view
@@ -20,7 +20,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change-version: 0.1.0.0+version: 0.2.0.0 -- A short (one-line) description of the package. synopsis: A parser combinator library for STIM quantum circuit files@@ -48,7 +48,8 @@ build-type: Simple -- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.-extra-doc-files: CHANGELOG.md+extra-doc-files: README.md+ , CHANGELOG.md -- Extra source files to be distributed with the package, such as examples, or a tutorial module. -- extra-source-files:@@ -73,6 +74,8 @@ , StimParser.Parse , StimParser.ParseUtils , StimParser.Trans+ , StimParser.DEM.Expr+ , StimParser.DEM.Parse executable stim-parser-unit-example@@ -133,6 +136,8 @@ , Test.ParseUtils , Test.Parse , Test.Trans+ , Test.DEM.Expr+ , Test.DEM.Parse build-depends: base >= 4.11.0.0 && < 4.22 , stim-parser , HUnit >= 1.6 && < 1.7
test/Main.hs view
@@ -7,6 +7,8 @@ import qualified Test.ParseUtils as ParseUtils import qualified Test.Parse as Parse import qualified Test.Trans as Trans+import qualified Test.DEM.Expr as DEMExpr+import qualified Test.DEM.Parse as DEMParse main :: IO () main = do@@ -21,4 +23,6 @@ , ParseUtils.tests , Parse.tests , Trans.tests+ , DEMExpr.tests+ , DEMParse.tests ]
+ test/Test/DEM/Expr.hs view
@@ -0,0 +1,187 @@+module Test.DEM.Expr where++import Test.HUnit+import StimParser.DEM.Expr++-- Helper to compare using show+tAssertShowEqual :: (Show a) => String -> a -> a -> Test+tAssertShowEqual msg expected actual = TestCase $+ assertEqual msg (show expected) (show actual)++tests :: Test+tests = TestList+ [ testFlattenDEMSimple+ , testFlattenDEMRepeat+ , testFlattenDEMShift+ , testFlattenDEMShiftAndRepeat+ , testFlattenDEMDetIdShift+ , testFlattenDEMDimensionMismatch+ , testFlattenDEMIdempotent+ ]++-- | Flattening a DEM with no repeats or shifts is identity+testFlattenDEMSimple :: Test+testFlattenDEMSimple = TestList+ [ "flatten simple error" ~:+ let dem = DEM [DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])]+ in dem ~=? flattenDEM dem++ , "flatten multiple instructions" ~:+ let dem = DEM+ [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])+ , DEMInstrError (DEMError 0.02 [TargetDetector (DetectorId 1), TargetObservable (ObservableId 0)])+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])+ , DEMInstrObservable (DEMObservable (ObservableId 0))+ ]+ in dem ~=? flattenDEM dem+ ]++-- | Repeat blocks are expanded+testFlattenDEMRepeat :: Test+testFlattenDEMRepeat = TestList+ [ "flatten repeat 2" ~:+ let input = DEM+ [ DEMInstrRepeat 2+ [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])+ ]+ ]+ expected = DEM+ [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])+ , DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])+ ]+ in expected ~=? flattenDEM input++ , "flatten repeat 0" ~:+ let input = DEM [DEMInstrRepeat 0 [DEMInstrError (DEMError 0.01 [])]]+ in DEM [] ~=? flattenDEM input++ , "flatten nested repeat" ~:+ let input = DEM+ [ DEMInstrRepeat 2+ [ DEMInstrRepeat 3+ [ DEMInstrError (DEMError 0.01 []) ]+ ]+ ]+ expected = DEM (replicate 6 (DEMInstrError (DEMError 0.01 [])))+ in expected ~=? flattenDEM input+ ]++-- | shift_detectors coordinates are applied to subsequent detectors+testFlattenDEMShift :: Test+testFlattenDEMShift = TestList+ [ "flatten shift detectors" ~:+ let input = DEM+ [ DEMInstrShift (DEMShift [1.0, 0.0] 0)+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])+ , DEMInstrDetector (DEMDetector (DetectorId 1) [2.0, 3.0])+ ]+ expected = DEM+ [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0, 0.0])+ , DEMInstrDetector (DEMDetector (DetectorId 1) [3.0, 3.0])+ ]+ in expected ~=? flattenDEM input++ , "flatten cumulative shift" ~:+ let input = DEM+ [ DEMInstrShift (DEMShift [1.0] 0)+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0])+ , DEMInstrShift (DEMShift [2.0] 0)+ , DEMInstrDetector (DEMDetector (DetectorId 1) [0.0])+ ]+ expected = DEM+ [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0])+ , DEMInstrDetector (DEMDetector (DetectorId 1) [3.0])+ ]+ in expected ~=? flattenDEM input+ ]++-- | Combined shift and repeat+testFlattenDEMShiftAndRepeat :: Test+testFlattenDEMShiftAndRepeat = TestList+ [ "flatten shift inside repeat" ~:+ let input = DEM+ [ DEMInstrRepeat 2+ [ DEMInstrShift (DEMShift [1.0] 0)+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0])+ ]+ ]+ -- Note: shifts are cumulative across repeat iterations.+ -- Iteration 1: shift=[1.0], detector gets [1.0]+ -- Iteration 2: shift becomes [2.0], detector gets [2.0]+ expected = DEM+ [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0])+ , DEMInstrDetector (DEMDetector (DetectorId 0) [2.0])+ ]+ in expected ~=? flattenDEM input+ ]++-- | Detector ID shifting+testFlattenDEMDetIdShift :: Test+testFlattenDEMDetIdShift = TestList+ [ "flatten detector id shift" ~:+ let input = DEM+ [ DEMInstrShift (DEMShift [0.0] 5)+ , DEMInstrDetector (DEMDetector (DetectorId 0) [1.0])+ , DEMInstrDetector (DEMDetector (DetectorId 1) [2.0])+ ]+ expected = DEM+ [ DEMInstrDetector (DEMDetector (DetectorId 5) [1.0])+ , DEMInstrDetector (DEMDetector (DetectorId 6) [2.0])+ ]+ in expected ~=? flattenDEM input++ , "flatten combined coord and id shift" ~:+ let input = DEM+ [ DEMInstrShift (DEMShift [1.0, 0.0] 10)+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])+ ]+ expected = DEM+ [ DEMInstrDetector (DEMDetector (DetectorId 10) [1.0, 0.0])+ ]+ in expected ~=? flattenDEM input+ ]++-- | Dimension mismatch between shift and detector coordinates+testFlattenDEMDimensionMismatch :: Test+testFlattenDEMDimensionMismatch = TestList+ [ "2D shift on 3D detector" ~:+ let input = DEM+ [ DEMInstrShift (DEMShift [1.0, 0.0] 0)+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0, 0.0])+ ]+ expected = DEM+ [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0, 0.0, 0.0])+ ]+ in expected ~=? flattenDEM input++ , "3D shift on 2D detector" ~:+ let input = DEM+ [ DEMInstrShift (DEMShift [1.0, 0.0, 0.0] 0)+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])+ ]+ expected = DEM+ [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0, 0.0])+ ]+ in expected ~=? flattenDEM input+ ]++-- | Idempotency: flattening an already-flattened DEM is a no-op+testFlattenDEMIdempotent :: Test+testFlattenDEMIdempotent = TestList+ [ "flatten is idempotent on simple" ~:+ let dem = DEM+ [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])+ , DEMInstrDetector (DEMDetector (DetectorId 1) [1.0, 2.0])+ , DEMInstrObservable (DEMObservable (ObservableId 0))+ ]+ in flattenDEM dem ~=? flattenDEM (flattenDEM dem)++ , "flatten is idempotent on nested" ~:+ let dem = DEM+ [ DEMInstrRepeat 2+ [ DEMInstrShift (DEMShift [1.0] 0)+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0])+ ]+ ]+ in flattenDEM dem ~=? flattenDEM (flattenDEM dem)+ ]
+ test/Test/DEM/Parse.hs view
@@ -0,0 +1,267 @@+module Test.DEM.Parse where++import Test.HUnit++import StimParser.DEM.Expr+import StimParser.DEM.Parse+import StimParser.ParseUtils (run)++-- Helper to compare using show+tAssertShowEqual :: (Show a) => String -> a -> a -> Test+tAssertShowEqual msg expected actual = TestCase $+ assertEqual msg (show expected) (show actual)++tests :: Test+tests = TestList+ [ testParseDEMError+ , testParseDEMDetector+ , testParseDEMObservable+ , testParseDEMShift+ , testParseDEMRepeat+ , testParseDEMTarget+ , testParseDEMFull+ , testParseDEMHighPrecision+ , testParseDEMWhitespace+ , testParseDEMEdgeCases+ , testParseDEMComments+ , testParseDEMScientificCoords+ , testParseDEMMixedTargets+ , testParseDEMEmpty+ ]++-- | Parse individual error instructions+testParseDEMError :: Test+testParseDEMError = TestList+ [ "error single detector" ~:+ DEMError 0.01 [TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error(0.01) D0"++ , "error multiple detectors" ~:+ DEMError 0.005 [TargetDetector (DetectorId 0), TargetDetector (DetectorId 1)]+ ~=? run parseDEMError "error(0.005) D0 D1"++ , "error with observable" ~:+ DEMError 0.003 [TargetDetector (DetectorId 0), TargetObservable (ObservableId 0)]+ ~=? run parseDEMError "error(0.003) D0 L0"++ , "error multiple observables" ~:+ DEMError 0.1 [TargetObservable (ObservableId 0), TargetObservable (ObservableId 1)]+ ~=? run parseDEMError "error(0.1) L0 L1"++ , "error no targets" ~:+ DEMError 0.01 []+ ~=? run parseDEMError "error(0.01)"++ , "error probability 0" ~:+ DEMError 0.0 [TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error(0) D0"++ , "error probability 1" ~:+ DEMError 1.0 [TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error(1) D0"++ , "error scientific notation" ~:+ DEMError 1.5e-2 [TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error(1.5e-2) D0"++ , "error very small probability" ~:+ DEMError 1e-15 [TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error(1e-15) D0"+ ]++-- | Parse detector declarations+testParseDEMDetector :: Test+testParseDEMDetector = TestList+ [ "detector 2D" ~:+ DEMDetector (DetectorId 0) [0.0, 0.0]+ ~=? run parseDEMDetector "detector(0, 0) D0"++ , "detector 3D" ~:+ DEMDetector (DetectorId 5) [1.0, 2.0, 0.0]+ ~=? run parseDEMDetector "detector(1, 2, 0) D5"++ , "detector negative coords" ~:+ DEMDetector (DetectorId 1) [-1.0, 0.5]+ ~=? run parseDEMDetector "detector(-1, 0.5) D1"++ , "detector 1D" ~:+ DEMDetector (DetectorId 3) [2.0]+ ~=? run parseDEMDetector "detector(2) D3"+ ]++-- | Parse logical observable declarations+testParseDEMObservable :: Test+testParseDEMObservable = TestList+ [ "observable L0" ~:+ DEMObservable (ObservableId 0)+ ~=? run parseDEMObservable "logical_observable L0"++ , "observable L5" ~:+ DEMObservable (ObservableId 5)+ ~=? run parseDEMObservable "logical_observable L5"+ ]++-- | Parse shift_detectors+testParseDEMShift :: Test+testParseDEMShift = TestList+ [ "shift 2D" ~:+ DEMShift [1.0, 0.0] 0+ ~=? run parseDEMShift "shift_detectors(1, 0) 0"++ , "shift with det offset" ~:+ DEMShift [0.0, 1.0] 10+ ~=? run parseDEMShift "shift_detectors(0, 1) 10"+ ]++-- | Parse repeat blocks+testParseDEMRepeat :: Test+testParseDEMRepeat = TestList+ [ "repeat simple" ~:+ (2, [DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])])+ ~=? run parseDEMRepeat "repeat 2 { error(0.01) D0 }"++ , "repeat multiple" ~:+ (3, [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])+ , DEMInstrError (DEMError 0.02 [TargetDetector (DetectorId 1)])+ ])+ ~=? run parseDEMRepeat "repeat 3 { error(0.01) D0 error(0.02) D1 }"+ ]++-- | Parse individual targets+testParseDEMTarget :: Test+testParseDEMTarget = TestList+ [ "target D0" ~:+ TargetDetector (DetectorId 0)+ ~=? run parseDEMTarget "D0"++ , "target L3" ~:+ TargetObservable (ObservableId 3)+ ~=? run parseDEMTarget "L3"+ ]++-- | Parse full DEM documents+testParseDEMFull :: Test+testParseDEMFull = TestList+ [ "simple dem" ~:+ let input = unlines+ [ "error(0.01) D0 D1 L0"+ , "error(0.005) D1 D2"+ , "detector(0, 0) D0"+ , "logical_observable L0"+ ]+ expected = DEM+ [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0), TargetDetector (DetectorId 1), TargetObservable (ObservableId 0)])+ , DEMInstrError (DEMError 0.005 [TargetDetector (DetectorId 1), TargetDetector (DetectorId 2)])+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])+ , DEMInstrObservable (DEMObservable (ObservableId 0))+ ]+ in expected ~=? run parseDEM input++ , "dem with repeat" ~:+ let input = "repeat 2 { error(0.01) D0 } detector(0, 0) D0"+ expected = DEM+ [ DEMInstrRepeat 2 [DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])]+ , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])+ ]+ in expected ~=? run parseDEM input++ , "dem only observables" ~:+ let input = "error(0.1) L0 L1"+ expected = DEM [DEMInstrError (DEMError 0.1 [TargetObservable (ObservableId 0), TargetObservable (ObservableId 1)])]+ in expected ~=? run parseDEM input+ ]++-- | High-precision probability parsing (the motivating case for Double)+testParseDEMHighPrecision :: Test+testParseDEMHighPrecision = TestList+ [ "high precision probability" ~:+ let input = "error(0.002673815958446297981) D0"+ result = run parseDEMError input+ in case result of+ DEMError p [TargetDetector (DetectorId 0)] ->+ -- Double preserves ~15 significant digits+ assertBool "high precision preserved" (abs (p - 0.002673815958446298) < 1e-19)+ _ -> assertFailure "parse failed"+ ]++-- | Whitespace tolerance+testParseDEMWhitespace :: Test+testParseDEMWhitespace = TestList+ [ "error with extra spaces" ~:+ DEMError 0.01 [TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error( 0.01 ) D0"++ , "detector with tabs" ~:+ DEMDetector (DetectorId 0) [1.0, 2.0]+ ~=? run parseDEMDetector "detector(1,\t2)\tD0"+ ]++-- | Edge cases+testParseDEMEdgeCases :: Test+testParseDEMEdgeCases = TestList+ [ "large detector id" ~:+ TargetDetector (DetectorId 999999)+ ~=? run parseDEMTarget "D999999"++ , "large observable id" ~:+ TargetObservable (ObservableId 999999)+ ~=? run parseDEMTarget "L999999"++ , "repeated same target" ~:+ DEMError 0.1 [TargetDetector (DetectorId 0), TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error(0.1) D0 D0"++ , "empty dem string" ~:+ DEM [] ~=? run parseDEM ""++ , "dem with trailing newline" ~:+ DEM [DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])]+ ~=? run parseDEM "error(0.01) D0\n"+ ]++-- | Block and line comments+testParseDEMComments :: Test+testParseDEMComments = TestList+ [ "block comment between tokens" ~:+ DEMError 0.01 [TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error(0.01) (* comment *) D0"++ , "line comment at end" ~:+ DEMError 0.01 [TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error(0.01) D0 # end of line comment"++ , "block comment in tuple" ~:+ DEMDetector (DetectorId 0) [1.0, 2.0]+ ~=? run parseDEMDetector "detector(1, (* x *) 2) D0"+ ]++-- | Scientific notation in coordinates+testParseDEMScientificCoords :: Test+testParseDEMScientificCoords = TestList+ [ "scientific notation in detector coords" ~:+ DEMDetector (DetectorId 0) [150.0, 0.0]+ ~=? run parseDEMDetector "detector(1.5e2, 0) D0"++ , "negative scientific in coords" ~:+ DEMDetector (DetectorId 1) [-1.5e-2, 2.0]+ ~=? run parseDEMDetector "detector(-1.5e-2, 2) D1"+ ]++-- | Mixed target orders+testParseDEMMixedTargets :: Test+testParseDEMMixedTargets = TestList+ [ "observable before detector" ~:+ DEMError 0.1 [TargetObservable (ObservableId 0), TargetDetector (DetectorId 0)]+ ~=? run parseDEMError "error(0.1) L0 D0"++ , "interleaved targets" ~:+ DEMError 0.1 [TargetObservable (ObservableId 0), TargetDetector (DetectorId 1), TargetObservable (ObservableId 2)]+ ~=? run parseDEMError "error(0.1) L0 D1 L2"+ ]++-- | Empty / minimal DEMs+testParseDEMEmpty :: Test+testParseDEMEmpty = TestList+ [ "empty string is empty dem" ~:+ DEM [] ~=? run parseDEM ""+ ]