packages feed

exchangealgebra 0.4.1.0 → 0.4.1.1

raw patch · 4 files changed

+51/−6 lines, 4 files

Files

ChangeLog.md view
@@ -1,5 +1,24 @@ # Changelog for ExchangeAlgebra +## 0.4.1.1 - 2026-06-07++### Fixed+- `union` (and therefore `(.+)` / `mappend` / `fromList`) misassociated a value+  with the wrong base when one operand was a **zero-valued singleton**. For+  `(v1:@b1) .+ (v2:@b2)` with `isZeroValue v1`, the result was `v2:@b1` — the+  surviving nonzero value relabeled onto the *zero posting's* base (symmetrically+  `v1:@b2` when `v2` was zero). A zero contributes nothing, so the result must be+  `v2:@b2` / `v1:@b1` (the nonzero value on its **own** base).+  The bug preserved `norm` (the total was unchanged) but corrupted **per-base+  projection** (`proj` / `projWithBase` / `balanceBy` / stock & profit queries):+  a value silently moved to a neighboring base. It was construction-order+  sensitive — ledgers that build explicit `0:@base` singletons via the raw `(:@)`+  constructor (e.g. sparsified input coefficients in agent-based simulations)+  would, depending on accumulation order, invent a phantom posting on an adjacent+  base. In the bundled simulation example this shifted a company's reported stock+  by up to ~30% over 100 terms. One-line fix in `Algebra.hs union`, covered by the+  new `testUnionZeroSingletonBase` unit test. (Independently confirmed root cause.)+ ## 0.4.1.0 - 2026-06-06  ### Added
exchangealgebra.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 7d4bb7802873f6ce2b4888ce4717621e5ccc4400b73ebc12b9d4ea69f0e994b5+-- hash: fe5fab919197e65dce7b698dc174e0447987248ecd7a81108d260e187a1a248f  name:           exchangealgebra-version:        0.4.1.0+version:        0.4.1.1 synopsis:       Exchange Algebra for bookkeeping and economic simulation description:    Please see the README on GitHub at <https://github.com/yakagika/ExchangeAlgebra#readme> category:       Accounting, Finance, Math
src/ExchangeAlgebra/Algebra.hs view
@@ -643,11 +643,17 @@ union Zero x  = x union x Zero  = x -- singletons+-- NOTE: a zero-valued singleton contributes nothing, so the result must keep the+-- /nonzero/ value on its OWN base. Earlier code returned @v2:@b1@ / @v1:@b2@,+-- relabeling the surviving value onto the zero posting's base — this preserved+-- 'norm' but silently moved the value to the wrong base, corrupting per-base+-- projection and making construction order observable (raw @(:@)@ on a sparsified+-- zero coefficient builds an explicit @0:@base@ singleton). Keep @v2:@b2@ / @v1:@b1@. union (v1:@b1) (v2:@b2)     | isZeroValue v1 = case isZeroValue v2 of                             True  -> Zero-                            False -> v2:@b1-    | isZeroValue v2 = v1:@b2+                            False -> v2:@b2+    | isZeroValue v2 = v1:@b1     | otherwise      = insert b2 v2 (v1:@b1) -- If one side is a singleton union x (v:@b) = insert b v x
test/Spec.hs view
@@ -144,6 +144,25 @@     assertEqual "bases Hat label count" 1 hatCount     assertEqual "bases Not label count" 3 notCount +-- | Regression test for the @union@ zero-singleton base-relabel bug+-- (Algebra.hs). When one operand of @(.+)@ is a /zero-valued/ singleton on base+-- @b1@ and the other a /real/ singleton on a different base @b2@, the result must+-- keep the real value on its OWN base (@v2:@b2@), not relabel it onto the zero+-- posting's base. The old code returned @v2:@b1@ / @v1:@b2@, which silently moved+-- a value to the wrong base — preserving @norm@ but corrupting per-base projection.+testUnionZeroSingletonBase :: IO ()+testUnionZeroSingletonBase = do+    let zb = 0 :@ (Hat :< Yen)    :: TestAlg   -- zero value, base Yen+        rb = 5 :@ (Hat :< Amount) :: TestAlg   -- real value, base Amount+    assertEqual "union zero(.+)real keeps real value on its own base"+        rb (EA.proj [Hat :< Amount] (zb .+ rb))+    assertEqual "union real(.+)zero keeps real value on its own base"+        rb (EA.proj [Hat :< Amount] (rb .+ zb))+    assertEqual "union zero(.+)real: nothing relabeled onto the zero's base"+        (EA.Zero :: TestAlg) (EA.proj [Hat :< Yen] (zb .+ rb))+    assertEqual "union real(.+)zero: nothing relabeled onto the zero's base"+        (EA.Zero :: TestAlg) (EA.proj [Hat :< Yen] (rb .+ zb))+ testSigmaMergePath :: IO () testSigmaMergePath = do     let xs = [1 .. 5 :: Int]@@ -655,13 +674,13 @@     -- Stock at t=1     assertSimNear "sim1 stock(t=1,c=1)" 28.487224703666264 (stocks1 !! 0)     assertSimNear "sim1 stock(t=1,c=3)" 30.0               (stocks1 !! 2)-    assertSimNear "sim1 stock(t=1,c=6)" 29.01925920375148  (stocks1 !! 5)+    assertSimNear "sim1 stock(t=1,c=6)" 30.0               (stocks1 !! 5)  -- re-baselined: union zero-base fix removed a phantom self-input     -- Stock at t=50     assertSimNear "sim1 stock(t=50,c=1)" 304.9028131162567  (stocks50 !! 0)     assertSimNear "sim1 stock(t=50,c=4)" 292.4764622201871  (stocks50 !! 3)     -- Stock at t=100     assertSimNear "sim1 stock(t=100,c=1)" 586.9595359862476  (stocks100 !! 0)-    assertSimNear "sim1 stock(t=100,c=6)" 592.9260354913148  (stocks100 !! 5)+    assertSimNear "sim1 stock(t=100,c=6)" 767.960563480499   (stocks100 !! 5)  -- re-baselined: union zero-base fix (bug compounded over terms)     -- Gross profit at t=50     assertSimNear "sim1 profit(t=50,c=1)" 0.35886554260018855 (profits50 !! 0)     assertSimNear "sim1 profit(t=50,c=2)" 1.572544209772035   (profits50 !! 1)@@ -771,6 +790,7 @@     testProjWithNoteNorm     testBasesNotSideRegression     testNumericToleranceScaleAware+    testUnionZeroSingletonBase     testSigmaMergePath     testSigma2When     testSigmaFromMap