fork download
  1. {-|
  2.  
  3. v 0.2.1
  4.  
  5. Welcome to GDB Online.
  6. GDB online is an online compiler and debugger tool for C, C++, Python, Java, PHP, Ruby, Perl,
  7. C#, VB, Swift, Pascal, Fortran, Haskell, Objective-C, Assembly, HTML, CSS, JS, SQLite, Prolog.
  8. Code, Compile, Run and Debug online from anywhere in world.
  9.  
  10.  
  11. TP todo:
  12.   - different solutions should be possible
  13.   Whenever it is possible to assign more than one operation,
  14.   user should be asked, which operation should be assigned.
  15.   It can be achieved by the solve function to return either a new
  16.   solution, or a choice of operations. (One of many ways...)
  17.  
  18. Beware: the code was written in a hurry. It's not pretty, and shouldn't be
  19. regarded as something exemplary in any way (both Haskell, and ALB).
  20.  
  21. -}
  22. import Data.List as DL
  23. import Data.Char as Char
  24. import Debug.Trace as T
  25.  
  26. -- Op <id/number> <time> "succs"|"preds" [<id of related operations]
  27.  
  28. problem :: [Op]
  29. problem = [
  30. Op 1 1 "succs" [3, 4],
  31. Op 2 2 "succs" [5],
  32. Op 3 1 "succs" [6],
  33. Op 4 5 "succs" [6],
  34. Op 5 2 "succs" [6, 7],
  35. Op 6 2 "succs" [9, 10],
  36. Op 7 2 "succs" [10],
  37. Op 8 4 "succs" [10],
  38. Op 9 5 "succs" [11, 12],
  39. Op 10 2 "succs" [12],
  40. Op 11 4 "preds" [9],
  41. Op 12 3 "succs" [13],
  42. Op 13 5 "preds" [12]
  43. ]
  44.  
  45. cycle_time = 8
  46.  
  47. -- rpwWeights, wetWeights, nopWeights, noipWeights,
  48. -- nofWeights, noifWeights
  49. -- 8 = cycle time
  50. sol = solve1 tmp_problem rpwWeights cycle_time
  51.  
  52. data OpTime
  53. data OpId
  54. data OpWeight
  55. data OpTimeStart
  56. data OpTimeEnd
  57.  
  58. -- The integer type.
  59. data IntVal a = IntVal Int deriving (Eq, Show)
  60.  
  61. data RelOps
  62. = Succs [IntVal OpId]
  63. | Preds [IntVal OpId]
  64. deriving (Show)
  65.  
  66. data Operation = Operation {
  67. oid :: IntVal OpId,
  68. time :: IntVal OpTime,
  69. relops :: RelOps
  70. } deriving (Show)
  71.  
  72. type BLMProblem = [Operation]
  73.  
  74.  
  75. data Op = Op Int Int String [Int]
  76.  
  77.  
  78. opId :: Int -> IntVal OpId
  79. opId = IntVal
  80.  
  81. opTime :: Int -> IntVal OpTime
  82. opTime = IntVal
  83.  
  84. opWeight :: Int -> IntVal OpWeight
  85. opWeight = IntVal
  86.  
  87. opTimeStart :: Int -> IntVal OpTimeStart
  88. opTimeStart = IntVal
  89.  
  90. opTimeEnd :: Int -> IntVal OpTimeEnd
  91. opTimeEnd = IntVal
  92.  
  93.  
  94. operation :: Op -> Operation
  95. operation (Op op_id op_time op_rel op_id_lst) =
  96. let o_id = opId op_id
  97. o_time = opTime op_time
  98. id_lst = map opId op_id_lst
  99. constructor =
  100. case op_rel of
  101. "succs" -> Succs
  102. _ -> Preds
  103. in Operation o_id o_time $ constructor id_lst
  104. --in case op_rel of
  105. -- "succs" -> Operation o_id o_time $ Succs id_lst
  106. -- _ -> Operation o_id o_time $ Preds id_lst
  107.  
  108. blmProblem :: [Op] -> BLMProblem
  109. blmProblem = map operation
  110.  
  111. type Weight = (IntVal OpId, IntVal OpWeight)
  112.  
  113. type WeightFunc = BLMProblem -> [Weight]
  114.  
  115. noifWeights :: WeightFunc
  116. noifWeights problem = map noifWeight problem
  117. where
  118. noifWeight :: Operation -> Weight
  119. noifWeight op = (oid op, opWeight $ length $ immediateSuccs problem op)
  120.  
  121. noipWeights :: WeightFunc
  122. noipWeights problem = map noipWeight problem
  123. where
  124. noipWeight :: Operation -> Weight
  125. noipWeight op = (oid op, opWeight $ length $ immediatePreds problem op)
  126.  
  127. wetWeights :: WeightFunc
  128. wetWeights problem = map wetWeight problem
  129. where
  130. wetWeight :: Operation -> Weight
  131. wetWeight (Operation id (IntVal op_time) _rel) =
  132. (id, opWeight op_time)
  133.  
  134. nextWeights :: BLMProblem -> [Weight] -> [Operation]
  135. nextWeights blm_problem wgts = filter (canCalcWeight blm_problem wgts) blm_problem
  136. where
  137. canCalcWeight :: BLMProblem -> [Weight] -> Operation -> Bool
  138. canCalcWeight blm_problem wgts op =
  139. let succs = immediateSuccs blm_problem op
  140. wgts_ids = map fst wgts
  141. can_calc = all (\op_id -> elem op_id wgts_ids) succs
  142. in can_calc && (not $ elem (oid op) wgts_ids)
  143.  
  144. type WeightsOperationFunc = ([Weight] -> Operation -> Weight)
  145.  
  146. rpwWeights :: WeightFunc
  147. rpwWeights problem = calcWeights problem (rpwWeight problem) []
  148.  
  149. nofWeights :: WeightFunc
  150. nofWeights problem = map (nofWeight problem) problem
  151.  
  152. nopWeights :: WeightFunc
  153. nopWeights problem = map (nopWeight problem) problem
  154.  
  155. calcWeights :: BLMProblem -> WeightsOperationFunc -> [Weight] -> [Weight]
  156. calcWeights problem weight_func wgts =
  157. let op_ids = nextWeights problem wgts
  158. add_wgts = map (weight_func wgts) op_ids
  159. new_wgts = concat [wgts, add_wgts]
  160. in if allWeightsKnown problem new_wgts
  161. then new_wgts
  162. else calcWeights problem weight_func new_wgts
  163.  
  164. allWeightsKnown :: BLMProblem -> [Weight] -> Bool
  165. allWeightsKnown problem wgts =
  166. let problem_op_ids = map oid problem
  167. wgts_op_ids = map fst wgts
  168. in all (\op_id -> elem op_id wgts_op_ids) problem_op_ids
  169.  
  170. rpwWeight :: BLMProblem -> WeightsOperationFunc
  171. rpwWeight problem wgts the_op@(Operation op_id op_time _) =
  172. let succs = immediateSuccs problem the_op
  173. in case succs of
  174. [] -> let (IntVal tm) = op_time
  175. in (op_id, opWeight tm)
  176. _ -> let the_wgts = filter (\(op_id, _) -> elem op_id succs) wgts
  177. int_wgts = map (\(_, IntVal w) -> w) the_wgts
  178. the_weight = maximum int_wgts
  179. (IntVal tm) = time the_op
  180. in (oid the_op, opWeight $ the_weight + tm)
  181.  
  182. nofWeight :: BLMProblem -> Operation -> Weight
  183. nofWeight blm_problem the_op =
  184. let w = length $ allRels immSuccsId blm_problem the_op
  185. in (oid the_op, opWeight (w-1))
  186.  
  187. nopWeight :: BLMProblem -> Operation -> Weight
  188. nopWeight blm_problem the_op =
  189. let w = length $ allRels immPredsId blm_problem the_op
  190. in (oid the_op, opWeight (w-1))
  191.  
  192. operationWeight :: [Weight] -> Operation -> Weight
  193. operationWeight weights op =
  194. head $ filter (\(w_op, _) -> w_op == (oid op)) weights
  195.  
  196. allRels :: (BLMProblem -> IntVal OpId -> [IntVal OpId]) -> BLMProblem -> Operation -> [IntVal OpId]
  197. allRels func_id blm_problem (Operation op_id _ _) = calcRels op_id ([], [])
  198. where
  199. calcRels :: IntVal OpId -> ([IntVal OpId], [IntVal OpId]) -> [IntVal OpId]
  200. calcRels op_id (counted, to_count) =
  201. if elem op_id counted
  202. then countNext counted to_count
  203. else
  204. case func_id blm_problem op_id of
  205. [] -> countNext (op_id:counted) to_count
  206. succs ->
  207. let new_counted = op_id:counted
  208. new_to_count = concat [to_count, succs]
  209. in countNext new_counted new_to_count
  210.  
  211. countNext :: [IntVal OpId] -> [IntVal OpId] -> [IntVal OpId]
  212. countNext counted to_count =
  213. case to_count of
  214. [] -> counted
  215. _ ->
  216. let op_id:rest = to_count
  217. in calcRels op_id (counted, rest)
  218.  
  219. immSuccsId :: BLMProblem -> IntVal OpId -> [IntVal OpId]
  220. immSuccsId = immRelsId immediateSuccs
  221. --immSuccsId blm_problem op_id =
  222. -- let the_op = head $ filter (\op -> oid op == op_id) blm_problem
  223. -- in immediateSuccs blm_problem the_op
  224.  
  225. immPredsId :: BLMProblem -> IntVal OpId -> [IntVal OpId]
  226. immPredsId = immRelsId immediatePreds
  227.  
  228. immRelsId :: (BLMProblem -> Operation -> [IntVal OpId]) -> BLMProblem -> IntVal OpId -> [IntVal OpId]
  229. immRelsId func blm_problem op_id =
  230. let the_op = head $ filter (\op -> oid op == op_id) blm_problem
  231. in func blm_problem the_op
  232.  
  233. immediateSuccs :: BLMProblem -> Operation -> [IntVal OpId]
  234. immediateSuccs blm_problem (Operation op_id _ op_rels) =
  235. case op_rels of
  236. Succs rels -> rels
  237. Preds rels -> immSuccs blm_problem op_id
  238.  
  239. immediatePreds :: BLMProblem -> Operation -> [IntVal OpId]
  240. immediatePreds blm_problem (Operation op_id _ op_rels) =
  241. case op_rels of
  242. Preds rels -> rels
  243. Succs rels -> immPreds blm_problem op_id
  244.  
  245. immSuccs :: BLMProblem -> IntVal OpId -> [IntVal OpId]
  246. immSuccs blm_problem op_id =
  247. map oid $ filter (hasPred op_id) blm_problem
  248.  
  249. immPreds :: BLMProblem -> IntVal OpId -> [IntVal OpId]
  250. immPreds blm_problem op_id =
  251. -- find all the ops, for which op_id is one of succs
  252. -- those are THE operations
  253. map oid $ filter (hasSucc op_id) blm_problem
  254. --let succ_ops = filter (hasSucc op_id) blm_problem
  255. -- in map oid succ_ops
  256.  
  257. hasPred :: IntVal OpId -> Operation -> Bool
  258. hasPred op_id op =
  259. case (relops op) of
  260. Preds preds ->
  261. let ops = filter ((==) op_id) preds
  262. in case ops of
  263. [_] -> True
  264. _ -> False
  265. _ -> False
  266.  
  267. hasSucc :: IntVal OpId -> Operation -> Bool
  268. hasSucc op_id op =
  269. case (relops op) of
  270. Succs succs ->
  271. let ops = filter ((==) op_id) succs
  272. in case ops of
  273. [_] -> True
  274. _ -> False
  275. _ -> False
  276.  
  277. -- 1) policz wagi
  278. -- 2) otwórz stację roboczą
  279. -- 3) wybierz operację do przypisania
  280. -- a) relacje kolejnościowe
  281. -- b) czas stacji
  282. -- c) wagi
  283. -- 4) brak operacji? goto 2
  284. -- 5) wiele operacji? wybierz dowolną
  285. -- 6) przypisz operację do stacji; goto 3
  286.  
  287.  
  288. type OpScheduled = (IntVal OpId, IntVal OpTimeStart, IntVal OpTimeEnd)
  289. type Station = [OpScheduled]
  290. type Solution1 = [Station]
  291.  
  292. data ErrOpsTodo
  293. = ErrNoOps
  294. | ErrNoTime
  295. | ErrNoWeightWTF
  296. deriving (Show)
  297.  
  298. -- Yep, I do realise, that 1113 is VERY ugly.
  299. -- Let's get serious, though.
  300. -- How many students are ACTUALLY going to make up so complex examples,
  301. -- that 1113 is not enough?
  302. -- Therefore, I'm going to leave the number here.
  303. solve1 :: BLMProblem -> WeightFunc -> Int -> Solution1
  304. solve1 blm_problem w_func cycle = solve 1113 []
  305. where
  306. weights = w_func blm_problem
  307.  
  308. solve :: Int -> Solution1 -> Solution1
  309. solve n sol =
  310. if n > 0
  311. then if solutionComplete blm_problem sol
  312. then sol
  313. else case (nextSolution sol) of
  314. Nothing -> sol
  315. Just new_sol -> solve (n-1) new_sol
  316. else sol
  317.  
  318. solutionComplete :: BLMProblem -> Solution1 -> Bool
  319. solutionComplete blm_problem sol =
  320. case blm_problem of
  321. [] -> True
  322. (op:rest) ->
  323. if opDone sol (oid op)
  324. then solutionComplete rest sol
  325. else False
  326.  
  327. nextSolution :: Solution1 -> Maybe Solution1
  328. nextSolution old_sol =
  329. let e_ops = availableOps blm_problem weights old_sol
  330. in case (e_ops, old_sol) of
  331. (Left ErrNoTime, _) -> Just $ []:old_sol -- open a new station
  332. (Left err, []) -> T.trace (show err) Nothing -- WTF?
  333. (Right ops, []) -> -- schedule the first operation
  334. let the_op = head ops
  335. (IntVal op_time) = time the_op
  336. op_id = oid the_op
  337. sch_op = (op_id, opTimeStart 0, opTimeEnd op_time)
  338. in Just [[sch_op]]
  339.  
  340. (Right ops, (st:rest)) ->
  341. case st of
  342. [] -> -- schedule the first operation
  343. let the_op = head ops
  344. (IntVal op_time) = time the_op
  345. op_id = oid the_op
  346. sch_op = (op_id, opTimeStart 0, opTimeEnd op_time)
  347. in Just $ [sch_op]:rest
  348.  
  349. (last_op:_) -> -- schedule the next operation
  350. let the_op = head ops
  351. (IntVal op_time) = time the_op
  352. op_id = oid the_op
  353. (_, _, IntVal time_start) = last_op
  354. sch_op = (op_id, opTimeStart time_start, opTimeEnd (op_time + time_start))
  355. in Just $ (sch_op:st):rest
  356.  
  357. availableOps :: BLMProblem -> [Weight] -> Solution1 -> Either ErrOpsTodo [Operation]
  358. availableOps blm_problem weights solution =
  359. canBeDoneRels blm_problem solution
  360. >>= canBeDoneTime blm_problem solution cycle
  361. >>= bestWeight weights
  362.  
  363. processWeights :: [Weight] -> Maybe ([Operation], Weight) -> Operation -> Maybe ([Operation], Weight)
  364. processWeights weights mb_op op =
  365. let weight = operationWeight weights op
  366. in
  367. case mb_op of
  368. Nothing -> Just ([op], weight)
  369. Just (old_ops, old_w) ->
  370. let (_, IntVal old_w_int) = old_w
  371. (_, IntVal w_int) = weight
  372. in
  373. if old_w_int < w_int
  374. then Just ([op], weight)
  375. else if old_w_int == w_int
  376. then Just (op:old_ops, weight)
  377. else mb_op
  378.  
  379. bestWeight :: [Weight] -> [Operation] -> Either ErrOpsTodo [Operation]
  380. bestWeight weights ops =
  381. let result = foldl (processWeights weights) Nothing ops
  382. in case result of
  383. Nothing -> Left ErrNoWeightWTF
  384. Just (ops, _) -> Right ops
  385.  
  386. canBeDoneTime :: BLMProblem -> Solution1 -> Int -> [Operation] -> Either ErrOpsTodo [Operation]
  387. canBeDoneTime blm_problem solution cycle ops =
  388. let st_time = currentStationTime solution
  389. rem_time = cycle - st_time
  390. ops_within_time = foldl (processOpTime blm_problem rem_time) [] ops
  391. in case ops_within_time of
  392. [] -> Left ErrNoTime
  393. the_ops -> Right the_ops
  394.  
  395. processOpTime :: BLMProblem -> Int -> [Operation] -> Operation -> [Operation]
  396. processOpTime blm_problem st_time acc op =
  397. let (IntVal op_time) = time op
  398. in if op_time <= st_time
  399. then op:acc
  400. else acc
  401.  
  402. currentStationTime :: Solution1 -> Int
  403. currentStationTime sol =
  404. case sol of
  405. [] -> 0
  406. (st:_) ->
  407. case st of
  408. [] -> 0
  409. sch_ops ->
  410. let end_times = map (\(_,_, IntVal end_time) -> end_time) sch_ops
  411. in maximum end_times
  412.  
  413. canBeDoneRels :: BLMProblem -> Solution1 -> Either ErrOpsTodo [Operation]
  414. canBeDoneRels blm_problem solution =
  415. let ops_todo1 = filter (notScheduled solution) blm_problem
  416. ops_todo2 = filter (opCanBeDoneRel blm_problem solution) ops_todo1
  417. in case ops_todo2 of
  418. [] -> Left ErrNoOps
  419. _ -> Right ops_todo2
  420. -- lista operacji BEZ tych, które już są przypisane,
  421. -- lista operacji, które mogą zostać wykonane (rel)
  422.  
  423. notScheduled :: Solution1 -> Operation -> Bool
  424. notScheduled sol op = not $ opDone sol $ oid op
  425.  
  426. opCanBeDoneRel :: BLMProblem -> Solution1 -> Operation -> Bool
  427. opCanBeDoneRel blm_problem solution op =
  428. -- find the (immediate) predecessors of operation
  429. let op_preds = immediatePreds blm_problem op
  430. in case op_preds of
  431. [] -> True -- no predecessors = operation can be done
  432. _ -> -- if all of them are done, operation can be done
  433. let preds_not_done = filter (not . opDone solution) op_preds
  434. in case preds_not_done of
  435. [] -> True
  436. _ -> False
  437.  
  438. opDone :: Solution1 -> IntVal OpId -> Bool
  439. opDone sol op_id =
  440. --let all_scheduled = concat sol
  441. -- ops_scheduled = map (\(x, _, _) -> x) all_scheduled
  442. -- the_op = filter ((==) op_id) ops_scheduled
  443. let the_op = filter ((==) op_id) $ map (\(x,_,_) -> x) $ concat sol
  444. in case the_op of
  445. [_] -> True
  446. _ -> False
  447.  
  448.  
  449. minCycle :: BLMProblem -> Int -> Int
  450. minCycle ops num_stations =
  451. let op_times = map time ops
  452. times_int = map (\(IntVal t) -> t) op_times
  453. s = sum times_int
  454. val = (fromIntegral s :: Float) / (fromIntegral num_stations :: Float)
  455. in ceiling val
  456.  
  457. showSolution :: Solution1 -> String
  458. showSolution sol = DL.intercalate "\n" $ map (\(s, n) -> DL.intercalate " " ["ST" ++ (show n), s]) $ zip lst_stations [1..]
  459. where
  460. lst_stations :: [String]
  461. lst_stations = reverse $ map showStation sol
  462.  
  463. showStation :: Station -> String
  464. showStation st = DL.intercalate " " $ reverse $ map showOp st
  465.  
  466. showOp :: OpScheduled -> String
  467. showOp (IntVal op_id, IntVal time_start, IntVal time_end) =
  468. DL.intercalate " " ["O" ++ (show op_id),
  469. "(" ++ (show time_start) ++ "-" ++ (show time_end ++ ")")]
  470.  
  471. ---------------------------------------------------------------------
  472. -- Stats
  473.  
  474. -- Assumptions:
  475. -- - in the solution1, the stations order os always from the last one, to the first one
  476. -- - the order of operations is also reversed, for each station
  477.  
  478. stTime :: Station -> Int
  479. stTime ops_scheduled = maximum $ map (\(_, _, IntVal t) -> t) ops_scheduled
  480.  
  481. timeLine :: Int -> Solution1 -> Int
  482. timeLine cycle sol = cycle * (length sol)
  483.  
  484. timeLineWG :: Int -> Solution1 -> Int
  485. timeLineWG cycle sol =
  486. let last_station = head sol
  487. len = length sol
  488. in cycle * (len - 1) + stTime last_station
  489.  
  490. lineEfficiency :: Int -> Solution1 -> Float
  491. lineEfficiency cycle sol =
  492. let st_times = map stTime sol
  493. num = fromIntegral (sum st_times) :: Float
  494. den = fromIntegral (cycle * (length sol)) :: Float
  495. in num * 100 / den
  496.  
  497. smoothnessIndex :: Solution1 -> Float
  498. smoothnessIndex sol =
  499. let st_times = map stTime sol
  500. mx = maximum st_times
  501. diffs = map (\st_time -> mx - st_time) st_times
  502. squares = map (\x -> x * x) diffs
  503. s = sum squares
  504.  
  505. stats :: Solution1 -> String
  506. stats sol = DL.intercalate "\n" [
  507. DL.intercalate "" ["T = ", show $ timeLine cycle_time sol],
  508. DL.intercalate "" ["(T = ", show $ timeLineWG cycle_time sol, ")"],
  509. DL.intercalate "" ["LE = ", show $ lineEfficiency cycle_time sol],
  510. DL.intercalate "" ["SI = ", show $ smoothnessIndex sol]
  511. ]
  512.  
  513. ---------------------------------------------------------------------
  514. -- The Gantt Chart (wip)
  515.  
  516. axis :: String
  517. axis = DL.intercalate " " $ concat [["---"], numList (cycle_time + 1)]
  518.  
  519. numList :: Int -> [String]
  520. numList mx = map showPlus $ take mx [0..]
  521.  
  522. showPlus :: Int -> String
  523. showPlus n =
  524. if n == 0
  525. then show n
  526. else case n `mod` 10 of
  527. 0 ->
  528. let k = n `div` 10
  529. in [Char.chr $ 64 + k]
  530.  
  531. x -> show x
  532.  
  533. showTaskNum :: Int -> String
  534. showTaskNum n =
  535. if n < 10
  536. then show n
  537. else [Char.chr $ 64 + n - 9]
  538.  
  539. task :: OpScheduled -> String
  540. task (IntVal num, IntVal start, IntVal end) =
  541. let time = (end - start - 1) * 2
  542. before_num = time `div` 2
  543. after_num = (time `div` 2) + (time `mod` 2)
  544. before = concat $ replicate before_num "-"
  545. after = concat $ replicate after_num "-"
  546. in DL.intercalate "" [before, showTaskNum num, after]
  547.  
  548. ganttStation :: (Int, Station) -> String
  549. ganttStation (n, ops_scheduled) = DL.concat ["ST", show n, " |", (DL.intercalate "|" $ map task $ reverse ops_scheduled), "|"]
  550.  
  551. gantt :: Solution1 -> String
  552. gantt sol = DL.intercalate "\n" $ map ganttStation $ zip [1..] $ reverse sol
  553.  
  554.  
  555. minCycleInfo :: Int -> String
  556. minCycleInfo num_stations =
  557. DL.intercalate " " [
  558. "For", show num_stations, "stations the minimal cycle time is",
  559. show $ minCycle tmp_problem num_stations
  560. ]
  561.  
  562. tmp_problem = blmProblem problem
  563.  
  564. sol_str = showSolution sol
  565.  
  566. main :: IO ()
  567. main = do putStrLn $ "Cycle time: " ++ (show cycle_time)
  568. putStrLn $ minCycleInfo 3
  569. putStrLn $ "Solution:\n" ++ sol_str
  570. putStrLn "(Experimental stuff follows...)"
  571. putStrLn axis
  572. putStrLn $ gantt sol
  573. putStrLn $ "Stats:\n" ++ (stats sol)
  574.  
  575.  
  576.  
Success #stdin #stdout 0.01s 5528KB
stdin
Standard input is empty
stdout
Cycle time: 8
For 3 stations the minimal cycle time is 13
Solution:
ST1 O1 (0-1) O4 (1-6) O2 (6-8)
ST2 O5 (0-2) O3 (2-3) O6 (3-5) O7 (5-7)
ST3 O8 (0-4) O10 (4-6)
ST4 O9 (0-5) O12 (5-8)
ST5 O13 (0-5)
ST6 O11 (0-4)
(Experimental stuff follows...)
--- 0 1 2 3 4 5 6 7 8
ST1 |1|----4----|-2-|
ST2 |-5-|3|-6-|-7-|
ST3 |---8---|-A-|
ST4 |----9----|--C--|
ST5 |----D----|
ST6 |---B---|
Stats:
T = 48
(T = 44)
LE = 79.166664
SI = 5.477226