packages feed

stim-parser 0.1.0.0 → 0.2.0.0

raw patch · 11 files changed

+763/−41 lines, 11 files

Files

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 ""+  ]