diff --git a/packages/cultmath/src/CultMath/CultMath.csproj b/packages/cultmath/src/CultMath/CultMath.csproj
index cbf89929..6d2f9983 100644
--- a/packages/cultmath/src/CultMath/CultMath.csproj
+++ b/packages/cultmath/src/CultMath/CultMath.csproj
@@ -10,7 +10,7 @@
GameCult.Math
MIT AND MPL-2.0
- 0.4.0
+ 0.4.1
Clean-room C# math shaped for HLSL-style renderer code.
GameCult
README.md
diff --git a/packages/cultmath/unity/org.gamecult.cultmath/CHANGELOG.md b/packages/cultmath/unity/org.gamecult.cultmath/CHANGELOG.md
index 2cb099ce..21511bc8 100644
--- a/packages/cultmath/unity/org.gamecult.cultmath/CHANGELOG.md
+++ b/packages/cultmath/unity/org.gamecult.cultmath/CHANGELOG.md
@@ -5,6 +5,38 @@ All notable changes to this package are documented in this file.
Earlier releases (0.2.0-0.2.3) predate this package's changelog and are not
backfilled here; see `docs/semver-policy.md` for why.
+## [0.4.1]
+
+### Fixed
+
+- `BoundedLeastSquares.Solve` no longer reports `Converged` right after releasing
+ a bound while another bound still violates the KKT conditions by more than the
+ tolerance. Before, the cost-progress rule judged the pass that followed a
+ release, and near a minimiser a cost difference resolves only about the square
+ root of what a gradient comparison resolves, so the solve could stop with a
+ bound's gradient ten thousand times the tolerance. Now a bound is released
+ only when its violation exceeds both the tolerance and the rounding bound of
+ its double gradient, and the cost-progress rule judges only passes that
+ release nothing; a release resets the progress mark. Measured case: an
+ allocator-shaped problem (15 rows, 12 columns) solved from `x = 0` at
+ tolerance 1e-12 returned `Converged` with one throttle at 1, and re-solving
+ from that answer moved it to 0.92221445, so the answer depended on the start by
+ 0.078. It now stops at 0.92221445 and the re-solve takes 0 iterations
+ (`TheStopAfterAReleaseIsOnTheGradientNotTheCost` at 1e-12 and 1e-9, and for
+ every column position of the released bound,
+ `TheStopAfterAReleaseIsOnTheGradientWhicheverColumnWasReleased`). At the
+ default tolerance the results of the existing tests are unchanged. Results
+ move only where the old solver stopped early; a caller that compared such a
+ result against a stored value sees the corrected one.
+
+### Added
+
+- `BoundedLeastSquares.Validate(m, n, a, b, lo, hi, kktRelativeTolerance)` and
+ the enum `BoundedLeastSquaresInput` (`Valid`, `InvalidTolerance`,
+ `InvalidBounds`, `NonFiniteMatrix`, `NonFiniteTarget`): names the first input
+ that breaks the contract, in that order. `Solve` calls it, so it returns
+ `InvalidInput` exactly when `Validate` returns anything but `Valid`.
+
## [0.4.0]
### Breaking
diff --git a/packages/cultmath/unity/org.gamecult.cultmath/package.json b/packages/cultmath/unity/org.gamecult.cultmath/package.json
index 368ee715..6e0a0b75 100644
--- a/packages/cultmath/unity/org.gamecult.cultmath/package.json
+++ b/packages/cultmath/unity/org.gamecult.cultmath/package.json
@@ -1,7 +1,7 @@
{
"name": "org.gamecult.cultmath",
"displayName": "CultMath",
- "version": "0.4.0",
+ "version": "0.4.1",
"unity": "2021.3",
"description": "Precompiled CultMath numeric primitives and matching HLSL semantics for GameCult Unity runtimes.",
"license": "MIT AND MPL-2.0",
diff --git a/unity/org.gamecult.cultlib/package.json b/unity/org.gamecult.cultlib/package.json
index ca4e940d..85008e8c 100644
--- a/unity/org.gamecult.cultlib/package.json
+++ b/unity/org.gamecult.cultlib/package.json
@@ -4,7 +4,7 @@
"version": "1.0.60",
"unity": "2021.3",
"dependencies": {
- "org.gamecult.cultmath": "0.4.0"
+ "org.gamecult.cultmath": "0.4.1"
},
"description": "CultCache, CultNet, and CultMesh managed runtime assemblies for Unity.",
"license": "MIT",