carto_flow.flow_cartogram.anisotropy¶
Velocity modulation system for cartogram algorithms.
This module provides a comprehensive system for velocity field modulation, supporting boundary decay, smoothing, and anisotropy transformations. Distance and sigma parameters are specified in world/data-coordinate units (same CRS as the input geometries) and converted to pixel space automatically at call time, so modulators work consistently across grid resolutions.
Classes:
-
VelocityModulator–Base class for velocity field modulators.
-
BoundaryDecay–Distance-based multiplicative falloff applied to all velocity components near geometry boundaries.
-
BoundaryNormalDecay–Damps only the boundary-normal velocity component while preserving tangential flow near geometry boundaries.
-
DirectionalTensor–Anisotropy aligned with a direction field (uniform angle, callable, or raster). Convenience constructors:
radial,tangential,from_seeds. -
LocalizedTensor–Anisotropy from seed points with Gaussian spatial influence; blends toward identity where seeds don't reach.
-
Smooth–Gaussian smoothing of velocity field.
-
Multiplicative–Multiplicative velocity modulation.
-
Tensor–Low-level 2x2 tensor-based velocity modulation.
-
Pipeline–Sequence of modulators applied in order.
Functions:
-
apply_anisotropy_tensor–Apply anisotropy tensor to velocity field components.
-
build_axis_aligned_tensor–Construct axis-aligned anisotropy field with optional rotation.
Examples:
>>> from carto_flow.flow_cartogram.anisotropy import DirectionalTensor, BoundaryDecay, BoundaryNormalDecay, Smooth
>>> import numpy as np
>>>
>>> # Uniform 30° tilt
>>> DirectionalTensor(theta=np.pi / 6, Dpar=2.0, Dperp=0.5)
>>>
>>> # Radially outward from a fixed centre
>>> DirectionalTensor.radial(center=(500_000, 200_000), Dpar=2.0)
>>>
>>> # Tangential (counter-clockwise vortex)
>>> DirectionalTensor.tangential(center=(500_000, 200_000), Dpar=2.0)
>>>
>>> # Direction field from control points, blended with boundary decay
>>> seeds = [(1e5, 2e5, 0), (4e5, 5e5, np.pi / 2)]
>>> mod = DirectionalTensor.from_seeds(seeds, Dpar=3.0) + BoundaryDecay(decay_length=5)
BoundaryDecay
¶
BoundaryDecay(
decay_length: float = 3.0,
damping_floor: float = 0.0,
smooth: float | None = None,
outside_decay: float | None = None,
)
Bases: VelocityModulator
Distance-based multiplicative velocity falloff near geometry boundaries.
Multiplies the entire velocity field by a smooth spatial mask that
transitions from damping_floor at the outer boundary to 1 deep in
the interior over decay_length world-coordinate units. Outside
cells decay from damping_floor to 0 over outside_decay units.
This is the simpler of the two boundary-decay modulators: it treats
all velocity components identically. For a geometrically aware variant
that preserves tangential flow, use :class:BoundaryNormalDecay.
Parameters:
-
decay_length(float, default:3.0) –Width of the inside transition zone in world/data-coordinate units. Controls how quickly the velocity recovers from
damping_floorto 1 moving inward from the boundary. -
damping_floor(float, default:0.0) –Velocity factor applied exactly at the outer boundary; in [0, 1). A small positive value (e.g. 0.1–0.3) prevents complete freezing of boundary vertices and avoids jagged edges.
-
smooth(float or None, default:None) –Optional Gaussian smoothing sigma applied to the signed distance field before computing the falloff, in world-coordinate units. Smoothing blurs the boundary mask, creating a softer transition.
-
outside_decay(float or None, default:None) –Width of the outside decay zone in world-coordinate units. If
None, uses the same value asdecay_length. Smaller values suppress outside-cell drift more aggressively while leaving the interior unaffected.
Examples:
>>> BoundaryDecay(decay_length=5, damping_floor=0.1)
>>> BoundaryDecay(decay_length=10) + Smooth(sigma=2)
Methods:
-
__call__–Apply distance-based multiplicative falloff to velocity field.
__call__
¶
Apply distance-based multiplicative falloff to velocity field.
BoundaryNormalDecay
¶
BoundaryNormalDecay(
decay_length: float = 3.0,
damping_floor: float = 0.0,
smooth: float | None = None,
renormalize: bool = False,
)
Bases: VelocityModulator
Damp the boundary-normal velocity component while preserving tangential flow.
Near the outer boundary the velocity field is decomposed into a
component normal to the boundary and a tangential component.
The normal component is exponentially damped over decay_length
world-coordinate units; the tangential component is left unchanged.
This suppresses the outward drift responsible for boundary distortion
without affecting flow parallel to the boundary.
Parameters:
-
decay_length(float, default:3.0) –Length scale over which the normal velocity component is damped, in world/data-coordinate units.
-
damping_floor(float, default:0.0) –Minimum damping factor at the boundary; in [0, 1]. At distance
dfrom the boundary, the normal factor isfloor + (1 - floor) * (1 - exp(-d / decay_length)). -
smooth(float or None, default:None) –If not
None, applies Gaussian smoothing to the signed distance field before computing boundary normals, in world-coordinate units. Useful to reduce noise in the normal direction for complex geometries. -
renormalize(bool, default:False) –If
True, the output velocity is rescaled to preserve the original magnitude at each grid cell. This keeps the speed of flow constant while only rotating the direction.
Examples:
>>> BoundaryNormalDecay(decay_length=5)
>>> BoundaryNormalDecay(decay_length=3, damping_floor=0.1, smooth=1.0)
Methods:
-
__call__–Damp the normal velocity component near geometry boundaries.
__call__
¶
Damp the normal velocity component near geometry boundaries.
DirectionalTensor
¶
Bases: VelocityModulator
Anisotropic velocity modulation aligned with a direction field.
Amplifies velocity along a preferred direction (Dpar) and optionally
suppresses the perpendicular component (Dperp). Works correctly at any
grid resolution: the rotation tensor is built lazily on the first call at
each grid shape and cached for all subsequent iterations at that resolution.
Parameters:
-
theta(float | callable | ndarray) –Preferred flow direction in radians (0 = positive x-axis, π/2 = positive y-axis).
- float: uniform angle applied everywhere.
- callable
(grid) -> (ny, nx) array: evaluated once per grid shape. The callable receives the current :class:Gridobject, giving access togrid.X,grid.Y,grid.shape, etc. - ndarray: pre-computed angle field; automatically interpolated to the current grid shape with bilinear interpolation if shapes differ.
-
Dpar(float, default:2.0) –Amplification factor along the preferred direction.
-
Dperp(float, default:1.0) –Amplification factor perpendicular to the preferred direction.
Examples:
Uniform 45° tilt with double amplification along that axis:
Flow aligned radially outward from the domain centre:
>>> DirectionalTensor(
... theta=lambda g: np.arctan2(g.Y - g.Y.mean(), g.X - g.X.mean()),
... Dpar=2.0,
... )
Direction field from an external raster (e.g. slope aspect) — auto-resized:
Methods:
-
__call__–Apply directional tensor modulation, using a cached tensor when possible.
-
from_seeds–Build a direction field from a sparse set of (x, y, θ) control points.
-
radial–Flow aligned radially outward from (or inward toward) a point.
-
tangential–Flow aligned tangentially around a point (counter-clockwise by default).
__call__
¶
Apply directional tensor modulation, using a cached tensor when possible.
from_seeds
classmethod
¶
from_seeds(
seeds,
Dpar: float = 2.0,
Dperp: float = 1.0,
power: float = 2.0,
) -> DirectionalTensor
Build a direction field from a sparse set of (x, y, θ) control points.
The direction at each grid cell is computed as the inverse-distance-weighted
(IDW) average of the seed angles. Angle averaging is circular: each seed
contributes (cos θ, sin θ) proportional to its weight, and the
resultant angle is recovered with arctan2.
Parameters:
-
seeds(array-like of shape (n, 3)) –Each row is
(x, y, theta)wherex, yare data coordinates andthetais the preferred flow direction in radians. -
Dpar(float, default:2.0) –Amplification along the preferred direction.
-
Dperp(float, default:1.0) –Amplification perpendicular to the preferred direction.
-
power(float, default:2.0) –IDW distance exponent. Higher values give more localised influence (1 = linear falloff, 2 = classic IDW, ≥3 = near-Voronoi).
Examples:
radial
classmethod
¶
radial(
center=None,
Dpar: float = 2.0,
Dperp: float = 1.0,
inward: bool = False,
) -> DirectionalTensor
Flow aligned radially outward from (or inward toward) a point.
Parameters:
-
center((float, float) or None, default:None) –(x, y)of the origin in data coordinates. IfNone, the centroid of the grid bounding box is used (evaluated lazily). -
Dpar(float, default:2.0) –Amplification along the radial direction.
-
Dperp(float, default:1.0) –Amplification tangential to the radial direction.
-
inward(bool, default:False) –If
True, preferred direction points toward center instead of away from it.
Examples:
tangential
classmethod
¶
tangential(
center=None,
Dpar: float = 2.0,
Dperp: float = 1.0,
clockwise: bool = False,
) -> DirectionalTensor
Flow aligned tangentially around a point (counter-clockwise by default).
Parameters:
-
center((float, float) or None, default:None) –(x, y)of the rotation origin in data coordinates. IfNone, the centroid of the grid bounding box is used. -
Dpar(float, default:2.0) –Amplification along the tangential direction.
-
Dperp(float, default:1.0) –Amplification along the radial direction.
-
clockwise(bool, default:False) –If
True, the preferred rotation direction is clockwise.
Examples:
LocalizedTensor
¶
Bases: VelocityModulator
Velocity modulation from localized seed points with Gaussian influence.
Each seed defines a preferred flow direction and amplification at a
specific location. Influence decays as a Gaussian whose shape is
controlled by sigma. Where seeds do not reach (total Gaussian
weight < 1) the field blends toward the identity — isotropic, velocity
unchanged.
Parameters:
-
seeds(list of dict or list of tuple) –Each element describes one seed. Dict keys:
x,y: location in data coordinates (required)theta: preferred flow direction in radians, 0 = +x axis. Required unlesssigmais a (2, 2) covariance matrix, in which case it is derived from the matrix's major eigenvector.-
sigma: influence zone — three forms accepted: -
float — isotropic circle of radius σ
- array-like (σ_par, σ_perp) — ellipse aligned with
theta; σ_par along the flow direction, σ_perp across it.thetais required. -
(2, 2) array Σ — full covariance matrix;
thetamay be omitted (derived from the major eigenvector of Σ) or specified independently -
Dpar,Dperp: optional per-seed amplification overrides
Tuple form
(x, y, theta, sigma)or(x, y, theta, sigma, Dpar, Dperp)is also accepted; the tuple form always requiresthetaandsigmamust be a scalar. -
default_Dpar(float, default:2.0) –Amplification along preferred direction for seeds that omit
Dpar. -
default_Dperp(float, default:1.0) –Amplification perpendicular to preferred direction for seeds that omit
Dperp.
Examples:
Isotropic influence zone:
Elliptical zone aligned with flow (3× wider along flow than across):
>>> LocalizedTensor([
... dict(x=500_000, y=200_000, theta=np.pi/4,
... sigma=(120_000, 40_000), Dpar=3.0, Dperp=0.5),
... ])
Full covariance; theta derived from the major eigenvector:
>>> theta = np.pi / 4
>>> c, s = np.cos(theta), np.sin(theta)
>>> Sigma = (120_000**2 * np.outer([c, s], [c, s])
... + 40_000**2 * np.outer([-s, c], [-s, c]))
>>> LocalizedTensor([dict(x=500_000, y=200_000, sigma=Sigma, Dpar=3.0)])
Methods:
-
__call__–Apply localised tensor modulation, using cached arrays when possible.
__call__
¶
Apply localised tensor modulation, using cached arrays when possible.
Multiplicative
¶
Bases: VelocityModulator
Element-wise scaling of velocity components.
Multiplies vx by fx and vy by fy independently.
Each factor can be a scalar, a pre-computed (ny, nx) array, or a
callable (grid) -> (ny, nx) array evaluated lazily at call time.
Parameters:
-
fx(float or ndarray or callable) –Scaling factor for the x-component of velocity.
-
fy(float or ndarray or callable) –Scaling factor for the y-component of velocity.
Examples:
Suppress horizontal flow everywhere:
Spatially varying x-suppression based on grid position:
Methods:
-
__call__–Apply multiplicative modulation to velocity field.
__call__
¶
Apply multiplicative modulation to velocity field.
Pipeline
¶
Bases: VelocityModulator
Sequence of velocity modulators applied left-to-right.
Constructed automatically when two modulators are combined with +.
Further + calls append to the same pipeline rather than nesting
pipelines::
pipe = BoundaryDecay() + Smooth(sigma=2)
Methods:
Smooth
¶
Bases: VelocityModulator
Gaussian smoothing of the velocity field.
Convolves both velocity components with an isotropic Gaussian kernel. Smoothing reduces high-frequency oscillations and can improve convergence, at the cost of some sharpness in the final cartogram.
Parameters:
-
sigma(float, default:3.0) –Standard deviation of the Gaussian kernel in world/data-coordinate units. Converted to pixels automatically at call time.
Examples:
Methods:
-
__call__–Apply Gaussian smoothing to velocity field.
__call__
¶
Apply Gaussian smoothing to velocity field.
Tensor
¶
Bases: VelocityModulator
Low-level 2×2 matrix transform of the velocity field.
Applies [vx', vy'] = A @ [vx, vy] pointwise, where A has
components Axx, Axy, Ayx, Ayy. Each component can be
a scalar, a pre-computed (ny, nx) array, or a callable
(grid) -> (ny, nx) array.
This is the most general velocity modulator; higher-level classes such
as :class:DirectionalTensor and :class:LocalizedTensor construct
their tensors automatically from geometric parameters.
Parameters:
-
Axx(float or ndarray or callable) –Components of the 2×2 transform matrix. The identity is
Axx=Ayy=1, Axy=Ayx=0. -
Axy(float or ndarray or callable) –Components of the 2×2 transform matrix. The identity is
Axx=Ayy=1, Axy=Ayx=0. -
Ayx(float or ndarray or callable) –Components of the 2×2 transform matrix. The identity is
Axx=Ayy=1, Axy=Ayx=0. -
Ayy(float or ndarray or callable) –Components of the 2×2 transform matrix. The identity is
Axx=Ayy=1, Axy=Ayx=0.
Examples:
Uniform 45° rotation:
>>> import numpy as np
>>> c, s = np.cos(np.pi / 4), np.sin(np.pi / 4)
>>> Tensor(Axx=c, Axy=-s, Ayx=s, Ayy=c)
Methods:
-
__call__–Apply tensor modulation to velocity field.
__call__
¶
Apply tensor modulation to velocity field.
VelocityModulator
¶
Abstract base class for velocity field modulators.
Subclasses implement __call__(vx, vy, grid, mask) and return
(vx_new, vy_new). Modulators can be chained with + into a
:class:Pipeline that applies them left-to-right::
mod = BoundaryDecay(decay_length=5) + Smooth(sigma=2)
Methods:
apply_anisotropy_tensor
¶
Apply anisotropy tensor to velocity field components.
Axx etc are (ny,nx) arrays. Compute: vx' = Axxvx + Axyvy vy' = Ayxvx + Ayyvy
Parameters:
-
vx(ndarray) –Velocity field components with shape (ny, nx)
-
vy(ndarray) –Velocity field components with shape (ny, nx)
-
Axx(ndarray) –Anisotropy tensor components with shape (ny, nx)
-
Axy(ndarray) –Anisotropy tensor components with shape (ny, nx)
-
Ayx(ndarray) –Anisotropy tensor components with shape (ny, nx)
-
Ayy(ndarray) –Anisotropy tensor components with shape (ny, nx)
Returns:
-
vx_new, vy_new : np.ndarray–Transformed velocity field components
build_axis_aligned_tensor
¶
Build axis-aligned anisotropy field (scalar Dx, Dy) optionally rotated by theta(x,y).
Returns Axx, Axy, Ayx, Ayy arrays. If theta_field is None, axis-aligned (no rotation). theta_field shape (ny,nx) in radians if provided.
Parameters:
-
nx(int) –Grid dimensions (x, y)
-
ny(int) –Grid dimensions (x, y)
-
Dx(float, default:1.0) –Anisotropy scaling factors in x and y directions
-
Dy(float, default:1.0) –Anisotropy scaling factors in x and y directions
-
theta_field(ndarray, default:None) –Rotation field with shape (ny, nx) in radians
Returns:
-
Axx, Axy, Ayx, Ayy : np.ndarray–Anisotropy tensor components
preview_modulator
¶
preview_modulator(
modulator: VelocityModulator | None = None,
gdf=None,
grid_size: int = 64,
skip: int = 4,
input_angle: float = 0.0,
margin: float = 0.1,
show_geometry: bool = True,
ax=None,
cmap="viridis",
show_colorbar: bool = True,
arrows_kwargs: dict | None = None,
colorbar_kwargs: dict | None = None,
values=None,
column: str | None = None,
Dx: float = 1.0,
Dy: float = 1.0,
show_vectors: bool | str | tuple = "output",
input_arrows_kwargs: dict | None = None,
diff_cmap: str = "Reds",
diff_arrows_kwargs: dict | None = None,
diff_colorbar_kwargs: dict | None = None,
heatmap: str | None = None,
heatmap_type: str = "magnitude",
heatmap_cmap: str | None = None,
heatmap_kwargs: dict | None = None,
heatmap_colorbar_kwargs: dict | None = None,
heatmap_alpha_from_magnitude: bool = False,
arrow_scale: float = 1.0,
)
Preview a velocity modulator on a uniform probe field.
Applies the modulator to a spatially uniform input field and plots the
result as a quiver diagram. Arrow colour encodes the local amplification
factor (output magnitude ÷ input magnitude = 1.0 for identity). For
:class:LocalizedTensor, an optional background heatmap shows the total
Gaussian seed weight (how much each location is dominated by seeds vs.
the isotropic background).
The geometry mask passed to the modulator is rasterized from gdf, so
modulators that use boundary proximity (e.g. :class:BoundaryDecay) work
correctly in the preview.
Parameters:
-
modulator(VelocityModulator, default:None) –The modulator to preview.
-
gdf(GeoDataFrame, default:None) –Geometries used to derive the spatial extent and the geometry mask passed to the modulator.
-
grid_size(int, default:64) –Number of grid cells along the longer axis. Kept small for a fast, clutter-free preview.
-
skip(int, default:4) –Plot every skip-th arrow to reduce clutter.
-
input_angle(float, default:0.0) –Direction of the uniform probe field in radians (0 = +x = east). Change this to see how the modulator responds to a different incoming flow direction.
-
margin(float, default:0.05) –Fractional margin added around the bounds before building the grid, matching the convention used by the real algorithm.
-
show_geometry(bool, default:True) –When
True, draw the geometry outlines as a light background layer. -
show_colorbar(bool, default:True) –When
False, the colorbar for the arrows is omitted. Useful when embedding the preview in a figure that provides its own colorbar. -
ax(Axes, default:None) –Axes to draw on. A new figure is created if
None. -
values(array - like, default:None) –Per-geometry values (same length as gdf) used to compute a realistic density field and velocity field via the FFT Poisson solver — identical to what :func:
morph_geometriescomputes internally. When provided, the probe field is the normalised velocity field derived from those values rather than a uniform field. Useful for modulators that depend on the spatial structure of the field. -
column(str, default:None) –Column name in gdf to use as values. Equivalent to passing
values=gdf[column].to_numpy(). Takes precedence over values if both are given. -
Dx(float, default:1.0) –Anisotropic diffusion factor in x passed to :class:
VelocityComputerFFTWwhen computing the density-based velocity field. -
Dy(float, default:1.0) –Anisotropic diffusion factor in y passed to :class:
VelocityComputerFFTW. -
show_vectors(bool or str or tuple, default:'output') –Controls which velocity quiver layers are drawn:
'output'— only the modulated output field (default).True— input + output (comparison view).FalseorNone— no quivers (useful when only a heatmap is wanted).'input'or'diff'— only that single layer.- tuple, e.g.
('output', 'diff')— any combination.
-
input_arrows_kwargs(dict, default:None) –Extra keyword arguments for the input quiver
ax.quivercall. -
diff_cmap(str or Colormap, default:'Reds') –Colormap for the difference quiver.
-
diff_arrows_kwargs(dict, default:None) –Extra keyword arguments for the difference quiver
ax.quivercall. -
arrow_scale(float, default:1.0) –Length of the longest output arrow as a fraction of the skipped-cell diagonal (
sqrt((dx*skip)² + (dy*skip)²)). All quivers share this scale so arrows are directly comparable. Values < 1 add a gap between adjacent arrows; values > 1 allow overlap. -
cmap(str or Colormap, default:'viridis') –Colormap for the quiver arrows, encoding amplification factor.
-
arrows_kwargs(dict, default:None) –Extra keyword arguments merged into the
ax.quivercall for the modulated-velocity arrows. Override any of the defaults (cmap,pivot,zorder, …). -
colorbar_kwargs(dict, default:None) –Extra keyword arguments merged into the
plt.colorbarcall (e.g.label,shrink,pad). -
heatmap((weight, input, output, diff), default:'weight') –Optional background heatmap overlay:
'weight'— Gaussian seed-weight field for :class:LocalizedTensor(replaces the old show_weight parameter).'input','output','diff'— scalar reduction of the corresponding velocity field controlled by heatmap_type.
-
heatmap_type((magnitude, angle, magnitude_diff, angle_diff), default:'magnitude') –How to reduce the 2-D vector field to a scalar for the heatmap:
'magnitude'—‖field‖; valid for all heatmap modes.'angle'— direction of the field vector in [−π, π]; valid for all heatmap modes.'magnitude_diff'—‖v_out‖ − ‖v_in‖; signed speed change. Positive where the modulator amplified speed, negative where it suppressed it. Uses a divergent colormap. Only valid withheatmap='diff'.'angle_diff'—angle(v_out) − angle(v_in)wrapped to [−π, π]; how much the modulator rotated the flow direction. Uses a cyclic colormap ('twilight_shifted') so that −π and +π (both a 180° flip) have the same colour. Only valid withheatmap='diff'.
-
heatmap_alpha_from_magnitude(bool, default:False) –When
Trueandheatmap_type='angle', the imshow alpha channel is set to the normalised velocity magnitude so regions with near-zero velocity become transparent. Has no effect forheatmap='weight'.
Returns:
-
ModulatorPreviewResult–Named container with all produced artists:
ax— the axesarrows— the :class:~matplotlib.quiver.Quiverartistcolorbar— :class:~matplotlib.colorbar.Colorbarfor arrowsgeometry_collections— list of collections fromgdf.plot(); empty when show_geometry isFalseweight_image— :class:~matplotlib.image.AxesImagefor the seed-weight heatmap, orNoneinput_arrows— :class:~matplotlib.quiver.Quiverfor the pre-modulation arrows, orNonediff_arrows— :class:~matplotlib.quiver.Quiverfor the difference field, orNone
Examples: