packages feed

stim-parser-0.2.0.0: test/Test/DEM/Parse.hs

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