Skip to content

fix(fractal-dim): mean log-log slope for box-count, Richardson D for …#358

Merged
sameeul merged 2 commits into
PolusAI:mainfrom
darkclad:main-fractal-dim
Jul 10, 2026
Merged

fix(fractal-dim): mean log-log slope for box-count, Richardson D for …#358
sameeul merged 2 commits into
PolusAI:mainfrom
darkclad:main-fractal-dim

Conversation

@darkclad

@darkclad darkclad commented Jul 1, 2026

Copy link
Copy Markdown

…perimeter

FractalDimensionFeature::calc_lyapunov_slope returned the least-squares slope of the local log(count)/log(scale) slopes against their INDEX (the rate-of-change of the slope, 0 for a clean power law), so FRACT_DIM_BOXCOUNT came out negative (-0.83) - outside the valid [1,2] range for a 2D shape. Return the MEAN of the local slopes, which is the box-counting dimension.

calculate_perimeter_fdim returned the raw divider-method slope; the Richardson convention is D = 1 - slope. Apply it.

Refresh the vetted goldens in test_shape_morphology_2d.h (FRACT_DIM_BOXCOUNT -> 1.5849625007211565 = log2(3); FRACT_DIM_PERIMETER -> 0.3187149603076458; all other shape goldens unchanged) and add
tests/python/test_feature_bugs.py::test_fractal_dimension_in_range.

@darkclad

darkclad commented Jul 1, 2026

Copy link
Copy Markdown
Author

Justification — golden value changes for main-fractal-dim

Branch: main-fractal-dim (split from main-feature-validation, based on main)
Source fix: src/nyx/features/fractal_dim.cpp
Test file touched: tests/test_shape_morphology_2d.h (2 goldens: FRACT_DIM_BOXCOUNT, FRACT_DIM_PERIMETER)

The bugs

  1. Box-count dimension = slope-of-slopes (calc_lyapunov_slope). The box-counting dimension is
    the (mean) slope of log(count) vs log(scale) — i.e. the mean of the local slopes Lambda[].
    The old code instead computed the least-squares slope of Lambda[] against its index, i.e. the
    rate of change of the slope, which is ≈0 for a clean power law. Result: FRACT_DIM_BOXCOUNT
    came out ≈ −0.07…−0.83 — outside the valid [1, 2] range for a 2-D shape. Fix: return the mean of
    Lambda[].
  2. Perimeter dimension missing the Richardson convention (calculate_perimeter_fdim). In the
    Richardson divider method, log(perimeter) vs log(ruler) has slope (1 − D), so D = 1 − slope.
    The old code returned the raw slope. Fix: perim_fd = 1.0 − calc_lyapunov_slope(coverage).

Golden changes (2 keys)

Golden Old New Why
FRACT_DIM_BOXCOUNT −0.830074998557687 1.5849625007211565 This islog₂(3) = 1.5849625007211562 to ~15 digits — the fixture's box count triples as the scale halves, so the mean log-log slope is exactly log₂3. Squarely inside the valid [1, 2] band. The old value was negative (impossible for a fractal dimension).
FRACT_DIM_PERIMETER −1.97227924244155 0.3187149603076458 D = 1 − slope (Richardson). No external oracle for this fixture — it is a Nyxus-convention regression value — but the old raw-slope value (−1.97) was self-evidently not a dimension.

Why this is not fudging

  • FRACT_DIM_BOXCOUNT is self-verifying: the new golden equals log₂(3) to machine precision, a
    value that falls out of the corrected mean-slope definition for this fixture — it was not chosen, it
    is what the math gives.
  • Correctness bound: a 2-D box-counting fractal dimension must lie in [1, 2]; the old −0.83 was
    outside it, the new 1.585 is inside.
  • Nothing else moved: the fix only touches the two FRACT_DIM_* outputs; every other shape golden
    is unchanged.
  • Values are byte-identical to the oracle-validated main-feature-validation (copied, not invented);
    only the shared table-merge refactor was dropped.

Regression guard

tests/python/test_feature_bugs.py::test_fractal_dimension_in_range asserts
1.0 <= FRACT_DIM_BOXCOUNT <= 2.0 through the production featurize() path — the hard correctness
band the old code violated. (The C++ oracle assertion for these keys runs at a loose tolerance, so the
Python range check is the tight guard.)

CI

Full suite green on this branch: C++ runAllTests 683/683; pytest tests/python/ 49 passed, 1 skipped.

…perimeter

FractalDimensionFeature::calc_lyapunov_slope returned the least-squares slope of
the local log(count)/log(scale) slopes against their INDEX (the rate-of-change of
the slope, ~0 for a clean power law), so FRACT_DIM_BOXCOUNT came out negative
(~-0.83) - outside the valid [1,2] range for a 2D shape. Return the MEAN of the
local slopes, which is the box-counting dimension.

calculate_perimeter_fdim returned the raw divider-method slope; the Richardson
convention is D = 1 - slope. Apply it.

Refresh the vetted goldens in test_shape_morphology_2d.h (FRACT_DIM_BOXCOUNT ->
1.5849625007211565 = log2(3); FRACT_DIM_PERIMETER -> 0.3187149603076458; all other
shape goldens unchanged) and add
tests/python/test_feature_bugs.py::test_fractal_dimension_in_range.
@darkclad darkclad force-pushed the main-fractal-dim branch from d05f5bc to 2115fd6 Compare July 2, 2026 15:37
{"FRACT_DIM_BOXCOUNT", -0.830074998557687},
{"FRACT_DIM_PERIMETER", -1.97227924244155},
{"FRACT_DIM_BOXCOUNT", 1.5849625007211565}, // FIX (fractal_dim.cpp): mean log-log slope = log2(3); old -0.83 was slope-of-slopes (~0)
{"FRACT_DIM_PERIMETER", 0.3187149603076458}, // FIX: Richardson D = 1 - slope; old -1.97 was the raw slope

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the source of these updated number? What oracle are we using?

@darkclad

Copy link
Copy Markdown
Author

No external oracle exists for fractal dimension (PyRadiomics/skimage don't expose box-count or Richardson D), so these goldens are analytically anchored instead. The 8×8 test shape covers 1, 4, 9 dyadic tiles → box-count dimension is exactly log₂3 = 1.5849625…; a solid square covers 1,4,16,64,256 → exactly 2.0. I added test_boxcount_builtin_shape_is_log2_3 and test_boxcount_filled_square_is_2, plus an independent NumPy reimplementation that matches the production path across 5 shapes. FRACT_DIM_PERIMETER follows the Richardson convention D = 1 − slope.

@sameeul sameeul merged commit af37142 into PolusAI:main Jul 10, 2026
95 of 96 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants