Skip to main content

Neutral Landscape Models — fast native implementation with Python bindings

Project description

NLMrs

A Rust crate for building Neutral Landscape Models.

Inspired by nlmpy and nlmr.

Usage

NLMrs can be installed as a Rust crate, but language bindings also exist for Python, R, WASM and C.

Installation

cargo add nlmrs

Example

use nlmrs;

fn main() {
    let arr = nlmrs::midpoint_displacement(10, 10, 1.);
    println!("{:?}", arr);
}

Export

The export module holds a collection of user-friendly functions to export your 2D NLM vector.

use nlmrs::{distance_gradient, export};

fn main() {
    let arr = distance_gradient(50, 50);
    export::write_to_csv(arr, "./data/data.csv");
}

Algorithms

Gradient

Deterministic spatial fields derived from direction, distance, or position.

Planar Gradient

planar_gradient(rows: 100, cols: 100, direction: 45.0, seed: 42)

Linear ramp at a given direction angle, increasing uniformly from 0 to 1 across the grid.

Edge Gradient

edge_gradient(rows: 100, cols: 100, direction: 45.0, seed: 42)

Symmetric version of the planar gradient: values peak at 1.0 along the central axis and fall to 0.0 at both edges.

Distance Gradient

distance_gradient(rows: 100, cols: 100, seed: 42)

Euclidean distance transform from random seed cells, producing a smooth radial falloff from 0 at the seeds outward to 1 at the most distant point.

Wave Gradient

wave_gradient(rows: 100, cols: 100, period: 3.0, seed: 42)

Sinusoidal wave oriented at a given direction angle, cycling repeatedly from 0 to 1 and back at the specified period.

Noise

Continuous stochastic fields, from single-layer lattice noise to multi-octave fractal composites.

Perlin Noise

perlin_noise(rows: 100, cols: 100, scale: 4.0, seed: 42)

Smooth gradient noise built from dot products of random gradient vectors at lattice points, producing continuous, natural-looking variation.

Source: Perlin (1985)

Value Noise

value_noise(rows: 100, cols: 100, scale: 4.0, seed: 42)

Interpolated lattice noise that is smoother and more rounded than Perlin noise.

Worley Noise

worley_noise(rows: 100, cols: 100, scale: 4.0, seed: 42)

Cell noise built from distances to random feature points, producing cellular, cracked-earth, or mosaic-like patterns.

Source: Worley (1996)

Gaussian Field

gaussian_field(rows: 100, cols: 100, sigma: 10.0, seed: 42)

White noise smoothed by a Gaussian blur kernel with standard deviation sigma, producing spatially correlated fields where patch size scales directly with sigma.

fBm Noise

fbm_noise(rows: 100, cols: 100, scale: 4.0, octaves: 6, seed: 42)

Fractal Brownian motion layers multiple octaves of Perlin noise for more natural-looking terrain detail.

Source: Mandelbrot & Van Ness (1968); Voss (1985)

Ridged Noise

ridged_noise(rows: 100, cols: 100, scale: 4.0, octaves: 6, seed: 42)

Multi-octave noise where each octave is inverted and folded, producing sharp mountain ridges and valleys.

Source: Musgrave, Kolb & Mace (1989)

Billow Noise

billow_noise(rows: 100, cols: 100, scale: 4.0, octaves: 6, seed: 42)

Multi-octave noise with absolute-value folding applied before accumulation, producing rounded billowing clouds or rolling dune shapes.

Source: Ebert et al., Texturing and Modeling: A Procedural Approach (2002)

Hybrid Noise

hybrid_noise(rows: 100, cols: 100, scale: 4.0, octaves: 6, seed: 42)

Hybrid multifractal noise combines fBm-style layering with a multiplicative weighting that amplifies high-frequency detail near peaks.

Source: Musgrave, Kolb & Mace (1989)

Turbulence

turbulence(rows: 100, cols: 100, scale: 4.0, octaves: 6, seed: 42)

fBm with absolute-value folding per octave, producing sharp ridges and a storm-cloud appearance.

Source: Perlin (1985)

Domain Warp

domain_warp(rows: 100, cols: 100, scale: 4.0, warp_strength: 1.0, seed: 42)

Perlin noise sampled at coordinates displaced by a second Perlin field, producing organic swirling patterns.

Source: Quilez (2002)

Spectral Synthesis

spectral_synthesis(rows: 100, cols: 100, beta: 2.0, seed: 42)

Generates correlated noise in the frequency domain by scaling each component's amplitude by f^(-beta/2), giving a power spectrum proportional to 1/f^beta. Higher beta produces smoother, more spatially correlated landscapes.

Source: Peitgen & Saupe (1988)

Simplex Noise

simplex_noise(rows: 100, cols: 100, scale: 4.0, seed: 42)

An open-source alternative to Perlin noise with fewer directional artefacts.

Voronoi Distance

voronoi_distance(rows: 100, cols: 100, n: 50, seed: 42)

Scatters n random feature points across the grid and fills each cell with the Euclidean distance to the nearest point, producing smooth conical gradients centred on each point.

Sine Composite

sine_composite(rows: 100, cols: 100, waves: 8, seed: 42)

Superposes waves sinusoidal plane waves, each with a random orientation, frequency, and phase. The interference of multiple waves produces standing-wave patterns whose complexity grows with the number of waves.

Curl Noise

curl_noise(rows: 100, cols: 100, scale: 4.0, seed: 42)

Computes the curl (gradient rotated 90 degrees) of a Perlin potential field using finite differences, producing a divergence-free velocity field. Sample coordinates of a second Perlin generator are warped by this field, yielding swirling, flow-aligned patterns without directional clumping.

Patch

Discrete spatial patterns built from random processes, clustering, or hierarchical partitioning.

Random

random(rows: 100, cols: 100, seed: 42)

Independent uniform random values at each cell, with no spatial structure.

Percolation

percolation(rows: 100, cols: 100, p: 0.55, seed: 42)

Binary Bernoulli lattice where each cell is independently set to 1 with probability p, producing binary habitat maps. The critical percolation threshold for 4-connectivity is approximately 0.593.

Source: Gardner et al. (1987)

Random Element

random_element(rows: 100, cols: 100, n: 5000, seed: 42)

Places n labelled seed cells at random positions, then fills all remaining cells with the value of the nearest seed using nearest-neighbour interpolation.

Source: Etherington, Holland & O'Sullivan (2015)

Mosaic

mosaic(rows: 100, cols: 100, n: 300, seed: 42)

Discrete Voronoi map where each region is a flat colour determined by its nearest seed point, producing a stained-glass or territory effect.

Random Cluster

random_cluster(rows: 100, cols: 100, n: 200, seed: 42)

Applies n random fault-line cuts across the grid, accumulating the field on each side, then rescales. Produces spatially clustered landscapes with the linear structural elements characteristic of geological fault patterns.

Source: Saura & Martínez-Millán (2000)

Rectangular Cluster

rectangular_cluster(rows: 100, cols: 100, n: 300, seed: 42)

Overlapping random axis-aligned rectangles accumulated and scaled, producing blocky clustered patches.

Binary Space Partitioning

binary_space_partitioning(rows: 100, cols: 100, n: 200, seed: 42)

Hierarchical rectilinear partition: the largest rectangle is repeatedly split along its longest dimension until n leaf regions remain, each assigned a random value. Produces structured blocky landscapes.

Source: Etherington, Morgan & O'Sullivan (2022)

Neighbourhood Clustering

neighbourhood_clustering(rows: 100, cols: 100, k: 5, iterations: 10, seed: 42)

Initialises a grid with k random classes then repeatedly applies a majority-vote rule: each cell adopts the most common class in its 3×3 Moore neighbourhood. More iterations produce larger, smoother organic patches.

Cellular Automaton

cellular_automaton(rows: 100, cols: 100, p: 0.45, iterations: 5, seed: 42)

Random binary grid evolved by Conway-style birth/survival rules: a dead cell is born if it has at least birth_threshold live neighbours; a live cell survives if it has at least survival_threshold. Produces cave-like binary landscapes.

Reaction-Diffusion

reaction_diffusion(rows: 100, cols: 100, iterations: 1000, feed: 0.055, kill: 0.062, seed: 42)

Gray-Scott reaction-diffusion model where two chemicals (A and B) diffuse and react across the grid. Different feed/kill combinations produce spots, stripes, labyrinths, and other Turing-pattern morphologies.

Eden Growth

eden_growth(rows: 100, cols: 100, n: 2000, seed: 42)

Compact cluster grown from the grid centre by randomly selecting a boundary cell at each step. Produces irregular blob shapes with fractal perimeters.

Fractal Brownian Surface

fractal_brownian_surface(rows: 100, cols: 100, h: 0.5, seed: 42)

Spectral synthesis parameterised by the Hurst exponent h ∈ (0, 1), which has direct ecological meaning. h near 0 is rough; h near 1 is smooth. Related to spectral_synthesis by β = 2h + 2.

Landscape Gradient

landscape_gradient(rows: 100, cols: 100, direction: 45.0, aspect: 2.0, seed: 42)

Elliptical gradient centred at the grid midpoint. direction orients the major axis; aspect controls elongation (1.0 = circular). More flexible than distance_gradient.

Diffusion-Limited Aggregation

diffusion_limited_aggregation(rows: 100, cols: 100, n: 2000, seed: 42)

Random-walking particles released from a spawn ring stick when adjacent to the growing cluster, producing intricate branching fractal structures.

Invasion Percolation

invasion_percolation(rows: 100, cols: 100, n: 2000, seed: 42)

Grows a cluster from the grid centre by always invading the boundary cell with the lowest random weight, producing fractal-like connected binary patches.

Gaussian Blobs

gaussian_blobs(rows: 100, cols: 100, n: 50, sigma: 5.0, seed: 42)

Places random Gaussian kernel centres and accumulates their contributions across the grid, then rescales to [0, 1]. Produces smooth blob-like elevation fields.

Ising Model

ising_model(rows: 100, cols: 100, beta: 0.4, iterations: 1000, seed: 42)

Simulates a 2D Ising spin lattice via Glauber dynamics. Near the critical inverse temperature (β ≈ 0.44) the model produces scale-free, patchy binary patterns reminiscent of habitat mosaics.

Hydraulic Erosion

hydraulic_erosion(rows: 100, cols: 100, n: 500, seed: 42)

Generates a random initial heightmap, then simulates n water droplets flowing downhill. Each droplet carries sediment, eroding steeper terrain and depositing on flatter areas. The result resembles naturally worn terrain with drainage channels, alluvial fans, and rounded ridges.

Levy Flight

levy_flight(rows: 100, cols: 100, n: 1000, seed: 42)

Simulates a Levy flight: a random walk where step lengths follow a power-law (heavy-tailed) distribution. The resulting visit-density map has clustered hotspots with occasional long-range jumps, modelling dispersal or foraging patterns.

Poisson Disk

poisson_disk(rows: 100, cols: 100, min_dist: 5.0, seed: 42)

Uses Bridson's algorithm to place points such that no two are closer than min_dist. The resulting inhibition pattern has regular, even spacing compared to random point placement, modelling processes such as territorial behaviour or tree canopy competition.

Hill Grow

hill_grow(rows: 100, cols: 100, n: 20000, seed: 42)

Iteratively stamps a smooth convolution kernel at randomly selected cells, building up hill-like mounds. With runaway=True, taller cells attract more growth, causing hills to cluster into ridges.

Source: Etherington, Holland & O'Sullivan (2015)

Midpoint Displacement

midpoint_displacement(rows: 100, cols: 100, h: 0.8, seed: 42)

Recursive fractal terrain generation: grid midpoints are displaced by decreasing random amounts at each subdivision step, with h controlling the roughness (0 = rough, 1 = smooth).

Source: Fournier, Fussell & Carpenter (1982)

Usage

NLMrs can be installed as a Rust crate, but language bindings also exist for Python, R, WASM and C.

cargo add nlmrs
use nlmrs;

fn main() {
    let grid = nlmrs::midpoint_displacement(100, 100, 1.0, seed: Some(42));
    println!("{:?}", grid.data);
}

Export

The export module provides functions to save a grid to disk.

use nlmrs::{midpoint_displacement, export};

fn main() {
    let grid = midpoint_displacement(rows: 100, cols: 100, h: 0.8, Some(42));

    export::write_to_png(&grid, "terrain.png").unwrap();
    export::write_to_png_grayscale(&grid, "terrain_gray.png").unwrap();
    export::write_to_tiff(&grid, "terrain.tif").unwrap();
    export::write_to_csv(&grid, "terrain.csv").unwrap();
    export::write_to_json(&grid, "terrain.json").unwrap();
    export::write_to_ascii_grid(&grid, "terrain.asc").unwrap();
}

CLI

A command-line binary is included. Output format is inferred from the file extension (.png, .csv, .json, .tif, .asc).

cargo install nlmrs

nlmrs midpoint-displacement 200 200 --h 0.8 --seed 42 --output terrain.png
nlmrs fbm 300 300 --scale 6.0 --octaves 8 --seed 99 --output landscape.png
nlmrs hill-grow 200 200 --n 20000 --runaway --output hills.csv
nlmrs perlin 500 500 --scale 4.0 --grayscale --output noise.png

nlmrs --help   # list all subcommands and options

Grid operations

The operation module exposes combinators for building composite NLMs:

use nlmrs::{midpoint_displacement, planar_gradient, operation};

fn main() {
    let mut terrain = midpoint_displacement(100, 100, 0.8, Some(1));
    let gradient   = planar_gradient(100, 100, Some(90.), Some(2));

    operation::multiply(&mut terrain, &gradient);
    operation::scale(&mut terrain);
}

Available operations: add, add_value, multiply, multiply_value, invert, abs, scale, min, max, min_and_max, classify, threshold.

Python bindings

NLMrs is available as a Python package. Every function returns a 2D numpy array.

Install

pip install nlmrs

Or build from source (requires Rust and maturin):

maturin develop --features python   # editable install into the active venv

Usage

import nlmrs
import matplotlib.pyplot as plt

# All functions accept an optional seed for reproducible output.
grid = nlmrs.midpoint_displacement(100, 100, h=0.8, seed=42)  # numpy array (100, 100)

plt.imshow(grid, cmap="terrain")
plt.axis("off")
plt.show()

All parameters are keyword-friendly with sensible defaults:

# Gradient
nlmrs.planar_gradient(100, 100, direction=45.0)
nlmrs.edge_gradient(100, 100)
nlmrs.distance_gradient(100, 100)
nlmrs.wave_gradient(100, 100, period=2.5, direction=90.0)

# Noise
nlmrs.perlin_noise(100, 100, scale=4.0)
nlmrs.value_noise(100, 100, scale=4.0)
nlmrs.worley_noise(100, 100, scale=4.0)
nlmrs.gaussian_field(100, 100, sigma=10.0)
nlmrs.fbm_noise(100, 100, scale=4.0, octaves=6, persistence=0.5, lacunarity=2.0)
nlmrs.ridged_noise(100, 100, scale=4.0, octaves=6)
nlmrs.billow_noise(100, 100, scale=4.0, octaves=6)
nlmrs.hybrid_noise(100, 100, scale=4.0, octaves=6)
nlmrs.turbulence(100, 100, scale=4.0, octaves=6)
nlmrs.domain_warp(100, 100, scale=4.0, warp_strength=1.0)
nlmrs.spectral_synthesis(100, 100, beta=2.0)
nlmrs.simplex_noise(100, 100, scale=4.0)
nlmrs.voronoi_distance(100, 100, n=50)
nlmrs.sine_composite(100, 100, waves=8)
nlmrs.curl_noise(100, 100, scale=4.0)

# Patch-based
nlmrs.random(100, 100)
nlmrs.random_element(100, 100, n=50000.0)
nlmrs.hill_grow(100, 100, n=10000, runaway=True)
nlmrs.midpoint_displacement(100, 100, h=1.0)
nlmrs.random_cluster(100, 100, n=200)
nlmrs.mosaic(100, 100, n=200)
nlmrs.rectangular_cluster(100, 100, n=200)
nlmrs.percolation(100, 100, p=0.5)
nlmrs.binary_space_partitioning(100, 100, n=100)
nlmrs.cellular_automaton(100, 100, p=0.45, iterations=5)
nlmrs.neighbourhood_clustering(100, 100, k=5, iterations=10)
nlmrs.reaction_diffusion(100, 100, iterations=1000, feed=0.055, kill=0.062)
nlmrs.eden_growth(100, 100, n=2000)
nlmrs.fractal_brownian_surface(100, 100, h=0.5)
nlmrs.landscape_gradient(100, 100, direction=45.0, aspect=2.0)
nlmrs.diffusion_limited_aggregation(100, 100, n=2000)
nlmrs.invasion_percolation(100, 100, n=2000)
nlmrs.gaussian_blobs(100, 100, n=50, sigma=5.0)
nlmrs.ising_model(100, 100, beta=0.4, iterations=1000)
nlmrs.hydraulic_erosion(100, 100, n=500)
nlmrs.levy_flight(100, 100, n=1000)
nlmrs.poisson_disk(100, 100, min_dist=5.0)

Post-processing functions are also available:

grid = nlmrs.fbm_noise(100, 100, scale=4.0)
nlmrs.classify(grid, n=5)    # quantise into n equal-width classes
nlmrs.threshold(grid, t=0.5) # binarise at threshold t

R bindings

NLMrs is available as an R package via the extendr framework. Every function returns a numeric matrix.

Install

# Install from source (requires Rust)
remotes::install_github("tom-draper/nlmrs", subdir = "bindings/r")

Usage

library(nlmrs)

# All functions accept an optional integer seed.
m <- nlm_midpoint_displacement(100, 100, h = 0.8, seed = 42L)
image(m, col = terrain.colors(256))

All 41 algorithms are available with the nlm_ prefix:

# Gradient
nlm_planar_gradient(100, 100, direction = 45)
nlm_edge_gradient(100, 100)
nlm_distance_gradient(100, 100)
nlm_wave_gradient(100, 100, period = 2.5)

# Noise
nlm_perlin_noise(100, 100, scale = 4.0)
nlm_value_noise(100, 100, scale = 4.0)
nlm_worley_noise(100, 100, scale = 4.0)
nlm_gaussian_field(100, 100, sigma = 10.0)
nlm_fbm_noise(100, 100, scale = 4.0, octaves = 6L)
nlm_ridged_noise(100, 100, scale = 4.0, octaves = 6L)
nlm_billow_noise(100, 100, scale = 4.0, octaves = 6L)
nlm_hybrid_noise(100, 100, scale = 4.0, octaves = 6L)
nlm_turbulence(100, 100, scale = 4.0, octaves = 6L)
nlm_domain_warp(100, 100, scale = 4.0, warp_strength = 1.0)
nlm_spectral_synthesis(100, 100, beta = 2.0)
nlm_simplex_noise(100, 100, scale = 4.0)
nlm_voronoi_distance(100, 100, n = 50L)
nlm_sine_composite(100, 100, waves = 8L)
nlm_curl_noise(100, 100, scale = 4.0)

# Patch-based
nlm_random(100, 100)
nlm_random_element(100, 100, n = 50000)
nlm_hill_grow(100, 100, n = 10000L, runaway = TRUE)
nlm_midpoint_displacement(100, 100, h = 1.0)
nlm_random_cluster(100, 100, n = 200L)
nlm_mosaic(100, 100, n = 200L)
nlm_rectangular_cluster(100, 100, n = 200L)
nlm_percolation(100, 100, p = 0.5)
nlm_binary_space_partitioning(100, 100, n = 100L)
nlm_cellular_automaton(100, 100, p = 0.45, iterations = 5L)
nlm_neighbourhood_clustering(100, 100, k = 5L, iterations = 10L)
nlm_reaction_diffusion(100, 100, iterations = 1000L, feed = 0.055, kill = 0.062)
nlm_eden_growth(100, 100, n = 2000L)
nlm_fractal_brownian_surface(100, 100, h = 0.5)
nlm_landscape_gradient(100, 100, direction = 45.0, aspect = 2.0)
nlm_diffusion_limited_aggregation(100, 100, n = 2000L)
nlm_invasion_percolation(100, 100, n = 2000L)
nlm_gaussian_blobs(100, 100, n = 50L, sigma = 5.0)
nlm_ising_model(100, 100, beta = 0.4, iterations = 1000L)
nlm_hydraulic_erosion(100, 100, n = 500L)
nlm_levy_flight(100, 100, n = 1000L)
nlm_poisson_disk(100, 100, min_dist = 5.0)

C bindings

NLMrs exposes a C-compatible shared/static library, making it usable from any language with C FFI support (C++, Go, MATLAB, Fortran, etc.).

Build

cd bindings/c
cargo build --release
# → ../../target/release/libnlmrs_c.so   (Linux shared)
# → ../../target/release/libnlmrs_c.a    (Linux static)
# → include/nlmrs.h                       (generated header)

Usage

#include "nlmrs.h"
#include <stdio.h>

int main(void) {
    uint64_t seed = 42;

    // Generate a 200×200 midpoint displacement grid.
    NlmGrid grid = nlmrs_midpoint_displacement(200, 200, 0.8, &seed);

    printf("rows=%zu cols=%zu\n", grid.rows, grid.cols);

    // Access row-major data: value at (r, c) = grid.data[r * grid.cols + c]
    printf("value at (0,0): %f\n", grid.data[0]);

    nlmrs_free(grid);   // release Rust-owned memory
    return 0;
}

Compile and link against the shared library:

gcc example.c -I bindings/c/include -L target/release -lnlmrs_c -o example

Optional parameters

Seeds and optional floats (e.g. gradient direction) are passed as pointers. Pass NULL to use the default (random seed / random direction):

// Random seed
NlmGrid g1 = nlmrs_perlin_noise(200, 200, 4.0, NULL);

// Fixed direction, random seed
double dir = 45.0;
NlmGrid g2 = nlmrs_planar_gradient(200, 200, &dir, NULL);

nlmrs_free(g1);
nlmrs_free(g2);

All 41 algorithms are available as nlmrs_<name>. The header include/nlmrs.h is generated automatically by cbindgen during the build.

WASM bindings

NLMrs can run in the browser or Node.js via WebAssembly.

Build

cd bindings/wasm
wasm-pack build --target web

Usage

import init, * as nlmrs from "./pkg/nlmrs_wasm.js";

await init();

const grid = nlmrs.midpoint_displacement(100, 100, 0.8, 42);
console.log(grid.rows, grid.cols);   // 100 100

// Flat Float64Array in row-major order
const flat = grid.data;
const value = flat[r * grid.cols + c];

grid.free();  // release Rust memory

All 41 algorithms are available. Seeds are passed as plain integers. Omit the seed argument for random output.

Contributions

Contributions, issues and feature requests are welcome.

  • Fork it (https://github.com/tom-draper/nlmrs)
  • Create your feature branch (git checkout -b my-new-feature)
  • Commit your changes (`git commit -am 'Add some feature')
  • Push to the branch (git push origin my-new-feature)
  • Create a new Pull Request

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nlmrs-0.2.0.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nlmrs-0.2.0-cp38-abi3-manylinux_2_34_x86_64.whl (767.8 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.34+ x86-64

File details

Details for the file nlmrs-0.2.0.tar.gz.

File metadata

  • Download URL: nlmrs-0.2.0.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.12.4

File hashes

Hashes for nlmrs-0.2.0.tar.gz
Algorithm Hash digest
SHA256 329316fa6a6f3412b2cd3b68e67bfb29bbc41e59e3a367b7a244a19e7889dd30
MD5 54e8ad74a7211e088217d75929ffe3ef
BLAKE2b-256 0e0eefb72df2a7a1ca039d9babebaa60322e4ac01f0cd5fa8439957cd097c133

See more details on using hashes here.

File details

Details for the file nlmrs-0.2.0-cp38-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for nlmrs-0.2.0-cp38-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 870dd2ffe8525923f5ab7f521c2221779b3c4c7d6ad7fc8db1ce5673791cdb1e
MD5 5727708dd96a07b8045b09ed11fa0352
BLAKE2b-256 fcda0921493477b0824fdcf169beabc9b78382f86c3e5107b7ea2479d256a509

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page