exchangealgebra 0.4.1.0 → 0.4.1.1
raw patch · 4 files changed
+51/−6 lines, 4 files
Files
- ChangeLog.md +19/−0
- exchangealgebra.cabal +2/−2
- src/ExchangeAlgebra/Algebra.hs +8/−2
- test/Spec.hs +22/−2
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