Multiresolution Flow Cartogram Creation¶
Flow cartograms distort geography so that each region's area is proportional to a data value. The underlying algorithm iteratively displaces coordinates on a velocity field derived from a density grid. Three key parameters control the trade-off between accuracy and compute time:
| Parameter | What it controls |
|---|---|
grid_size |
Resolution of the density / velocity grid |
dt |
Size of each displacement step |
recompute_every |
How often the density and velocity fields are recomputed |
This tutorial walks through each parameter's impact and then shows how multiresolution morphing lets you get accurate results without paying the full cost of a fine grid from the start.
What You'll Learn¶
- How
grid_size,dt, andrecompute_everyaffect convergence and compute time - How these parameters interact
- How multiresolution morphing improves the accuracy / speed trade-off
Prerequisites¶
Install carto-flow first:
pip install carto-flow
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import pandas as pd
import carto_flow.data as examples
import carto_flow.flow_cartogram as flow
from carto_flow.geo_utils import densify_coverage
# Helper function to compare two flow cartogram maps
def _plot_error_maps(r1, r2, title1, title2, figsize=(12, 5)):
"""Side-by-side error maps with a shared linear symmetric color scale.
Scale spans ±max_tolerance (white = no error, red = over-expanded,
blue = under-expanded). Regions outside ±tolerance are shown in a
distinct saturated color with triangular colorbar caps.
"""
max_tol_pct = (
max(
r1.options.max_tol if r1.options else 0.10,
r2.options.max_tol if r2.options else 0.10,
)
* 100
)
norm = mcolors.Normalize(vmin=-max_tol_pct, vmax=max_tol_pct)
cmap = plt.cm.RdBu_r.copy()
cmap.set_over("#47000f") # dark red: over-expanded beyond tolerance
cmap.set_under("#032041") # dark blue: under-expanded beyond tolerance
fig, axes = plt.subplots(1, 2, figsize=figsize)
r1.plot(column="_morph_error_pct", ax=axes[0], norm=norm, cmap=cmap, legend=False)
axes[0].set_title(title1)
axes[0].axis("off")
r2.plot(column="_morph_error_pct", ax=axes[1], norm=norm, cmap=cmap, legend=False)
axes[1].set_title(title2)
axes[1].axis("off")
# Shared colorbar; triangular caps show the out-of-tolerance color
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
cbar = fig.colorbar(sm, ax=list(axes), fraction=0.03, pad=0.04, extend="both", shrink=0.7)
ticks = [-max_tol_pct, -max_tol_pct / 2, 0, max_tol_pct / 2, max_tol_pct]
cbar.set_ticks(ticks)
cbar.set_ticklabels(["0%" if t == 0 else f"{t:+g}%" for t in ticks])
cbar.set_label("Area error")
# Load population and geometry data for the lower 48 US States
us_states = examples.load_us_census(population=True)
column = "Population"
1. The accuracy / speed trade-off¶
Every iteration of the flow cartogram algorithm:
- Rasterizes current geometries onto a grid to compute a density field
- Solves a Poisson equation (via FFT) to derive a velocity field
- Displaces all coordinates by one time step
Accuracy improves as:
- the grid gets finer (more spatial detail captured)
- the time step gets smaller (smoother, more stable displacements)
- the density field is recomputed more often (tracks geometry changes)
But each of these improvements also increases computation time. The next three sections isolate each factor.
2. Effect of grid_size¶
grid_size sets the resolution of the internal density and velocity grid.
A finer grid resolves small features better and produces smoother velocity
fields, but each FFT solve is O(N log N) in the number of grid cells.
Too coarse: small regions may not register in the density field at all, causing the algorithm to stall because the velocity field lacks the spatial detail needed to push them toward their target area.
Too fine: each iteration is slower, and the gain in accuracy diminishes once the grid is finer than the smallest polygon.
A practical starting point: run at grid_size=256 first and inspect the final
error map (column="_morph_error_pct"). If small regions show persistently high
error, increase the grid size. If the result converges quickly and error is
uniform, the grid is sufficient.
# Compute flow cartograms for multiple grid sizes
grid_sizes = [64, 128, 256, 512]
results_grid = {}
for gs in grid_sizes:
opts = flow.MorphOptions(grid_size=gs, n_iter=300, show_progress=False, mean_tol=0.005, max_tol=0.02)
results_grid[gs] = flow.morph_gdf(us_states, column, options=opts)
# Summary: status / iterations / duration / final mean error for all grid sizes
summary_grid = pd.DataFrame([
{
"grid_size": gs,
"status": r.status.value,
"iterations": r.niterations,
"duration_s": round(r.duration, 2),
"time per iteration_ms": round(1000 * r.duration / r.niterations, 1),
"mean_error_pct": round(r.get_errors().mean_error_pct, 2),
}
for gs, r in results_grid.items()
])
display(summary_grid)
| grid_size | status | iterations | duration_s | time per iteration_ms | mean_error_pct | |
|---|---|---|---|---|---|---|
| 0 | 64 | stalled | 39 | 0.28 | 7.3 | 21.30 |
| 1 | 128 | stalled | 69 | 0.17 | 2.4 | 12.97 |
| 2 | 256 | stalled | 119 | 0.36 | 3.0 | 6.19 |
| 3 | 512 | converged | 215 | 0.60 | 2.8 | 0.19 |
fig, axes = plt.subplots(1, 2, figsize=(10, 4), gridspec_kw={"width_ratios": [5, 2]})
# Convergence curves for all grid sizes
for gs, r in results_grid.items():
axes[0].plot(
r.convergence.iterations,
r.convergence.mean_errors_pct,
label=f"grid={gs}" + f" {'✔' if r.status == 'converged' else '✗'}",
)
axes[0].set(title="Convergence by grid size", xlabel="Iteration", ylabel="Mean error (%)")
axes[0].legend()
# Compute time for all grid sizes
_ = summary_grid.plot(
x="grid_size", y="duration_s", kind="line", marker="o", ylabel="duration (s)", legend=False, ax=axes[1]
)
axes[1].set(xticks=grid_sizes, ylim=(0, None))
plt.tight_layout()
# Compare the cartogram results
_plot_error_maps(
results_grid[64],
results_grid[512],
f"grid=64 — {results_grid[64].status.value}",
f"grid=512 — {results_grid[512].status.value}",
)
3. Effect of dt¶
dt scales how far coordinates move in each iteration.
The actual displacement per step is proportional to dt × min(dx, dy),
where dx, dy are the grid cell dimensions.
Large dt (e.g. 0.5): fewer iterations needed to reach the target density,
but large steps can overshoot — a region contracts past its target, the velocity
reverses, and the algorithm oscillates without converging.
Small dt (e.g. 0.05): stable and precise, but many more iterations are
needed to move geometries a meaningful distance.
The default dt=0.2 is a balanced starting point. For smooth, relatively
uniform input data you can increase it; for data with large contrasts
(some regions much smaller / larger than others) reduce it.
Oscillation signal: if the mean error stops decreasing and bounces up and down,
dtmay be too large. Reduce it by half and re-run. Thestatusfield will reportSTALLEDwhenstall_patiencetriggers.
# Compute flow cartograms for multiple dt values
dt_values = [0.05, 0.1, 0.2, 0.3, 0.5]
results_dt = {}
opts_base = flow.MorphOptions(grid_size=512, n_iter=300, show_progress=False, mean_tol=0.005, max_tol=0.02)
for dt in dt_values:
results_dt[dt] = flow.morph_gdf(us_states, column, options=opts_base.copy_with(dt=dt))
# Summary: status / iterations / duration / final mean error for all dt values
summary_dt = pd.DataFrame([
{
"dt": dt,
"status": r.status.value,
"iterations": r.niterations,
"duration_s": round(r.duration, 2),
"time per iteration_ms": round(1000 * r.duration / r.niterations, 1),
"mean_error_pct": round(r.get_errors().mean_error_pct, 2),
}
for dt, r in results_dt.items()
])
display(summary_dt)
| dt | status | iterations | duration_s | time per iteration_ms | mean_error_pct | |
|---|---|---|---|---|---|---|
| 0 | 0.05 | completed | 300 | 0.49 | 1.6 | 89.20 |
| 1 | 0.10 | completed | 300 | 0.50 | 1.7 | 37.10 |
| 2 | 0.20 | converged | 215 | 0.39 | 1.8 | 0.19 |
| 3 | 0.30 | converged | 154 | 0.29 | 1.9 | 0.13 |
| 4 | 0.50 | stalled | 97 | 0.19 | 2.0 | 7.97 |
fig, axes = plt.subplots(1, 2, figsize=(10, 4), gridspec_kw={"width_ratios": [5, 2]})
# Convergence curves for all dt values
for dt, r in results_dt.items():
axes[0].plot(
r.convergence.iterations,
r.convergence.mean_errors_pct,
label=f"dt={dt}" + f" {'✔' if r.status == 'converged' else '✗'}",
)
axes[0].set(title="Convergence by dt", xlabel="Iteration", ylabel="Mean error (%)")
axes[0].legend()
# Compute time for all dt values
_ = summary_dt.plot(x="dt", y="duration_s", kind="line", marker="o", ylabel="duration (s)", legend=False, ax=axes[1])
axes[1].set(xticks=dt_values, ylim=(0, None))
axes[1].set_xticklabels(dt_values, rotation=45)
plt.tight_layout()
# Compare the cartogram results
_plot_error_maps(
results_dt[0.2],
results_dt[0.5],
f"dt=0.2 (default) — {results_dt[0.2].status.value}",
f"dt=0.5 — {results_dt[0.5].status.value}",
)
4. Effect of recompute_every¶
Computing the density and velocity fields (rasterization + FFT solve) is the most expensive
part of each iteration. recompute_every=N skips this on N-1 out of every N
steps, reusing the cached velocity field instead.
recompute_every=1: maximum accuracy — the velocity field always reflects
the current geometry. Slowest per-iteration, but least likely to stall.
recompute_every=10 (default): a good balance. Geometries do not move
much in 10 steps with dt=0.2, so the cached field remains a good approximation.
recompute_every=50+: the velocity field can become stale, especially
early in morphing when geometries are still far from their targets.
The algorithm may stall or oscillate.
Reducing recompute_every is particularly helpful when morphing data with
extreme value contrasts.
# Compute flow cartograms for multiple recompute_every values
recompute_values = [1, 5, 10, 25, 50]
results_rc = {}
opts_base = flow.MorphOptions(grid_size=512, n_iter=300, dt=0.2, show_progress=False, mean_tol=0.005, max_tol=0.02)
for rc in recompute_values:
results_rc[rc] = flow.morph_gdf(us_states, column, options=opts_base.copy_with(recompute_every=rc))
# Summary: status / iterations / duration / final mean error for all recompute_every values
summary_rc = pd.DataFrame([
{
"recompute_every": rc,
"status": r.status.value,
"iterations": r.niterations,
"duration_s": round(r.duration, 2),
"time per iteration_ms": round(1000 * r.duration / r.niterations, 1),
"mean_error_pct": round(r.get_errors().mean_error_pct, 2),
}
for rc, r in results_rc.items()
])
display(summary_rc)
| recompute_every | status | iterations | duration_s | time per iteration_ms | mean_error_pct | |
|---|---|---|---|---|---|---|
| 0 | 1 | converged | 212 | 2.41 | 11.4 | 0.23 |
| 1 | 5 | converged | 212 | 0.60 | 2.8 | 0.12 |
| 2 | 10 | converged | 215 | 0.37 | 1.7 | 0.19 |
| 3 | 25 | converged | 229 | 0.25 | 1.1 | 0.16 |
| 4 | 50 | stalled | 245 | 0.20 | 0.8 | 3.89 |
fig, axes = plt.subplots(1, 2, figsize=(10, 4), gridspec_kw={"width_ratios": [5, 2]})
# Convergence curves for all recompute_every values
for rc, r in results_rc.items():
axes[0].plot(
r.convergence.iterations,
r.convergence.mean_errors_pct,
label=f"recompute_every={rc}" + f" {'✔' if r.status == 'converged' else '✗'}",
)
axes[0].set(title="Convergence by recompute_every", xlabel="Iteration", ylabel="Mean error (%)")
axes[0].legend()
# Compute time for all recompute_every values
_ = summary_rc.plot(
x="recompute_every", y="duration_s", kind="line", marker="o", ylabel="duration (s)", legend=False, ax=axes[1]
)
axes[1].set(xticks=recompute_values, ylim=(0, None))
plt.tight_layout()
# Compare the cartogram results
_plot_error_maps(
results_rc[1],
results_rc[50],
f"recompute_every=1 — {results_rc[1].status.value}",
f"recompute_every=50 — {results_rc[50].status.value}",
)
Oscillations and the recompute_every cycle¶
With the recompute_every option, the velocity field is refreshed only every N
iterations. After the velocity field is computed, the error typically decreases, but
this decline may slow down or even reverse as the cached velocity field no longer
accurately reflects the actual situation. This cycle continues until the velocity
field is recomputed, correcting the accumulated drift. As a result, an oscillatory
pattern emerges in the convergence curve. The default setting for stall_patience is 5,
which will terminate the run if errors increase for more than 5 iterations. If you set
stall_patience to None, this check is disabled, allowing for the full oscillation
pattern to be observed.
The example below illustrates an oscillatory pattern, demonstrating that the algorithm
does not converge. The orange lines indicate each field recomputation. By default, the
function will stop when a stall is detected. However, to visualize the oscillation
phenomenon, stall detection has been disabled (with stall_patience=None).
# Oscillation demo: disable stall detection to see the full recompute_every cycle
result_osc = flow.morph_gdf(
us_states,
column,
options=flow.MorphOptions(
grid_size=128,
n_iter=300,
dt=0.2,
recompute_every=20, # long cycle → visible oscillations
stall_patience=None, # disable stall detection so run completes
show_progress=False,
mean_tol=0.005,
max_tol=0.02,
),
)
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(result_osc.convergence.iterations, result_osc.convergence.mean_errors_pct, color="steelblue")
# mark each field recompute step
for k in range(0, result_osc.niterations, 20):
ax.axvline(k, color="orange", linewidth=0.8, alpha=0.6, label="field recompute" if k == 0 else None)
ax.set(
title="Oscillations tied to recompute_every=20 (stall_patience=None)", xlabel="Iteration", ylabel="Mean error (%)"
)
ax.legend()
plt.tight_layout()
5. A note on how the parameters interact¶
The three parameters are not independent. Understanding their interactions helps you avoid counterproductive combinations.
the effective step size¶
The actual displacement per iteration is computed as:
$$\Delta x = dt \times \frac{\min(dx, dy)}{v_{\max}}$$
where $dx$, $dy$ are the physical size of one grid cell. A finer grid has
smaller cells, so the absolute step size shrinks proportionally.
Doubling grid_size halves the absolute step size, meaning the algorithm
needs roughly twice as many iterations to achieve the same displacement —
even with the same dt.
When convergence is slow or stalling, changing only one of grid_size or dt
(not both simultaneously) is a good diagnostic step. If the error plateaus on a
coarse grid, try increasing grid_size. If it oscillates, reduce dt.
Changing both at once makes it hard to attribute the effect.
velocity field staleness¶
If dt is large, geometries move a lot each step. The density field changes
rapidly, and the cached velocity field becomes a poor guide sooner.
Large dt should be paired with small recompute_every to keep the
velocity field fresh.
Conversely, with a small dt (e.g. after increasing grid_size), geometries
barely move between steps, so the cached field stays accurate for longer.
You can safely increase recompute_every without losing much accuracy,
trading some quality for speed.
6. Multiresolution morphing¶
The three parameters interact: a large grid requires many iterations; a small
dt requires many iterations; frequent recomputation makes each iteration
slower. Choosing parameters for a single run forces you to pay the full cost.
Multiresolution morphing breaks the problem into stages:
- Coarse grid (fast) — large-scale shape rearrangements happen quickly. Convergence may be approximate; the key is to get geometries close to their targets without the expense of a fine grid.
- Finer grid — start from the approximately-morphed geometries of the previous stage. Less work remains, so convergence is fast even on a larger grid.
- Finest grid — polish remaining errors. Typically only a few dozen iterations are needed because coarser stages already did the heavy lifting.
This strategy is analogous to multigrid solvers in numerical analysis: coarse grids propagate information across long distances efficiently; fine grids resolve local detail.
The min_resolution parameter sets the coarsest grid; each subsequent level
doubles the resolution. With min_resolution=128, levels=4 the grids are
128 → 256 → 512 → 1024. The workflow stops as soon as a level converges, so it
may not reach the finest grid — which is the point: the final resolution is
chosen by the tolerances rather than guessed in advance.
Note:
multiresolution_morph()is a convenience wrapper that returns only the final-levelCartogram. To access per-level results and plot per-level convergence histories, useCartogramWorkflowdirectly as shown below.
# A 1024 grid resolves detail about 17 km across, and the morph stretches
# boundary segments as regions expand — roughly fifteenfold on this dataset.
# The bundled geometry is densified to 5 km, which leaves segments too long to
# bend once the grid is this fine, so densify first.
us_states_fine = densify_coverage(us_states, max_segment_length=2000)
# Single resolution: the grid has to be chosen up front, and the right choice
# is not obvious.
opts_single = flow.MorphOptions(n_iter=500, show_progress=False, mean_tol=0.005, max_tol=0.02)
results_single = {
gs: flow.morph_gdf(us_states_fine, column, options=opts_single.copy_with(grid_size=gs)) for gs in (256, 512, 1024)
}
for gs, r in results_single.items():
print(
f"single {gs:>4}: {r.duration:5.2f}s {r.status.value:<10} "
f"{r.niterations:>3} iterations mean error {r.get_errors().mean_error_pct:.2f}%"
)
result_single = results_single[1024]
single 256: 0.18s stalled 119 iterations mean error 6.18% single 512: 0.45s converged 215 iterations mean error 0.19% single 1024: 3.42s converged 423 iterations mean error 0.09%
from carto_flow.flow_cartogram import CartogramWorkflow
# Multiresolution: start coarse and refine until the tolerances are met. The
# workflow stops as soon as a level converges, so the final grid is chosen for
# you rather than guessed.
multi_workflow = CartogramWorkflow(
us_states_fine, column, options=flow.MorphOptions(n_iter=200, mean_tol=0.005, max_tol=0.02, show_progress=False)
)
multi_workflow.morph_multiresolution(min_resolution=128, levels=4)
result_multi = multi_workflow.latest
levels_run = [r for r in multi_workflow if r is not multi_workflow.original]
total_multi_duration = sum(r.duration for r in levels_run)
final_grid = levels_run[-1].grid.sx
print(
f"multi: {total_multi_duration:.2f}s {result_multi.status.value} "
f"stopped at grid {final_grid} after {len(levels_run)} levels"
)
print(f" mean error: {result_multi.get_errors().mean_error_pct:.2f}%")
multi: 0.67s converged stopped at grid 512 after 3 levels mean error: 0.34%
# Summary: every single-resolution choice against the multiresolution run
summary_multi = pd.DataFrame(
[
{
"method": f"single {gs}",
"status": r.status.value,
"iterations": r.niterations,
"duration_s": round(r.duration, 2),
"mean_error_pct": round(r.get_errors().mean_error_pct, 2),
}
for gs, r in results_single.items()
]
+ [
{
"method": f"multi →{final_grid}",
"status": result_multi.status.value,
"iterations": sum(r.niterations for r in levels_run),
"duration_s": round(total_multi_duration, 2),
"mean_error_pct": round(result_multi.get_errors().mean_error_pct, 2),
}
]
)
display(summary_multi)
| method | status | iterations | duration_s | mean_error_pct | |
|---|---|---|---|---|---|
| 0 | single 256 | stalled | 119 | 0.18 | 6.18 |
| 1 | single 512 | converged | 215 | 0.45 | 0.19 |
| 2 | single 1024 | converged | 423 | 3.42 | 0.09 |
| 3 | multi →512 | converged | 95 | 0.67 | 0.34 |
# Convergence comparison: stack per-level curves on a single axis
fig, axes = plt.subplots(1, 2, figsize=(9, 4), gridspec_kw={"width_ratios": (5, 1)})
# Single-resolution curve
axes[0].plot(
result_single.convergence.iterations,
result_single.convergence.mean_errors_pct,
color="gray",
label=f"single {result_single.grid.sx} ({result_single.duration:.1f}s)",
)
# Multiresolution: one curve per level, offset on x-axis
offset = 0
colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"]
level_idx = 0
for level_result in multi_workflow:
if level_result is multi_workflow.original:
continue # skip unmorphed state
iters = level_result.convergence.iterations + offset
gs = level_result.grid.sx # actual grid width used
color = colors[level_idx % len(colors)]
axes[0].plot(
iters,
level_result.convergence.mean_errors_pct,
color=color,
label=f"level {level_idx + 1}: grid={gs} ({level_result.duration:.1f}s)",
)
axes[0].axvline(offset, color=color, lw=1, alpha=0.4)
offset += level_result.niterations
level_idx += 1
axes[0].set(
title="Convergence: single-resolution vs multi-resolution", xlabel="Cumulative iteration", ylabel="Mean error (%)"
)
axes[0].legend()
# Compute time
_ = summary_multi.plot(x="method", y="duration_s", kind="bar", ylabel="duration (s)", legend=False, ax=axes[1])
axes[1].set(ylim=(0, None))
plt.tight_layout()
# Compare the cartogram results
total_multi_duration = sum(r.duration for r in multi_workflow if r is not multi_workflow.original)
_plot_error_maps(
result_single,
result_multi,
f"Single-resolution {result_single.grid.sx}\n{result_single.duration:.1f}s, {result_single.status.value}",
f"Multiresolution (128→{final_grid})\n{total_multi_duration:.1f}s, {result_multi.status.value}",
figsize=(14, 5),
)
When the grid choice is the whole problem¶
On the states the multiresolution run matches a single run at the grid it settles on — its value there is that it finds that grid, rather than being faster than a well-chosen one.
On a harder input the difference is larger. Congressional districts have 432 regions instead of 49, so the density field carries far more small-scale structure and a single run has to resolve all of it at once.
districts = densify_coverage(
examples.load_us_census(level="congressional_district", population=True, contiguous_only=True),
max_segment_length=2000,
)
opts_d = flow.MorphOptions(n_iter=500, show_progress=False, mean_tol=0.005, max_tol=0.02)
results_single_d = {
gs: flow.morph_gdf(districts, "Population", options=opts_d.copy_with(grid_size=gs)) for gs in (512, 1024)
}
rows = []
for gs, r in results_single_d.items():
rows.append({
"method": f"single {gs}",
"status": r.status.value,
"iterations": r.niterations,
"duration_s": round(r.duration, 2),
"mean_error_pct": round(r.get_errors().mean_error_pct, 2),
})
wf_d = CartogramWorkflow(
districts,
"Population",
options=flow.MorphOptions(n_iter=200, mean_tol=0.005, max_tol=0.02, show_progress=False),
)
wf_d.morph_multiresolution(min_resolution=128, levels=4)
levels_d = [r for r in wf_d if r is not wf_d.original]
total_multi_duration_d = sum(r.duration for r in levels_d)
rows.append({
"method": f"multi →{levels_d[-1].grid.sx}",
"status": wf_d.latest.status.value,
"iterations": sum(r.niterations for r in levels_d),
"duration_s": round(total_multi_duration_d, 2),
"mean_error_pct": round(wf_d.latest.get_errors().mean_error_pct, 2),
})
display(pd.DataFrame(rows))
/home/runner/work/carto-flow/carto-flow/src/carto_flow/flow_cartogram/workflow.py:259: UserWarning: Input geometries contain a straight segment of length 99660.5 (grid units), which exceeds 4 grid cells (17008.6 each). Segments with no interior vertices cannot bend during the morph and may prevent convergence. Consider densifying the input with carto_flow.geo_utils.densify_coverage before morphing. result = morph_geometries(
| method | status | iterations | duration_s | mean_error_pct | |
|---|---|---|---|---|---|
| 0 | single 512 | stalled | 270 | 1.15 | 2.38 |
| 1 | single 1024 | completed | 500 | 3.72 | 2.67 |
| 2 | multi →512 | converged | 199 | 0.86 | 0.06 |
Neither single-resolution run reaches the tolerances: 512 stalls and 1024 exhausts its iteration budget. The multiresolution run converges, and does so in less time than either. Starting coarse removes the large-scale error cheaply, so the fine level has little left to do.
# Where the error sits: the best single-resolution attempt against multiresolution
_single_d = results_single_d[1024]
_plot_error_maps(
_single_d,
wf_d.latest,
f"Single-resolution 1024\n{_single_d.duration:.1f}s, {_single_d.status.value}",
f"Multiresolution (128→{levels_d[-1].grid.sx})\n{total_multi_duration_d:.1f}s, {wf_d.latest.status.value}",
figsize=(14, 5),
)
7. Choosing parameters in practice¶
The status field tells you what happened and what to do next:
CONVERGED— tolerances met; result is accurateCOMPLETED— iteration limit reached; try increasingn_iteror adjusting parametersSTALLED— error started growing; reducedtorrecompute_every, or increasegrid_size
When diagnosing convergence problems, change one parameter at a time
- Error plateaus at high value → try a finer
grid_size - Error oscillates / STALLED → increase
grid_sizeor reducedt - Converges but slowly → use multiresolution
Additional Resources¶
- API Reference: morph_gdf
- API Reference: MorphOptions
- API Reference: Cartogram object
- API Reference: CartogramWorkflow
This notebook is part of the carto-flow documentation. Report issues at https://github.com/bright-fakl/carto-flow/issues