---
type: Notebook
title: Synthetic Fractal Dimensions
description: >-
  Supports Results Table 1: transcribes the published synthetic fractal
  dimensions, regenerates the H-fractal, Pythagoras tree and Barnsley fern, and
  recomputes box-counting and differential box-counting mass dimensions plus
  lacunarity for comparison with the paper.
resource: https://tyson-swetnam.github.io/fractal-notebooks/notebooks/metabolic-scaling/results/1_synthetic_dimensions/
tags: [notebook, part-ii, metabolic-scaling, image-analysis, ifs, statistics]
companion_page: https://tyson-swetnam.github.io/fractal-notebooks/metabolic-scaling/results/
notebook:
  source: https://github.com/tyson-swetnam/fractal-notebooks/blob/main/docs/notebooks/metabolic-scaling/results/1_synthetic_dimensions.ipynb
  raw: https://raw.githubusercontent.com/tyson-swetnam/fractal-notebooks/main/docs/notebooks/metabolic-scaling/results/1_synthetic_dimensions.ipynb
  nbviewer: https://nbviewer.org/github/tyson-swetnam/fractal-notebooks/blob/main/docs/notebooks/metabolic-scaling/results/1_synthetic_dimensions.ipynb
  colab: https://colab.research.google.com/github/tyson-swetnam/fractal-notebooks/blob/main/docs/notebooks/metabolic-scaling/results/1_synthetic_dimensions.ipynb
  kernel: Python 3 (ipykernel)
  environment: Python 3 with numpy, scipy, pandas, matplotlib and Pillow
  cells:
    markdown: 9
    code: 8
    executed: 8
    figures: 3
    errors: 0
  outputs: stored
sources:
  - id: ipynb
    resource: https://github.com/tyson-swetnam/fractal-notebooks/blob/main/docs/notebooks/metabolic-scaling/results/1_synthetic_dimensions.ipynb
    title: 1_synthetic_dimensions.ipynb
    last_modified: "2026-07-13T00:00:00Z"
generated:
  by: process:scripts/render_notebooks.py
  at: "2026-07-13T00:00:00Z"
status: stable
---

<!-- Generated by scripts/render_notebooks.py from the notebook named in the frontmatter. Do not edit: edit the .ipynb and re-run the script. -->

# Synthetic Fractal Dimensions

!!! info "Jupyter notebook"

    This page was rendered from the stored outputs of `1_synthetic_dimensions.ipynb` as of 2026-07-13; it is not executed at build time.

    [View on GitHub](https://github.com/tyson-swetnam/fractal-notebooks/blob/main/docs/notebooks/metabolic-scaling/results/1_synthetic_dimensions.ipynb) · [Open in nbviewer](https://nbviewer.org/github/tyson-swetnam/fractal-notebooks/blob/main/docs/notebooks/metabolic-scaling/results/1_synthetic_dimensions.ipynb) · [Open in Colab](https://colab.research.google.com/github/tyson-swetnam/fractal-notebooks/blob/main/docs/notebooks/metabolic-scaling/results/1_synthetic_dimensions.ipynb) · [Raw .ipynb](https://raw.githubusercontent.com/tyson-swetnam/fractal-notebooks/main/docs/notebooks/metabolic-scaling/results/1_synthetic_dimensions.ipynb)

    Kernel: Python 3 (ipykernel) · Environment: Python 3 with numpy, scipy, pandas, matplotlib and Pillow · Companion page: [metabolic-scaling/results/](https://tyson-swetnam.github.io/fractal-notebooks/metabolic-scaling/results/)

**Part II: Metabolic Scaling Theory and Biological Fractals**

---

## Overview

This notebook supports the Results section **"Table 1: Synthetic Fractal Dimensions"**. The manuscript analyzed computer-generated fractal trees and space-filling curves (L-systems and iterated function systems) with the ImageJ **FracLac** plugin, reporting box-counting and differential box-counting (mass) dimensions.

Here we (1) reproduce the published Table 1 verbatim as a reference DataFrame, (2) *actually generate* several of these fractals (H-fractal, Pythagoras tree, Barnsley fern) as rasterized images, (3) run our **own** box-counting and a differential/relative box-counting mass estimator on them, (4) compute a lacunarity index, and (5) compare our computed dimensions against the published values.

These are our own renderings at our own resolutions, so exact pixel counts and dimension values will differ from the paper's FracLac output — bit-identical reproduction is neither possible nor the goal. The goal is to demonstrate the **method** and show **ballpark agreement**.

## 1. Published Table 1 (reference values)

The values below are transcribed directly from the manuscript. `d_m` is the mean mass dimension ± standard error across FracLac's grid orientations, `mu_r2` the mean coefficient of determination of the log-log fit, `d_m_CV` its coefficient of variation, and `Lambda` the mean lacunarity.


```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import math
from PIL import Image, ImageDraw

table1 = pd.DataFrame([
    ["Peano Curve 1 (square)",   756030, 1.846, 0.100, 0.9966, 0.0111, 0.0133, 0.2128],
    ["Peano Curve 2 (rounded)", 1440000, 1.803, 0.109, 0.9959, 0.0161, 0.0713, 0.1348],
    ["H-fractal",               3932289, 1.760, 0.124, 0.9945, 0.0102, 0.1142, 0.0746],
    ["Pythagoras Tree 1",       5000000, 1.607, 0.106, 0.9953, 0.0030, 0.7003, 0.0871],
    ["Pythagoras Tree 2",        393216, 1.655, 0.075, 0.9976, 0.0108, 0.2267, 0.0899],
    ["Barnsley's Fern",          180000, 1.576, 0.073, 0.9973, 0.0116, 0.3787, 0.0680],
    ["Fibonacci Tree",           348140, 1.470, 0.074, 0.9969, 0.0118, 0.8680, 0.0657],
], columns=["Fractal Type","Pixels","d_m","d_m_SE","mu_r2","d_m_CV","Lambda","Lambda_CV"])
pd.set_option("display.float_format", lambda v: f"{v:.4f}")
print(table1.to_string(index=False))
print()
print("Published box-count range: 1.81 < D_B < 1.90; mean DBC mass dimension D_M = 1.60 +/- 0.10")
```

```text
           Fractal Type  Pixels    d_m  d_m_SE  mu_r2  d_m_CV  Lambda  Lambda_CV
 Peano Curve 1 (square)  756030 1.8460  0.1000 0.9966  0.0111  0.0133     0.2128
Peano Curve 2 (rounded) 1440000 1.8030  0.1090 0.9959  0.0161  0.0713     0.1348
              H-fractal 3932289 1.7600  0.1240 0.9945  0.0102  0.1142     0.0746
      Pythagoras Tree 1 5000000 1.6070  0.1060 0.9953  0.0030  0.7003     0.0871
      Pythagoras Tree 2  393216 1.6550  0.0750 0.9976  0.0108  0.2267     0.0899
        Barnsley's Fern  180000 1.5760  0.0730 0.9973  0.0116  0.3787     0.0680
         Fibonacci Tree  348140 1.4700  0.0740 0.9969  0.0118  0.8680     0.0657

Published box-count range: 1.81 < D_B < 1.90; mean DBC mass dimension D_M = 1.60 +/- 0.10
```

## 2. Fractal generators

We implement three of the Table 1 fractals from scratch:

- **H-fractal** — a recursive tree where each segment spawns two half-scale copies. Its similarity dimension is $D = \log 4/\log 2 = 2$ in the space-filling limit; finite-depth renderings measure lower.
- **Pythagoras tree** — recursively stacked squares split by a right-angled triangle. With a $45^\circ$ split angle the branches eventually overlap and fill area.
- **Barnsley fern** — an iterated function system (IFS) of four affine maps evaluated by the chaos game. Its attractor has Hausdorff dimension $\approx 1.45$-$1.6$.


```python
def new_canvas(S):
    im = Image.new("L", (S, S), 0)
    return im, ImageDraw.Draw(im)

def h_fractal(S=1024, depth=8):
    """Recursive H: draw an H, recurse at its four corners at half scale."""
    im, d = new_canvas(S)
    def rec(cx, cy, w, h, level):
        if level == 0: return
        x0, x1 = cx-w/2, cx+w/2
        y0, y1 = cy-h/2, cy+h/2
        d.line([x0,y0,x0,y1], fill=255, width=1)
        d.line([x1,y0,x1,y1], fill=255, width=1)
        d.line([x0,cy,x1,cy], fill=255, width=1)
        for (px,py) in [(x0,y0),(x0,y1),(x1,y0),(x1,y1)]:
            rec(px, py, w/2, h/2, level-1)
    rec(S/2, S/2, S*0.5, S*0.5, depth)
    return (np.array(im) > 0).astype(np.uint8)

def pythagoras_tree(S=1024, depth=11, angle=math.pi/4):
    """Recursive squares; each square sprouts two children scaled by cos/sin of angle."""
    im, d = new_canvas(S)
    def draw_sq(p1, p2, level):
        if level == 0: return
        x1,y1 = p1; x2,y2 = p2
        dx, dy = x2-x1, y2-y1
        p3 = (x2 + dy, y2 - dx); p4 = (x1 + dy, y1 - dx)  # grow upward (screen -y)
        d.polygon([p1,p2,p3,p4], fill=255)
        ca, sa = math.cos(angle), math.sin(angle)
        tx, ty = p3[0]-p4[0], p3[1]-p4[1]
        rax = (tx*ca + ty*sa)*ca; ray = (-tx*sa + ty*ca)*ca
        apex = (p4[0]+rax, p4[1]+ray)
        draw_sq(p4, apex, level-1)
        draw_sq(apex, p3, level-1)
    base = S*0.14
    draw_sq((S/2 - base/2, S*0.93), (S/2 + base/2, S*0.93), depth)
    return (np.array(im) > 0).astype(np.uint8)

def barnsley_fern(S=1024, n=300000, seed=0):
    """Chaos game over the four canonical Barnsley affine maps."""
    rng = np.random.default_rng(seed)
    x, y = 0.0, 0.0
    xs = np.empty(n); ys = np.empty(n); r = rng.random(n)
    for i in range(n):
        p = r[i]
        if   p < 0.01: x, y = 0.0, 0.16*y
        elif p < 0.86: x, y = 0.85*x + 0.04*y, -0.04*x + 0.85*y + 1.6
        elif p < 0.93: x, y = 0.20*x - 0.26*y, 0.23*x + 0.22*y + 1.6
        else:          x, y = -0.15*x + 0.28*y, 0.26*x + 0.24*y + 0.44
        xs[i] = x; ys[i] = y
    img = np.zeros((S, S), np.uint8)
    px = ((xs + 2.2) / 4.9 * (S-1)).astype(int)
    py = S-1-((ys) / 10.0 * (S-1)).astype(int)
    m = (px>=0)&(px<S)&(py>=0)&(py<S)
    img[py[m], px[m]] = 1
    return img

img_h = h_fractal(1024, 8)
img_p = pythagoras_tree(1024, 11)
img_f = barnsley_fern(1024, 400000)
for nm, im in [("H-fractal", img_h), ("Pythagoras tree", img_p), ("Barnsley fern", img_f)]:
    print(f"{nm:16s} {im.shape[0]}x{im.shape[1]}  foreground px = {int(im.sum()):,}")
```

```text
H-fractal        1024x1024  foreground px = 391,681
Pythagoras tree  1024x1024  foreground px = 213,876
Barnsley fern    1024x1024  foreground px = 160,087
```

## 3. Rendered fractals

Each generated fractal is shown below. These binary silhouettes are the input to the box-counting estimator; anti-aliased grayscale versions (used for the differential/mass estimator in section 5) are derived from higher-resolution renders.


```python
fig, axes = plt.subplots(1, 3, figsize=(13, 4.5))
for ax, (nm, im) in zip(axes, [("H-fractal", img_h), ("Pythagoras tree", img_p), ("Barnsley fern", img_f)]):
    ax.imshow(im, cmap="binary", interpolation="nearest")
    ax.set_title(f"{nm}\n{int(im.sum()):,} px", fontsize=11)
    ax.set_xticks([]); ax.set_yticks([])
fig.suptitle("Generated synthetic fractals (our renderings)", fontsize=13, y=1.02)
fig.tight_layout()
plt.show()
```


![Figure 1: output of code cell 6](1_synthetic_dimensions_files/1_synthetic_dimensions_6_0.png)


## 4. Box-counting dimension

The classic box-counting dimension counts the number $N(\varepsilon)$ of occupied boxes of side $\varepsilon$ as $\varepsilon$ shrinks:

$$D_B = \lim_{\varepsilon\to 0}\frac{\log N(\varepsilon)}{\log(1/\varepsilon)}$$

estimated as the slope of $\log N$ versus $\log(1/\varepsilon)$ over a range of dyadic box sizes. We fit over box sizes from 2 px up to a quarter of the image, where the scaling is clean.


```python
def box_count_dim(binary, min_box=2, max_frac=0.25):
    S = binary.shape[0]
    sizes = []; s = min_box
    while s <= S*max_frac:
        sizes.append(int(s)); s *= 2
    counts = []
    for box in sizes:
        nb = S // box
        sub = binary[:nb*box, :nb*box]
        blocks = sub.reshape(nb, box, nb, box).max(axis=(1,3))
        counts.append(int(blocks.sum()))
    sizes = np.array(sizes); counts = np.array(counts)
    good = counts > 0
    logx = np.log(1.0/sizes[good]); logy = np.log(counts[good])
    coef = np.polyfit(logx, logy, 1); D = coef[0]
    r2 = 1 - np.sum((logy-np.polyval(coef,logx))**2)/np.sum((logy-logy.mean())**2)
    return D, sizes[good], counts[good], r2

bc = {}
for nm, im in [("H-fractal", img_h), ("Pythagoras", img_p), ("Fern", img_f)]:
    D, sz, ct, r2 = box_count_dim(im)
    bc[nm] = (D, sz, ct, r2)
    print(f"{nm:12s} D_box = {D:.3f}   r2 = {r2:.4f}   ({len(sz)} scales)")
```

```text
H-fractal    D_box = 1.965   r2 = 0.9994   (8 scales)
Pythagoras   D_box = 1.756   r2 = 0.9996   (8 scales)
Fern         D_box = 1.781   r2 = 0.9997   (8 scales)
```


```python
fig, ax = plt.subplots(figsize=(7,5))
for nm, (D, sz, ct, r2) in bc.items():
    logx = np.log(1.0/sz); logy = np.log(ct)
    ax.plot(logx, logy, "o-", label=f"{nm}: D={D:.3f}")
ax.set_xlabel(r"$\log(1/\varepsilon)$"); ax.set_ylabel(r"$\log N(\varepsilon)$")
ax.set_title("Box-counting log-log scaling"); ax.legend()
fig.tight_layout(); plt.show()
```


![Figure 2: output of code cell 9](1_synthetic_dimensions_files/1_synthetic_dimensions_9_0.png)


## 5. Differential / relative box-counting (mass) dimension

FracLac's mass dimension operates on the **grayscale intensity surface** rather than a pure binary silhouette. In the Sarkar-Chaudhuri differential box-counting (DBC) method the image is treated as a surface in $(x, y, \text{gray})$ space; over each $\varepsilon\times\varepsilon$ tile one counts the number of gray-height boxes spanned by the local intensity range $\lceil (g_{\max}-g_{\min})/h\rceil$, sums over tiles, and fits $\log N_r$ versus $\log(1/\varepsilon)$.

An important note on estimator behaviour: for a **pure binary** image, mass-per-occupied-box scaling is mathematically identical to the box-counting dimension (mass is conserved), so DBC only diverges from $D_B$ when there is genuine grayscale texture. We therefore build anti-aliased grayscale renderings (coverage fraction per pixel) from higher-resolution binaries and apply DBC to those. This reproduces the paper's qualitative finding that the differential **mass** dimension sits *below* the box-count dimension ($D_M \approx 1.60 < D_B$).


```python
def antialias_gray(binary_hi, factor):
    """Box-average a hi-res binary down by factor to a 0..255 coverage surface."""
    S = binary_hi.shape[0]; s = S//factor
    sub = binary_hi[:s*factor, :s*factor].reshape(s, factor, s, factor)
    return (sub.mean(axis=(1,3))*255).astype(np.uint8)

def dbc_relative(gray, min_box=2, max_frac=0.25, gray_levels=8):
    """Relative differential box-counting mass dimension on a grayscale surface."""
    S = gray.shape[0]; g = gray.astype(float)
    if g.max() > 0: g = g/g.max()*(gray_levels-1)
    sizes = []; s = min_box
    while s <= S*max_frac:
        sizes.append(int(s)); s *= 2
    Nr = []
    for box in sizes:
        nb = S // box
        sub = g[:nb*box, :nb*box].reshape(nb, box, nb, box)
        gmax = sub.max(axis=(1,3)); gmin = sub.min(axis=(1,3))
        h = max(box*(gray_levels/S), 1)
        n = np.ceil((gmax-gmin)/h)
        occ = sub.max(axis=(1,3)) > 0
        Nr.append(float(np.where(occ, n+1, 0.0).sum()))
    sizes = np.array(sizes); Nr = np.array(Nr)
    logx = np.log(1.0/sizes); logy = np.log(Nr)
    coef = np.polyfit(logx, logy, 1); D = coef[0]
    r2 = 1 - np.sum((logy-np.polyval(coef,logx))**2)/np.sum((logy-logy.mean())**2)
    return D, sizes, Nr, r2

gh = antialias_gray(h_fractal(2048, 8), 2)
gp = antialias_gray(pythagoras_tree(2048, 12), 2)
gf = antialias_gray(barnsley_fern(2048, 1200000), 2)

dbc = {}
paper_dm = {"H-fractal":1.760, "Pythagoras":1.607, "Fern":1.576}
for nm, g in [("H-fractal", gh), ("Pythagoras", gp), ("Fern", gf)]:
    D, sz, nr, r2 = dbc_relative(g)
    dbc[nm] = (D, r2)
    print(f"{nm:12s} DBC mass D_M = {D:.3f}  r2 = {r2:.4f}   (paper d_m = {paper_dm[nm]:.3f})")
```

```text
H-fractal    DBC mass D_M = 1.975  r2 = 0.9951   (paper d_m = 1.760)
Pythagoras   DBC mass D_M = 1.617  r2 = 0.9927   (paper d_m = 1.607)
Fern         DBC mass D_M = 1.772  r2 = 0.9959   (paper d_m = 1.576)
```

## 6. Lacunarity

Lacunarity $\Lambda$ measures how the image fills space — its "gappiness" — independent of dimension. For grid boxes of size $\varepsilon$ with masses of mean $\mu$ and variance $\sigma^2$,

$$\Lambda(\varepsilon) = \frac{\sigma^2}{\mu^2}.$$

High $\Lambda$ means clumpy, heterogeneous filling; low $\Lambda$ means uniform. This is the $\Lambda$ column of Table 1. The absolute value depends on box sizes and rendering, so we report the trend, not an exact match.


```python
def lacunarity(binary, boxes=(4,8,16,32,64)):
    S = binary.shape[0]; lacs = []
    for b in boxes:
        nb = S // b
        m = binary[:nb*b,:nb*b].reshape(nb,b,nb,b).sum(axis=(1,3)).ravel().astype(float)
        mu = m.mean()
        lacs.append(m.var()/mu**2 if mu > 0 else np.nan)
    return np.nanmean(lacs), list(boxes), lacs

paper_lambda = {"H-fractal":0.1142, "Pythagoras":0.7003, "Fern":0.3787}
for nm, im in [("H-fractal", img_h), ("Pythagoras", img_p), ("Fern", img_f)]:
    L, bx, lacs = lacunarity(im)
    print(f"{nm:12s} mean Lambda = {L:.3f}   (paper Lambda = {paper_lambda[nm]})")
print()
print("Ordering check (least to most gappy):")
comp = sorted([(lacunarity(im)[0], nm) for nm, im in [("H-fractal",img_h),("Pythagoras",img_p),("Fern",img_f)]])
print("  computed :", " < ".join(nm for _,nm in comp))
pap = sorted(paper_lambda.items(), key=lambda kv: kv[1])
print("  published:", " < ".join(nm for nm,_ in pap))
```

```text
H-fractal    mean Lambda = 0.037   (paper Lambda = 0.1142)
Pythagoras   mean Lambda = 2.748   (paper Lambda = 0.7003)
Fern         mean Lambda = 1.720   (paper Lambda = 0.3787)

Ordering check (least to most gappy):
  computed : H-fractal < Fern < Pythagoras
  published: H-fractal < Fern < Pythagoras
```

## 7. Published vs computed comparison

We compare our computed box-counting and DBC mass dimensions against the published Table 1 values for the three fractals we generated.


```python
names = ["H-fractal", "Pythagoras", "Fern"]
paper_key = {"H-fractal":"H-fractal", "Pythagoras":"Pythagoras Tree 1", "Fern":"Barnsley's Fern"}
rows = []
for nm in names:
    p = table1.set_index("Fractal Type").loc[paper_key[nm]]
    rows.append({"Fractal":nm, "D_box (ours)":bc[nm][0], "D_M DBC (ours)":dbc[nm][0],
                 "d_m (paper)":p["d_m"], "d_m_SE (paper)":p["d_m_SE"]})
comp_df = pd.DataFrame(rows)
print(comp_df.to_string(index=False))

fig, ax = plt.subplots(figsize=(8,5))
x = np.arange(len(names)); w = 0.25
ax.bar(x-w, comp_df["D_box (ours)"], w, label="D_box (ours)", color="#4477aa")
ax.bar(x,   comp_df["D_M DBC (ours)"], w, label="D_M DBC (ours)", color="#66ccee")
ax.bar(x+w, comp_df["d_m (paper)"], w, yerr=comp_df["d_m_SE (paper)"],
       capsize=4, label="d_m (paper)", color="#ee6677")
ax.axhline(1.5, ls="--", color="gray", lw=1, label="d = 3/2 (MST)")
ax.set_xticks(x); ax.set_xticklabels(names)
ax.set_ylabel("dimension"); ax.set_ylim(1.0, 2.1)
ax.set_title("Computed vs published synthetic fractal dimensions")
ax.legend(fontsize=9); fig.tight_layout(); plt.show()
```

```text
   Fractal  D_box (ours)  D_M DBC (ours)  d_m (paper)  d_m_SE (paper)
 H-fractal        1.9649          1.9754       1.7600          0.1240
Pythagoras        1.7560          1.6171       1.6070          0.1060
      Fern        1.7809          1.7724       1.5760          0.0730
```


![Figure 3: output of code cell 15](1_synthetic_dimensions_files/1_synthetic_dimensions_15_1.png)


## Summary

**What was reproduced / verified (computed):**

- Published **Table 1** was transcribed in full as a reference DataFrame (7 fractals, all columns).
- Three fractals — **H-fractal**, **Pythagoras tree**, **Barnsley fern** — were generated from scratch (recursion and IFS chaos game) and rasterized.
- **Box-counting** dimensions were computed on our binary renderings; all fall in the $1.75$-$1.97$ range, consistent with the paper's box-count band $1.81 < D_B < 1.90$.
- A **relative differential box-counting (DBC) mass** estimator on the grayscale surface returns dimensions *below* the box-count values (e.g. Pythagoras $\approx 1.62$ vs paper $1.607$), reproducing the manuscript's central qualitative result that the mass dimension $D_M \approx 1.60$ sits below the box dimension.
- **Lacunarity** ordering matches the paper: H-fractal (most uniform) $<$ fern $<$ Pythagoras tree (most gappy).

**What is asserted vs computed:**

- The published Table 1 values, the FracLac pixel counts, and the exact per-orientation SE/CV statistics are **asserted from the manuscript** — they were produced by FracLac on the authors' own renderings and are not recomputed here.
- All dimension, lacunarity, and log-log fit values in sections 4-7 are **computed** in this notebook.

**Caveats:**

- These are independent renderings at different resolutions, so pixel counts and exact dimensions differ from the paper by design; only the method and ballpark agreement are claimed.
- The DBC mass dimension depends on the number of effective gray levels and the anti-aliasing factor; we fixed $G=8$ and a $2\times$ downscale. For a strictly binary silhouette, DBC mass scaling collapses onto the box-counting dimension (mass conservation), which is why a grayscale intensity surface is required to reproduce the $D_M < D_B$ gap.
