Skip to content

DGGS Statistics

Statistics module for vgrid.

This module provides functions to calculate and display statistics for various discrete global grid systems (DGGS), including cell counts, areas, and edge lengths.

a5inspect(resolution, options={'segments': 'auto'}, split_antimeridian=False)

Generate comprehensive inspection data for A5 DGGS cells at a given resolution.

This function creates a detailed analysis of A5 cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

A5 resolution level (0-29)

required
options

Optional dictionary of options for grid generation

{'segments': 'auto'}
split_antimeridian bool

When True, apply antimeridian splitting to the resulting polygons. Defaults to False when None or omitted.

False

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing A5 cell inspection data with columns: - a5: A5 cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/a5stats.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def a5inspect(resolution: int, options={"segments": 'auto'}, split_antimeridian: bool = False):
    """
    Generate comprehensive inspection data for A5 DGGS cells at a given resolution.

    This function creates a detailed analysis of A5 cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: A5 resolution level (0-29)
        options: Optional dictionary of options for grid generation
        split_antimeridian: When True, apply antimeridian splitting to the resulting polygons.
            Defaults to False when None or omitted.

    Returns:
        geopandas.GeoDataFrame: DataFrame containing A5 cell inspection data with columns:
            - a5: A5 cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    a5_gdf = a5grid(
        resolution, output_format="gpd", options=options, split_antimeridian=split_antimeridian
    )
    a5_gdf["crossed"] = a5_gdf["geometry"].apply(check_crossing_geom)
    mean_area = a5_gdf["cell_area"].mean()
    # Calculate normalized area
    a5_gdf["norm_area"] = a5_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    a5_gdf["ipq"] = 4 * np.pi * a5_gdf["cell_area"] / (a5_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    a5_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * a5_gdf["cell_area"]
            - np.power(a5_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / a5_gdf["cell_perimeter"]
    )

    convex_hull = a5_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    a5_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        a5_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    a5_gdf["cvh"] = a5_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return a5_gdf

a5inspect_cli()

Command-line interface for A5 cell inspection.

CLI options

-r, --resolution: A5 resolution level (0-29) -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)

Source code in vgrid/stats/a5stats.py
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def a5inspect_cli():
    """
    Command-line interface for A5 cell inspection.

    CLI options:
      -r, --resolution: A5 resolution level (0-29)
      -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-split",
        "--split_antimeridian",
        action="store_true",
        default=False,  # default is False to avoid splitting the Antimeridian by default
        help="Enable antimeridian splitting",
    )
    args = parser.parse_args()
    resolution = args.resolution
    print(a5inspect(resolution, split_antimeridian=args.split_antimeridian))

a5stats_cli()

Command-line interface for generating A5 DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/a5stats.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def a5stats_cli():
    """
    Command-line interface for generating A5 DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()
    unit = args.unit
    # Get the DataFrame
    df = a5stats(unit=unit)
    # Display the DataFrame
    print(df)

dggalinspect(dggs_type, resolution, split_antimeridian=False)

Generate detailed inspection data for a DGGAL DGGS type at a given resolution.

Parameters:

Name Type Description Default
dggs_type str

DGGS type supported by DGGAL

required
resolution int

Resolution level

required
split_antimeridian bool

When True, apply antimeridian splitting to the resulting polygons. Defaults to True when None or omitted.

False

Returns:

Type Description
GeoDataFrame

geopandas.GeoDataFrame with columns: - ZoneID (as provided by DGGAL output; no renaming is performed) - resolution - geometry - cell_area (m^2) - cell_perimeter (m) - crossed (bool) - norm_area (area/mean_area) - ipq (4πA/P²) - zsc (sqrt(4πA - A²/R²)/P), with R=WGS84 a

Source code in vgrid/stats/dggalstats.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def dggalinspect(
    dggs_type: str, resolution: int, split_antimeridian: bool = False
) -> gpd.GeoDataFrame:
    """
    Generate detailed inspection data for a DGGAL DGGS type at a given resolution.

    Args:
        dggs_type: DGGS type supported by DGGAL
        resolution: Resolution level
        split_antimeridian: When True, apply antimeridian splitting to the resulting polygons.
            Defaults to True when None or omitted.

    Returns:
        geopandas.GeoDataFrame with columns:
          - ZoneID (as provided by DGGAL output; no renaming is performed)
          - resolution
          - geometry
          - cell_area (m^2)
          - cell_perimeter (m)
          - crossed (bool)
          - norm_area (area/mean_area)
          - ipq (4πA/P²)
          - zsc (sqrt(4πA - A²/R²)/P), with R=WGS84 a
    """
    dggal_gdf = dggalgen(
        dggs_type,
        resolution,
        output_format="gpd",
        split_antimeridian=split_antimeridian,
    )

    # Determine whether current CRS is geographic; compute metrics accordingly
    if dggal_gdf.crs.is_geographic:
        dggal_gdf["cell_area"] = dggal_gdf.geometry.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[0])
        )
        dggal_gdf["cell_perimeter"] = dggal_gdf.geometry.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[1])
        )
        dggal_gdf["crossed"] = dggal_gdf.geometry.apply(check_crossing_geom)
        convex_hull = dggal_gdf["geometry"].convex_hull
        convex_hull_area = convex_hull.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[0])
        )
    else:
        dggal_gdf["cell_area"] = dggal_gdf.geometry.area
        dggal_gdf["cell_perimeter"] = dggal_gdf.geometry.length
        dggal_gdf["crossed"] = False
        convex_hull = dggal_gdf["geometry"].convex_hull
        convex_hull_area = convex_hull.area

    mean_area = dggal_gdf["cell_area"].mean()
    dggal_gdf["norm_area"] = (
        dggal_gdf["cell_area"] / mean_area if mean_area and mean_area != 0 else np.nan
    )
    # Robust formulas avoiding division by zero
    dggal_gdf["ipq"] = (
        4 * np.pi * dggal_gdf["cell_area"] / (dggal_gdf["cell_perimeter"] ** 2)
    )

    dggal_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * dggal_gdf["cell_area"]
            - np.power(dggal_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / dggal_gdf["cell_perimeter"]
    )

    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    dggal_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        dggal_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    dggal_gdf["cvh"] = dggal_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return dggal_gdf

dggalinspect_cli()

Command-line interface for DGGAL cell inspection.

CLI options

-t, --dggs_type -r, --resolution -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)

Source code in vgrid/stats/dggalstats.py
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
def dggalinspect_cli():
    """
    Command-line interface for DGGAL cell inspection.

    CLI options:
      -t, --dggs_type
      -r, --resolution
      -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-t", "--dggs_type", dest="dggs_type", choices=DGGAL_TYPES.keys(), required=True
    )
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-split",
        "--split_antimeridian",
        action="store_true",
        default=False,  # default is False to avoid splitting the Antimeridian by default
        help="Enable antimeridian splitting",
    )
    args = parser.parse_args()
    dggs_type = args.dggs_type
    resolution = args.resolution
    print(
        dggalinspect(
            dggs_type, resolution, split_antimeridian=args.split_antimeridian
        )
    )

dggalstats_cli()

Command-line interface for generating DGGAL DGGS statistics.

CLI options

-dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix} -unit, --unit {m,km} --minres, --maxres

Source code in vgrid/stats/dggalstats.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def dggalstats_cli():
    """
    Command-line interface for generating DGGAL DGGS statistics.

    CLI options:
      -dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix}
      -unit, --unit {m,km}
      --minres, --maxres
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-dggs", "--dggs_type", dest="dggs_type", choices=DGGAL_TYPES.keys()
    )
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()

    dggs_type = args.dggs_type
    unit = args.unit

    result = dggalstats(dggs_type, unit)
    if result is not None:
        print(result)

dggridinspect(dggrid_instance, dggs_type, resolution, split_antimeridian=False, aggregate=False)

Generate detailed inspection data for a DGGRID DGGS type at a given resolution.

Parameters:

Name Type Description Default
dggrid_instance

DGGRID instance for grid operations

required
dggs_type str

DGGS type supported by DGGRID (see dggs_types)

required
resolution int

Resolution level

required
split_antimeridian bool

When True, apply antimeridian fixing to the resulting polygons.

False
aggregate bool

When True, aggregate the resulting polygons. Defaults to False to avoid aggregation by default.

False
Source code in vgrid/stats/dggridstats.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def dggridinspect(
    dggrid_instance,
    dggs_type: str,
    resolution: int,
    split_antimeridian: bool = False,
    aggregate: bool = False,
) -> gpd.GeoDataFrame:
    """
    Generate detailed inspection data for a DGGRID DGGS type at a given resolution.

    Args:
        dggrid_instance: DGGRID instance for grid operations
        dggs_type: DGGS type supported by DGGRID (see dggs_types)
        resolution: Resolution level
        split_antimeridian: When True, apply antimeridian fixing to the resulting polygons.
        Defaults to False to avoid splitting the Antimeridian by default.
        aggregate: When True, aggregate the resulting polygons. Defaults to False to avoid aggregation by default.
    Returns:
        geopandas.GeoDataFrame: DataFrame containing inspection data with columns:
          - name (cell identifier from DGGRID)
          - resolution
          - geometry
          - cell_area (m^2)
          - cell_perimeter (m)
          - crossed (bool)
          - norm_area (area/mean_area)
          - ipq (4πA/P²)
          - zsc (sqrt(4πA - A²/R²)/P), with R=WGS84 a
    """

    # Generate grid using dggridgen
    dggrid_gdf = dggridgen(
        dggrid_instance,
        dggs_type,
        resolution,
        output_format="gpd",
        split_antimeridian=split_antimeridian,
        aggregate=aggregate,
    )

    # Remove cells with null or invalid geometry
    dggrid_gdf = dggrid_gdf.dropna(subset=["geometry"])
    dggrid_gdf = dggrid_gdf[dggrid_gdf.geometry.is_valid]

    # Add dggs_type column
    dggrid_gdf["dggs_type"] = f"dggrid_{dggs_type.lower()}"

    # Rename global_id to cell_id
    if "global_id" in dggrid_gdf.columns:
        dggrid_gdf = dggrid_gdf.rename(columns={"global_id": "cell_id"})

    # Determine whether current CRS is geographic; compute metrics accordingly
    if dggrid_gdf.crs.is_geographic:
        dggrid_gdf["cell_area"] = dggrid_gdf.geometry.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[0])
        )
        dggrid_gdf["cell_perimeter"] = dggrid_gdf.geometry.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[1])
        )
        dggrid_gdf["crossed"] = dggrid_gdf.geometry.apply(check_crossing_geom)
    else:
        dggrid_gdf["cell_area"] = dggrid_gdf.geometry.area
        dggrid_gdf["cell_perimeter"] = dggrid_gdf.geometry.length
        dggrid_gdf["crossed"] = False

    # Add resolution column
    dggrid_gdf["resolution"] = resolution

    # Calculate normalized area
    mean_area = dggrid_gdf["cell_area"].mean()
    dggrid_gdf["norm_area"] = (
        dggrid_gdf["cell_area"] / mean_area if mean_area and mean_area != 0 else np.nan
    )

    # Calculate compactness metrics (robust formulas avoiding division by zero)
    dggrid_gdf["ipq"] = (
        4 * np.pi * dggrid_gdf["cell_area"] / (dggrid_gdf["cell_perimeter"] ** 2)
    )
    dggrid_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * dggrid_gdf["cell_area"]
            - np.power(dggrid_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / dggrid_gdf["cell_perimeter"]
    )

    convex_hull = dggrid_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    dggrid_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        dggrid_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    dggrid_gdf["cvh"] = dggrid_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return dggrid_gdf

dggridinspect_cli()

Command-line interface for DGGRID cell inspection.

CLI options

-dggs, --dggs_type: DGGS type from dggs_types -r, --resolution: Resolution level --no-split_antimeridian: Disable antimeridian fixing (default: enabled) --no-aggregate: Disable aggregation (default: enabled)

Source code in vgrid/stats/dggridstats.py
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
def dggridinspect_cli():
    """
    Command-line interface for DGGRID cell inspection.

    CLI options:
      -dggs, --dggs_type: DGGS type from dggs_types
      -r, --resolution: Resolution level
      --no-split_antimeridian: Disable antimeridian fixing (default: enabled)
      --no-aggregate: Disable aggregation (default: enabled)
    """
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-dggs", "--dggs_type", dest="dggs_type", choices=dggs_types, required=True
    )
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-split",
        "--split_antimeridian",
        action="store_true",
        default=False,  # default is False to avoid splitting the Antimeridian by default
        help="Enable antimeridian fixing",
    )
    parser.add_argument(
        "-aggregate",
        "--aggregate",
        action="store_true",
        default=False,  # default is False to avoid aggregation by default
        help="Enable aggregation",
    )
    args = parser.parse_args()
    dggrid_instance = create_dggrid_instance()
    dggs_type = args.dggs_type
    resolution = args.resolution
    print(
        dggridinspect(
            dggrid_instance,
            dggs_type,
            resolution,
            split_antimeridian=args.split_antimeridian,
            aggregate=args.aggregate,
        )
    )

dggridstats_cli()

Command-line interface for generating DGGAL DGGS statistics.

CLI options

-dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix} -unit, --unit {m,km} --minres, --maxres

Source code in vgrid/stats/dggridstats.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def dggridstats_cli():
    """
    Command-line interface for generating DGGAL DGGS statistics.

    CLI options:
      -dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix}
      -unit, --unit {m,km}
      --minres, --maxres
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-dggs", "--dggs_type", dest="dggs_type", choices=DGGRID_TYPES.keys()
    )
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()

    dggs_type = args.dggs_type
    unit = args.unit

    dggrid_instance = create_dggrid_instance()
    result = dggridstats(dggrid_instance, dggs_type, unit)
    if result is not None:
        print(result)

digipininspect(resolution)

Generate comprehensive inspection data for DIGIPIN DGGS cells at a given resolution.

This function creates a detailed analysis of DIGIPIN cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution

DIGIPIN resolution level (1-10)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing DIGIPIN cell inspection data with columns: - digipin: DIGIPIN cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness - cvh: Convex Hull compactness

Source code in vgrid/stats/digipinstats.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def digipininspect(resolution):
    """
    Generate comprehensive inspection data for DIGIPIN DGGS cells at a given resolution.

    This function creates a detailed analysis of DIGIPIN cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: DIGIPIN resolution level (1-10)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing DIGIPIN cell inspection data with columns:
            - digipin: DIGIPIN cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
            - cvh: Convex Hull compactness
    """
    resolution = validate_digipin_resolution(resolution)
    digipin_gdf = digipingrid(resolution, output_format="gpd")
    digipin_gdf["crossed"] = digipin_gdf["geometry"].apply(check_crossing_geom)
    mean_area = digipin_gdf["cell_area"].mean()
    # Calculate normalized area
    digipin_gdf["norm_area"] = digipin_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    digipin_gdf["ipq"] = (
        4 * np.pi * digipin_gdf["cell_area"] / (digipin_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    digipin_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * digipin_gdf["cell_area"]
            - np.power(digipin_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / digipin_gdf["cell_perimeter"]
    )

    convex_hull = digipin_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    digipin_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        digipin_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    digipin_gdf["cvh"] = digipin_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return digipin_gdf

digipininspect_cli()

Command-line interface for DIGIPIN cell inspection.

CLI options

-r, --resolution: DIGIPIN resolution level (1-10)

Source code in vgrid/stats/digipinstats.py
523
524
525
526
527
528
529
530
531
532
533
534
def digipininspect_cli():
    """
    Command-line interface for DIGIPIN cell inspection.

    CLI options:
      -r, --resolution: DIGIPIN resolution level (1-10)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=1)
    args, _ = parser.parse_known_args()  # type: ignore
    resolution = args.resolution
    print(digipininspect(resolution))

digipinstats_cli()

Command-line interface for generating DIGIPIN DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/digipinstats.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def digipinstats_cli():
    """
    Command-line interface for generating DIGIPIN DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args, _ = parser.parse_known_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = digipinstats(unit=unit)

    # Display the DataFrame
    print(df)

easeinspect(resolution)

Generate comprehensive inspection data for EASE-DGGS cells at a given resolution.

This function creates a detailed analysis of EASE cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

EASE-DGGS resolution level (0-6)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing EASE cell inspection data with columns: - ease: EASE cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/easestats.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def easeinspect(resolution: int):  # length unit is m, area unit is m2
    """
    Generate comprehensive inspection data for EASE-DGGS cells at a given resolution.

    This function creates a detailed analysis of EASE cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: EASE-DGGS resolution level (0-6)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing EASE cell inspection data with columns:
            - ease: EASE cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    ease_gdf = easegrid(resolution, output_format="gpd")    
    ease_gdf["crossed"] = ease_gdf["geometry"].apply(check_crossing_geom)
    mean_area = ease_gdf["cell_area"].mean()
    # Calculate normalized area
    ease_gdf["norm_area"] = ease_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    ease_gdf["ipq"] = (
        4 * np.pi * ease_gdf["cell_area"] / (ease_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    ease_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * ease_gdf["cell_area"]
            - np.power(ease_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / ease_gdf["cell_perimeter"]
    )

    convex_hull = ease_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    ease_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        ease_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    ease_gdf["cvh"] = ease_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return ease_gdf

easeinspect_cli()

Command-line interface for EASE cell inspection.

CLI options

-r, --resolution: EASE resolution level (0-6)

Source code in vgrid/stats/easestats.py
482
483
484
485
486
487
488
489
490
491
492
493
def easeinspect_cli():
    """
    Command-line interface for EASE cell inspection.

    CLI options:
      -r, --resolution: EASE resolution level (0-6)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    print(easeinspect(resolution))

easestats_cli()

Command-line interface for generating EASE-DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/easestats.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def easestats_cli():
    """
    Command-line interface for generating EASE-DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )

    args = parser.parse_args()  # type: ignore
    unit = args.unit

    # Get the DataFrame
    df = easestats(unit=unit)

    # Display the DataFrame
    print(df)

garsinspect(resolution)

Generate comprehensive inspection data for GARS DGGS cells at a given resolution.

This function creates a detailed analysis of GARS cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

GARS resolution level (0-4)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing GARS cell inspection data with columns: - gars: GARS cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/garsstats.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def garsinspect(resolution: int):  # length unit is km, area unit is km2
    """
    Generate comprehensive inspection data for GARS DGGS cells at a given resolution.

    This function creates a detailed analysis of GARS cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: GARS resolution level (0-4)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing GARS cell inspection data with columns:
            - gars: GARS cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    gars_gdf = garsgrid(resolution, output_format="gpd")        
    gars_gdf["crossed"] = gars_gdf["geometry"].apply(check_crossing_geom)
    mean_area = gars_gdf["cell_area"].mean()
    # Calculate normalized area
    gars_gdf["norm_area"] = gars_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    gars_gdf["ipq"] = (
        4 * np.pi * gars_gdf["cell_area"] / (gars_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    gars_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * gars_gdf["cell_area"]
            - np.power(gars_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / gars_gdf["cell_perimeter"]
    )

    convex_hull = gars_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    gars_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        gars_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    gars_gdf["cvh"] = gars_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return gars_gdf

garsinspect_cli()

Command-line interface for GARS cell inspection.

CLI options

-r, --resolution: GARS resolution level (0-4)

Source code in vgrid/stats/garsstats.py
488
489
490
491
492
493
494
495
496
497
498
499
def garsinspect_cli():
    """
    Command-line interface for GARS cell inspection.

    CLI options:
      -r, --resolution: GARS resolution level (0-4)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    print(garsinspect(resolution))

garsstats_cli()

Command-line interface for generating GARS DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/garsstats.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def garsstats_cli():
    """
    Command-line interface for generating GARS DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()

    unit = args.unit

    # Get the DataFrame
    df = garsstats(unit=unit)

    # Display the DataFrame
    print(df)

geohashinspect(resolution)

Generate comprehensive inspection data for Geohash DGGS cells at a given resolution.

This function creates a detailed analysis of Geohash cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

Geohash resolution level (0-12)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing Geohash cell inspection data with columns: - geohash: Geohash cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/geohashstats.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def geohashinspect(resolution: int):
    """
    Generate comprehensive inspection data for Geohash DGGS cells at a given resolution.

    This function creates a detailed analysis of Geohash cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: Geohash resolution level (0-12)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing Geohash cell inspection data with columns:
            - geohash: Geohash cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    geohash_gdf = geohashgrid(resolution, output_format="gpd")
    geohash_gdf["crossed"] = geohash_gdf["geometry"].apply(check_crossing_geom)
    mean_area = geohash_gdf["cell_area"].mean()
    # Calculate normalized area
    geohash_gdf["norm_area"] = geohash_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    geohash_gdf["ipq"] = (
        4 * np.pi * geohash_gdf["cell_area"] / (geohash_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    geohash_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * geohash_gdf["cell_area"]
            - np.power(geohash_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / geohash_gdf["cell_perimeter"]
    )

    convex_hull = geohash_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    geohash_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        geohash_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    geohash_gdf["cvh"] = geohash_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return geohash_gdf

geohashinspect_cli()

Command-line interface for Geohash cell inspection.

CLI options

-r, --resolution: Geohash resolution level (0-12)

Source code in vgrid/stats/geohashstats.py
494
495
496
497
498
499
500
501
502
503
504
505
def geohashinspect_cli():
    """
    Command-line interface for Geohash cell inspection.

    CLI options:
      -r, --resolution: Geohash resolution level (0-12)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()
    resolution = args.resolution
    print(geohashinspect(resolution))

geohashstats_cli()

Command-line interface for generating Geohash DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/geohashstats.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def geohashstats_cli():
    """
    Command-line interface for generating Geohash DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()

    unit = args.unit

    # Get the DataFrame
    df = geohashstats(unit=unit)

    # Display the DataFrame
    print(df)

georefinspect(resolution)

Generate comprehensive inspection data for GEOREF DGGS cells at a given resolution.

This function creates a detailed analysis of GEOREF cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

GEOREF resolution level (0-10)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing GEOREF cell inspection data with columns: - georef: GEOREF cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/georefstats.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def georefinspect(resolution: int):
    """
    Generate comprehensive inspection data for GEOREF DGGS cells at a given resolution.

    This function creates a detailed analysis of GEOREF cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: GEOREF resolution level (0-10)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing GEOREF cell inspection data with columns:
            - georef: GEOREF cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    georef_gdf = georefgrid(resolution, output_format="gpd")
    georef_gdf["crossed"] = georef_gdf["geometry"].apply(check_crossing_geom)
    mean_area = georef_gdf["cell_area"].mean()
    # Calculate normalized area
    georef_gdf["norm_area"] = georef_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    georef_gdf["ipq"] = (
        4 * np.pi * georef_gdf["cell_area"] / (georef_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    georef_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * georef_gdf["cell_area"]
            - np.power(georef_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / georef_gdf["cell_perimeter"]
    )

    convex_hull = georef_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    georef_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        georef_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    georef_gdf["cvh"] = georef_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return georef_gdf

georefinspect_cli()

Command-line interface for GEOREF cell inspection.

CLI options

-r, --resolution: GEOREF resolution level (0-10)

Source code in vgrid/stats/georefstats.py
495
496
497
498
499
500
501
502
503
504
505
506
def georefinspect_cli():
    """
    Command-line interface for GEOREF cell inspection.

    CLI options:
      -r, --resolution: GEOREF resolution level (0-10)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    print(georefinspect(resolution))

georefstats_cli()

Command-line interface for generating GEOREF DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/georefstats.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def georefstats_cli():
    """
    Command-line interface for generating GEOREF DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = georefstats(unit=unit)

    # Display the DataFrame
    print(df)

h3inspect(resolution, fix_antimeridian=None)

Generate comprehensive inspection data for H3 DGGS cells at a given resolution.

This function creates a detailed analysis of H3 cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

H3 resolution level (0-15)

required
fix_antimeridian None

Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

None

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing H3 cell inspection data with columns: - h3: H3 cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - is_pentagon: Whether cell is a pentagon - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/h3stats.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def h3inspect(resolution: int, fix_antimeridian: None = None):
    """
    Generate comprehensive inspection data for H3 DGGS cells at a given resolution.

    This function creates a detailed analysis of H3 cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: H3 resolution level (0-15)
        fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

    Returns:
        geopandas.GeoDataFrame: DataFrame containing H3 cell inspection data with columns:
            - h3: H3 cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - is_pentagon: Whether cell is a pentagon
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    h3_gdf = h3grid(resolution, output_format="gpd", fix_antimeridian=fix_antimeridian)
    h3_gdf["crossed"] = h3_gdf["geometry"].apply(check_crossing_geom)
    h3_gdf["is_pentagon"] = h3_gdf["h3"].apply(h3.is_pentagon)
    mean_area = h3_gdf["cell_area"].mean()
    # Calculate normalized area
    h3_gdf["norm_area"] = h3_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    h3_gdf["ipq"] = 4 * np.pi * h3_gdf["cell_area"] / (h3_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    h3_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * h3_gdf["cell_area"]
            - np.power(h3_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / h3_gdf["cell_perimeter"]
    )

    convex_hull = h3_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    h3_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        h3_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    h3_gdf["cvh"] = h3_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return h3_gdf

h3inspect_cli()

Command-line interface for H3 cell inspection.

CLI options

-r, --resolution: H3 resolution level (0-15) -split, --split_antimeridian: Apply antimeridian fixing to the resulting polygons

Source code in vgrid/stats/h3stats.py
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
def h3inspect_cli():
    """
    Command-line interface for H3 cell inspection.

    CLI options:
      -r, --resolution: H3 resolution level (0-15)
      -split, --split_antimeridian: Apply antimeridian fixing to the resulting polygons
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-fix--fix_antimeridian",
        type=str,
        choices=[
            "shift",
            "shift_balanced",
            "shift_west",
            "shift_east",
            "split",
            "none",
        ],
        default=None,
        help="Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none",
    )
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    print(h3inspect(resolution, fix_antimeridian=args.fix_antimeridian))

h3stats_cli()

Command-line interface for generating H3 DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/h3stats.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def h3stats_cli():
    """
    Command-line interface for generating H3 DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    df = h3stats(unit=unit)
    df["number_of_cells"] = df["number_of_cells"].apply(lambda x: "{:,.0f}".format(x))
    print(df)

isea3hinspect(resolution, fix_antimeridian=None)

Generate comprehensive inspection data for ISEA3H DGGS cells at a given resolution.

This function creates a detailed analysis of ISEA3H cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

ISEA3H resolution level (0-40)

required
fix_antimeridian None

Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

None
Source code in vgrid/stats/isea3hstats.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def isea3hinspect(resolution: int, fix_antimeridian: None = None):
    """
    Generate comprehensive inspection data for ISEA3H DGGS cells at a given resolution.

    This function creates a detailed analysis of ISEA3H cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: ISEA3H resolution level (0-40)
        fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none
    Returns:
        geopandas.GeoDataFrame: DataFrame containing ISEA3H cell inspection data with columns:
            - isea3h: ISEA3H cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    # Allow running on all platforms

    isea3h_gdf = isea3hgrid(
        resolution, output_format="gpd", fix_antimeridian=fix_antimeridian
    )  # remove cells that cross the Antimeridian
    isea3h_gdf["crossed"] = isea3h_gdf["geometry"].apply(check_crossing_geom)
    mean_area = isea3h_gdf["cell_area"].mean()
    # Calculate normalized area
    isea3h_gdf["norm_area"] = isea3h_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    isea3h_gdf["ipq"] = (
        4 * np.pi * isea3h_gdf["cell_area"] / (isea3h_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    isea3h_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * isea3h_gdf["cell_area"]
            - np.power(isea3h_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / isea3h_gdf["cell_perimeter"]
    )

    convex_hull = isea3h_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    isea3h_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        isea3h_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    isea3h_gdf["cvh"] = isea3h_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return isea3h_gdf

isea3hinspect_cli()

Command-line interface for ISEA3H cell inspection.

CLI options

-r, --resolution: ISEA3H resolution level (0-40) -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)

Source code in vgrid/stats/isea3hstats.py
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
def isea3hinspect_cli():
    """
    Command-line interface for ISEA3H cell inspection.

    CLI options:
      -r, --resolution: ISEA3H resolution level (0-40)
      -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)
    """

    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-split",
        "--split_antimeridian",
        action="store_true",
        default=False,  # default is False to avoid splitting the Antimeridian by default
        help="Enable antimeridian splitting",
    )
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    split_antimeridian = args.split_antimeridian
    print(isea3hinspect(resolution, split_antimeridian=split_antimeridian))

isea3hstats_cli()

Command-line interface for generating ISEA3H DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/isea3hstats.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def isea3hstats_cli():
    """
    Command-line interface for generating ISEA3H DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = isea3hstats(unit=unit)

    # Display the DataFrame
    print(df)

isea4tinspect(resolution, fix_antimeridian=None)

Generate comprehensive inspection data for ISEA4T DGGS cells at a given resolution.

This function creates a detailed analysis of ISEA4T cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution

ISEA4T resolution level (0-15)

required
fix_antimeridian None

Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

None
Source code in vgrid/stats/isea4tstats.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def isea4tinspect(resolution, fix_antimeridian: None = None):
    """
    Generate comprehensive inspection data for ISEA4T DGGS cells at a given resolution.

    This function creates a detailed analysis of ISEA4T cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: ISEA4T resolution level (0-15)
        fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none
    Returns:
        geopandas.GeoDataFrame: DataFrame containing ISEA4T cell inspection data with columns:
            - isea4t: ISEA4T cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    # Allow running on all platforms
    resolution = validate_isea4t_resolution(resolution)
    isea4t_gdf = isea4tgrid(
        resolution, output_format="gpd", fix_antimeridian=fix_antimeridian
    )
    isea4t_gdf["crossed"] = isea4t_gdf["geometry"].apply(check_crossing_geom)
    mean_area = isea4t_gdf["cell_area"].mean()
    # Calculate normalized area
    isea4t_gdf["norm_area"] = isea4t_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    isea4t_gdf["ipq"] = (
        4 * np.pi * isea4t_gdf["cell_area"] / (isea4t_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    isea4t_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * isea4t_gdf["cell_area"]
            - np.power(isea4t_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / isea4t_gdf["cell_perimeter"]
    )

    convex_hull = isea4t_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    isea4t_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        isea4t_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    isea4t_gdf["cvh"] = isea4t_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return isea4t_gdf

isea4tinspect_cli()

Command-line interface for ISEA4T cell inspection.

CLI options

-r, --resolution: ISEA4T resolution level (0-15)

-fix, --fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

Source code in vgrid/stats/isea4tstats.py
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
def isea4tinspect_cli():
    """
    Command-line interface for ISEA4T cell inspection.

    CLI options:
      -r, --resolution: ISEA4T resolution level (0-15)
    -fix, --fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-fix",
        "--fix_antimeridian",
        type=str,
        choices=[
            "shift",
            "shift_balanced",
            "shift_west",
            "shift_east",
            "split",
            "none",
        ],
        default=None,
        help="Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none",
    )
    args = parser.parse_args()
    resolution = args.resolution
    fix_antimeridian = args.fix_antimeridian
    print(isea4tinspect(resolution, fix_antimeridian=fix_antimeridian))

isea4tstats_cli()

Command-line interface for generating ISEA4T DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/isea4tstats.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def isea4tstats_cli():
    """
    Command-line interface for generating ISEA4T DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args, _ = parser.parse_known_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = isea4tstats(unit=unit)

    # Display the DataFrame
    print(df)

maidenheadinspect(resolution)

Generate comprehensive inspection data for Maidenhead DGGS cells at a given resolution.

This function creates a detailed analysis of Maidenhead cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

Maidenhead resolution level (1-4)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing Maidenhead cell inspection data with columns: - maidenhead: Maidenhead cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/maidenheadstats.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def maidenheadinspect(resolution: int):
    """
    Generate comprehensive inspection data for Maidenhead DGGS cells at a given resolution.

    This function creates a detailed analysis of Maidenhead cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: Maidenhead resolution level (1-4)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing Maidenhead cell inspection data with columns:
            - maidenhead: Maidenhead cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    resolution = validate_maidenhead_resolution(resolution)
    maidenhead_gdf = maidenheadgrid(resolution, output_format="gpd")    
    maidenhead_gdf["crossed"] = maidenhead_gdf["geometry"].apply(check_crossing_geom)
    mean_area = maidenhead_gdf["cell_area"].mean()
    # Calculate normalized area
    maidenhead_gdf["norm_area"] = maidenhead_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    maidenhead_gdf["ipq"] = (
        4
        * np.pi
        * maidenhead_gdf["cell_area"]
        / (maidenhead_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    maidenhead_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * maidenhead_gdf["cell_area"]
            - np.power(maidenhead_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / maidenhead_gdf["cell_perimeter"]
    )

    convex_hull = maidenhead_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    maidenhead_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        maidenhead_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    maidenhead_gdf["cvh"] = maidenhead_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return maidenhead_gdf

maidenheadinspect_cli()

Command-line interface for Maidenhead cell inspection.

CLI options

-r, --resolution: Maidenhead resolution level (1-4)

Source code in vgrid/stats/maidenheadstats.py
504
505
506
507
508
509
510
511
512
513
514
515
def maidenheadinspect_cli():
    """
    Command-line interface for Maidenhead cell inspection.

    CLI options:
      -r, --resolution: Maidenhead resolution level (1-4)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args, _ = parser.parse_known_args()  # type: ignore
    resolution = args.resolution
    print(maidenheadinspect(resolution))

maidenheadstats_cli()

Command-line interface for generating Maidenhead DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/maidenheadstats.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def maidenheadstats_cli():
    """
    Command-line interface for generating Maidenhead DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args, _ = parser.parse_known_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = maidenheadstats(unit=unit)

    # Display the DataFrame
    print(df)

mgrsstats_cli()

Command-line interface for generating MGRS DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/mgrsstats.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def mgrsstats_cli():
    """
    Command-line interface for generating MGRS DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    print("Resolution 0: 100 x 100 km")
    print("Resolution 1: 10 x 10 km")
    print("2 <= Resolution <= 5 = Finer subdivisions (1 x 1 km, 0.1 x 0.11 km, etc.)")

    # Get the DataFrame
    df = mgrsstats(unit=unit)

    # Display the DataFrame
    print(df)

olcinspect(resolution)

Generate comprehensive inspection data for OLC DGGS cells at a given resolution.

This function creates a detailed analysis of OLC cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

OLC resolution level (2-15)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing OLC cell inspection data with columns: - olc: OLC cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/olcstats.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def olcinspect(resolution: int):
    """
    Generate comprehensive inspection data for OLC DGGS cells at a given resolution.

    This function creates a detailed analysis of OLC cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: OLC resolution level (2-15)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing OLC cell inspection data with columns:
            - olc: OLC cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    olc_gdf = olcgrid(resolution, output_format="gpd")          
    olc_gdf["crossed"] = olc_gdf["geometry"].apply(check_crossing_geom)
    mean_area = olc_gdf["cell_area"].mean()
    # Calculate normalized area
    olc_gdf["norm_area"] = olc_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    olc_gdf["ipq"] = 4 * np.pi * olc_gdf["cell_area"] / (olc_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    olc_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * olc_gdf["cell_area"]
            - np.power(olc_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / olc_gdf["cell_perimeter"]
    )

    convex_hull = olc_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    olc_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        olc_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    olc_gdf["cvh"] = olc_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return olc_gdf

olcinspect_cli()

Command-line interface for OLC cell inspection.

CLI options

-r, --resolution: OLC resolution level (2-15)

Source code in vgrid/stats/olcstats.py
482
483
484
485
486
487
488
489
490
491
492
493
def olcinspect_cli():
    """
    Command-line interface for OLC cell inspection.

    CLI options:
      -r, --resolution: OLC resolution level (2-15)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args, _ = parser.parse_known_args()  # type: ignore
    resolution = args.resolution
    print(olcinspect(resolution))

olcstats_cli()

Command-line interface for generating OLC DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/olcstats.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def olcstats_cli():
    """
    Command-line interface for generating OLC DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args, _ = parser.parse_known_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = olcstats(unit=unit)

    # Display the DataFrame
    print(df)

qtminspect(resolution)

Generate comprehensive inspection data for QTM DGGS cells at a given resolution.

This function creates a detailed analysis of QTM cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

QTM resolution level (1-24)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing QTM cell inspection data with columns: - qtm: QTM cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/qtmstats.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def qtminspect(resolution: int):
    """
    Generate comprehensive inspection data for QTM DGGS cells at a given resolution.

    This function creates a detailed analysis of QTM cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: QTM resolution level (1-24)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing QTM cell inspection data with columns:
            - qtm: QTM cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    qtm_gdf = qtm_grid(resolution)
    qtm_gdf["crossed"] = qtm_gdf["geometry"].apply(check_crossing_geom)
    mean_area = qtm_gdf["cell_area"].mean()
    # Calculate normalized area
    qtm_gdf["norm_area"] = qtm_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    qtm_gdf["ipq"] = 4 * np.pi * qtm_gdf["cell_area"] / (qtm_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    qtm_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * qtm_gdf["cell_area"]
            - np.power(qtm_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / qtm_gdf["cell_perimeter"]
    )

    convex_hull = qtm_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    qtm_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        qtm_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    qtm_gdf["cvh"] = qtm_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return qtm_gdf

qtminspect_cli()

Command-line interface for QTM cell inspection.

CLI options

-r, --resolution: QTM resolution level (1-24)

Source code in vgrid/stats/qtmstats.py
476
477
478
479
480
481
482
483
484
485
486
487
def qtminspect_cli():
    """
    Command-line interface for QTM cell inspection.

    CLI options:
      -r, --resolution: QTM resolution level (1-24)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()
    resolution = args.resolution
    print(qtminspect(resolution))

qtmstats_cli()

Command-line interface for generating QTM DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/qtmstats.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def qtmstats_cli():
    """
    Command-line interface for generating QTM DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()
    unit = args.unit
    # Get the DataFrame
    df = qtmstats(unit=unit)
    # Display the DataFrame
    print(df)

quadkeyinspect(resolution)

Generate comprehensive inspection data for Quadkey DGGS cells at a given resolution.

This function creates a detailed analysis of Quadkey cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

Quadkey resolution level (0-29)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing Quadkey cell inspection data with columns: - quadkey: Quadkey cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/quadkeystats.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def quadkeyinspect(resolution: int):
    """
    Generate comprehensive inspection data for Quadkey DGGS cells at a given resolution.

    This function creates a detailed analysis of Quadkey cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: Quadkey resolution level (0-29)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing Quadkey cell inspection data with columns:
            - quadkey: Quadkey cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    quadkey_gdf = quadkeygrid(resolution, output_format="gpd")
    quadkey_gdf["crossed"] = quadkey_gdf["geometry"].apply(check_crossing_geom)
    mean_area = quadkey_gdf["cell_area"].mean()
    # Calculate normalized area
    quadkey_gdf["norm_area"] = quadkey_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    quadkey_gdf["ipq"] = (
        4 * np.pi * quadkey_gdf["cell_area"] / (quadkey_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    quadkey_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * quadkey_gdf["cell_area"]
            - np.power(quadkey_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / quadkey_gdf["cell_perimeter"]
    )

    convex_hull = quadkey_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    quadkey_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        quadkey_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    quadkey_gdf["cvh"] = quadkey_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return quadkey_gdf

quadkeyinspect_cli()

Command-line interface for Quadkey cell inspection.

CLI options

-r, --resolution: Quadkey resolution level (0-29)

Source code in vgrid/stats/quadkeystats.py
489
490
491
492
493
494
495
496
497
498
499
500
def quadkeyinspect_cli():
    """
    Command-line interface for Quadkey cell inspection.

    CLI options:
      -r, --resolution: Quadkey resolution level (0-29)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=None)
    args, _ = parser.parse_known_args()
    res = args.resolution if args.resolution is not None else 2
    print(quadkeyinspect(res))

quadkeystats_cli()

Command-line interface for generating Quadkey DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/quadkeystats.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def quadkeystats_cli():
    """
    Command-line interface for generating Quadkey DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()
    unit = args.unit
    # Get the DataFrame
    df = quadkeystats(unit=unit)
    # Display the DataFrame
    print(df)

rhealpixinspect(resolution=0, fix_antimeridian=None)

Generate comprehensive inspection data for rHEALPix DGGS cells at a given resolution.

This function creates a detailed analysis of rHEALPix cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

rHEALPix resolution level (0-15)

0
fix_antimeridian str

Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

None
Source code in vgrid/stats/rhealpixstats.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def rhealpixinspect(resolution: int = 0, fix_antimeridian: str = None):
    """
    Generate comprehensive inspection data for rHEALPix DGGS cells at a given resolution.

    This function creates a detailed analysis of rHEALPix cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: rHEALPix resolution level (0-15)
        fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none
        Defaults to False to avoid splitting the Antimeridian by default.
    Returns:
        geopandas.GeoDataFrame: DataFrame containing rHEALPix cell inspection data with columns:
            - rhealpix: rHEALPix cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
            - cvh: Convex Hull Compactness
    """
    rhealpix_gdf = rhealpixgrid(
        resolution, output_format="gpd", fix_antimeridian=fix_antimeridian
    )  # type: ignore
    rhealpix_gdf["crossed"] = rhealpix_gdf["geometry"].apply(check_crossing_geom)
    mean_area = rhealpix_gdf["cell_area"].mean()
    # Calculate normalized area
    rhealpix_gdf["norm_area"] = rhealpix_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    rhealpix_gdf["ipq"] = (
        4 * np.pi * rhealpix_gdf["cell_area"] / (rhealpix_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    rhealpix_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * rhealpix_gdf["cell_area"]
            - np.power(rhealpix_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / rhealpix_gdf["cell_perimeter"]
    )
    convex_hull = rhealpix_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    rhealpix_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        rhealpix_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    rhealpix_gdf["cvh"] = rhealpix_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return rhealpix_gdf

rhealpixinspect_cli()

Command-line interface for rHEALPix cell inspection.

CLI options

-r, --resolution: rHEALPix resolution level (0-15) -fix, --fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none (default: none)

Source code in vgrid/stats/rhealpixstats.py
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
def rhealpixinspect_cli():
    """
    Command-line interface for rHEALPix cell inspection.

    CLI options:
      -r, --resolution: rHEALPix resolution level (0-15)
      -fix, --fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none (default: none)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-fix",
        "--fix_antimeridian",
        type=str,
        choices=[
            "shift",
            "shift_balanced",
            "shift_west",
            "shift_east",
            "split",
            "none",
        ],
        default=None,
        help="Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none (default: none)",
    )
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    fix_antimeridian = args.fix_antimeridian
    print(rhealpixinspect(resolution, fix_antimeridian=fix_antimeridian))

rhealpixstats_cli()

Command-line interface for generating rHEALPix DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/rhealpixstats.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def rhealpixstats_cli():
    """
    Command-line interface for generating rHEALPix DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = rhealpixstats(unit=unit)

    # Display the DataFrame
    print(df)

s2inspect(resolution, fix_antimeridian=None)

Generate comprehensive inspection data for S2 DGGS cells at a given resolution.

Parameters:

Name Type Description Default
resolution int

S2 resolution level (0-30)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing S2 cell inspection data with columns: - s2: S2 cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/s2stats.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def s2inspect(resolution: int, fix_antimeridian=None):
    """
    Generate comprehensive inspection data for S2 DGGS cells at a given resolution.

    Args:
        resolution: S2 resolution level (0-30)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing S2 cell inspection data with columns:
            - s2: S2 cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    s2_gdf = s2grid(resolution, output_format="gpd", fix_antimeridian=fix_antimeridian)
    s2_gdf["crossed"] = s2_gdf["geometry"].apply(check_crossing_geom)
    mean_area = s2_gdf["cell_area"].mean()
    # Calculate normalized area
    s2_gdf["norm_area"] = s2_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    s2_gdf["ipq"] = 4 * np.pi * s2_gdf["cell_area"] / (s2_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    s2_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * s2_gdf["cell_area"]
            - np.power(s2_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / s2_gdf["cell_perimeter"]
    )
    convex_hull = s2_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    s2_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        s2_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    s2_gdf["cvh"] = s2_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return s2_gdf

s2inspect_cli()

Command-line interface for S2 cell inspection. CLI options: -r, --resolution: S2 resolution level (0-30) -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)

Source code in vgrid/stats/s2stats.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
def s2inspect_cli():
    """
    Command-line interface for S2 cell inspection.
    CLI options:
      -r, --resolution: S2 resolution level (0-30)
      -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-fix",
        "--fix_antimeridian",
        type=str,
        choices=[
            "shift",
            "shift_balanced",
            "shift_west",
            "shift_east",
            "split",
            "none",
        ],
        default=None,
        help="Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none",
    )
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    fix_antimeridian = args.fix_antimeridian
    print(s2inspect(resolution, fix_antimeridian=fix_antimeridian))

s2stats_cli()

Command-line interface for generating S2 DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/s2stats.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def s2stats_cli():
    """
    Command-line interface for generating S2 DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = s2stats(unit=unit)

    # Display the DataFrame
    print(df)

tilecodeinspect(resolution)

Generate comprehensive inspection data for Tilecode DGGS cells at a given resolution.

This function creates a detailed analysis of Tilecode cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

Tilecode resolution level (0-29)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing Tilecode cell inspection data with columns: - tilecode: Tilecode cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/tilecodestats.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def tilecodeinspect(resolution: int):
    """
    Generate comprehensive inspection data for Tilecode DGGS cells at a given resolution.

    This function creates a detailed analysis of Tilecode cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: Tilecode resolution level (0-29)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing Tilecode cell inspection data with columns:
            - tilecode: Tilecode cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    tilecode_gdf = tilecodegrid(resolution, output_format="gpd")
    tilecode_gdf["crossed"] = tilecode_gdf["geometry"].apply(check_crossing_geom)
    mean_area = tilecode_gdf["cell_area"].mean()
    # Calculate normalized area
    tilecode_gdf["norm_area"] = tilecode_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    tilecode_gdf["ipq"] = (
        4 * np.pi * tilecode_gdf["cell_area"] / (tilecode_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    tilecode_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * tilecode_gdf["cell_area"]
            - np.power(tilecode_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / tilecode_gdf["cell_perimeter"]
    )

    convex_hull = tilecode_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    tilecode_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        tilecode_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    tilecode_gdf["cvh"] = tilecode_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return tilecode_gdf

tilecodeinspect_cli()

Command-line interface for Tilecode cell inspection.

CLI options

-r, --resolution: Tilecode resolution level (0-30)

Source code in vgrid/stats/tilecodestats.py
492
493
494
495
496
497
498
499
500
501
502
503
def tilecodeinspect_cli():
    """
    Command-line interface for Tilecode cell inspection.

    CLI options:
      -r, --resolution: Tilecode resolution level (0-30)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()
    resolution = args.resolution
    print(tilecodeinspect(resolution))

tilecodestats_cli()

Command-line interface for generating Tilecode DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/tilecodestats.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def tilecodestats_cli():
    """
    Command-line interface for generating Tilecode DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_known_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = tilecodestats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for H3 DGGS cells.

h3_compactness_cvh(h3_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for H3 cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/h3stats.py
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
def h3_compactness_cvh(h3_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for H3 cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # h3_gdf = h3_gdf[~h3_gdf["crossed"]]  # remove cells that cross the Antimeridian
    h3_gdf = h3_gdf[np.isfinite(h3_gdf["cvh"])]
    h3_gdf = h3_gdf[h3_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    h3_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="H3 CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

h3_compactness_cvh_hist(h3_gdf)

Plot histogram of CVH (cell area / convex hull area) for H3 cells.

Source code in vgrid/stats/h3stats.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
def h3_compactness_cvh_hist(h3_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for H3 cells.
    """
    # Filter out cells that cross the Antimeridian
    #  h3_gdf = h3_gdf[~h3_gdf["crossed"]]
    h3_gdf = h3_gdf[np.isfinite(h3_gdf["cvh"])]
    h3_gdf = h3_gdf[h3_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        h3_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {h3_gdf['cvh'].mean():.6f}\n"
        f"Std: {h3_gdf['cvh'].std():.6f}\n"
        f"Min: {h3_gdf['cvh'].min():.6f}\n"
        f"Max: {h3_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("H3 CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

h3_compactness_ipq(h3_gdf, crs='proj=moll')

Plot IPQ compactness map for H3 cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of H3 cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.907 indicating more regular hexagons.

Parameters:

Name Type Description Default
h3_gdf GeoDataFrame

GeoDataFrame from h3inspect function

required
Source code in vgrid/stats/h3stats.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
def h3_compactness_ipq(h3_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for H3 cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of H3 cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.907 indicating more regular hexagons.

    Args:
        h3_gdf: GeoDataFrame from h3inspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = h3_gdf['ipq'].min(), h3_gdf['ipq'].max(),np.mean([h3_gdf['ipq'].min(), h3_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_HEX, vcenter=VCENTER_HEX, vmax=VMAX_HEX)
    # h3_gdf = h3_gdf[~h3_gdf["crossed"]]  # remove cells that cross the Antimeridian
    h3_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="H3 IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

h3_compactness_ipq_hist(h3_gdf)

Plot histogram of IPQ compactness for H3 cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for H3 cells, helping to understand how close cells are to being regular hexagons.

Parameters:

Name Type Description Default
h3_gdf GeoDataFrame

GeoDataFrame from h3inspect function

required
Source code in vgrid/stats/h3stats.py
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
def h3_compactness_ipq_hist(h3_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for H3 cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for H3 cells, helping
    to understand how close cells are to being regular hexagons.

    Args:
        h3_gdf: GeoDataFrame from h3inspect function
    """
    # Filter out cells that cross the Antimeridian
    # h3_gdf = h3_gdf[~h3_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        h3_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_HEX, vcenter=VCENTER_HEX, vmax=VMAX_HEX)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal hexagon IPQ value (0.907)
    ax.axvline(
        x=0.907,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Hexagon (IPQ = 0.907)",
    )

    # Add statistics text box
    stats_text = f"Mean: {h3_gdf['ipq'].mean():.3f}\nStd: {h3_gdf['ipq'].std():.3f}\nMin: {h3_gdf['ipq'].min():.3f}\nMax: {h3_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("H3 IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

h3_metrics(resolution, unit='m')

Return comprehensive metrics for a resolution including number of cells, average edge length, average area, and area extrema analysis.

Parameters:

Name Type Description Default
resolution int

H3 resolution (0-15)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
dict

Dictionary containing all metrics for the resolution

Source code in vgrid/stats/h3stats.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def h3_metrics(resolution: int, unit: str = "m"):
    """
    Return comprehensive metrics for a resolution including number of cells,
    average edge length, average area, and area extrema analysis.

    Args:
        resolution: H3 resolution (0-15)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        dict: Dictionary containing all metrics for the resolution
    """
    length_unit = unit
    if length_unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    area_unit = {"m": "m^2", "km": "km^2"}[length_unit]

    # Basic metrics
    num_cells = h3.get_num_cells(resolution)
    avg_edge_len = h3.average_hexagon_edge_length(resolution, unit=length_unit)
    avg_area = h3.average_hexagon_area(resolution, area_unit)
    # Compute CLS (Characteristic Length Scale) always in meters first
    if length_unit == "m":
        cls = characteristic_length_scale(avg_area, unit=length_unit)
    elif length_unit == "km":
        avg_area_m2 = avg_area * (10**6)
        cls = characteristic_length_scale(avg_area_m2, unit=length_unit)

    # Return CLS in requested length unit
    # Area extrema analysis
    # Precompute base (resolution 0) hex cells (exclude pentagons)
    base_hex_cells = [idx for idx in h3.get_res0_cells() if not h3.is_pentagon(idx)]

    pentagons = list(h3.get_pentagons(resolution))

    # All hex neighbors of pentagons (exclude the pentagon cell itself)
    pentagon_neighbors = []
    for p in pentagons:
        neighbors = [n for n in h3.grid_disk(p, 1) if n != p]
        pentagon_neighbors.extend(neighbors)

    # Compute areas
    # Smallest hex area among pentagon neighbors
    min_hex_area = min(
        (h3.cell_area(idx, unit=area_unit) for idx in pentagon_neighbors),
        default=float("nan"),
    )

    # Largest hex area among center children of base hex cells
    center_children = [
        idx if resolution == 0 else h3.cell_to_center_child(idx, resolution) for idx in base_hex_cells
    ]
    max_hex_area = max(
        (h3.cell_area(idx, unit=area_unit) for idx in center_children),
        default=float("nan"),
    )

    # Smallest pentagon area
    min_pent_area = min(
        (h3.cell_area(idx, unit=area_unit) for idx in pentagons), default=float("nan")
    )

    # Ratios
    # hex_ratio = (
    #     (max_hex_area / min_hex_area)
    #     if (min_hex_area not in (0.0, float("nan")))
    #     else float("nan")
    # )
    hex_pent_ratio = (
        (max_hex_area / min_pent_area)
        if (min_pent_area not in (0.0, float("nan")))
        else float("nan")
    )

    return {
        "resolution": resolution,
        "number_of_cells": num_cells,
        "avg_edge_len": avg_edge_len,
        "avg_area": avg_area,
        "min_area": min_pent_area,
        "max_area": max_hex_area,
        "max_min_ratio": hex_pent_ratio,
        "cls": cls,
    }

h3_norm_area(h3_gdf, crs='proj=moll')

Plot normalized area map for H3 cells.

This function creates a visualization showing how H3 cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
h3_gdf GeoDataFrame

GeoDataFrame from h3inspect function

required
Source code in vgrid/stats/h3stats.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def h3_norm_area(h3_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for H3 cells.

    This function creates a visualization showing how H3 cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        h3_gdf: GeoDataFrame from h3inspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = (
        h3_gdf["norm_area"].min(),
        h3_gdf["norm_area"].mean(),
        h3_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # h3_gdf = h3_gdf[~h3_gdf["crossed"]]  # remove cells that cross the Antimeridian
    h3_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="H3 Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

h3_norm_area_hist(h3_gdf)

Plot histogram of normalized area for H3 cells.

This function creates a histogram visualization showing the distribution of normalized areas for H3 cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
h3_gdf GeoDataFrame

GeoDataFrame from h3inspect function

required
Source code in vgrid/stats/h3stats.py
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
def h3_norm_area_hist(h3_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for H3 cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for H3 cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        h3_gdf: GeoDataFrame from h3inspect function
    """
    # Filter out cells that cross the Antimeridian
    # h3_gdf = h3_gdf[~h3_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        h3_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        h3_gdf["norm_area"].min(),
        h3_gdf["norm_area"].mean(),
        h3_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {h3_gdf['norm_area'].mean():.3f}\nStd: {h3_gdf['norm_area'].std():.3f}\nMin: {h3_gdf['norm_area'].min():.3f}\nMax: {h3_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("H3 normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    # Set y-axis ticks to 200, 400, 600 intervals
    y_max = ax.get_ylim()[1]
    y_ticks = np.arange(0, y_max + 200, 200)
    ax.set_yticks(y_ticks)

    plt.tight_layout()

h3inspect(resolution, fix_antimeridian=None)

Generate comprehensive inspection data for H3 DGGS cells at a given resolution.

This function creates a detailed analysis of H3 cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

H3 resolution level (0-15)

required
fix_antimeridian None

Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

None

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing H3 cell inspection data with columns: - h3: H3 cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - is_pentagon: Whether cell is a pentagon - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/h3stats.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def h3inspect(resolution: int, fix_antimeridian: None = None):
    """
    Generate comprehensive inspection data for H3 DGGS cells at a given resolution.

    This function creates a detailed analysis of H3 cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: H3 resolution level (0-15)
        fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

    Returns:
        geopandas.GeoDataFrame: DataFrame containing H3 cell inspection data with columns:
            - h3: H3 cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - is_pentagon: Whether cell is a pentagon
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    h3_gdf = h3grid(resolution, output_format="gpd", fix_antimeridian=fix_antimeridian)
    h3_gdf["crossed"] = h3_gdf["geometry"].apply(check_crossing_geom)
    h3_gdf["is_pentagon"] = h3_gdf["h3"].apply(h3.is_pentagon)
    mean_area = h3_gdf["cell_area"].mean()
    # Calculate normalized area
    h3_gdf["norm_area"] = h3_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    h3_gdf["ipq"] = 4 * np.pi * h3_gdf["cell_area"] / (h3_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    h3_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * h3_gdf["cell_area"]
            - np.power(h3_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / h3_gdf["cell_perimeter"]
    )

    convex_hull = h3_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    h3_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        h3_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    h3_gdf["cvh"] = h3_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return h3_gdf

h3inspect_cli()

Command-line interface for H3 cell inspection.

CLI options

-r, --resolution: H3 resolution level (0-15) -split, --split_antimeridian: Apply antimeridian fixing to the resulting polygons

Source code in vgrid/stats/h3stats.py
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
def h3inspect_cli():
    """
    Command-line interface for H3 cell inspection.

    CLI options:
      -r, --resolution: H3 resolution level (0-15)
      -split, --split_antimeridian: Apply antimeridian fixing to the resulting polygons
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-fix--fix_antimeridian",
        type=str,
        choices=[
            "shift",
            "shift_balanced",
            "shift_west",
            "shift_east",
            "split",
            "none",
        ],
        default=None,
        help="Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none",
    )
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    print(h3inspect(resolution, fix_antimeridian=args.fix_antimeridian))

h3stats(unit='m')

Generate comprehensive statistics for H3 DGGS cells.

This function combines basic H3 statistics (number of cells, edge lengths, areas) with area extrema analysis (min/max areas and ratios).

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing comprehensive H3 DGGS statistics with columns: - resolution: Resolution level (0-15) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_area_{unit}2: Average cell area in the squared unit - min_area_{unit}2: Minimum pentagon area - max_area_{unit}2: Maximum hexagon area - max_min_ratio: Ratio of max hexagon area to min pentagon area

Source code in vgrid/stats/h3stats.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def h3stats(unit: str = "m"):
    """
    Generate comprehensive statistics for H3 DGGS cells.

    This function combines basic H3 statistics (number of cells, edge lengths, areas)
    with area extrema analysis (min/max areas and ratios).

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing comprehensive H3 DGGS statistics with columns:
            - resolution: Resolution level (0-15)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_area_{unit}2: Average cell area in the squared unit
            - min_area_{unit}2: Minimum pentagon area
            - max_area_{unit}2: Maximum hexagon area
            - max_min_ratio: Ratio of max hexagon area to min pentagon area
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_areas = []
    min_areas = []
    max_areas = []
    max_min_ratios = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        # Get comprehensive metrics
        metrics_data = h3_metrics(res, unit=unit)  # length unit is km, area unit is km2

        resolutions.append(res)
        num_cells_list.append(metrics_data["number_of_cells"])
        avg_edge_lens.append(metrics_data["avg_edge_len"])
        avg_areas.append(metrics_data["avg_area"])
        min_areas.append(metrics_data["min_area"])
        max_areas.append(metrics_data["max_area"])
        max_min_ratios.append(metrics_data["max_min_ratio"])
        cls_list.append(metrics_data["cls"])
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    avg_edge_len = f"avg_edge_len_{unit}"
    avg_area = f"avg_area_{unit}"
    min_area = f"min_area_{unit}"
    max_area = f"max_area_{unit}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_area: avg_areas,
            min_area: min_areas,
            max_area: max_areas,
            "max_min_ratio": max_min_ratios,
            cls_label: cls_list,
        }
    )

    return df

h3stats_cli()

Command-line interface for generating H3 DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/h3stats.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def h3stats_cli():
    """
    Command-line interface for generating H3 DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    df = h3stats(unit=unit)
    df["number_of_cells"] = df["number_of_cells"].apply(lambda x: "{:,.0f}".format(x))
    print(df)

This module provides functions for generating statistics for S2 DGGS cells.

s2_compactness_cvh(s2_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for H3 cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/s2stats.py
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
def s2_compactness_cvh(s2_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for H3 cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # s2_gdf = s2_gdf[~s2_gdf["crossed"]]  # remove cells that cross the dateline
    # Remove non-finite CVH values before plotting
    s2_gdf = s2_gdf[np.isfinite(s2_gdf["cvh"])]
    s2_gdf = s2_gdf[s2_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    s2_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="S2 CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

s2_compactness_cvh_hist(s2_gdf)

Plot histogram of CVH (cell area / convex hull area) for S2 cells.

Source code in vgrid/stats/s2stats.py
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
def s2_compactness_cvh_hist(s2_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for S2 cells.
    """
    # Filter out cells that cross the dateline
    # s2_gdf = s2_gdf[~s2_gdf["crossed"]]
    s2_gdf = s2_gdf[np.isfinite(s2_gdf["cvh"])]
    s2_gdf = s2_gdf[s2_gdf["cvh"] <= 1.1]
    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        s2_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {s2_gdf['cvh'].mean():.6f}\n"
        f"Std: {s2_gdf['cvh'].std():.6f}\n"
        f"Min: {s2_gdf['cvh'].min():.6f}\n"
        f"Max: {s2_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("S2 CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

s2_compactness_ipq(s2_gdf, crs='proj=moll')

Plot IPQ compactness map for S2 cells.

Parameters:

Name Type Description Default
s2_gdf GeoDataFrame

GeoDataFrame from s2inspect function

required
Source code in vgrid/stats/s2stats.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
def s2_compactness_ipq(s2_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for S2 cells.

    Args:
        s2_gdf: GeoDataFrame from s2inspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = s2_gdf['ipq'].min(), s2_gdf['ipq'].max(),np.mean([s2_gdf['ipq'].min(), s2_gdf['ipq'].max()])
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # s2_gdf = s2_gdf[~s2_gdf["crossed"]]  # remove cells that cross the dateline
    s2_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="S2 IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

s2_compactness_ipq_hist(s2_gdf)

Plot histogram of IPQ compactness for S2 cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for S2 cells, helping to understand how close cells are to being regular squares.

Parameters:

Name Type Description Default
s2_gdf GeoDataFrame

GeoDataFrame from s2inspect function

required
Source code in vgrid/stats/s2stats.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
def s2_compactness_ipq_hist(s2_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for S2 cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for S2 cells, helping
    to understand how close cells are to being regular squares.

    Args:
        s2_gdf: GeoDataFrame from s2inspect function
    """
    # Filter out cells that cross the dateline
    # s2_gdf = s2_gdf[~s2_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        s2_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal square IPQ value (1.0)
    ax.axvline(
        x=1.0,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Square (IPQ = 1.0)",
    )

    # Add statistics text box
    stats_text = f"Mean: {s2_gdf['ipq'].mean():.3f}\nStd: {s2_gdf['ipq'].std():.3f}\nMin: {s2_gdf['ipq'].min():.3f}\nMax: {s2_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("S2 IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

s2_metrics(resolution, unit='m')

Calculate metrics for S2 DGGS cells at a given resolution.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-30)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, edge_length_in_unit, cell_area_in_unit_squared)

Source code in vgrid/stats/s2stats.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def s2_metrics(resolution: int, unit: str = "m"):  # length unit is km, area unit is km2
    """
    Calculate metrics for S2 DGGS cells at a given resolution.

    Args:
        resolution: Resolution level (0-30)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, edge_length_in_unit, cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = 6 * (4**resolution)

    # Calculate area in km² first
    avg_cell_area = AUTHALIC_AREA / num_cells  # area in m2
    avg_edge_len = math.sqrt(avg_cell_area)
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m

    return num_cells, avg_edge_len, avg_cell_area, cls

s2_norm_area(s2_gdf, crs='proj=moll')

Plot normalized area map for S2 cells.

Parameters:

Name Type Description Default
s2_gdf GeoDataFrame

GeoDataFrame from s2inspect function

required
Source code in vgrid/stats/s2stats.py
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def s2_norm_area(s2_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for S2 cells.

    Args:
        s2_gdf: GeoDataFrame from s2inspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = s2_gdf["norm_area"].min(), 1.0, s2_gdf["norm_area"].max()
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # s2_gdf = s2_gdf[~s2_gdf["crossed"]]  # remove cells that cross the dateline
    s2_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="S2 Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

s2_norm_area_hist(s2_gdf)

Plot histogram of normalized area for S2 cells.

This function creates a histogram visualization showing the distribution of normalized areas for S2 cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
s2_gdf GeoDataFrame

GeoDataFrame from s2inspect function

required
Source code in vgrid/stats/s2stats.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
def s2_norm_area_hist(s2_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for S2 cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for S2 cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        s2_gdf: GeoDataFrame from s2inspect function
    """
    # Filter out cells that cross the dateline
    # s2_gdf = s2_gdf[~s2_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        s2_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (s2_gdf["norm_area"].min(), 1.0, s2_gdf["norm_area"].max())
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {s2_gdf['norm_area'].mean():.3f}\nStd: {s2_gdf['norm_area'].std():.3f}\nMin: {s2_gdf['norm_area'].min():.3f}\nMax: {s2_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("S2 normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

s2inspect(resolution, fix_antimeridian=None)

Generate comprehensive inspection data for S2 DGGS cells at a given resolution.

Parameters:

Name Type Description Default
resolution int

S2 resolution level (0-30)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing S2 cell inspection data with columns: - s2: S2 cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/s2stats.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def s2inspect(resolution: int, fix_antimeridian=None):
    """
    Generate comprehensive inspection data for S2 DGGS cells at a given resolution.

    Args:
        resolution: S2 resolution level (0-30)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing S2 cell inspection data with columns:
            - s2: S2 cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    s2_gdf = s2grid(resolution, output_format="gpd", fix_antimeridian=fix_antimeridian)
    s2_gdf["crossed"] = s2_gdf["geometry"].apply(check_crossing_geom)
    mean_area = s2_gdf["cell_area"].mean()
    # Calculate normalized area
    s2_gdf["norm_area"] = s2_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    s2_gdf["ipq"] = 4 * np.pi * s2_gdf["cell_area"] / (s2_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    s2_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * s2_gdf["cell_area"]
            - np.power(s2_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / s2_gdf["cell_perimeter"]
    )
    convex_hull = s2_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    s2_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        s2_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    s2_gdf["cvh"] = s2_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return s2_gdf

s2inspect_cli()

Command-line interface for S2 cell inspection. CLI options: -r, --resolution: S2 resolution level (0-30) -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)

Source code in vgrid/stats/s2stats.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
def s2inspect_cli():
    """
    Command-line interface for S2 cell inspection.
    CLI options:
      -r, --resolution: S2 resolution level (0-30)
      -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-fix",
        "--fix_antimeridian",
        type=str,
        choices=[
            "shift",
            "shift_balanced",
            "shift_west",
            "shift_east",
            "split",
            "none",
        ],
        default=None,
        help="Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none",
    )
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    fix_antimeridian = args.fix_antimeridian
    print(s2inspect(resolution, fix_antimeridian=fix_antimeridian))

s2stats(unit='m')

Generate statistics for S2 DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing S2 DGGS statistics with columns: - Resolution: Resolution level (0-30) - Number_of_Cells: Number of cells at each resolution - Avg_Edge_Length_{unit}: Average edge length in the given unit - Avg_Cell_Area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/s2stats.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def s2stats(unit: str = "m"):  # length unit is km, area unit is km2
    """
    Generate statistics for S2 DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing S2 DGGS statistics with columns:
            - Resolution: Resolution level (0-30)
            - Number_of_Cells: Number of cells at each resolution
            - Avg_Edge_Length_{unit}: Average edge length in the given unit
            - Avg_Cell_Area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = s2_metrics(
            res, unit=unit
        )  # length unit is km, area unit is km2
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

s2stats_cli()

Command-line interface for generating S2 DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/s2stats.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def s2stats_cli():
    """
    Command-line interface for generating S2 DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = s2stats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for A5 DGGS cells.

a5_compactness_cvh(a5_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for A5 cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/a5stats.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
def a5_compactness_cvh(a5_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for A5 cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # a5_gdf = a5_gdf[~a5_gdf["crossed"]]  # remove cells that cross the Antimeridian
    a5_gdf = a5_gdf[np.isfinite(a5_gdf["cvh"])]
    a5_gdf = a5_gdf[a5_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    a5_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="A5 CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

a5_compactness_cvh_hist(a5_gdf)

Plot histogram of CVH (cell area / convex hull area) for A5 cells.

Source code in vgrid/stats/a5stats.py
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
def a5_compactness_cvh_hist(a5_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for A5 cells.
    """
    # Filter out cells that cross the Antimeridian
    #  a5_gdf = a5_gdf[~a5_gdf["crossed"]]
    a5_gdf = a5_gdf[np.isfinite(a5_gdf["cvh"])]
    a5_gdf = a5_gdf[a5_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        a5_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {a5_gdf['cvh'].mean():.6f}\n"
        f"Std: {a5_gdf['cvh'].std():.6f}\n"
        f"Min: {a5_gdf['cvh'].min():.6f}\n"
        f"Max: {a5_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("A5 CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

a5_compactness_ipq(a5_gdf, crs='proj=moll')

Plot IPQ compactness map for A5 cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of A5 cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.907 indicating more regular hexagons.

Parameters:

Name Type Description Default
a5_gdf GeoDataFrame

GeoDataFrame from a5inspect function

required
Source code in vgrid/stats/a5stats.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def a5_compactness_ipq(a5_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for A5 cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of A5 cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.907 indicating more regular hexagons.

    Args:
        a5_gdf: GeoDataFrame from a5inspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = a5_gdf['ipq'].min(), a5_gdf['ipq'].max(),np.mean([a5_gdf['ipq'].min(), a5_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_PEN, vcenter=VCENTER_PEN, vmax=VMAX_PEN)
    # a5_gdf = a5_gdf[~a5_gdf["crossed"]]  # remove cells that cross the Antimeridian

    a5_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="A5 IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

a5_compactness_ipq_hist(a5_gdf)

Plot histogram of IPQ compactness for A5 cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for A5 cells, helping to understand how close cells are to being regular hexagons.

Parameters:

Name Type Description Default
a5_gdf GeoDataFrame

GeoDataFrame from a5inspect function

required
Source code in vgrid/stats/a5stats.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
def a5_compactness_ipq_hist(a5_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for A5 cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for A5 cells, helping
    to understand how close cells are to being regular hexagons.

    Args:
        a5_gdf: GeoDataFrame from a5inspect function
    """
    # Filter out cells that cross the Antimeridian
    # a5_gdf = a5_gdf[~a5_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        a5_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_PEN, vcenter=VCENTER_PEN, vmax=VMAX_PEN)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal hexagon IPQ value (0.907)
    ax.axvline(
        x=0.907,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Hexagon (IPQ = 0.907)",
    )

    # Add statistics text box
    stats_text = f"Mean: {a5_gdf['ipq'].mean():.6f}\nStd: {a5_gdf['ipq'].std():.6f}\nMin: {a5_gdf['ipq'].min():.6f}\nMax: {a5_gdf['ipq'].max():.6f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("A5 IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

a5_metrics(resolution, unit='m')

Calculate metrics for A5 DGGS cells at a given resolution.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-29)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, edge_length_in_unit, cell_area_in_unit_squared)

Source code in vgrid/stats/a5stats.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def a5_metrics(resolution: int, unit: str = "m"):  # length unit is m, area unit is m2
    """
    Calculate metrics for A5 DGGS cells at a given resolution.

    Args:
        resolution: Resolution level (0-29)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, edge_length_in_unit, cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = get_num_cells(resolution)
    avg_cell_area = cell_area(resolution)  # cell_area returns area in m²

    # Calculate edge length in meters
    k = math.sqrt(5 * (5 + 2 * math.sqrt(5)))
    avg_edge_len = math.sqrt(4 * avg_cell_area / k)  # edge length in m
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_edge_len = avg_edge_len / (10**3)
        avg_cell_area = avg_cell_area / (10**6)

    return num_cells, avg_edge_len, avg_cell_area, cls

a5_norm_area(a5_gdf, crs='proj=moll')

Plot normalized area map for A5 cells.

This function creates a visualization showing how A5 cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
a5_gdf GeoDataFrame

GeoDataFrame from a5inspect function

required
Source code in vgrid/stats/a5stats.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def a5_norm_area(a5_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for A5 cells.

    This function creates a visualization showing how A5 cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        a5_gdf: GeoDataFrame from a5inspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = a5_gdf["norm_area"].min(), 1.0, a5_gdf["norm_area"].max()
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # a5_gdf = a5_gdf[~a5_gdf["crossed"]]  # remove cells that cross the Antimeridian
    a5_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="A5 Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

a5_norm_area_hist(a5_gdf)

Plot histogram of normalized area for A5 cells.

This function creates a histogram visualization showing the distribution of normalized areas for A5 cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
a5_gdf GeoDataFrame

GeoDataFrame from a5inspect function

required
Source code in vgrid/stats/a5stats.py
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def a5_norm_area_hist(a5_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for A5 cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for A5 cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        a5_gdf: GeoDataFrame from a5inspect function
    """
    # Filter out cells that cross the Antimeridian
    # a5_gdf = a5_gdf[~a5_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        a5_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        a5_gdf["norm_area"].min(),
        1.0,
        a5_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {a5_gdf['norm_area'].mean():.6f}\nStd: {a5_gdf['norm_area'].std():.6f}\nMin: {a5_gdf['norm_area'].min():.6f}\nMax: {a5_gdf['norm_area'].max():.6f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("A5 normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

a5inspect(resolution, options={'segments': 'auto'}, split_antimeridian=False)

Generate comprehensive inspection data for A5 DGGS cells at a given resolution.

This function creates a detailed analysis of A5 cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

A5 resolution level (0-29)

required
options

Optional dictionary of options for grid generation

{'segments': 'auto'}
split_antimeridian bool

When True, apply antimeridian splitting to the resulting polygons. Defaults to False when None or omitted.

False

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing A5 cell inspection data with columns: - a5: A5 cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/a5stats.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def a5inspect(resolution: int, options={"segments": 'auto'}, split_antimeridian: bool = False):
    """
    Generate comprehensive inspection data for A5 DGGS cells at a given resolution.

    This function creates a detailed analysis of A5 cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: A5 resolution level (0-29)
        options: Optional dictionary of options for grid generation
        split_antimeridian: When True, apply antimeridian splitting to the resulting polygons.
            Defaults to False when None or omitted.

    Returns:
        geopandas.GeoDataFrame: DataFrame containing A5 cell inspection data with columns:
            - a5: A5 cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    a5_gdf = a5grid(
        resolution, output_format="gpd", options=options, split_antimeridian=split_antimeridian
    )
    a5_gdf["crossed"] = a5_gdf["geometry"].apply(check_crossing_geom)
    mean_area = a5_gdf["cell_area"].mean()
    # Calculate normalized area
    a5_gdf["norm_area"] = a5_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    a5_gdf["ipq"] = 4 * np.pi * a5_gdf["cell_area"] / (a5_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    a5_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * a5_gdf["cell_area"]
            - np.power(a5_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / a5_gdf["cell_perimeter"]
    )

    convex_hull = a5_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    a5_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        a5_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    a5_gdf["cvh"] = a5_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return a5_gdf

a5inspect_cli()

Command-line interface for A5 cell inspection.

CLI options

-r, --resolution: A5 resolution level (0-29) -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)

Source code in vgrid/stats/a5stats.py
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def a5inspect_cli():
    """
    Command-line interface for A5 cell inspection.

    CLI options:
      -r, --resolution: A5 resolution level (0-29)
      -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-split",
        "--split_antimeridian",
        action="store_true",
        default=False,  # default is False to avoid splitting the Antimeridian by default
        help="Enable antimeridian splitting",
    )
    args = parser.parse_args()
    resolution = args.resolution
    print(a5inspect(resolution, split_antimeridian=args.split_antimeridian))

a5stats(unit='m')

Generate statistics for A5 DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing A5 DGGS statistics with columns: - Resolution: Resolution level (0-29) - Number_of_Cells: Number of cells at each resolution - Avg_Edge_Length_{unit}: Average edge length in the given unit - CLS: Characteristic length scale in the given unit - Avg_Cell_Area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/a5stats.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def a5stats(unit: str = "m"):
    """
    Generate statistics for A5 DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing A5 DGGS statistics with columns:
            - Resolution: Resolution level (0-29)
            - Number_of_Cells: Number of cells at each resolution
            - Avg_Edge_Length_{unit}: Average edge length in the given unit
            - CLS: Characteristic length scale in the given unit
            - Avg_Cell_Area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Derive bounds from central constants registry

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = a5_metrics(
            res, unit=unit
        )  # length unit is m, area unit is m2
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)

    # Create DataFrame
    # Build column labels with unit awareness
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    cls_label = f"cls_{unit}"
    avg_cell_area = f"avg_cell_area_{unit_area_label}"

    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        },
        index=None,
    )

    return df

a5stats_cli()

Command-line interface for generating A5 DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/a5stats.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def a5stats_cli():
    """
    Command-line interface for generating A5 DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()
    unit = args.unit
    # Get the DataFrame
    df = a5stats(unit=unit)
    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for rHEALPix DGGS cells.

rhealpix_compactness_cvh(rhealpix_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for A5 cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/rhealpixstats.py
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
def rhealpix_compactness_cvh(
    rhealpix_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot CVH (cell area / convex hull area) compactness map for A5 cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # rhealpix_gdf = rhealpix_gdf[~rhealpix_gdf["crossed"]]  # remove cells that cross the dateline
    rhealpix_gdf = rhealpix_gdf[np.isfinite(rhealpix_gdf["cvh"])]
    rhealpix_gdf = rhealpix_gdf[rhealpix_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    rhealpix_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="rHEALPix CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

rhealpix_compactness_cvh_hist(rhealpix_gdf)

Plot histogram of CVH (cell area / convex hull area) for rHEALPix cells.

Source code in vgrid/stats/rhealpixstats.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
def rhealpix_compactness_cvh_hist(rhealpix_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for rHEALPix cells.
    """
    # Filter out cells that cross the dateline
    # rhealpix_gdf = rhealpix_gdf[~rhealpix_gdf["crossed"]]
    rhealpix_gdf = rhealpix_gdf[np.isfinite(rhealpix_gdf["cvh"])]
    rhealpix_gdf = rhealpix_gdf[rhealpix_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        rhealpix_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {rhealpix_gdf['cvh'].mean():.6f}\n"
        f"Std: {rhealpix_gdf['cvh'].std():.6f}\n"
        f"Min: {rhealpix_gdf['cvh'].min():.6f}\n"
        f"Max: {rhealpix_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("rHEALPix CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

rhealpix_compactness_ipq(rhealpix_gdf, crs='proj=moll')

Plot IPQ compactness map for rHEALPix cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of rHEALPix cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.907 indicating more regular hexagons.

Parameters:

Name Type Description Default
rhealpix_gdf GeoDataFrame

GeoDataFrame from rhealpixinspect function

required
Source code in vgrid/stats/rhealpixstats.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def rhealpix_compactness_ipq(
    rhealpix_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot IPQ compactness map for rHEALPix cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of rHEALPix cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.907 indicating more regular hexagons.

    Args:
        rhealpix_gdf: GeoDataFrame from rhealpixinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = rhealpix_gdf['ipq'].min(), rhealpix_gdf['ipq'].max(), np.mean([rhealpix_gdf['ipq'].min(), rhealpix_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)
    # rhealpix_gdf = rhealpix_gdf[~rhealpix_gdf["crossed"]]  # remove cells that cross the dateline
    rhealpix_gdf.to_crs(crs).plot(  # type: ignore
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="rHEALPix IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

rhealpix_compactness_ipq_hist(rhealpix_gdf)

Plot histogram of IPQ compactness for rHEALPix cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for rHEALPix cells, helping to understand how close cells are to being regular quadrilaterals.

Parameters:

Name Type Description Default
rhealpix_gdf GeoDataFrame

GeoDataFrame from rhealpixinspect function

required
Source code in vgrid/stats/rhealpixstats.py
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
def rhealpix_compactness_ipq_hist(rhealpix_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for rHEALPix cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for rHEALPix cells, helping
    to understand how close cells are to being regular quadrilaterals.

    Args:
        rhealpix_gdf: GeoDataFrame from rhealpixinspect function
    """
    # Filter out cells that cross the dateline
    # rhealpix_gdf = rhealpix_gdf[~rhealpix_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        rhealpix_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal quadrilateral IPQ value (1.0)
    ax.axvline(
        x=1.0,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Quadrilateral (IPQ = 1.0)",
    )

    # Add statistics text box
    stats_text = f"Mean: {rhealpix_gdf['ipq'].mean():.3f}\nStd: {rhealpix_gdf['ipq'].std():.3f}\nMin: {rhealpix_gdf['ipq'].min():.3f}\nMax: {rhealpix_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("rHEALPix IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

rhealpix_metrics(resolution, unit='m')

Calculate metrics for rHEALPix DGGS cells at a given resolution.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-30)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, edge_length_in_unit, cell_area_in_unit_squared)

Source code in vgrid/stats/rhealpixstats.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def rhealpix_metrics(resolution: int, unit: str = "m"):  # length unit is km, area unit is km2
    """
    Calculate metrics for rHEALPix DGGS cells at a given resolution.

    Args:
        resolution: Resolution level (0-30)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, edge_length_in_unit, cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = 6 * 9 ** (resolution)

    # Calculate area in km² first
    avg_cell_area = AUTHALIC_AREA / num_cells  # area in m2
    avg_edge_len = math.sqrt(avg_cell_area)
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m
        cls = cls / (10**3)  # Convert km to m
    return num_cells, avg_edge_len, avg_cell_area, cls

rhealpix_norm_area(rhealpix_gdf, crs='proj=moll')

Plot normalized area map for rHEALPix cells.

This function creates a visualization showing how rHEALPix cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
rhealpix_gdf GeoDataFrame

GeoDataFrame from rhealpixinspect function

required
Source code in vgrid/stats/rhealpixstats.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def rhealpix_norm_area(rhealpix_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for rHEALPix cells.

    This function creates a visualization showing how rHEALPix cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        rhealpix_gdf: GeoDataFrame from rhealpixinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = (
        rhealpix_gdf["norm_area"].min(),
        1.0,
        rhealpix_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    rhealpix_gdf = rhealpix_gdf[
        ~rhealpix_gdf["crossed"]
    ]  # remove cells that cross the dateline
    rhealpix_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="rHEALPix Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

rhealpix_norm_area_hist(rhealpix_gdf)

Plot histogram of normalized area for rHEALPix cells.

This function creates a histogram visualization showing the distribution of normalized areas for rHEALPix cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
rhealpix_gdf GeoDataFrame

GeoDataFrame from rhealpixinspect function

required
Source code in vgrid/stats/rhealpixstats.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
def rhealpix_norm_area_hist(rhealpix_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for rHEALPix cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for rHEALPix cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        rhealpix_gdf: GeoDataFrame from rhealpixinspect function
    """
    # Filter out cells that cross the dateline
    # rhealpix_gdf = rhealpix_gdf[~rhealpix_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        rhealpix_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        rhealpix_gdf["norm_area"].min(),
        1.0,
        rhealpix_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {rhealpix_gdf['norm_area'].mean():.3f}\nStd: {rhealpix_gdf['norm_area'].std():.3f}\nMin: {rhealpix_gdf['norm_area'].min():.3f}\nMax: {rhealpix_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("rHEALPix normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

rhealpixinspect(resolution=0, fix_antimeridian=None)

Generate comprehensive inspection data for rHEALPix DGGS cells at a given resolution.

This function creates a detailed analysis of rHEALPix cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

rHEALPix resolution level (0-15)

0
fix_antimeridian str

Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

None
Source code in vgrid/stats/rhealpixstats.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def rhealpixinspect(resolution: int = 0, fix_antimeridian: str = None):
    """
    Generate comprehensive inspection data for rHEALPix DGGS cells at a given resolution.

    This function creates a detailed analysis of rHEALPix cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: rHEALPix resolution level (0-15)
        fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none
        Defaults to False to avoid splitting the Antimeridian by default.
    Returns:
        geopandas.GeoDataFrame: DataFrame containing rHEALPix cell inspection data with columns:
            - rhealpix: rHEALPix cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
            - cvh: Convex Hull Compactness
    """
    rhealpix_gdf = rhealpixgrid(
        resolution, output_format="gpd", fix_antimeridian=fix_antimeridian
    )  # type: ignore
    rhealpix_gdf["crossed"] = rhealpix_gdf["geometry"].apply(check_crossing_geom)
    mean_area = rhealpix_gdf["cell_area"].mean()
    # Calculate normalized area
    rhealpix_gdf["norm_area"] = rhealpix_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    rhealpix_gdf["ipq"] = (
        4 * np.pi * rhealpix_gdf["cell_area"] / (rhealpix_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    rhealpix_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * rhealpix_gdf["cell_area"]
            - np.power(rhealpix_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / rhealpix_gdf["cell_perimeter"]
    )
    convex_hull = rhealpix_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    rhealpix_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        rhealpix_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    rhealpix_gdf["cvh"] = rhealpix_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return rhealpix_gdf

rhealpixinspect_cli()

Command-line interface for rHEALPix cell inspection.

CLI options

-r, --resolution: rHEALPix resolution level (0-15) -fix, --fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none (default: none)

Source code in vgrid/stats/rhealpixstats.py
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
def rhealpixinspect_cli():
    """
    Command-line interface for rHEALPix cell inspection.

    CLI options:
      -r, --resolution: rHEALPix resolution level (0-15)
      -fix, --fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none (default: none)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-fix",
        "--fix_antimeridian",
        type=str,
        choices=[
            "shift",
            "shift_balanced",
            "shift_west",
            "shift_east",
            "split",
            "none",
        ],
        default=None,
        help="Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none (default: none)",
    )
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    fix_antimeridian = args.fix_antimeridian
    print(rhealpixinspect(resolution, fix_antimeridian=fix_antimeridian))

rhealpixstats(unit='m')

Generate statistics for rHEALPix DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing rHEALPix DGGS statistics with columns: - Resolution: Resolution level (0-30) - Number_of_Cells: Number of cells at each resolution - Avg_Edge_Length_{unit}: Average edge length in the given unit - Avg_Cell_Area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/rhealpixstats.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def rhealpixstats(unit: str = "m"):
    """
    Generate statistics for rHEALPix DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing rHEALPix DGGS statistics with columns:
            - Resolution: Resolution level (0-30)
            - Number_of_Cells: Number of cells at each resolution
            - Avg_Edge_Length_{unit}: Average edge length in the given unit
            - Avg_Cell_Area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = rhealpix_metrics(
            res, unit=unit
        )  # length unit is km, area unit is km2
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

rhealpixstats_cli()

Command-line interface for generating rHEALPix DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/rhealpixstats.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def rhealpixstats_cli():
    """
    Command-line interface for generating rHEALPix DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = rhealpixstats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for ISEA4T DGGS cells.

isea4t_compactness_cvh(isea4t_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for ISEA4T cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/isea4tstats.py
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
def isea4t_compactness_cvh(isea4t_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for ISEA4T cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # isea4t_gdf = isea4t_gdf[~isea4t_gdf["crossed"]]  # remove cells that cross the dateline if fix_antimeridian is None
    isea4t_gdf = isea4t_gdf[np.isfinite(isea4t_gdf["cvh"])]
    isea4t_gdf = isea4t_gdf[isea4t_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    isea4t_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="ISEA4T CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

isea4t_compactness_cvh_hist(isea4t_gdf)

Plot histogram of CVH (cell area / convex hull area) for ISEA4T cells.

Source code in vgrid/stats/isea4tstats.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def isea4t_compactness_cvh_hist(isea4t_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for ISEA4T cells.
    """
    # Filter out cells that cross the dateline
    # isea4t_gdf = isea4t_gdf[~isea4t_gdf["crossed"]]  # remove cells that cross the dateline if fix_antimeridian is None
    isea4t_gdf = isea4t_gdf[np.isfinite(isea4t_gdf["cvh"])]
    isea4t_gdf = isea4t_gdf[isea4t_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        isea4t_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {isea4t_gdf['cvh'].mean():.6f}\n"
        f"Std: {isea4t_gdf['cvh'].std():.6f}\n"
        f"Min: {isea4t_gdf['cvh'].min():.6f}\n"
        f"Max: {isea4t_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("ISEA4T CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

isea4t_compactness_ipq(isea4t_gdf, crs='proj=moll')

Plot IPQ compactness map for ISEA4T cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of ISEA4T cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.907 indicating more regular hexagons.

Parameters:

Name Type Description Default
isea4t_gdf GeoDataFrame

GeoDataFrame from isea4tinspect function

required
Source code in vgrid/stats/isea4tstats.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
def isea4t_compactness_ipq(isea4t_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for ISEA4T cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of ISEA4T cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.907 indicating more regular hexagons.

    Args:
        isea4t_gdf: GeoDataFrame from isea4tinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = isea4t_gdf['ipq'].min(), isea4t_gdf['ipq'].max(), np.mean([isea4t_gdf['ipq'].min(), isea4t_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_TRI, vcenter=VCENTER_TRI, vmax=VMAX_TRI)
    # isea4t_gdf = isea4t_gdf[~isea4t_gdf["crossed"]]  # remove cells that cross the dateline if fix_antimeridian is None
    isea4t_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="ISEA4T IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

isea4t_compactness_ipq_hist(isea4t_gdf)

Plot histogram of IPQ compactness for ISEA4T cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for ISEA4T cells, helping to understand how close cells are to being regular triangles.

Parameters:

Name Type Description Default
isea4t_gdf GeoDataFrame

GeoDataFrame from isea4tinspect function

required
Source code in vgrid/stats/isea4tstats.py
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
def isea4t_compactness_ipq_hist(isea4t_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for ISEA4T cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for ISEA4T cells, helping
    to understand how close cells are to being regular triangles.

    Args:
        isea4t_gdf: GeoDataFrame from isea4tinspect function
    """
    # Filter out cells that cross the dateline
    # isea4t_gdf = isea4t_gdf[~isea4t_gdf["crossed"]]  # remove cells that cross the dateline if fix_antimeridian is None

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        isea4t_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_TRI, vcenter=VCENTER_TRI, vmax=VMAX_TRI)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal triangle IPQ value (0.604)
    ax.axvline(
        x=0.604,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Triangle (IPQ = 0.604)",
    )

    # Add statistics text box
    stats_text = f"Mean: {isea4t_gdf['ipq'].mean():.3f}\nStd: {isea4t_gdf['ipq'].std():.3f}\nMin: {isea4t_gdf['ipq'].min():.3f}\nMax: {isea4t_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("ISEA4T IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

isea4t_metrics(resolution, unit='m')

Calculate metrics for ISEA4T DGGS cells at a given resolution.

Parameters:

Name Type Description Default
resolution

Resolution level (0-39)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, edge_length_in_unit, cell_area_in_unit_squared)

Source code in vgrid/stats/isea4tstats.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def isea4t_metrics(resolution, unit: str = "m"):  # length unit is km, area unit is km2
    """
    Calculate metrics for ISEA4T DGGS cells at a given resolution.

    Args:
        resolution: Resolution level (0-39)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, edge_length_in_unit, cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = 20 * (4**resolution)
    avg_cell_area = AUTHALIC_AREA / num_cells  # cell area in m2
    avg_edge_len = math.sqrt((4 * avg_cell_area) / math.sqrt(3))  # edge length in km
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_edge_len = avg_edge_len / (10**3)  # edge length in m
        avg_cell_area = avg_cell_area / (10**6)  # cell area in m²

    return num_cells, avg_edge_len, avg_cell_area, cls

isea4t_norm_area(isea4t_gdf, crs='proj=moll', fix_antimeridian=None)

Plot normalized area map for ISEA4T cells.

This function creates a visualization showing how ISEA4T cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
isea4t_gdf GeoDataFrame

GeoDataFrame from isea4tinspect function

required
Source code in vgrid/stats/isea4tstats.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def isea4t_norm_area(
    isea4t_gdf: gpd.GeoDataFrame,
    crs: str | None = "proj=moll",
    fix_antimeridian: None = None,
):
    """
    Plot normalized area map for ISEA4T cells.

    This function creates a visualization showing how ISEA4T cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        isea4t_gdf: GeoDataFrame from isea4tinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = (
        isea4t_gdf["norm_area"].min(),
        1.0,
        isea4t_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # isea4t_gdf = isea4t_gdf[~isea4t_gdf["crossed"]]  # remove cells that cross the dateline if fix_antimeridian is None
    isea4t_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="ISEA4T Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

isea4t_norm_area_hist(isea4t_gdf)

Plot histogram of normalized area for ISEA4T cells.

This function creates a histogram visualization showing the distribution of normalized areas for ISEA4T cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
isea4t_gdf GeoDataFrame

GeoDataFrame from isea4tinspect function

required
Source code in vgrid/stats/isea4tstats.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
def isea4t_norm_area_hist(isea4t_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for ISEA4T cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for ISEA4T cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        isea4t_gdf: GeoDataFrame from isea4tinspect function
    """
    # Filter out cells that cross the dateline
    # isea4t_gdf = isea4t_gdf[~isea4t_gdf["crossed"]]  # remove cells that cross the dateline if fix_antimeridian is None

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        isea4t_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vmax, vcenter = (
        isea4t_gdf["norm_area"].min(),
        isea4t_gdf["norm_area"].max(),
        1,
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {isea4t_gdf['norm_area'].mean():.3f}\nStd: {isea4t_gdf['norm_area'].std():.3f}\nMin: {isea4t_gdf['norm_area'].min():.3f}\nMax: {isea4t_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("ISEA4T normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

isea4tinspect(resolution, fix_antimeridian=None)

Generate comprehensive inspection data for ISEA4T DGGS cells at a given resolution.

This function creates a detailed analysis of ISEA4T cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution

ISEA4T resolution level (0-15)

required
fix_antimeridian None

Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

None
Source code in vgrid/stats/isea4tstats.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def isea4tinspect(resolution, fix_antimeridian: None = None):
    """
    Generate comprehensive inspection data for ISEA4T DGGS cells at a given resolution.

    This function creates a detailed analysis of ISEA4T cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: ISEA4T resolution level (0-15)
        fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none
    Returns:
        geopandas.GeoDataFrame: DataFrame containing ISEA4T cell inspection data with columns:
            - isea4t: ISEA4T cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    # Allow running on all platforms
    resolution = validate_isea4t_resolution(resolution)
    isea4t_gdf = isea4tgrid(
        resolution, output_format="gpd", fix_antimeridian=fix_antimeridian
    )
    isea4t_gdf["crossed"] = isea4t_gdf["geometry"].apply(check_crossing_geom)
    mean_area = isea4t_gdf["cell_area"].mean()
    # Calculate normalized area
    isea4t_gdf["norm_area"] = isea4t_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    isea4t_gdf["ipq"] = (
        4 * np.pi * isea4t_gdf["cell_area"] / (isea4t_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    isea4t_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * isea4t_gdf["cell_area"]
            - np.power(isea4t_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / isea4t_gdf["cell_perimeter"]
    )

    convex_hull = isea4t_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    isea4t_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        isea4t_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    isea4t_gdf["cvh"] = isea4t_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return isea4t_gdf

isea4tinspect_cli()

Command-line interface for ISEA4T cell inspection.

CLI options

-r, --resolution: ISEA4T resolution level (0-15)

-fix, --fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

Source code in vgrid/stats/isea4tstats.py
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
def isea4tinspect_cli():
    """
    Command-line interface for ISEA4T cell inspection.

    CLI options:
      -r, --resolution: ISEA4T resolution level (0-15)
    -fix, --fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-fix",
        "--fix_antimeridian",
        type=str,
        choices=[
            "shift",
            "shift_balanced",
            "shift_west",
            "shift_east",
            "split",
            "none",
        ],
        default=None,
        help="Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none",
    )
    args = parser.parse_args()
    resolution = args.resolution
    fix_antimeridian = args.fix_antimeridian
    print(isea4tinspect(resolution, fix_antimeridian=fix_antimeridian))

isea4tstats(unit='m')

Generate statistics for ISEA4T DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing ISEA4T DGGS statistics with columns: - Resolution: Resolution level (0-39) - Number_of_Cells: Number of cells at each resolution - Avg_Edge_Length_{unit}: Average edge length in the given unit - Avg_Cell_Area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/isea4tstats.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def isea4tstats(unit: str = "m"):  # length unit is km, area unit is km2
    """
    Generate statistics for ISEA4T DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing ISEA4T DGGS statistics with columns:
            - Resolution: Resolution level (0-39)
            - Number_of_Cells: Number of cells at each resolution
            - Avg_Edge_Length_{unit}: Average edge length in the given unit
            - Avg_Cell_Area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = isea4t_metrics(
            res, unit=unit
        )  # length unit is km, area unit is km2
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

isea4tstats_cli()

Command-line interface for generating ISEA4T DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/isea4tstats.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def isea4tstats_cli():
    """
    Command-line interface for generating ISEA4T DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args, _ = parser.parse_known_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = isea4tstats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for ISEA3H DGGS cells.

isea3h_compactness_cvh(isea3h_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for ISEA3H cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/isea3hstats.py
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
def isea3h_compactness_cvh(isea3h_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for ISEA3H cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # isea3h_gdf = isea3h_gdf[~isea3h_gdf["crossed"]]  # remove cells that cross the Antimeridian if split_antimeridian is False
    isea3h_gdf = isea3h_gdf[np.isfinite(isea3h_gdf["cvh"])]
    isea3h_gdf = isea3h_gdf[isea3h_gdf["cvh"] <= 1.1]

    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    isea3h_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="ISEA3H CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

isea3h_compactness_cvh_hist(isea3h_gdf)

Plot histogram of CVH (cell area / convex hull area) for ISEA3H cells.

Source code in vgrid/stats/isea3hstats.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
def isea3h_compactness_cvh_hist(isea3h_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for ISEA3H cells.
    """
    # Filter out cells that cross the Antimeridian
    # isea3h_gdf = isea3h_gdf[~isea3h_gdf["crossed"]]  # remove cells that cross the Antimeridian if split_antimeridian is False
    isea3h_gdf = isea3h_gdf[np.isfinite(isea3h_gdf["cvh"])]
    isea3h_gdf = isea3h_gdf[isea3h_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        isea3h_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {isea3h_gdf['cvh'].mean():.6f}\n"
        f"Std: {isea3h_gdf['cvh'].std():.6f}\n"
        f"Min: {isea3h_gdf['cvh'].min():.6f}\n"
        f"Max: {isea3h_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("ISEA3H CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

isea3h_compactness_ipq(isea3h_gdf, crs='proj=moll')

Plot IPQ compactness map for ISEA3H cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of ISEA3H cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.907 indicating more regular hexagons.

Parameters:

Name Type Description Default
isea3h_gdf GeoDataFrame

GeoDataFrame from isea3hinspect function

required
Source code in vgrid/stats/isea3hstats.py
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def isea3h_compactness_ipq(isea3h_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for ISEA3H cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of ISEA3H cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.907 indicating more regular hexagons.

    Args:
        isea3h_gdf: GeoDataFrame from isea3hinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vcenter, vmax = isea3h_gdf['ipq'].min(), isea3h_gdf['ipq'].max(), np.mean([isea3h_gdf['ipq'].min(), isea3h_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_HEX, vcenter=VCENTER_HEX, vmax=VMAX_HEX)
    # isea3h_gdf = isea3h_gdf[~isea3h_gdf["crossed"]]  # remove cells that cross the Antimeridian if split_antimeridian is False
    isea3h_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="ISEA3H IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

isea3h_compactness_ipq_hist(isea3h_gdf)

Plot histogram of IPQ compactness for ISEA3H cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for ISEA3H cells, helping to understand how close cells are to being regular hexagons.

Parameters:

Name Type Description Default
isea3h_gdf GeoDataFrame

GeoDataFrame from isea3hinspect function

required
Source code in vgrid/stats/isea3hstats.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def isea3h_compactness_ipq_hist(isea3h_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for ISEA3H cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for ISEA3H cells, helping
    to understand how close cells are to being regular hexagons.

    Args:
        isea3h_gdf: GeoDataFrame from isea3hinspect function
    """
    # Filter out cells that cross the Antimeridian
    # isea3h_gdf = isea3h_gdf[~isea3h_gdf["crossed"]]  # remove cells that cross the Antimeridian if split_antimeridian is False

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        isea3h_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_HEX, vcenter=VCENTER_HEX, vmax=VMAX_HEX)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal hexagon IPQ value (0.907)
    ax.axvline(
        x=0.907,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Hexagon (IPQ = 0.907)",
    )

    # Add statistics text box
    stats_text = f"Mean: {isea3h_gdf['ipq'].mean():.3f}\nStd: {isea3h_gdf['ipq'].std():.3f}\nMin: {isea3h_gdf['ipq'].min():.3f}\nMax: {isea3h_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("ISEA3H IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

isea3h_metrics(resolution, unit='m')

Calculate metrics for ISEA3H DGGS cells at a given resolution.

Parameters:

Name Type Description Default
resolution

Resolution level (0-40)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, edge_length_in_unit, cell_area_in_unit_squared)

Source code in vgrid/stats/isea3hstats.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def isea3h_metrics(resolution, unit: str = "m"):  # length unit is km, area unit is km2
    """
    Calculate metrics for ISEA3H DGGS cells at a given resolution.

    Args:
        resolution: Resolution level (0-40)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, edge_length_in_unit, cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = 10 * (3**resolution) + 2
    avg_cell_area = AUTHALIC_AREA / num_cells  # cell area in km²
    avg_edge_len = math.sqrt(
        (2 * avg_cell_area) / (3 * math.sqrt(3))
    )  # edge length in km
    cls = characteristic_length_scale(avg_cell_area, unit=unit)

    if resolution == 0:  # icosahedron faces
        avg_edge_len = math.sqrt((4 * avg_cell_area) / math.sqrt(3))

    # Convert to requested unit
    if unit == "km":
        avg_edge_len = avg_edge_len / (10**3)
        avg_cell_area = avg_cell_area / (10**6)

    return num_cells, avg_edge_len, avg_cell_area, cls

isea3h_norm_area(isea3h_gdf, crs='proj=moll')

Plot normalized area map for ISEA3H cells.

This function creates a visualization showing how ISEA3H cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
isea3h_gdf GeoDataFrame

GeoDataFrame from isea3hinspect function

required
Source code in vgrid/stats/isea3hstats.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def isea3h_norm_area(isea3h_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for ISEA3H cells.

    This function creates a visualization showing how ISEA3H cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        isea3h_gdf: GeoDataFrame from isea3hinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = (
        isea3h_gdf["norm_area"].min(),
        1.0,
        isea3h_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # isea3h_gdf = isea3h_gdf[~isea3h_gdf["crossed"]]  # remove cells that cross the Antimeridian if split_antimeridian is False
    isea3h_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="ISEA3H Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

isea3h_norm_area_hist(isea3h_gdf)

Plot histogram of normalized area for ISEA3H cells.

This function creates a histogram visualization showing the distribution of normalized areas for ISEA3H cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
isea3h_gdf GeoDataFrame

GeoDataFrame from isea3hinspect function

required
Source code in vgrid/stats/isea3hstats.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
def isea3h_norm_area_hist(isea3h_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for ISEA3H cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for ISEA3H cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        isea3h_gdf: GeoDataFrame from isea3hinspect function
    """
    # Filter out cells that cross the Antimeridian
    # isea3h_gdf = isea3h_gdf[~isea3h_gdf["crossed"]]  # remove cells that cross the Antimeridian if split_antimeridian is False

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        isea3h_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        isea3h_gdf["norm_area"].min(),
        1.0,
        isea3h_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {isea3h_gdf['norm_area'].mean():.3f}\nStd: {isea3h_gdf['norm_area'].std():.3f}\nMin: {isea3h_gdf['norm_area'].min():.3f}\nMax: {isea3h_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("ISEA3H normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

isea3hinspect(resolution, fix_antimeridian=None)

Generate comprehensive inspection data for ISEA3H DGGS cells at a given resolution.

This function creates a detailed analysis of ISEA3H cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

ISEA3H resolution level (0-40)

required
fix_antimeridian None

Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none

None
Source code in vgrid/stats/isea3hstats.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def isea3hinspect(resolution: int, fix_antimeridian: None = None):
    """
    Generate comprehensive inspection data for ISEA3H DGGS cells at a given resolution.

    This function creates a detailed analysis of ISEA3H cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: ISEA3H resolution level (0-40)
        fix_antimeridian: Antimeridian fixing method: shift, shift_balanced, shift_west, shift_east, split, none
    Returns:
        geopandas.GeoDataFrame: DataFrame containing ISEA3H cell inspection data with columns:
            - isea3h: ISEA3H cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    # Allow running on all platforms

    isea3h_gdf = isea3hgrid(
        resolution, output_format="gpd", fix_antimeridian=fix_antimeridian
    )  # remove cells that cross the Antimeridian
    isea3h_gdf["crossed"] = isea3h_gdf["geometry"].apply(check_crossing_geom)
    mean_area = isea3h_gdf["cell_area"].mean()
    # Calculate normalized area
    isea3h_gdf["norm_area"] = isea3h_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    isea3h_gdf["ipq"] = (
        4 * np.pi * isea3h_gdf["cell_area"] / (isea3h_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    isea3h_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * isea3h_gdf["cell_area"]
            - np.power(isea3h_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / isea3h_gdf["cell_perimeter"]
    )

    convex_hull = isea3h_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    isea3h_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        isea3h_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    isea3h_gdf["cvh"] = isea3h_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return isea3h_gdf

isea3hinspect_cli()

Command-line interface for ISEA3H cell inspection.

CLI options

-r, --resolution: ISEA3H resolution level (0-40) -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)

Source code in vgrid/stats/isea3hstats.py
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
def isea3hinspect_cli():
    """
    Command-line interface for ISEA3H cell inspection.

    CLI options:
      -r, --resolution: ISEA3H resolution level (0-40)
      -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)
    """

    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-split",
        "--split_antimeridian",
        action="store_true",
        default=False,  # default is False to avoid splitting the Antimeridian by default
        help="Enable antimeridian splitting",
    )
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    split_antimeridian = args.split_antimeridian
    print(isea3hinspect(resolution, split_antimeridian=split_antimeridian))

isea3hstats(unit='m')

Generate statistics for ISEA3H DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing ISEA3H DGGS statistics with columns: - Resolution: Resolution level (0-40) - Number_of_Cells: Number of cells at each resolution - Avg_Edge_Length_{unit}: Average edge length in the given unit - Avg_Cell_Area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/isea3hstats.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def isea3hstats(unit: str = "m"):  # length unit is km, area unit is km2
    """
    Generate statistics for ISEA3H DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing ISEA3H DGGS statistics with columns:
            - Resolution: Resolution level (0-40)
            - Number_of_Cells: Number of cells at each resolution
            - Avg_Edge_Length_{unit}: Average edge length in the given unit
            - Avg_Cell_Area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = isea3h_metrics(
            res, unit=unit
        )  # length unit is km, area unit is km2
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

isea3hstats_cli()

Command-line interface for generating ISEA3H DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/isea3hstats.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def isea3hstats_cli():
    """
    Command-line interface for generating ISEA3H DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = isea3hstats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for EASE-DGGS cells.

ease_compactness_cvh(ease_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for EASE cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/easestats.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
def ease_compactness_cvh(ease_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for EASE cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # ease_gdf = ease_gdf[~ease_gdf["crossed"]]  # remove cells that cross the dateline
    ease_gdf = ease_gdf[np.isfinite(ease_gdf["cvh"])]
    ease_gdf = ease_gdf[ease_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    ease_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="EASE CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

ease_compactness_cvh_hist(ease_gdf)

Plot histogram of CVH (cell area / convex hull area) for EASE cells.

Source code in vgrid/stats/easestats.py
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
def ease_compactness_cvh_hist(ease_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for EASE cells.
    """
    # Filter out cells that cross the dateline
    #  ease_gdf = ease_gdf[~ease_gdf["crossed"]]
    ease_gdf = ease_gdf[np.isfinite(ease_gdf["cvh"])]
    ease_gdf = ease_gdf[ease_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        ease_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {ease_gdf['cvh'].mean():.6f}\n"
        f"Std: {ease_gdf['cvh'].std():.6f}\n"
        f"Min: {ease_gdf['cvh'].min():.6f}\n"
        f"Max: {ease_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("EASE CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

ease_compactness_ipq(ease_gdf, crs='proj=moll')

Plot IPQ compactness map for EASE cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of EASE cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.785 indicating more regular squares.

Parameters:

Name Type Description Default
ease_gdf GeoDataFrame

GeoDataFrame from easeinspect function

required
Source code in vgrid/stats/easestats.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def ease_compactness_ipq(ease_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for EASE cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of EASE cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.785 indicating more regular squares.

    Args:
        ease_gdf: GeoDataFrame from easeinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # For EASE (square cells), ideal IPQ is π/4 ≈ 0.785
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # ease_gdf = ease_gdf[~ease_gdf["crossed"]]  # remove cells that cross the dateline
    ease_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="EASE IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

ease_compactness_ipq_hist(ease_gdf)

Plot histogram of IPQ compactness for EASE cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for EASE cells, helping to understand how close cells are to being regular squares.

Parameters:

Name Type Description Default
ease_gdf GeoDataFrame

GeoDataFrame from easeinspect function

required
Source code in vgrid/stats/easestats.py
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
def ease_compactness_ipq_hist(ease_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for EASE cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for EASE cells, helping
    to understand how close cells are to being regular squares.

    Args:
        ease_gdf: GeoDataFrame from easeinspect function
    """
    # Filter out cells that cross the dateline
    # ease_gdf = ease_gdf[~ease_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        ease_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal square IPQ value (0.785)
    ax.axvline(
        x=0.785,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Square (IPQ = 0.785)",
    )

    # Add statistics text box
    stats_text = f"Mean: {ease_gdf['ipq'].mean():.3f}\nStd: {ease_gdf['ipq'].std():.3f}\nMin: {ease_gdf['ipq'].min():.3f}\nMax: {ease_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("EASE IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

ease_metrics(resolution, unit='m')

Calculate metrics for EASE-DGGS cells at a given resolution.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-6)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, edge_length_in_unit, cell_area_in_unit_squared)

Source code in vgrid/stats/easestats.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def ease_metrics(resolution: int, unit: str = "m"):  # length unit is m, area unit is m2
    """
    Calculate metrics for EASE-DGGS cells at a given resolution.

    Args:
        resolution: Resolution level (0-6)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, edge_length_in_unit, cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = levels_specs[resolution]["n_row"] * levels_specs[resolution]["n_col"]

    # Get edge lengths in meters from constants
    avg_edge_length = levels_specs[resolution][
        "x_length"
    ]  # Assuming x_length and y_length are equal
    cell_area = avg_edge_length * levels_specs[resolution]["y_length"]  # cell area in m²
    cls = characteristic_length_scale(
        cell_area, unit=unit
    )  # cell_area is in m², function handles conversion
    # Convert to requested unit
    if unit == "km":
        avg_edge_length = avg_edge_length / (10**3)  # edge length in m
        cell_area = cell_area / (10**6)  # cell area in km²

    return num_cells, avg_edge_length, cell_area, cls

ease_norm_area(ease_gdf, crs='proj=moll')

Plot normalized area map for EASE cells.

This function creates a visualization showing how EASE cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
ease_gdf GeoDataFrame

GeoDataFrame from easeinspect function

required
Source code in vgrid/stats/easestats.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def ease_norm_area(ease_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for EASE cells.

    This function creates a visualization showing how EASE cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        ease_gdf: GeoDataFrame from easeinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = ease_gdf["norm_area"].min(), 1.0, ease_gdf["norm_area"].max()
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # ease_gdf = ease_gdf[~ease_gdf["crossed"]]  # remove cells that cross the dateline
    ease_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="EASE Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

ease_norm_area_hist(ease_gdf)

Plot histogram of normalized area for EASE cells.

This function creates a histogram visualization showing the distribution of normalized areas for EASE cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
ease_gdf GeoDataFrame

GeoDataFrame from easeinspect function

required
Source code in vgrid/stats/easestats.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
def ease_norm_area_hist(ease_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for EASE cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for EASE cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        ease_gdf: GeoDataFrame from easeinspect function
    """
    # Filter out cells that cross the dateline
    # ease_gdf = ease_gdf[~ease_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        ease_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        ease_gdf["norm_area"].min(),
        1.0,
        ease_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {ease_gdf['norm_area'].mean():.3f}\nStd: {ease_gdf['norm_area'].std():.3f}\nMin: {ease_gdf['norm_area'].min():.3f}\nMax: {ease_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("EASE normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

easeinspect(resolution)

Generate comprehensive inspection data for EASE-DGGS cells at a given resolution.

This function creates a detailed analysis of EASE cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

EASE-DGGS resolution level (0-6)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing EASE cell inspection data with columns: - ease: EASE cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/easestats.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def easeinspect(resolution: int):  # length unit is m, area unit is m2
    """
    Generate comprehensive inspection data for EASE-DGGS cells at a given resolution.

    This function creates a detailed analysis of EASE cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: EASE-DGGS resolution level (0-6)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing EASE cell inspection data with columns:
            - ease: EASE cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    ease_gdf = easegrid(resolution, output_format="gpd")    
    ease_gdf["crossed"] = ease_gdf["geometry"].apply(check_crossing_geom)
    mean_area = ease_gdf["cell_area"].mean()
    # Calculate normalized area
    ease_gdf["norm_area"] = ease_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    ease_gdf["ipq"] = (
        4 * np.pi * ease_gdf["cell_area"] / (ease_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    ease_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * ease_gdf["cell_area"]
            - np.power(ease_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / ease_gdf["cell_perimeter"]
    )

    convex_hull = ease_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    ease_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        ease_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    ease_gdf["cvh"] = ease_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return ease_gdf

easeinspect_cli()

Command-line interface for EASE cell inspection.

CLI options

-r, --resolution: EASE resolution level (0-6)

Source code in vgrid/stats/easestats.py
482
483
484
485
486
487
488
489
490
491
492
493
def easeinspect_cli():
    """
    Command-line interface for EASE cell inspection.

    CLI options:
      -r, --resolution: EASE resolution level (0-6)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    print(easeinspect(resolution))

easestats(unit='m')

Generate statistics for EASE-DGGS cells. length unit is m, area unit is m2 Args: unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

Returns:

Type Description

pandas.DataFrame: DataFrame containing EASE-DGGS statistics with columns: - Resolution: Resolution level (0-6) - Number_of_Cells: Number of cells at each resolution - Avg_Edge_Length_{unit}: Average edge length in the given unit - Avg_Cell_Area_{unit}2: Average cell area in the squared unit - CLS_{unit}: Characteristic Length Scale in the given unit

Source code in vgrid/stats/easestats.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def easestats(unit: str = "m"):  # length unit is m, area unit is m2
    """
    Generate statistics for EASE-DGGS cells.
    length unit is m, area unit is m2
    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing EASE-DGGS statistics with columns:
            - Resolution: Resolution level (0-6)
            - Number_of_Cells: Number of cells at each resolution
            - Avg_Edge_Length_{unit}: Average edge length in the given unit
            - Avg_Cell_Area_{unit}2: Average cell area in the squared unit
            - CLS_{unit}: Characteristic Length Scale in the given unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells_at_res, avg_edge_length, cell_area, cls = ease_metrics(res, unit)
        resolutions.append(res)
        num_cells_list.append(num_cells_at_res)
        avg_edge_lens.append(avg_edge_length)
        avg_cell_areas.append(cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

easestats_cli()

Command-line interface for generating EASE-DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/easestats.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def easestats_cli():
    """
    Command-line interface for generating EASE-DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )

    args = parser.parse_args()  # type: ignore
    unit = args.unit

    # Get the DataFrame
    df = easestats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides lightweight wrappers for DGGAL using the external dgg CLI directly.

Per request, dggalstats simply returns the direct output from dgg <dggs_type> level without computing any additional metrics.

dggal_compactness_cvh(dggs_type, dggal_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for DGGAL cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/dggalstats.py
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
def dggal_compactness_cvh(
    dggs_type: str, dggal_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot CVH (cell area / convex hull area) compactness map for DGGAL cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # dggal_gdf = dggal_gdf[~dggal_gdf["crossed"]]  # remove cells that cross the dateline
    dggal_gdf = dggal_gdf[np.isfinite(dggal_gdf["cvh"])]
    dggal_gdf = dggal_gdf[dggal_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    dggal_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel=f"{dggs_type.upper()} CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

dggal_compactness_cvh_hist(dggs_type, dggal_gdf)

Plot histogram of CVH (cell area / convex hull area) for DGGAL cells.

Source code in vgrid/stats/dggalstats.py
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
def dggal_compactness_cvh_hist(dggs_type: str, dggal_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for DGGAL cells.
    """
    # Filter out cells that cross the dateline
    # dggal_gdf = dggal_gdf[~dggal_gdf["crossed"]]
    dggal_gdf = dggal_gdf[np.isfinite(dggal_gdf["cvh"])]
    dggal_gdf = dggal_gdf[dggal_gdf["cvh"] <= 1.1]
    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        dggal_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {dggal_gdf['cvh'].mean():.6f}\n"
        f"Std: {dggal_gdf['cvh'].std():.6f}\n"
        f"Min: {dggal_gdf['cvh'].min():.6f}\n"
        f"Max: {dggal_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel(f"{dggs_type.upper()} CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

dggal_compactness_ipq(dggs_type, dggal_gdf, crs='proj=moll')

Plot IPQ compactness map for DGGAL cells (generic visualization).

Source code in vgrid/stats/dggalstats.py
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
def dggal_compactness_ipq(
    dggs_type: str,
    dggal_gdf: gpd.GeoDataFrame,
    crs: str | None = "proj=moll",  # type: ignore
):
    """
    Plot IPQ compactness map for DGGAL cells (generic visualization).
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)

    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD

    dggs_type_norm = str(dggs_type).strip().lower()
    if dggs_type_norm in ["isea3h", "ivea3h", "rtea3h"]:
        vmin, vcenter, vmax = VMIN_HEX, VCENTER_HEX, VMAX_HEX

    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # Only filter out antimeridian-crossed cells when plotting in EPSG:4326
    # dggal_gdf = dggal_gdf[~dggal_gdf["crossed"]]
    gdf_plot = dggal_gdf.to_crs(crs) if crs else dggal_gdf
    gdf_plot.plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    wc_plot = world_countries.boundary.to_crs(crs)
    wc_plot.plot(color=None, edgecolor="black", linewidth=0.2, ax=ax)
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel=f"{dggs_type.upper()} IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

dggal_compactness_ipq_hist(dggs_type, dggal_gdf)

Plot histogram of IPQ compactness for DGGAL cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for DGGAL cells, helping to understand how close cells are to being regular shapes.

Parameters:

Name Type Description Default
gdf

GeoDataFrame from dggalinspect function

required
dggs_type str

DGGS type name for labeling and determining ideal IPQ values

required
Source code in vgrid/stats/dggalstats.py
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
def dggal_compactness_ipq_hist(dggs_type: str, dggal_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for DGGAL cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for DGGAL cells, helping
    to understand how close cells are to being regular shapes.

    Args:
            gdf: GeoDataFrame from dggalinspect function
            dggs_type: DGGS type name for labeling and determining ideal IPQ values
    """
    # Filter out cells that cross the dateline
    # dggal_gdf = dggal_gdf[~dggal_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        dggal_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    dggs_type_norm = str(dggs_type).strip().lower()
    if dggs_type_norm in ["isea3h", "ivea3h", "rtea3h"]:
        # Hexagonal cells
        norm = TwoSlopeNorm(vmin=VMIN_HEX, vcenter=VCENTER_HEX, vmax=VMAX_HEX)
        ideal_ipq = 0.907  # Ideal hexagon
        shape_name = "Hexagon"
    else:
        # Quadrilateral cells (gnosis, isea9r, ivea9r, rtea9r, rhealpix)
        norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)
        ideal_ipq = 0.785  # Ideal square (π/4)
        shape_name = "Square"

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal IPQ value
    ax.axvline(
        x=ideal_ipq,
        color="red",
        linestyle="--",
        linewidth=2,
        label=f"Ideal {shape_name} (IPQ = {ideal_ipq:.6f})",
    )

    # Add statistics text box
    stats_text = f"Mean: {dggal_gdf['ipq'].mean():.6f}\nStd: {dggal_gdf['ipq'].std():.6f}\nMin: {dggal_gdf['ipq'].min():.6f}\nMax: {dggal_gdf['ipq'].max():.6f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel(f"{dggs_type.upper()} IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

dggal_metrics(dggs_type, resolution, unit='m')

Calculate metrics for DGGAL cells at a given resolution.

Parameters:

Name Type Description Default
dggs_type str

DGGS type supported by DGGAL (see vgrid.utils.constants.DGGAL_TYPES)

required
resolution int

Resolution level (0-29)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, edge_length_in_unit, cell_area_in_unit_squared)

Source code in vgrid/stats/dggalstats.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def dggal_metrics(
    dggs_type: str, resolution: int, unit: str = "m"
):  # length unit is m, area unit is m2
    """
    Calculate metrics for DGGAL cells at a given resolution.

    Args:
            dggs_type: DGGS type supported by DGGAL (see vgrid.utils.constants.DGGAL_TYPES)
            resolution: Resolution level (0-29)
            unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
            tuple: (num_cells, edge_length_in_unit, cell_area_in_unit_squared)
    """

    dggs_type = validate_dggal_type(dggs_type)
    resolution = validate_dggal_resolution(dggs_type, int(resolution))

    unit_norm = unit.strip().lower()
    if unit_norm not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # 'gnosis','isea4r','isea9r','isea3h','isea7h','isea7h_z7',
    # 'ivea4r','ivea9r','ivea3h','ivea7h','ivea7h_z7','rtea4r','rtea9r','rtea3h','rtea7h','rtea7h_z7','healpix','rhealpix'
    num_edges = 4
    if dggs_type in [
        "isea3h",
        "isea7h",
        "isea7h_z7",
        "ivea3h",
        "ivea7h",
        "ivea7h_z7",
        "rtea3h",
        "rtea7h",
        "rtea7h_z7",
    ]:
        num_edges = 6  # Hexagonal cells

    # Calculate number of cells using the original formulas
    # Need to be rechecked
    num_cells = 1
    if dggs_type == "gnosis":
        num_cells = (16 * (4**resolution) + 8) // 3
    elif dggs_type in ["isea3h", "ivea3h", "rtea3h"]:
        num_cells = 10 * (3**resolution) + 2
    elif dggs_type in ["isea4r", "ivea4r", "rtea4r"]:
        num_cells = 10 * (4**resolution)
    elif dggs_type in [
        "isea7h",
        "isea7h_z7",
        "ivea7h",
        "ivea7h_z7",
        "rtea7h",
        "rtea7h_z7",
    ]:
        num_cells = 10 * (7**resolution) + 2
    elif dggs_type in ["isea9r", "ivea9r", "rtea9r"]:
        num_cells = 10 * (9**resolution)
    elif dggs_type in ["healpix"]:
        num_cells = 12 * (4**resolution)
    elif dggs_type in ["rhealpix"]:
        num_cells = 6 * (9**resolution) 

    avg_cell_area = AUTHALIC_AREA / num_cells  # area in m2

    # Calculate average edge length based on the number of edges
    if num_edges == 6:  # Hexagonal cells
        avg_edge_len = math.sqrt((2 * avg_cell_area) / (3 * math.sqrt(3)))
    else:  # Square or other polygonal cells
        avg_edge_len = math.sqrt(avg_cell_area)

    cls = characteristic_length_scale(avg_cell_area, unit=unit)

    # Convert to requested unit
    if unit_norm == "km":
        avg_edge_len = avg_edge_len / (10**3)
        avg_cell_area = avg_cell_area / (10**6)

    return num_cells, avg_edge_len, avg_cell_area, cls

dggal_norm_area_hist(dggs_type, dggal_gdf)

Plot histogram of normalized area for DGGAL cells.

This function creates a histogram visualization showing the distribution of normalized areas for DGGAL cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
gdf

GeoDataFrame from dggalinspect function

required
dggs_type str

DGGS type name for labeling

required
Source code in vgrid/stats/dggalstats.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def dggal_norm_area_hist(dggs_type: str, dggal_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for DGGAL cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for DGGAL cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
            gdf: GeoDataFrame from dggalinspect function
            dggs_type: DGGS type name for labeling
    """
    # Filter out cells that cross the dateline
    # dggal_gdf = dggal_gdf[~dggal_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        dggal_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        dggal_gdf["norm_area"].min(),
        1.0,
        dggal_gdf["norm_area"].max(),
    )

    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {dggal_gdf['norm_area'].mean():.6f}\nStd: {dggal_gdf['norm_area'].std():.6f}\nMin: {dggal_gdf['norm_area'].min():.6f}\nMax: {dggal_gdf['norm_area'].max():.6f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel(f"{dggs_type.upper()} normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

dggalinfo(dggs_type)

Return the direct stdout from dgg <dggs_type> level.

Parameters:

Name Type Description Default
dggs_type str

DGGS type supported by DGGAL (see vgrid.utils.constants.dggs_type)

required

Returns:

Type Description
str | None

stdout string on success; None on failure.

Source code in vgrid/stats/dggalstats.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def dggalinfo(dggs_type: str) -> str | None:
    """
    Return the direct stdout from `dgg <dggs_type> level`.

    Args:
            dggs_type: DGGS type supported by DGGAL (see vgrid.utils.constants.dggs_type)

    Returns:
            stdout string on success; None on failure.
    """
    dggs_type = validate_dggal_type(dggs_type)

    dgg_exe = shutil.which("dgg")
    if dgg_exe is None:
        print(
            "Error: `dgg` command not found. Please ensure the `dggal` package is installed and `dgg` is on PATH.",
            file=sys.stderr,
        )
        return None

    # Use the style: `dgg <dggs_type> level`
    cmd = [dgg_exe, dggs_type, "level"]

    try:
        completed = subprocess.run(
            cmd,
            check=True,
            capture_output=True,
            text=True,
            encoding="utf-8",
            errors="replace",
        )
        stdout = completed.stdout
    except Exception as exc:
        print(f"Failed to run {' '.join(cmd)}: {exc}", file=sys.stderr)
        return None
    # Return the textual table directly for display
    return stdout

dggalinfo_cli()

Command-line interface for generating DGGAL DGGS statistics.

CLI options

-dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix} -unit, --unit {m,km} --minres, --maxres

Source code in vgrid/stats/dggalstats.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def dggalinfo_cli():
    """
    Command-line interface for generating DGGAL DGGS statistics.

    CLI options:
      -dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix}
      -unit, --unit {m,km}
      --minres, --maxres
    """
    parser = argparse.ArgumentParser(add_help=False)
    # Positional shorthand: dggalstats isea3h
    parser.add_argument("pos_dggs_type", nargs="?", choices=DGGAL_TYPES.keys())
    # Optional flag remains supported for type
    parser.add_argument(
        "-dggs", "--dggs_type", dest="dggs_type", choices=DGGAL_TYPES.keys()
    )
    args, _ = parser.parse_known_args()

    # Resolve parameters from positional or flagged inputs
    dggs_type = args.pos_dggs_type or args.dggs_type

    if dggs_type is None:
        raise SystemExit(
            "Error: dggs_type is required. Usage: dggalstats <dggs_type> or with flag -t"
        )

    result = dggalinfo(dggs_type)
    if result is not None:
        print(result)

dggalinspect(dggs_type, resolution, split_antimeridian=False)

Generate detailed inspection data for a DGGAL DGGS type at a given resolution.

Parameters:

Name Type Description Default
dggs_type str

DGGS type supported by DGGAL

required
resolution int

Resolution level

required
split_antimeridian bool

When True, apply antimeridian splitting to the resulting polygons. Defaults to True when None or omitted.

False

Returns:

Type Description
GeoDataFrame

geopandas.GeoDataFrame with columns: - ZoneID (as provided by DGGAL output; no renaming is performed) - resolution - geometry - cell_area (m^2) - cell_perimeter (m) - crossed (bool) - norm_area (area/mean_area) - ipq (4πA/P²) - zsc (sqrt(4πA - A²/R²)/P), with R=WGS84 a

Source code in vgrid/stats/dggalstats.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def dggalinspect(
    dggs_type: str, resolution: int, split_antimeridian: bool = False
) -> gpd.GeoDataFrame:
    """
    Generate detailed inspection data for a DGGAL DGGS type at a given resolution.

    Args:
        dggs_type: DGGS type supported by DGGAL
        resolution: Resolution level
        split_antimeridian: When True, apply antimeridian splitting to the resulting polygons.
            Defaults to True when None or omitted.

    Returns:
        geopandas.GeoDataFrame with columns:
          - ZoneID (as provided by DGGAL output; no renaming is performed)
          - resolution
          - geometry
          - cell_area (m^2)
          - cell_perimeter (m)
          - crossed (bool)
          - norm_area (area/mean_area)
          - ipq (4πA/P²)
          - zsc (sqrt(4πA - A²/R²)/P), with R=WGS84 a
    """
    dggal_gdf = dggalgen(
        dggs_type,
        resolution,
        output_format="gpd",
        split_antimeridian=split_antimeridian,
    )

    # Determine whether current CRS is geographic; compute metrics accordingly
    if dggal_gdf.crs.is_geographic:
        dggal_gdf["cell_area"] = dggal_gdf.geometry.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[0])
        )
        dggal_gdf["cell_perimeter"] = dggal_gdf.geometry.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[1])
        )
        dggal_gdf["crossed"] = dggal_gdf.geometry.apply(check_crossing_geom)
        convex_hull = dggal_gdf["geometry"].convex_hull
        convex_hull_area = convex_hull.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[0])
        )
    else:
        dggal_gdf["cell_area"] = dggal_gdf.geometry.area
        dggal_gdf["cell_perimeter"] = dggal_gdf.geometry.length
        dggal_gdf["crossed"] = False
        convex_hull = dggal_gdf["geometry"].convex_hull
        convex_hull_area = convex_hull.area

    mean_area = dggal_gdf["cell_area"].mean()
    dggal_gdf["norm_area"] = (
        dggal_gdf["cell_area"] / mean_area if mean_area and mean_area != 0 else np.nan
    )
    # Robust formulas avoiding division by zero
    dggal_gdf["ipq"] = (
        4 * np.pi * dggal_gdf["cell_area"] / (dggal_gdf["cell_perimeter"] ** 2)
    )

    dggal_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * dggal_gdf["cell_area"]
            - np.power(dggal_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / dggal_gdf["cell_perimeter"]
    )

    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    dggal_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        dggal_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    dggal_gdf["cvh"] = dggal_gdf["cvh"].replace([np.inf, -np.inf], np.nan)
    return dggal_gdf

dggalinspect_cli()

Command-line interface for DGGAL cell inspection.

CLI options

-t, --dggs_type -r, --resolution -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)

Source code in vgrid/stats/dggalstats.py
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
def dggalinspect_cli():
    """
    Command-line interface for DGGAL cell inspection.

    CLI options:
      -t, --dggs_type
      -r, --resolution
      -split, --split_antimeridian: Enable antimeridian splitting (default: enabled)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-t", "--dggs_type", dest="dggs_type", choices=DGGAL_TYPES.keys(), required=True
    )
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-split",
        "--split_antimeridian",
        action="store_true",
        default=False,  # default is False to avoid splitting the Antimeridian by default
        help="Enable antimeridian splitting",
    )
    args = parser.parse_args()
    dggs_type = args.dggs_type
    resolution = args.resolution
    print(
        dggalinspect(
            dggs_type, resolution, split_antimeridian=args.split_antimeridian
        )
    )

dggalstats(dggs_type, unit='m')

Compute and return a DataFrame of DGGAL metrics per resolution for the given type.

Parameters:

Name Type Description Default
dggs_type str

DGGS type supported by DGGAL (see vgrid.utils.constants.DGGAL_TYPES)

required
unit str

'm' or 'km' for length; area columns will reflect the squared unit

'm'

Returns:

Type Description
DataFrame | None

pandas DataFrame with columns for resolution, number of cells, average edge length,

DataFrame | None

and average cell area in the requested units.

Source code in vgrid/stats/dggalstats.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def dggalstats(
    dggs_type: str, unit: str = "m"
) -> pd.DataFrame | None:  # length unit is km, area unit is km2
    """
    Compute and return a DataFrame of DGGAL metrics per resolution for the given type.

    Args:
            dggs_type: DGGS type supported by DGGAL (see vgrid.utils.constants.DGGAL_TYPES)
            unit: 'm' or 'km' for length; area columns will reflect the squared unit

    Returns:
            pandas DataFrame with columns for resolution, number of cells, average edge length,
            and average cell area in the requested units.
    """
    dggs_type = validate_dggal_type(dggs_type)
    min_res = int(DGGAL_TYPES[dggs_type]["min_res"])
    max_res = int(DGGAL_TYPES[dggs_type]["max_res"])

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = dggal_metrics(
            dggs_type, res, unit=unit
        )  # length unit is km, area unit is km2
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Build column labels with unit awareness
    avg_edge_len_col = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area_col = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len_col: avg_edge_lens,
            avg_cell_area_col: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

dggalstats_cli()

Command-line interface for generating DGGAL DGGS statistics.

CLI options

-dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix} -unit, --unit {m,km} --minres, --maxres

Source code in vgrid/stats/dggalstats.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def dggalstats_cli():
    """
    Command-line interface for generating DGGAL DGGS statistics.

    CLI options:
      -dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix}
      -unit, --unit {m,km}
      --minres, --maxres
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-dggs", "--dggs_type", dest="dggs_type", choices=DGGAL_TYPES.keys()
    )
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()

    dggs_type = args.dggs_type
    unit = args.unit

    result = dggalstats(dggs_type, unit)
    if result is not None:
        print(result)

DGGRID Statistics Module

This module provides functions to calculate and display statistics for DGGRID Discrete Global Grid System (DGGS) types. It supports both command-line interface and direct function calls.

Key Functions: - dggrid_stats: Calculate and display statistics for a given DGGRID DGGS type and resolution - dggridinspect: Generate detailed inspection data for a given DGGRID DGGS type and resolution - main: Command-line interface for dggrid_stats

dggrid_compactness_cvh(dggs_type='DGGRID', dggrid_gdf=None, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for DGGRID cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/dggridstats.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def dggrid_compactness_cvh(
    dggs_type="DGGRID",
    dggrid_gdf: gpd.GeoDataFrame = None,
    crs: str | None = "proj=moll",
):
    """
    Plot CVH (cell area / convex hull area) compactness map for DGGRID cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # dggrid_gdf = dggrid_gdf[~dggrid_gdf["crossed"]]  # remove cells that cross the dateline
    dggrid_gdf = dggrid_gdf[np.isfinite(dggrid_gdf["cvh"])]
    dggrid_gdf = dggrid_gdf[dggrid_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    dggrid_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="DGGRID CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

dggrid_compactness_cvh_hist(dggs_type='DGGRID', dggrid_gdf=None)

Plot histogram of CVH (cell area / convex hull area) for DGGRID cells.

Source code in vgrid/stats/dggridstats.py
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
def dggrid_compactness_cvh_hist(
    dggs_type="DGGRID", dggrid_gdf: gpd.GeoDataFrame = None
):
    """
    Plot histogram of CVH (cell area / convex hull area) for DGGRID cells.
    """
    # Filter out cells that cross the dateline
    # dggrid_gdf = dggrid_gdf[~dggrid_gdf["crossed"]]
    dggrid_gdf = dggrid_gdf[np.isfinite(dggrid_gdf["cvh"])]
    dggrid_gdf = dggrid_gdf[dggrid_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        dggrid_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {dggrid_gdf['cvh'].mean():.6f}\n"
        f"Std: {dggrid_gdf['cvh'].std():.6f}\n"
        f"Min: {dggrid_gdf['cvh'].min():.6f}\n"
        f"Max: {dggrid_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("DGGRID CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

dggrid_compactness_ipq(dggs_type='DGGRID', dggrid_gdf=None, crs='proj=moll')

Plot IPQ compactness map for DGGRID cells.

Source code in vgrid/stats/dggridstats.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
def dggrid_compactness_ipq(
    dggs_type: str = "DGGRID",
    dggrid_gdf: gpd.GeoDataFrame = None,
    crs: str | None = "proj=moll",
):
    """
    Plot IPQ compactness map for DGGRID cells.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)

    # Determine compactness bounds based on topology
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD

    dggs_type_norm = str(dggs_type).strip().lower()
    if any(hex_type in dggs_type_norm for hex_type in ["3h", "4h", "7h", "43h"]):
        vmin, vcenter, vmax = VMIN_HEX, VCENTER_HEX, VMAX_HEX

    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # Only filter out antimeridian-crossed cells when plotting in EPSG:4326
    # dggrid_gdf = dggrid_gdf[~dggrid_gdf["crossed"]]  # remove cells that cross the dateline
    dggrid_gdf_plot = dggrid_gdf.to_crs(crs) if crs else dggrid_gdf
    dggrid_gdf_plot.plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    wc_plot = world_countries.boundary.to_crs(crs)
    wc_plot.plot(color=None, edgecolor="black", linewidth=0.2, ax=ax)
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel=f"{dggs_type.upper()} IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

dggrid_compactness_ipq_hist(dggs_type='DGGRID', dggrid_gdf=None)

Plot histogram of IPQ compactness for DGGRID cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for DGGRID cells, helping to understand how close cells are to being regular shapes.

Parameters:

Name Type Description Default
gdf

GeoDataFrame from dggridinspect function

required
dggs_type

DGGS type name for labeling and determining ideal IPQ values

'DGGRID'
Source code in vgrid/stats/dggridstats.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
def dggrid_compactness_ipq_hist(
    dggs_type="DGGRID", dggrid_gdf: gpd.GeoDataFrame = None
):
    """
    Plot histogram of IPQ compactness for DGGRID cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for DGGRID cells, helping
    to understand how close cells are to being regular shapes.

    Args:
            gdf: GeoDataFrame from dggridinspect function
            dggs_type: DGGS type name for labeling and determining ideal IPQ values
    """
    # Filter out cells that cross the dateline
    # dggrid_gdf = dggrid_gdf[~dggrid_gdf["crossed"]]  # remove cells that cross the dateline

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        dggrid_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    dggs_type_norm = str(dggs_type).strip().lower()
    if any(hex_type in dggs_type_norm for hex_type in ["3h", "4h", "7h", "43h"]):
        # Hexagonal cells
        norm = TwoSlopeNorm(vmin=VMIN_HEX, vcenter=VCENTER_HEX, vmax=VMAX_HEX)
        ideal_ipq = 0.907  # Ideal hexagon
        shape_name = "Hexagon"
    else:
        # Quadrilateral cells
        norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)
        ideal_ipq = 0.785  # Ideal square (π/4)
        shape_name = "Square"

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal IPQ value
    ax.axvline(
        x=ideal_ipq,
        color="red",
        linestyle="--",
        linewidth=2,
        label=f"Ideal {shape_name} (IPQ = {ideal_ipq:.3f})",
    )

    # Add statistics text box
    stats_text = f"Mean: {dggrid_gdf['ipq'].mean():.3f}\nStd: {dggrid_gdf['ipq'].std():.3f}\nMin: {dggrid_gdf['ipq'].min():.3f}\nMax: {dggrid_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel(f"{dggs_type.upper()} IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

dggrid_norm_area(dggs_type='DGGRID', dggrid_gdf=None, crs='proj=moll')

Plot normalized area map for DGGRID cells.

Source code in vgrid/stats/dggridstats.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
def dggrid_norm_area(
    dggs_type="DGGRID",
    dggrid_gdf: gpd.GeoDataFrame = None,
    crs: str | None = "proj=moll",
):
    """
    Plot normalized area map for DGGRID cells.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = (
        dggrid_gdf["norm_area"].min(),
        1.0,
        dggrid_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # dggrid_gdf = dggrid_gdf[~dggrid_gdf["crossed"]]  # remove cells that cross the dateline
    dggrid_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel=f"{dggs_type.upper()} Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

dggrid_norm_area_hist(dggs_type='DGGRID', dggrid_gdf=None)

Plot histogram of normalized area for DGGRID cells.

This function creates a histogram visualization showing the distribution of normalized areas for DGGRID cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
gdf

GeoDataFrame from dggridinspect function

required
dggs_type

DGGS type name for labeling

'DGGRID'
Source code in vgrid/stats/dggridstats.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
def dggrid_norm_area_hist(dggs_type="DGGRID", dggrid_gdf: gpd.GeoDataFrame = None):
    """
    Plot histogram of normalized area for DGGRID cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for DGGRID cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
            gdf: GeoDataFrame from dggridinspect function
            dggs_type: DGGS type name for labeling
    """
    # Filter out cells that cross the dateline
    # dggrid_gdf = dggrid_gdf[~dggrid_gdf["crossed"]]  # remove cells that cross the dateline

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        dggrid_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        dggrid_gdf["norm_area"].min(),
        1.0,
        dggrid_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {dggrid_gdf['norm_area'].mean():.3f}\nStd: {dggrid_gdf['norm_area'].std():.3f}\nMin: {dggrid_gdf['norm_area'].min():.3f}\nMax: {dggrid_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel(f"{dggs_type.upper()} normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

dggridinspect(dggrid_instance, dggs_type, resolution, split_antimeridian=False, aggregate=False)

Generate detailed inspection data for a DGGRID DGGS type at a given resolution.

Parameters:

Name Type Description Default
dggrid_instance

DGGRID instance for grid operations

required
dggs_type str

DGGS type supported by DGGRID (see dggs_types)

required
resolution int

Resolution level

required
split_antimeridian bool

When True, apply antimeridian fixing to the resulting polygons.

False
aggregate bool

When True, aggregate the resulting polygons. Defaults to False to avoid aggregation by default.

False
Source code in vgrid/stats/dggridstats.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def dggridinspect(
    dggrid_instance,
    dggs_type: str,
    resolution: int,
    split_antimeridian: bool = False,
    aggregate: bool = False,
) -> gpd.GeoDataFrame:
    """
    Generate detailed inspection data for a DGGRID DGGS type at a given resolution.

    Args:
        dggrid_instance: DGGRID instance for grid operations
        dggs_type: DGGS type supported by DGGRID (see dggs_types)
        resolution: Resolution level
        split_antimeridian: When True, apply antimeridian fixing to the resulting polygons.
        Defaults to False to avoid splitting the Antimeridian by default.
        aggregate: When True, aggregate the resulting polygons. Defaults to False to avoid aggregation by default.
    Returns:
        geopandas.GeoDataFrame: DataFrame containing inspection data with columns:
          - name (cell identifier from DGGRID)
          - resolution
          - geometry
          - cell_area (m^2)
          - cell_perimeter (m)
          - crossed (bool)
          - norm_area (area/mean_area)
          - ipq (4πA/P²)
          - zsc (sqrt(4πA - A²/R²)/P), with R=WGS84 a
    """

    # Generate grid using dggridgen
    dggrid_gdf = dggridgen(
        dggrid_instance,
        dggs_type,
        resolution,
        output_format="gpd",
        split_antimeridian=split_antimeridian,
        aggregate=aggregate,
    )

    # Remove cells with null or invalid geometry
    dggrid_gdf = dggrid_gdf.dropna(subset=["geometry"])
    dggrid_gdf = dggrid_gdf[dggrid_gdf.geometry.is_valid]

    # Add dggs_type column
    dggrid_gdf["dggs_type"] = f"dggrid_{dggs_type.lower()}"

    # Rename global_id to cell_id
    if "global_id" in dggrid_gdf.columns:
        dggrid_gdf = dggrid_gdf.rename(columns={"global_id": "cell_id"})

    # Determine whether current CRS is geographic; compute metrics accordingly
    if dggrid_gdf.crs.is_geographic:
        dggrid_gdf["cell_area"] = dggrid_gdf.geometry.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[0])
        )
        dggrid_gdf["cell_perimeter"] = dggrid_gdf.geometry.apply(
            lambda g: abs(geod.geometry_area_perimeter(g)[1])
        )
        dggrid_gdf["crossed"] = dggrid_gdf.geometry.apply(check_crossing_geom)
    else:
        dggrid_gdf["cell_area"] = dggrid_gdf.geometry.area
        dggrid_gdf["cell_perimeter"] = dggrid_gdf.geometry.length
        dggrid_gdf["crossed"] = False

    # Add resolution column
    dggrid_gdf["resolution"] = resolution

    # Calculate normalized area
    mean_area = dggrid_gdf["cell_area"].mean()
    dggrid_gdf["norm_area"] = (
        dggrid_gdf["cell_area"] / mean_area if mean_area and mean_area != 0 else np.nan
    )

    # Calculate compactness metrics (robust formulas avoiding division by zero)
    dggrid_gdf["ipq"] = (
        4 * np.pi * dggrid_gdf["cell_area"] / (dggrid_gdf["cell_perimeter"] ** 2)
    )
    dggrid_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * dggrid_gdf["cell_area"]
            - np.power(dggrid_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / dggrid_gdf["cell_perimeter"]
    )

    convex_hull = dggrid_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    dggrid_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        dggrid_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    dggrid_gdf["cvh"] = dggrid_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return dggrid_gdf

dggridinspect_cli()

Command-line interface for DGGRID cell inspection.

CLI options

-dggs, --dggs_type: DGGS type from dggs_types -r, --resolution: Resolution level --no-split_antimeridian: Disable antimeridian fixing (default: enabled) --no-aggregate: Disable aggregation (default: enabled)

Source code in vgrid/stats/dggridstats.py
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
def dggridinspect_cli():
    """
    Command-line interface for DGGRID cell inspection.

    CLI options:
      -dggs, --dggs_type: DGGS type from dggs_types
      -r, --resolution: Resolution level
      --no-split_antimeridian: Disable antimeridian fixing (default: enabled)
      --no-aggregate: Disable aggregation (default: enabled)
    """
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-dggs", "--dggs_type", dest="dggs_type", choices=dggs_types, required=True
    )
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    parser.add_argument(
        "-split",
        "--split_antimeridian",
        action="store_true",
        default=False,  # default is False to avoid splitting the Antimeridian by default
        help="Enable antimeridian fixing",
    )
    parser.add_argument(
        "-aggregate",
        "--aggregate",
        action="store_true",
        default=False,  # default is False to avoid aggregation by default
        help="Enable aggregation",
    )
    args = parser.parse_args()
    dggrid_instance = create_dggrid_instance()
    dggs_type = args.dggs_type
    resolution = args.resolution
    print(
        dggridinspect(
            dggrid_instance,
            dggs_type,
            resolution,
            split_antimeridian=args.split_antimeridian,
            aggregate=args.aggregate,
        )
    )

dggridstats(dggrid_instance, dggs_type, unit='m')

length unit is m, area unit is m2 Return a DataFrame of DGGRID stats per resolution.

'km' or 'm' for length columns; area is squared unit.

DGGRID native output is km^2 for area and km for CLS.

Source code in vgrid/stats/dggridstats.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def dggridstats(
    dggrid_instance, dggs_type: str, unit: str = "m"
) -> pd.DataFrame:  # length unit is km, area unit is km2
    """length unit is m, area unit is m2
    Return a DataFrame of DGGRID stats per resolution.

    unit: 'km' or 'm' for length columns; area is squared unit.
          DGGRID native output is km^2 for area and km for CLS.
    """
    dggs_type = validate_dggrid_type(dggs_type)
    unit_norm = unit.strip().lower()

    if unit_norm not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    max_res = int(DGGRID_TYPES[dggs_type]["max_res"])

    dggrid_stats_table = dggrid_instance.grid_stats_table(dggs_type, max_res)
    # Characteristic Length Scale (CLS): the diameter of a spherical cap of the same area as a cell of the specified res
    if isinstance(dggrid_stats_table, pd.DataFrame):
        rename_map = {
            "Resolution": "resolution",
            "Cells": "number_of_cells",
            "Area (km^2)": "area_km2",
            "CLS (km)": "cls_km",
        }
        dggrid_stats = dggrid_stats_table.rename(columns=rename_map).copy()
    else:
        dggrid_stats = pd.DataFrame(
            dggrid_stats_table,
            columns=["resolution", "number_of_cells", "area_km2", "cls_km"],
        )

    if unit_norm == "m":
        dggrid_stats = dggrid_stats.rename(
            columns={"area_km2": "area_m2", "cls_km": "cls_m"}
        )
        dggrid_stats["area_m2"] = dggrid_stats["area_m2"] * (10**6)
        dggrid_stats["cls_m"] = dggrid_stats["cls_m"] * (10**3)

    # Add intercell distance in requested unit
    intercell_col = f"intercell_{unit_norm}"
    dggrid_stats[intercell_col] = dggrid_stats["resolution"].apply(
        lambda r: dggrid_intercell_distance(dggs_type, int(r), unit=unit_norm)
    )
    return dggrid_stats

dggridstats_cli()

Command-line interface for generating DGGAL DGGS statistics.

CLI options

-dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix} -unit, --unit {m,km} --minres, --maxres

Source code in vgrid/stats/dggridstats.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def dggridstats_cli():
    """
    Command-line interface for generating DGGAL DGGS statistics.

    CLI options:
      -dggs, --dggs_type {gnosis, isea3h, isea9r, ivea3h, ivea9r, rtea3h, rtea9r, rhealpix}
      -unit, --unit {m,km}
      --minres, --maxres
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-dggs", "--dggs_type", dest="dggs_type", choices=DGGRID_TYPES.keys()
    )
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()

    dggs_type = args.dggs_type
    unit = args.unit

    dggrid_instance = create_dggrid_instance()
    result = dggridstats(dggrid_instance, dggs_type, unit)
    if result is not None:
        print(result)

This module provides functions for generating statistics for QTM DGGS cells.

qtm_compactness_cvh(qtm_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for ISEA4T cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/qtmstats.py
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
def qtm_compactness_cvh(qtm_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for ISEA4T cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # qtm_gdf = qtm_gdf[~qtm_gdf["crossed"]]  # remove cells that cross the dateline
    qtm_gdf = qtm_gdf[np.isfinite(qtm_gdf["cvh"])]
    qtm_gdf = qtm_gdf[qtm_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    qtm_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="QTM CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

qtm_compactness_cvh_hist(qtm_gdf)

Plot histogram of CVH (cell area / convex hull area) for ISEA4T cells.

Source code in vgrid/stats/qtmstats.py
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
def qtm_compactness_cvh_hist(qtm_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for ISEA4T cells.
    """
    # Filter out cells that cross the dateline
    #  qtm_gdf = qtm_gdf[~qtm_gdf["crossed"]]
    qtm_gdf = qtm_gdf[np.isfinite(qtm_gdf["cvh"])]
    qtm_gdf = qtm_gdf[qtm_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        qtm_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {qtm_gdf['cvh'].mean():.6f}\n"
        f"Std: {qtm_gdf['cvh'].std():.6f}\n"
        f"Min: {qtm_gdf['cvh'].min():.6f}\n"
        f"Max: {qtm_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("QTM CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

qtm_compactness_ipq(qtm_gdf, crs='proj=moll')

Plot IPQ compactness map for QTM cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of QTM cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.907 indicating more regular hexagons.

Parameters:

Name Type Description Default
qtm_gdf GeoDataFrame

GeoDataFrame from qtminspect function

required
Source code in vgrid/stats/qtmstats.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def qtm_compactness_ipq(qtm_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for QTM cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of QTM cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.907 indicating more regular hexagons.

    Args:
        qtm_gdf: GeoDataFrame from qtminspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = qtm_gdf['ipq'].min(), qtm_gdf['ipq'].max(), np.mean([qtm_gdf['ipq'].min(), qtm_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_TRI, vcenter=VCENTER_TRI, vmax=VMAX_TRI)
    # qtm_gdf = qtm_gdf[~qtm_gdf["crossed"]]  # remove cells that cross the dateline
    qtm_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="QTM IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

qtm_compactness_ipq_hist(qtm_gdf)

Plot histogram of IPQ compactness for QTM cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for QTM cells, helping to understand how close cells are to being regular triangles.

Parameters:

Name Type Description Default
qtm_gdf GeoDataFrame

GeoDataFrame from qtminspect function

required
Source code in vgrid/stats/qtmstats.py
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
def qtm_compactness_ipq_hist(qtm_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for QTM cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for QTM cells, helping
    to understand how close cells are to being regular triangles.

    Args:
        qtm_gdf: GeoDataFrame from qtminspect function
    """
    # Filter out cells that cross the dateline
    # qtm_gdf = qtm_gdf[~qtm_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        qtm_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_TRI, vcenter=VCENTER_TRI, vmax=VMAX_TRI)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal triangle IPQ value (0.604)
    ax.axvline(
        x=0.604,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Triangle (IPQ = 0.604)",
    )

    # Add statistics text box
    stats_text = f"Mean: {qtm_gdf['ipq'].mean():.3f}\nStd: {qtm_gdf['ipq'].std():.3f}\nMin: {qtm_gdf['ipq'].min():.3f}\nMax: {qtm_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("QTM IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

qtm_metrics(resolution, unit='m')

Calculate metrics for QTM DGGS cells.

Parameters:

Name Type Description Default
resolution int

Resolution level (1-24)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)

Source code in vgrid/stats/qtmstats.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def qtm_metrics(resolution: int, unit: str = "m"):  # length unit is km, area unit is km2
    """
    Calculate metrics for QTM DGGS cells.

    Args:
        resolution: Resolution level (1-24)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = 8 * 4 ** (resolution - 1)

    avg_cell_area = AUTHALIC_AREA / num_cells  # area in m2
    avg_edge_len = math.sqrt((4 * avg_cell_area) / math.sqrt(3))
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m

    return num_cells, avg_edge_len, avg_cell_area, cls

qtm_norm_area(qtm_gdf, crs='proj=moll')

Plot normalized area map for QTM cells.

This function creates a visualization showing how QTM cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
qtm_gdf GeoDataFrame

GeoDataFrame from qtminspect function

required
Source code in vgrid/stats/qtmstats.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def qtm_norm_area(qtm_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for QTM cells.

    This function creates a visualization showing how QTM cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        qtm_gdf: GeoDataFrame from qtminspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = (
        qtm_gdf["norm_area"].min(),
        1.0,
        qtm_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # qtm_gdf = qtm_gdf[~qtm_gdf["crossed"]]  # remove cells that cross the dateline
    qtm_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="QTM Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

qtm_norm_area_hist(qtm_gdf)

Plot histogram of normalized area for QTM cells.

This function creates a histogram visualization showing the distribution of normalized areas for QTM cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
qtm_gdf GeoDataFrame

GeoDataFrame from qtminspect function

required
Source code in vgrid/stats/qtmstats.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
def qtm_norm_area_hist(qtm_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for QTM cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for QTM cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        qtm_gdf: GeoDataFrame from qtminspect function
    """
    # Filter out cells that cross the dateline
    # qtm_gdf = qtm_gdf[~qtm_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        qtm_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (qtm_gdf["norm_area"].min(), 1.0, qtm_gdf["norm_area"].max())
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {qtm_gdf['norm_area'].mean():.3f}\nStd: {qtm_gdf['norm_area'].std():.3f}\nMin: {qtm_gdf['norm_area'].min():.3f}\nMax: {qtm_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("QTM normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

qtminspect(resolution)

Generate comprehensive inspection data for QTM DGGS cells at a given resolution.

This function creates a detailed analysis of QTM cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

QTM resolution level (1-24)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing QTM cell inspection data with columns: - qtm: QTM cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/qtmstats.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def qtminspect(resolution: int):
    """
    Generate comprehensive inspection data for QTM DGGS cells at a given resolution.

    This function creates a detailed analysis of QTM cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: QTM resolution level (1-24)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing QTM cell inspection data with columns:
            - qtm: QTM cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    qtm_gdf = qtm_grid(resolution)
    qtm_gdf["crossed"] = qtm_gdf["geometry"].apply(check_crossing_geom)
    mean_area = qtm_gdf["cell_area"].mean()
    # Calculate normalized area
    qtm_gdf["norm_area"] = qtm_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    qtm_gdf["ipq"] = 4 * np.pi * qtm_gdf["cell_area"] / (qtm_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    qtm_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * qtm_gdf["cell_area"]
            - np.power(qtm_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / qtm_gdf["cell_perimeter"]
    )

    convex_hull = qtm_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    qtm_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        qtm_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    qtm_gdf["cvh"] = qtm_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return qtm_gdf

qtminspect_cli()

Command-line interface for QTM cell inspection.

CLI options

-r, --resolution: QTM resolution level (1-24)

Source code in vgrid/stats/qtmstats.py
476
477
478
479
480
481
482
483
484
485
486
487
def qtminspect_cli():
    """
    Command-line interface for QTM cell inspection.

    CLI options:
      -r, --resolution: QTM resolution level (1-24)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()
    resolution = args.resolution
    print(qtminspect(resolution))

qtmstats(unit='m')

Generate statistics for QTM DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing QTM DGGS statistics with columns: - resolution: Resolution level (1-24) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_cell_area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/qtmstats.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def qtmstats(unit: str = "m"):  # length unit is km, area unit is km2
    """
    Generate statistics for QTM DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing QTM DGGS statistics with columns:
            - resolution: Resolution level (1-24)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_cell_area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = qtm_metrics(res, unit=unit)
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

qtmstats_cli()

Command-line interface for generating QTM DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/qtmstats.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def qtmstats_cli():
    """
    Command-line interface for generating QTM DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()
    unit = args.unit
    # Get the DataFrame
    df = qtmstats(unit=unit)
    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for OLC DGGS cells.

olc_compactness_cvh(olc_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for ISEA4T cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/olcstats.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
def olc_compactness_cvh(olc_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for ISEA4T cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # olc_gdf = olc_gdf[~olc_gdf["crossed"]]  # remove cells that cross the dateline
    olc_gdf = olc_gdf[np.isfinite(olc_gdf["cvh"])]
    olc_gdf = olc_gdf[olc_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    olc_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="OLC CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

olc_compactness_cvh_hist(olc_gdf)

Plot histogram of CVH (cell area / convex hull area) for ISEA4T cells.

Source code in vgrid/stats/olcstats.py
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
def olc_compactness_cvh_hist(olc_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for ISEA4T cells.
    """
    # Filter out cells that cross the dateline
    # olc_gdf = olc_gdf[~olc_gdf["crossed"]]
    olc_gdf = olc_gdf[np.isfinite(olc_gdf["cvh"])]
    olc_gdf = olc_gdf[olc_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        olc_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {olc_gdf['cvh'].mean():.6f}\n"
        f"Std: {olc_gdf['cvh'].std():.6f}\n"
        f"Min: {olc_gdf['cvh'].min():.6f}\n"
        f"Max: {olc_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("OLC CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

olc_compactness_ipq(olc_gdf, crs='proj=moll')

Plot IPQ compactness map for OLC cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of OLC cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.785 indicating more regular squares.

Parameters:

Name Type Description Default
olc_gdf GeoDataFrame

GeoDataFrame from olcinspect function

required
Source code in vgrid/stats/olcstats.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def olc_compactness_ipq(olc_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for OLC cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of OLC cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.785 indicating more regular squares.

    Args:
        olc_gdf: GeoDataFrame from olcinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = olc_gdf['ipq'].min(), olc_gdf['ipq'].max(), np.mean([olc_gdf['ipq'].min(), olc_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)
    # olc_gdf = olc_gdf[~olc_gdf["crossed"]]  # remove cells that cross the dateline
    olc_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="OLC IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

olc_compactness_ipq_hist(olc_gdf)

Plot histogram of IPQ compactness for OLC cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for OLC cells, helping to understand how close cells are to being regular squares.

Parameters:

Name Type Description Default
olc_gdf GeoDataFrame

GeoDataFrame from olcinspect function

required
Source code in vgrid/stats/olcstats.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
def olc_compactness_ipq_hist(olc_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for OLC cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for OLC cells, helping
    to understand how close cells are to being regular squares.

    Args:
        olc_gdf: GeoDataFrame from olcinspect function
    """
    # Filter out cells that cross the dateline
    # olc_gdf = olc_gdf[~olc_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        olc_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal square IPQ value (0.785)
    ax.axvline(
        x=0.785,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Square (IPQ = 0.785)",
    )

    # Add statistics text box
    stats_text = f"Mean: {olc_gdf['ipq'].mean():.3f}\nStd: {olc_gdf['ipq'].std():.3f}\nMin: {olc_gdf['ipq'].min():.3f}\nMax: {olc_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("OLC IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

olc_metrics(resolution, unit='m')

Calculate metrics for OLC DGGS cells.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-15)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)

Source code in vgrid/stats/olcstats.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def olc_metrics(resolution: int, unit: str = "m"):  # length unit is km, area unit is km2
    """
    Calculate metrics for OLC DGGS cells.

    Args:
        resolution: Resolution level (0-15)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Length 2 starts with 162 cells globally
    if resolution <= 10:
        num_cells = 162 * (400 ** ((resolution // 2) - 1))
    else:
        # Length > 10: start from length 10 count, multiply by 20 per extra char
        base = 162 * (400 ** ((10 // 2) - 1))  # N(10)
        extra = resolution - 10
        num_cells = base * (20**extra)

    # Calculate area in km² first
    avg_cell_area = AUTHALIC_AREA / num_cells  # area in m2
    avg_edge_len = math.sqrt(avg_cell_area)
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m

    return num_cells, avg_edge_len, avg_cell_area, cls

olc_norm_area(olc_gdf, crs='proj=moll')

Plot normalized area map for OLC cells.

This function creates a visualization showing how OLC cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
olc_gdf GeoDataFrame

GeoDataFrame from olcinspect function

required
Source code in vgrid/stats/olcstats.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def olc_norm_area(olc_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for OLC cells.

    This function creates a visualization showing how OLC cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        olc_gdf: GeoDataFrame from olcinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vmax, vcenter = olc_gdf["norm_area"].min(), olc_gdf["norm_area"].max(), 1
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # olc_gdf = olc_gdf[~olc_gdf["crossed"]]  # remove cells that cross the dateline
    olc_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="OLC Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

olc_norm_area_hist(olc_gdf)

Plot histogram of normalized area for OLC cells.

This function creates a histogram visualization showing the distribution of normalized areas for OLC cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
olc_gdf GeoDataFrame

GeoDataFrame from olcinspect function

required
Source code in vgrid/stats/olcstats.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
def olc_norm_area_hist(olc_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for OLC cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for OLC cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        olc_gdf: GeoDataFrame from olcinspect function
    """
    # Filter out cells that cross the dateline
    # olc_gdf = olc_gdf[~olc_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        olc_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vmax, vcenter = (
        olc_gdf["norm_area"].min(),
        olc_gdf["norm_area"].max(),
        1,
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {olc_gdf['norm_area'].mean():.3f}\nStd: {olc_gdf['norm_area'].std():.3f}\nMin: {olc_gdf['norm_area'].min():.3f}\nMax: {olc_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("OLC normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

olcinspect(resolution)

Generate comprehensive inspection data for OLC DGGS cells at a given resolution.

This function creates a detailed analysis of OLC cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

OLC resolution level (2-15)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing OLC cell inspection data with columns: - olc: OLC cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/olcstats.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def olcinspect(resolution: int):
    """
    Generate comprehensive inspection data for OLC DGGS cells at a given resolution.

    This function creates a detailed analysis of OLC cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: OLC resolution level (2-15)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing OLC cell inspection data with columns:
            - olc: OLC cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    olc_gdf = olcgrid(resolution, output_format="gpd")          
    olc_gdf["crossed"] = olc_gdf["geometry"].apply(check_crossing_geom)
    mean_area = olc_gdf["cell_area"].mean()
    # Calculate normalized area
    olc_gdf["norm_area"] = olc_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    olc_gdf["ipq"] = 4 * np.pi * olc_gdf["cell_area"] / (olc_gdf["cell_perimeter"] ** 2)
    # Calculate zonal standardized compactness
    olc_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * olc_gdf["cell_area"]
            - np.power(olc_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / olc_gdf["cell_perimeter"]
    )

    convex_hull = olc_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    olc_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        olc_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    olc_gdf["cvh"] = olc_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return olc_gdf

olcinspect_cli()

Command-line interface for OLC cell inspection.

CLI options

-r, --resolution: OLC resolution level (2-15)

Source code in vgrid/stats/olcstats.py
482
483
484
485
486
487
488
489
490
491
492
493
def olcinspect_cli():
    """
    Command-line interface for OLC cell inspection.

    CLI options:
      -r, --resolution: OLC resolution level (2-15)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args, _ = parser.parse_known_args()  # type: ignore
    resolution = args.resolution
    print(olcinspect(resolution))

olcstats(unit='m')

Generate statistics for OLC DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing OLC DGGS statistics with columns: - resolution: Resolution level (2,4,6,8,10,11,12,13,14,15) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_cell_area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/olcstats.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def olcstats(unit: str = "m"):  # length unit is km, area unit is km2
    """
    Generate statistics for OLC DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing OLC DGGS statistics with columns:
            - resolution: Resolution level (2,4,6,8,10,11,12,13,14,15)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_cell_area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Only specific resolutions are supported

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in s2_resolutions:
        num_cells, avg_edge_len, avg_cell_area, cls = olc_metrics(res, unit=unit)
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

olcstats_cli()

Command-line interface for generating OLC DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/olcstats.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def olcstats_cli():
    """
    Command-line interface for generating OLC DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args, _ = parser.parse_known_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = olcstats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for Geohash DGGS cells.

geohash_compactness_cvh(geohash_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for Geohash cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/geohashstats.py
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
def geohash_compactness_cvh(
    geohash_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot CVH (cell area / convex hull area) compactness map for Geohash cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # geohash_gdf = geohash_gdf[~geohash_gdf["crossed"]]  # remove cells that cross the Antimeridian
    geohash_gdf = geohash_gdf[np.isfinite(geohash_gdf["cvh"])]
    geohash_gdf = geohash_gdf[geohash_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    geohash_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Geohash CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

geohash_compactness_cvh_hist(geohash_gdf)

Plot histogram of CVH (cell area / convex hull area) for Geohash cells.

Source code in vgrid/stats/geohashstats.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def geohash_compactness_cvh_hist(geohash_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for Geohash cells.
    """
    # Filter out cells that cross the dateline
    # geohash_gdf = geohash_gdf[~geohash_gdf["crossed"]]
    geohash_gdf = geohash_gdf[np.isfinite(geohash_gdf["cvh"])]
    geohash_gdf = geohash_gdf[geohash_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        geohash_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {geohash_gdf['cvh'].mean():.6f}\n"
        f"Std: {geohash_gdf['cvh'].std():.6f}\n"
        f"Min: {geohash_gdf['cvh'].min():.6f}\n"
        f"Max: {geohash_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("Geohash CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

geohash_compactness_ipq(geohash_gdf, crs='proj=moll')

Plot IPQ compactness map for Geohash cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of Geohash cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.785 indicating more regular squares.

Parameters:

Name Type Description Default
geohash_gdf GeoDataFrame

GeoDataFrame from geohashinspect function

required
Source code in vgrid/stats/geohashstats.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def geohash_compactness_ipq(
    geohash_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot IPQ compactness map for Geohash cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of Geohash cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.785 indicating more regular squares.

    Args:
        geohash_gdf: GeoDataFrame from geohashinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = geohash_gdf['ipq'].min(), geohash_gdf['ipq'].max(), np.mean([geohash_gdf['ipq'].min(), geohash_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)
    geohash_gdf = geohash_gdf[
        ~geohash_gdf["crossed"]
    ]  # remove cells that cross the Antimeridian
    geohash_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Geohash IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

geohash_compactness_ipq_hist(geohash_gdf)

Plot histogram of IPQ compactness for Geohash cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for Geohash cells, helping to understand how close cells are to being regular squares.

Parameters:

Name Type Description Default
geohash_gdf GeoDataFrame

GeoDataFrame from geohashinspect function

required
Source code in vgrid/stats/geohashstats.py
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def geohash_compactness_ipq_hist(geohash_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for Geohash cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for Geohash cells, helping
    to understand how close cells are to being regular squares.

    Args:
        geohash_gdf: GeoDataFrame from geohashinspect function
    """
    # Filter out cells that cross the dateline
    # geohash_gdf = geohash_gdf[~geohash_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        geohash_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal square IPQ value (0.785)
    ax.axvline(
        x=0.785,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Square (IPQ = 0.785)",
    )

    # Add statistics text box
    stats_text = f"Mean: {geohash_gdf['ipq'].mean():.3f}\nStd: {geohash_gdf['ipq'].std():.3f}\nMin: {geohash_gdf['ipq'].min():.3f}\nMax: {geohash_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("Geohash IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

geohash_metrics(resolution, unit='m')

Calculate metrics for Geohash DGGS cells.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-12)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)

Source code in vgrid/stats/geohashstats.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def geohash_metrics(resolution: int, unit: str = "m"):
    """
    Calculate metrics for Geohash DGGS cells.

    Args:
        resolution: Resolution level (0-12)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = 32**resolution

    # Calculate area in km² first
    avg_cell_area = AUTHALIC_AREA / num_cells  # area in m2
    avg_edge_len = math.sqrt(avg_cell_area)
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m

    return num_cells, avg_edge_len, avg_cell_area, cls

geohash_norm_area(geohash_gdf, crs='proj=moll')

Plot normalized area map for Geohash cells.

This function creates a visualization showing how Geohash cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
geohash_gdf GeoDataFrame

GeoDataFrame from geohashinspect function

required
Source code in vgrid/stats/geohashstats.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
def geohash_norm_area(geohash_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for Geohash cells.

    This function creates a visualization showing how Geohash cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        geohash_gdf: GeoDataFrame from geohashinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vmax, vcenter = (
        geohash_gdf["norm_area"].min(),
        geohash_gdf["norm_area"].max(),
        1,
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    geohash_gdf = geohash_gdf[
        ~geohash_gdf["crossed"]
    ]  # remove cells that cross the Antimeridian
    geohash_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Geohash Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

geohash_norm_area_hist(geohash_gdf)

Plot histogram of normalized area for Geohash cells.

This function creates a histogram visualization showing the distribution of normalized areas for Geohash cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
geohash_gdf GeoDataFrame

GeoDataFrame from geohashinspect function

required
Source code in vgrid/stats/geohashstats.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
def geohash_norm_area_hist(geohash_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for Geohash cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for Geohash cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        geohash_gdf: GeoDataFrame from geohashinspect function
    """
    # Filter out cells that cross the dateline
    # geohash_gdf = geohash_gdf[~geohash_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        geohash_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        geohash_gdf["norm_area"].min(),
        1.0,
        geohash_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {geohash_gdf['norm_area'].mean():.3f}\nStd: {geohash_gdf['norm_area'].std():.3f}\nMin: {geohash_gdf['norm_area'].min():.3f}\nMax: {geohash_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("Geohash normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

geohashinspect(resolution)

Generate comprehensive inspection data for Geohash DGGS cells at a given resolution.

This function creates a detailed analysis of Geohash cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

Geohash resolution level (0-12)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing Geohash cell inspection data with columns: - geohash: Geohash cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/geohashstats.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def geohashinspect(resolution: int):
    """
    Generate comprehensive inspection data for Geohash DGGS cells at a given resolution.

    This function creates a detailed analysis of Geohash cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: Geohash resolution level (0-12)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing Geohash cell inspection data with columns:
            - geohash: Geohash cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    geohash_gdf = geohashgrid(resolution, output_format="gpd")
    geohash_gdf["crossed"] = geohash_gdf["geometry"].apply(check_crossing_geom)
    mean_area = geohash_gdf["cell_area"].mean()
    # Calculate normalized area
    geohash_gdf["norm_area"] = geohash_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    geohash_gdf["ipq"] = (
        4 * np.pi * geohash_gdf["cell_area"] / (geohash_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    geohash_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * geohash_gdf["cell_area"]
            - np.power(geohash_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / geohash_gdf["cell_perimeter"]
    )

    convex_hull = geohash_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    geohash_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        geohash_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    geohash_gdf["cvh"] = geohash_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return geohash_gdf

geohashinspect_cli()

Command-line interface for Geohash cell inspection.

CLI options

-r, --resolution: Geohash resolution level (0-12)

Source code in vgrid/stats/geohashstats.py
494
495
496
497
498
499
500
501
502
503
504
505
def geohashinspect_cli():
    """
    Command-line interface for Geohash cell inspection.

    CLI options:
      -r, --resolution: Geohash resolution level (0-12)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()
    resolution = args.resolution
    print(geohashinspect(resolution))

geohashstats(unit='m')

Generate statistics for Geohash DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing Geohash DGGS statistics with columns: - resolution: Resolution level (0-12) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_cell_area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/geohashstats.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def geohashstats(unit: str = "m"):
    """
    Generate statistics for Geohash DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing Geohash DGGS statistics with columns:
            - resolution: Resolution level (0-12)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_cell_area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = geohash_metrics(res, unit=unit)
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

geohashstats_cli()

Command-line interface for generating Geohash DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/geohashstats.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def geohashstats_cli():
    """
    Command-line interface for generating Geohash DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()

    unit = args.unit

    # Get the DataFrame
    df = geohashstats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for GEOREF DGGS cells.

georef_compactness_cvh(georef_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for GEOREF cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/georefstats.py
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
def georef_compactness_cvh(georef_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for GEOREF cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # georef_gdf = georef_gdf[~georef_gdf["crossed"]]  # remove cells that cross the Antimeridian
    georef_gdf = georef_gdf[np.isfinite(georef_gdf["cvh"])]
    georef_gdf = georef_gdf[georef_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    georef_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="GEOREF CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

georef_compactness_cvh_hist(georef_gdf)

Plot histogram of CVH (cell area / convex hull area) for GEOREF cells.

Source code in vgrid/stats/georefstats.py
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
def georef_compactness_cvh_hist(georef_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for GEOREF cells.
    """
    # Filter out cells that cross the Antimeridian
    #  georef_gdf = georef_gdf[~georef_gdf["crossed"]]
    georef_gdf = georef_gdf[np.isfinite(georef_gdf["cvh"])]
    georef_gdf = georef_gdf[georef_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        georef_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {georef_gdf['cvh'].mean():.6f}\n"
        f"Std: {georef_gdf['cvh'].std():.6f}\n"
        f"Min: {georef_gdf['cvh'].min():.6f}\n"
        f"Max: {georef_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("GEOREF CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

georef_compactness_ipq(georef_gdf, crs='proj=moll')

Plot IPQ compactness map for GEOREF cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of GEOREF cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.785 indicating more regular squares.

Parameters:

Name Type Description Default
georef_gdf GeoDataFrame

GeoDataFrame from georefinspect function

required
Source code in vgrid/stats/georefstats.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def georef_compactness_ipq(georef_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for GEOREF cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of GEOREF cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.785 indicating more regular squares.

    Args:
        georef_gdf: GeoDataFrame from georefinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = georef_gdf['ipq'].min(), georef_gdf['ipq'].max(), np.mean([georef_gdf['ipq'].min(), georef_gdf['ipq'].max()])
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # georef_gdf = georef_gdf[~georef_gdf["crossed"]]  # remove cells that cross the Antimeridian
    georef_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="GEOREF IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

georef_compactness_ipq_hist(georef_gdf)

Plot histogram of IPQ compactness for GEOREF cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for GEOREF cells, helping to understand how close cells are to being regular squares.

Parameters:

Name Type Description Default
georef_gdf GeoDataFrame

GeoDataFrame from georefinspect function

required
Source code in vgrid/stats/georefstats.py
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def georef_compactness_ipq_hist(georef_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for GEOREF cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for GEOREF cells, helping
    to understand how close cells are to being regular squares.

    Args:
        georef_gdf: GeoDataFrame from georefinspect function
    """
    # Filter out cells that cross the Antimeridian
    # georef_gdf = georef_gdf[~georef_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        georef_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal square IPQ value (0.785)
    ax.axvline(
        x=0.785,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Square (IPQ = 0.785)",
    )

    # Add statistics text box
    stats_text = f"Mean: {georef_gdf['ipq'].mean():.3f}\nStd: {georef_gdf['ipq'].std():.3f}\nMin: {georef_gdf['ipq'].min():.3f}\nMax: {georef_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("GEOREF IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

georef_metrics(resolution, unit='m')

Calculate metrics for GEOREF DGGS cells.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-7)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)

Source code in vgrid/stats/georefstats.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def georef_metrics(resolution: int, unit: str = "m"):
    """
    Calculate metrics for GEOREF DGGS cells.

    Args:
        resolution: Resolution level (0-7)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Compute grid size in degrees per your formula
    grid_size_deg = GEOREF_RESOLUTION_DEGREES.get(resolution)

    # Number of cells across longitude and latitude
    num_lon = int(round(360.0 / grid_size_deg))
    num_lat = int(round(180.0 / grid_size_deg))
    num_cells = num_lon * num_lat

    # Calculate area in km² first
    avg_cell_area = AUTHALIC_AREA / num_cells  # area in m2
    avg_edge_len = math.sqrt(avg_cell_area)
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m

    return num_cells, avg_edge_len, avg_cell_area, cls

georef_norm_area(georef_gdf, crs='proj=moll')

Plot normalized area map for GEOREF cells.

This function creates a visualization showing how GEOREF cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
georef_gdf GeoDataFrame

GeoDataFrame from georefinspect function

required
Source code in vgrid/stats/georefstats.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def georef_norm_area(georef_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for GEOREF cells.

    This function creates a visualization showing how GEOREF cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        georef_gdf: GeoDataFrame from georefinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = (
        georef_gdf["norm_area"].min(),
        1.0,
        georef_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # georef_gdf = georef_gdf[~georef_gdf["crossed"]]  # remove cells that cross the Antimeridian
    georef_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="GEOREF Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

georef_norm_area_hist(georef_gdf)

Plot histogram of normalized area for GEOREF cells.

This function creates a histogram visualization showing the distribution of normalized areas for GEOREF cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
georef_gdf GeoDataFrame

GeoDataFrame from georefinspect function

required
Source code in vgrid/stats/georefstats.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
def georef_norm_area_hist(georef_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for GEOREF cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for GEOREF cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        georef_gdf: GeoDataFrame from georefinspect function
    """
    # Filter out cells that cross the Antimeridian
    # georef_gdf = georef_gdf[~georef_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        georef_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        georef_gdf["norm_area"].min(),
        1.0,
        georef_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {georef_gdf['norm_area'].mean():.3f}\nStd: {georef_gdf['norm_area'].std():.3f}\nMin: {georef_gdf['norm_area'].min():.3f}\nMax: {georef_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("GEOREF normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

georefinspect(resolution)

Generate comprehensive inspection data for GEOREF DGGS cells at a given resolution.

This function creates a detailed analysis of GEOREF cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

GEOREF resolution level (0-10)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing GEOREF cell inspection data with columns: - georef: GEOREF cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/georefstats.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def georefinspect(resolution: int):
    """
    Generate comprehensive inspection data for GEOREF DGGS cells at a given resolution.

    This function creates a detailed analysis of GEOREF cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: GEOREF resolution level (0-10)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing GEOREF cell inspection data with columns:
            - georef: GEOREF cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    georef_gdf = georefgrid(resolution, output_format="gpd")
    georef_gdf["crossed"] = georef_gdf["geometry"].apply(check_crossing_geom)
    mean_area = georef_gdf["cell_area"].mean()
    # Calculate normalized area
    georef_gdf["norm_area"] = georef_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    georef_gdf["ipq"] = (
        4 * np.pi * georef_gdf["cell_area"] / (georef_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    georef_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * georef_gdf["cell_area"]
            - np.power(georef_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / georef_gdf["cell_perimeter"]
    )

    convex_hull = georef_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    georef_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        georef_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    georef_gdf["cvh"] = georef_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return georef_gdf

georefinspect_cli()

Command-line interface for GEOREF cell inspection.

CLI options

-r, --resolution: GEOREF resolution level (0-10)

Source code in vgrid/stats/georefstats.py
495
496
497
498
499
500
501
502
503
504
505
506
def georefinspect_cli():
    """
    Command-line interface for GEOREF cell inspection.

    CLI options:
      -r, --resolution: GEOREF resolution level (0-10)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    print(georefinspect(resolution))

georefstats(unit='m')

Generate statistics for GEOREF DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing GEOREF DGGS statistics with columns: - resolution: Resolution level (0-7) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_cell_area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/georefstats.py
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def georefstats(unit: str = "m"):
    """
    Generate statistics for GEOREF DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing GEOREF DGGS statistics with columns:
            - resolution: Resolution level (0-7)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_cell_area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = georef_metrics(res, unit=unit)
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    length_col = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    area_col = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            length_col: avg_edge_lens,
            area_col: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

georefstats_cli()

Command-line interface for generating GEOREF DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/georefstats.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def georefstats_cli():
    """
    Command-line interface for generating GEOREF DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = georefstats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for MGRS DGGS cells.

mgrs_metrics(resolution, unit='m')

Calculate metrics for MGRS DGGS cells.

Parameters:

Name Type Description Default
resolution

Resolution level (0-5)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)

Source code in vgrid/stats/mgrsstats.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def mgrs_metrics(resolution, unit: str = "m"):
    """
    Calculate metrics for MGRS DGGS cells.

    Args:
        resolution: Resolution level (0-5)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    latitude_degrees = 8  # The latitude span of each GZD cell in degrees
    longitude_degrees = 6  # The longitude span of each GZD cell in degrees
    km_per_degree = 111  # Approximate kilometers per degree of latitude/longitude
    gzd_cells = 1200  # Total number of GZD cells

    # Convert degrees to kilometers
    latitude_span = latitude_degrees * km_per_degree
    longitude_span = longitude_degrees * km_per_degree

    # Calculate cell size in kilometers based on resolution
    # Resolution 1: 100 km, each subsequent resolution divides by 10
    cell_size_km = 100 / (10 ** (resolution))   
    # Calculate number of cells in latitude and longitude for the chosen cell size
    cells_latitude = latitude_span / cell_size_km
    cells_longitude = longitude_span / cell_size_km

    # Total number of cells for each GZD cell
    cells_per_gzd_cell = cells_latitude * cells_longitude

    # Total number of cells for all GZD cells
    num_cells = cells_per_gzd_cell * gzd_cells
    avg_edge_len = cell_size_km  # in km
    avg_cell_area = avg_edge_len**2  # in km2
    cls = characteristic_length_scale(
        avg_cell_area * (10 ** (-6)), unit=unit
    )  # convert avg_cell_area to m2 before calling characteristic_length_scale

    if unit == "m":
        avg_edge_len = cell_size_km * (10**3)  # Convert km to m
        avg_cell_area = avg_cell_area * (10**6)

    return num_cells, avg_edge_len, avg_cell_area, cls

mgrsstats(unit='m')

Generate statistics for MGRS DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing MGRS DGGS statistics with columns: - resolution: Resolution level (0-5) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_cell_area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/mgrsstats.py
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def mgrsstats(unit: str = "m"):
    """
    Generate statistics for MGRS DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing MGRS DGGS statistics with columns:
            - resolution: Resolution level (0-5)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_cell_area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for resolution in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = mgrs_metrics(resolution, unit=unit)
        resolutions.append(resolution)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

mgrsstats_cli()

Command-line interface for generating MGRS DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/mgrsstats.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def mgrsstats_cli():
    """
    Command-line interface for generating MGRS DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()  # type: ignore

    unit = args.unit

    print("Resolution 0: 100 x 100 km")
    print("Resolution 1: 10 x 10 km")
    print("2 <= Resolution <= 5 = Finer subdivisions (1 x 1 km, 0.1 x 0.11 km, etc.)")

    # Get the DataFrame
    df = mgrsstats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for Tilecode DGGS cells.

tilecode_compactness_cvh(tilecode_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for Tilecode cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/tilecodestats.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
def tilecode_compactness_cvh(
    tilecode_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot CVH (cell area / convex hull area) compactness map for Tilecode cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # tilecode_gdf = tilecode_gdf[~tilecode_gdf["crossed"]]  # remove cells that cross the dateline
    tilecode_gdf = tilecode_gdf[np.isfinite(tilecode_gdf["cvh"])]
    tilecode_gdf = tilecode_gdf[tilecode_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    tilecode_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson",
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Tilecode CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

tilecode_compactness_cvh_hist(tilecode_gdf)

Plot histogram of CVH (cell area / convex hull area) for Tilecode cells.

Source code in vgrid/stats/tilecodestats.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
def tilecode_compactness_cvh_hist(tilecode_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for Tilecode cells.
    """
    # Filter out cells that cross the dateline
    # tilecode_gdf = tilecode_gdf[~tilecode_gdf["crossed"]]
    tilecode_gdf = tilecode_gdf[np.isfinite(tilecode_gdf["cvh"])]
    tilecode_gdf = tilecode_gdf[tilecode_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        tilecode_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {tilecode_gdf['cvh'].mean():.6f}\n"
        f"Std: {tilecode_gdf['cvh'].std():.6f}\n"
        f"Min: {tilecode_gdf['cvh'].min():.6f}\n"
        f"Max: {tilecode_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("Tilecode CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

tilecode_compactness_ipq(tilecode_gdf, crs='proj=moll')

Plot IPQ compactness map for Tilecode cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of Tilecode cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.785 indicating more regular squares.

Parameters:

Name Type Description Default
tilecode_gdf GeoDataFrame

GeoDataFrame from tilecodeinspect function

required
Source code in vgrid/stats/tilecodestats.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
def tilecode_compactness_ipq(
    tilecode_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot IPQ compactness map for Tilecode cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of Tilecode cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.785 indicating more regular squares.

    Args:
        tilecode_gdf: GeoDataFrame from tilecodeinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = tilecode_gdf['ipq'].min(), tilecode_gdf['ipq'].max(), np.mean([tilecode_gdf['ipq'].min(), tilecode_gdf['ipq'].max()])
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # tilecode_gdf = tilecode_gdf[~tilecode_gdf["crossed"]]  # remove cells that cross the dateline
    tilecode_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Tilecode IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

tilecode_compactness_ipq_hist(tilecode_gdf)

Plot histogram of IPQ compactness for Tilecode cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for Tilecode cells, helping to understand how close cells are to being regular squares.

Parameters:

Name Type Description Default
tilecode_gdf GeoDataFrame

GeoDataFrame from tilecodeinspect function

required
Source code in vgrid/stats/tilecodestats.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
def tilecode_compactness_ipq_hist(tilecode_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for Tilecode cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for Tilecode cells, helping
    to understand how close cells are to being regular squares.

    Args:
        tilecode_gdf: GeoDataFrame from tilecodeinspect function
    """
    # Filter out cells that cross the dateline
    # tilecode_gdf = tilecode_gdf[~tilecode_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        tilecode_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal square IPQ value (0.785)
    ax.axvline(
        x=0.785,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Square (IPQ = 0.785)",
    )

    # Add statistics text box
    stats_text = f"Mean: {tilecode_gdf['ipq'].mean():.3f}\nStd: {tilecode_gdf['ipq'].std():.3f}\nMin: {tilecode_gdf['ipq'].min():.3f}\nMax: {tilecode_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("Tilecode IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

tilecode_metrics(resolution, unit='m')

Calculate metrics for Tilecode DGGS cells.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-30)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)

Source code in vgrid/stats/tilecodestats.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def tilecode_metrics(resolution: int, unit: str = "m"):  # length unit is km, area unit is km2
    """
    Calculate metrics for Tilecode DGGS cells.

    Args:
        resolution: Resolution level (0-30)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = 4**resolution

    # Calculate area in km² first
    avg_cell_area = AUTHALIC_AREA / num_cells  # area in m2
    avg_edge_len = math.sqrt(avg_cell_area)
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m

    return num_cells, avg_edge_len, avg_cell_area, cls

tilecode_norm_area(tilecode_gdf, crs='proj=moll')

Plot normalized area map for Tilecode cells.

This function creates a visualization showing how Tilecode cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
tilecode_gdf GeoDataFrame

GeoDataFrame from tilecodeinspect function

required
Source code in vgrid/stats/tilecodestats.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def tilecode_norm_area(tilecode_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for Tilecode cells.

    This function creates a visualization showing how Tilecode cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        tilecode_gdf: GeoDataFrame from tilecodeinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = (
        tilecode_gdf["norm_area"].min(),
        1.0,
        tilecode_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # tilecode_gdf = tilecode_gdf[~tilecode_gdf["crossed"]]  # remove cells that cross the dateline
    tilecode_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Tilecode Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

tilecode_norm_area_hist(tilecode_gdf)

Plot histogram of normalized area for Tilecode cells.

This function creates a histogram visualization showing the distribution of normalized areas for Tilecode cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
tilecode_gdf GeoDataFrame

GeoDataFrame from tilecodeinspect function

required
Source code in vgrid/stats/tilecodestats.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def tilecode_norm_area_hist(tilecode_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for Tilecode cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for Tilecode cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        tilecode_gdf: GeoDataFrame from tilecodeinspect function
    """
    # Filter out cells that cross the dateline
    # tilecode_gdf = tilecode_gdf[~tilecode_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        tilecode_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        tilecode_gdf["norm_area"].min(),
        1.0,
        tilecode_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {tilecode_gdf['norm_area'].mean():.3f}\nStd: {tilecode_gdf['norm_area'].std():.3f}\nMin: {tilecode_gdf['norm_area'].min():.3f}\nMax: {tilecode_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("Tilecode normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

tilecodeinspect(resolution)

Generate comprehensive inspection data for Tilecode DGGS cells at a given resolution.

This function creates a detailed analysis of Tilecode cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

Tilecode resolution level (0-29)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing Tilecode cell inspection data with columns: - tilecode: Tilecode cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/tilecodestats.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def tilecodeinspect(resolution: int):
    """
    Generate comprehensive inspection data for Tilecode DGGS cells at a given resolution.

    This function creates a detailed analysis of Tilecode cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: Tilecode resolution level (0-29)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing Tilecode cell inspection data with columns:
            - tilecode: Tilecode cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    tilecode_gdf = tilecodegrid(resolution, output_format="gpd")
    tilecode_gdf["crossed"] = tilecode_gdf["geometry"].apply(check_crossing_geom)
    mean_area = tilecode_gdf["cell_area"].mean()
    # Calculate normalized area
    tilecode_gdf["norm_area"] = tilecode_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    tilecode_gdf["ipq"] = (
        4 * np.pi * tilecode_gdf["cell_area"] / (tilecode_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    tilecode_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * tilecode_gdf["cell_area"]
            - np.power(tilecode_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / tilecode_gdf["cell_perimeter"]
    )

    convex_hull = tilecode_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    tilecode_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        tilecode_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    tilecode_gdf["cvh"] = tilecode_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return tilecode_gdf

tilecodeinspect_cli()

Command-line interface for Tilecode cell inspection.

CLI options

-r, --resolution: Tilecode resolution level (0-30)

Source code in vgrid/stats/tilecodestats.py
492
493
494
495
496
497
498
499
500
501
502
503
def tilecodeinspect_cli():
    """
    Command-line interface for Tilecode cell inspection.

    CLI options:
      -r, --resolution: Tilecode resolution level (0-30)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()
    resolution = args.resolution
    print(tilecodeinspect(resolution))

tilecodestats(unit='m')

Generate statistics for Tilecode DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing Tilecode DGGS statistics with columns: - resolution: Resolution level (0-30) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_cell_area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/tilecodestats.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def tilecodestats(unit: str = "m"):
    """
    Generate statistics for Tilecode DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing Tilecode DGGS statistics with columns:
            - resolution: Resolution level (0-30)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_cell_area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = tilecode_metrics(res, unit=unit)
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

tilecodestats_cli()

Command-line interface for generating Tilecode DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/tilecodestats.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def tilecodestats_cli():
    """
    Command-line interface for generating Tilecode DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_known_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = tilecodestats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for Quadkey DGGS cells.

quadkey_compactness_cvh(quadkey_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for Quadkey cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/quadkeystats.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def quadkey_compactness_cvh(
    quadkey_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot CVH (cell area / convex hull area) compactness map for Quadkey cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # quadkey_gdf = quadkey_gdf[~quadkey_gdf["crossed"]]  # remove cells that cross the dateline
    quadkey_gdf = quadkey_gdf[np.isfinite(quadkey_gdf["cvh"])]
    quadkey_gdf = quadkey_gdf[quadkey_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    quadkey_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson",
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Quadkey CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

quadkey_compactness_cvh_hist(quadkey_gdf)

Plot histogram of CVH (cell area / convex hull area) for Quadkey cells.

Source code in vgrid/stats/quadkeystats.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
def quadkey_compactness_cvh_hist(quadkey_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for Quadkey cells.
    """
    # Filter out cells that cross the dateline
    # quadkey_gdf = quadkey_gdf[~quadkey_gdf["crossed"]]
    quadkey_gdf = quadkey_gdf[np.isfinite(quadkey_gdf["cvh"])]
    quadkey_gdf = quadkey_gdf[quadkey_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        quadkey_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {quadkey_gdf['cvh'].mean():.6f}\n"
        f"Std: {quadkey_gdf['cvh'].std():.6f}\n"
        f"Min: {quadkey_gdf['cvh'].min():.6f}\n"
        f"Max: {quadkey_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("Quadkey CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

quadkey_compactness_ipq(quadkey_gdf, crs='proj=moll')

Plot IPQ compactness map for Quadkey cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of Quadkey cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.785 indicating more regular squares.

Parameters:

Name Type Description Default
quadkey_gdf GeoDataFrame

GeoDataFrame from quadkeyinspect function

required
Source code in vgrid/stats/quadkeystats.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def quadkey_compactness_ipq(
    quadkey_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot IPQ compactness map for Quadkey cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of Quadkey cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.785 indicating more regular squares.

    Args:
        quadkey_gdf: GeoDataFrame from quadkeyinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = quadkey_gdf['ipq'].min(), quadkey_gdf['ipq'].max(), np.mean([quadkey_gdf['ipq'].min(), quadkey_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)
    # quadkey_gdf = quadkey_gdf[~quadkey_gdf["crossed"]]  # remove cells that cross the dateline
    quadkey_gdf = quadkey_gdf[np.isfinite(quadkey_gdf["ipq"])]
    quadkey_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    quadkey_gdf = quadkey_gdf[np.isfinite(quadkey_gdf["ipq"])]
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Quadkey IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

quadkey_compactness_ipq_hist(quadkey_gdf)

Plot histogram of IPQ compactness for Quadkey cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for Quadkey cells, helping to understand how close cells are to being regular squares.

Parameters:

Name Type Description Default
quadkey_gdf GeoDataFrame

GeoDataFrame from quadkeyinspect function

required
Source code in vgrid/stats/quadkeystats.py
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def quadkey_compactness_ipq_hist(quadkey_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for Quadkey cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for Quadkey cells, helping
    to understand how close cells are to being regular squares.

    Args:
        quadkey_gdf: GeoDataFrame from quadkeyinspect function
    """
    # Filter out cells that cross the dateline
    # quadkey_gdf = quadkey_gdf[~quadkey_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        quadkey_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal square IPQ value (0.785)
    ax.axvline(
        x=0.785,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Square (IPQ = 0.785)",
    )

    # Add statistics text box
    stats_text = f"Mean: {quadkey_gdf['ipq'].mean():.3f}\nStd: {quadkey_gdf['ipq'].std():.3f}\nMin: {quadkey_gdf['ipq'].min():.3f}\nMax: {quadkey_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("Quadkey IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

quadkey_metrics(resolution, unit='m')

Calculate metrics for Quadkey DGGS cells.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-30)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)

Source code in vgrid/stats/quadkeystats.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def quadkey_metrics(resolution: int, unit: str = "m"):  # length unit is km, area unit is km2
    """
    Calculate metrics for Quadkey DGGS cells.

    Args:
        resolution: Resolution level (0-30)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    num_cells = 4**resolution

    avg_cell_area = AUTHALIC_AREA / num_cells  # area in m2
    avg_edge_len = math.sqrt(avg_cell_area)
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m

    return num_cells, avg_edge_len, avg_cell_area, cls

quadkey_norm_area(quadkey_gdf, crs='proj=moll')

Plot normalized area map for Quadkey cells.

This function creates a visualization showing how Quadkey cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
quadkey_gdf GeoDataFrame

GeoDataFrame from quadkeyinspect function

required
Source code in vgrid/stats/quadkeystats.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def quadkey_norm_area(quadkey_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for Quadkey cells.

    This function creates a visualization showing how Quadkey cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        quadkey_gdf: GeoDataFrame from quadkeyinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vmax, vcenter = (
        quadkey_gdf["norm_area"].min(),
        quadkey_gdf["norm_area"].max(),
        1,
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # quadkey_gdf = quadkey_gdf[~quadkey_gdf["crossed"]]  # remove cells that cross the dateline
    quadkey_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Quadkey Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

quadkey_norm_area_hist(quadkey_gdf)

Plot histogram of normalized area for Quadkey cells.

This function creates a histogram visualization showing the distribution of normalized areas for Quadkey cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
quadkey_gdf GeoDataFrame

GeoDataFrame from quadkeyinspect function

required
Source code in vgrid/stats/quadkeystats.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
def quadkey_norm_area_hist(quadkey_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for Quadkey cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for Quadkey cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        quadkey_gdf: GeoDataFrame from quadkeyinspect function
    """
    # Filter out cells that cross the dateline
    # quadkey_gdf = quadkey_gdf[~quadkey_gdf["crossed"]]
    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        quadkey_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vmax, vcenter = (
        quadkey_gdf["norm_area"].min(),
        quadkey_gdf["norm_area"].max(),
        1,
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {quadkey_gdf['norm_area'].mean():.3f}\nStd: {quadkey_gdf['norm_area'].std():.3f}\nMin: {quadkey_gdf['norm_area'].min():.3f}\nMax: {quadkey_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("Quadkey normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

quadkeyinspect(resolution)

Generate comprehensive inspection data for Quadkey DGGS cells at a given resolution.

This function creates a detailed analysis of Quadkey cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

Quadkey resolution level (0-29)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing Quadkey cell inspection data with columns: - quadkey: Quadkey cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/quadkeystats.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def quadkeyinspect(resolution: int):
    """
    Generate comprehensive inspection data for Quadkey DGGS cells at a given resolution.

    This function creates a detailed analysis of Quadkey cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: Quadkey resolution level (0-29)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing Quadkey cell inspection data with columns:
            - quadkey: Quadkey cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    quadkey_gdf = quadkeygrid(resolution, output_format="gpd")
    quadkey_gdf["crossed"] = quadkey_gdf["geometry"].apply(check_crossing_geom)
    mean_area = quadkey_gdf["cell_area"].mean()
    # Calculate normalized area
    quadkey_gdf["norm_area"] = quadkey_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    quadkey_gdf["ipq"] = (
        4 * np.pi * quadkey_gdf["cell_area"] / (quadkey_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    quadkey_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * quadkey_gdf["cell_area"]
            - np.power(quadkey_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / quadkey_gdf["cell_perimeter"]
    )

    convex_hull = quadkey_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    quadkey_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        quadkey_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    quadkey_gdf["cvh"] = quadkey_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return quadkey_gdf

quadkeyinspect_cli()

Command-line interface for Quadkey cell inspection.

CLI options

-r, --resolution: Quadkey resolution level (0-29)

Source code in vgrid/stats/quadkeystats.py
489
490
491
492
493
494
495
496
497
498
499
500
def quadkeyinspect_cli():
    """
    Command-line interface for Quadkey cell inspection.

    CLI options:
      -r, --resolution: Quadkey resolution level (0-29)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=None)
    args, _ = parser.parse_known_args()
    res = args.resolution if args.resolution is not None else 2
    print(quadkeyinspect(res))

quadkeystats(unit='m')

Generate statistics for Quadkey DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing Quadkey DGGS statistics with columns: - resolution: Resolution level (0-30) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_cell_area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/quadkeystats.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def quadkeystats(unit: str = "m"):  # length unit is km, area unit is km2
    """
    Generate statistics for Quadkey DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing Quadkey DGGS statistics with columns:
            - resolution: Resolution level (0-30)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_cell_area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = quadkey_metrics(
            res, unit=unit
        )  # length unit is km, area unit is km2
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

quadkeystats_cli()

Command-line interface for generating Quadkey DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/quadkeystats.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def quadkeystats_cli():
    """
    Command-line interface for generating Quadkey DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()
    unit = args.unit
    # Get the DataFrame
    df = quadkeystats(unit=unit)
    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for Maidenhead DGGS cells.

maidenhead_compactness_cvh(maidenhead_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for Maidenhead cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/maidenheadstats.py
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
def maidenhead_compactness_cvh(
    maidenhead_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot CVH (cell area / convex hull area) compactness map for Maidenhead cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # maidenhead_gdf = maidenhead_gdf[~maidenhead_gdf["crossed"]]  # remove cells that cross the dateline
    maidenhead_gdf = maidenhead_gdf[np.isfinite(maidenhead_gdf["cvh"])]
    maidenhead_gdf = maidenhead_gdf[maidenhead_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    maidenhead_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson",
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Maidenhead CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

maidenhead_compactness_cvh_hist(maidenhead_gdf)

Plot histogram of CVH (cell area / convex hull area) for Maidenhead cells.

Source code in vgrid/stats/maidenheadstats.py
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
def maidenhead_compactness_cvh_hist(maidenhead_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for Maidenhead cells.
    """
    # Filter out cells that cross the dateline
    # maidenhead_gdf = maidenhead_gdf[~maidenhead_gdf["crossed"]]
    maidenhead_gdf = maidenhead_gdf[np.isfinite(maidenhead_gdf["cvh"])]
    maidenhead_gdf = maidenhead_gdf[maidenhead_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        maidenhead_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {maidenhead_gdf['cvh'].mean():.6f}\n"
        f"Std: {maidenhead_gdf['cvh'].std():.6f}\n"
        f"Min: {maidenhead_gdf['cvh'].min():.6f}\n"
        f"Max: {maidenhead_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("Maidenhead CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

maidenhead_compactness_ipq(maidenhead_gdf, crs='proj=moll')

Plot IPQ compactness map for Maidenhead cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of Maidenhead cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.785 indicating more regular squares.

Parameters:

Name Type Description Default
maidenhead_gdf GeoDataFrame

GeoDataFrame from maidenheadinspect function

required
Source code in vgrid/stats/maidenheadstats.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
def maidenhead_compactness_ipq(
    maidenhead_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot IPQ compactness map for Maidenhead cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of Maidenhead cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.785 indicating more regular squares.

    Args:
        maidenhead_gdf: GeoDataFrame from maidenheadinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = maidenhead_gdf['ipq'].min(), maidenhead_gdf['ipq'].max(), np.mean([maidenhead_gdf['ipq'].min(), maidenhead_gdf['ipq'].max()])
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)
    # maidenhead_gdf = maidenhead_gdf[~maidenhead_gdf["crossed"] ]  # remove cells that cross the dateline
    maidenhead_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Maidenhead IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

maidenhead_compactness_ipq_hist(maidenhead_gdf)

Plot histogram of IPQ compactness for Maidenhead cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for Maidenhead cells, helping to understand how close cells are to being regular squares.

Parameters:

Name Type Description Default
maidenhead_gdf GeoDataFrame

GeoDataFrame from maidenheadinspect function

required
Source code in vgrid/stats/maidenheadstats.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
def maidenhead_compactness_ipq_hist(maidenhead_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for Maidenhead cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for Maidenhead cells, helping
    to understand how close cells are to being regular squares.

    Args:
        maidenhead_gdf: GeoDataFrame from maidenheadinspect function
    """
    # Filter out cells that cross the dateline
    # maidenhead_gdf = maidenhead_gdf[~maidenhead_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        maidenhead_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    norm = TwoSlopeNorm(vmin=VMIN_QUAD, vcenter=VCENTER_QUAD, vmax=VMAX_QUAD)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal square IPQ value (0.785)
    ax.axvline(
        x=0.785,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Square (IPQ = 0.785)",
    )

    # Add statistics text box
    stats_text = f"Mean: {maidenhead_gdf['ipq'].mean():.3f}\nStd: {maidenhead_gdf['ipq'].std():.3f}\nMin: {maidenhead_gdf['ipq'].min():.3f}\nMax: {maidenhead_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("Maidenhead IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

maidenhead_metrics(resolution, unit='m')

Calculate metrics for Maidenhead DGGS cells.

Parameters:

Name Type Description Default
resolution

Resolution level (0-4)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)

Source code in vgrid/stats/maidenheadstats.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def maidenhead_metrics(
    resolution, unit: str = "m"
):  # length unit is km, area unit is km2
    """
    Calculate metrics for Maidenhead DGGS cells.

    Args:
        resolution: Resolution level (0-4)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Maidenhead grid has 324 (18x18) cells at base level
    # Each subdivision adds 10x10 = 100 cells per parent cell
    num_cells = maidenhead.num_cells(resolution)
    # Calculate area in km² first
    avg_cell_area = AUTHALIC_AREA / num_cells
    avg_edge_len = math.sqrt(avg_cell_area)
    cls = characteristic_length_scale(avg_cell_area, unit=unit)

    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m

    return num_cells, avg_edge_len, avg_cell_area, cls

maidenhead_norm_area(maidenhead_gdf, crs='proj=moll')

Plot normalized area map for Maidenhead cells.

This function creates a visualization showing how Maidenhead cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
maidenhead_gdf GeoDataFrame

GeoDataFrame from maidenheadinspect function

required
Source code in vgrid/stats/maidenheadstats.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def maidenhead_norm_area(
    maidenhead_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"
):
    """
    Plot normalized area map for Maidenhead cells.

    This function creates a visualization showing how Maidenhead cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        maidenhead_gdf: GeoDataFrame from maidenheadinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = (
        maidenhead_gdf["norm_area"].min(),
        1.0,
        maidenhead_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # maidenhead_gdf = maidenhead_gdf[~maidenhead_gdf["crossed"]]   # remove cells that cross the dateline
    maidenhead_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="Maidenhead Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

maidenhead_norm_area_hist(maidenhead_gdf)

Plot histogram of normalized area for Maidenhead cells.

This function creates a histogram visualization showing the distribution of normalized areas for Maidenhead cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
maidenhead_gdf GeoDataFrame

GeoDataFrame from maidenheadinspect function

required
Source code in vgrid/stats/maidenheadstats.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
def maidenhead_norm_area_hist(maidenhead_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for Maidenhead cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for Maidenhead cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        maidenhead_gdf: GeoDataFrame from maidenheadinspect function
    """
    # Filter out cells that cross the dateline
    # maidenhead_gdf = maidenhead_gdf[~maidenhead_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        maidenhead_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        maidenhead_gdf["norm_area"].min(),
        1.0,
        maidenhead_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {maidenhead_gdf['norm_area'].mean():.3f}\nStd: {maidenhead_gdf['norm_area'].std():.3f}\nMin: {maidenhead_gdf['norm_area'].min():.3f}\nMax: {maidenhead_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("Maidenhead normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

maidenheadinspect(resolution)

Generate comprehensive inspection data for Maidenhead DGGS cells at a given resolution.

This function creates a detailed analysis of Maidenhead cells including area variations, compactness measures, and dateline crossing detection.

Parameters:

Name Type Description Default
resolution int

Maidenhead resolution level (1-4)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing Maidenhead cell inspection data with columns: - maidenhead: Maidenhead cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the dateline - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/maidenheadstats.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def maidenheadinspect(resolution: int):
    """
    Generate comprehensive inspection data for Maidenhead DGGS cells at a given resolution.

    This function creates a detailed analysis of Maidenhead cells including area variations,
    compactness measures, and dateline crossing detection.

    Args:
        resolution: Maidenhead resolution level (1-4)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing Maidenhead cell inspection data with columns:
            - maidenhead: Maidenhead cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the dateline
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    resolution = validate_maidenhead_resolution(resolution)
    maidenhead_gdf = maidenheadgrid(resolution, output_format="gpd")    
    maidenhead_gdf["crossed"] = maidenhead_gdf["geometry"].apply(check_crossing_geom)
    mean_area = maidenhead_gdf["cell_area"].mean()
    # Calculate normalized area
    maidenhead_gdf["norm_area"] = maidenhead_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    maidenhead_gdf["ipq"] = (
        4
        * np.pi
        * maidenhead_gdf["cell_area"]
        / (maidenhead_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    maidenhead_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * maidenhead_gdf["cell_area"]
            - np.power(maidenhead_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / maidenhead_gdf["cell_perimeter"]
    )

    convex_hull = maidenhead_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    maidenhead_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        maidenhead_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    maidenhead_gdf["cvh"] = maidenhead_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return maidenhead_gdf

maidenheadinspect_cli()

Command-line interface for Maidenhead cell inspection.

CLI options

-r, --resolution: Maidenhead resolution level (1-4)

Source code in vgrid/stats/maidenheadstats.py
504
505
506
507
508
509
510
511
512
513
514
515
def maidenheadinspect_cli():
    """
    Command-line interface for Maidenhead cell inspection.

    CLI options:
      -r, --resolution: Maidenhead resolution level (1-4)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args, _ = parser.parse_known_args()  # type: ignore
    resolution = args.resolution
    print(maidenheadinspect(resolution))

maidenheadstats(unit='m')

Generate statistics for Maidenhead DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing Maidenhead DGGS statistics with columns: - resolution: Resolution level (0-4) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_cell_area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/maidenheadstats.py
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def maidenheadstats(unit: str = "m"):  # length unit is km, area unit is km2
    """
    Generate statistics for Maidenhead DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing Maidenhead DGGS statistics with columns:
            - resolution: Resolution level (0-4)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_cell_area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = maidenhead_metrics(
            res, unit=unit
        )  # length unit is km, area unit is km2
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

maidenheadstats_cli()

Command-line interface for generating Maidenhead DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/maidenheadstats.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def maidenheadstats_cli():
    """
    Command-line interface for generating Maidenhead DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args, _ = parser.parse_known_args()  # type: ignore

    unit = args.unit

    # Get the DataFrame
    df = maidenheadstats(unit=unit)

    # Display the DataFrame
    print(df)

This module provides functions for generating statistics for GARS DGGS cells.

gars_compactness_cvh(gars_gdf, crs='proj=moll')

Plot CVH (cell area / convex hull area) compactness map for GARS cells.

Values are in (0, 1], with 1 indicating the most compact (convex) shape.

Source code in vgrid/stats/garsstats.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def gars_compactness_cvh(gars_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot CVH (cell area / convex hull area) compactness map for GARS cells.

    Values are in (0, 1], with 1 indicating the most compact (convex) shape.
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # gars_gdf = gars_gdf[~gars_gdf["crossed"]]  # remove cells that cross the Antimeridian
    gars_gdf = gars_gdf[np.isfinite(gars_gdf["cvh"])]
    gars_gdf = gars_gdf[gars_gdf["cvh"] <= 1.1]
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    gars_gdf.to_crs(crs).plot(
        column="cvh",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson",
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="GARS CVH Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

gars_compactness_cvh_hist(gars_gdf)

Plot histogram of CVH (cell area / convex hull area) for GARS cells.

Source code in vgrid/stats/garsstats.py
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
def gars_compactness_cvh_hist(gars_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of CVH (cell area / convex hull area) for GARS cells.
    """
    # Filter out cells that cross the dateline
    #  gars_gdf = gars_gdf[~gars_gdf["crossed"]]
    gars_gdf = gars_gdf[np.isfinite(gars_gdf["cvh"])]
    gars_gdf = gars_gdf[gars_gdf["cvh"] <= 1.1]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    counts, bins, patches = ax.hist(
        gars_gdf["cvh"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Color mapping centered at 1
    vmin, vcenter, vmax = 0.90, 1.00, 1.10
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    for i, patch in enumerate(patches):
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Reference line at ideal compactness
    ax.axvline(x=1, color="red", linestyle="--", linewidth=2, label="Ideal (cvh = 1)")

    stats_text = (
        f"Mean: {gars_gdf['cvh'].mean():.6f}\n"
        f"Std: {gars_gdf['cvh'].std():.6f}\n"
        f"Min: {gars_gdf['cvh'].min():.6f}\n"
        f"Max: {gars_gdf['cvh'].max():.6f}"
    )
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    ax.set_xlabel("GARS CVH Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

gars_compactness_ipq(gars_gdf, crs='proj=moll')

Plot IPQ compactness map for GARS cells.

This function creates a visualization showing the Isoperimetric Quotient (IPQ) compactness of GARS cells across the globe. IPQ measures how close each cell is to being circular, with values closer to 0.785 indicating more regular squares.

Parameters:

Name Type Description Default
gars_gdf GeoDataFrame

GeoDataFrame from garsinspect function

required
Source code in vgrid/stats/garsstats.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def gars_compactness_ipq(gars_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot IPQ compactness map for GARS cells.

    This function creates a visualization showing the Isoperimetric Quotient (IPQ)
    compactness of GARS cells across the globe. IPQ measures how close each cell
    is to being circular, with values closer to 0.785 indicating more regular squares.

    Args:
        gars_gdf: GeoDataFrame from garsinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    # vmin, vmax, vcenter = gars_gdf['ipq'].min(), gars_gdf['ipq'].max(), np.mean([gars_gdf['ipq'].min(), gars_gdf['ipq'].max()])
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # gars_gdf = gars_gdf[~gars_gdf["crossed"]]  # remove cells that cross the Antimeridian
    gars_gdf.to_crs(crs).plot(
        column="ipq",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="viridis",
        legend_kwds={"orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="GARS IPQ Compactness", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

gars_compactness_ipq_hist(gars_gdf)

Plot histogram of IPQ compactness for GARS cells.

This function creates a histogram visualization showing the distribution of Isoperimetric Quotient (IPQ) compactness values for GARS cells, helping to understand how close cells are to being regular squares.

Parameters:

Name Type Description Default
gars_gdf GeoDataFrame

GeoDataFrame from garsinspect function

required
Source code in vgrid/stats/garsstats.py
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
def gars_compactness_ipq_hist(gars_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of IPQ compactness for GARS cells.

    This function creates a histogram visualization showing the distribution
    of Isoperimetric Quotient (IPQ) compactness values for GARS cells, helping
    to understand how close cells are to being regular squares.

    Args:
        gars_gdf: GeoDataFrame from garsinspect function
    """
    # Filter out cells that cross the dateline
    # gars_gdf = gars_gdf[~gars_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        gars_gdf["ipq"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = VMIN_QUAD, VCENTER_QUAD, VMAX_QUAD
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.viridis(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at ideal square IPQ value (0.785)
    ax.axvline(
        x=0.785,
        color="red",
        linestyle="--",
        linewidth=2,
        label="Ideal Square (IPQ = 0.785)",
    )

    # Add statistics text box
    stats_text = f"Mean: {gars_gdf['ipq'].mean():.3f}\nStd: {gars_gdf['ipq'].std():.3f}\nMin: {gars_gdf['ipq'].min():.3f}\nMax: {gars_gdf['ipq'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("GARS IPQ Compactness", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

gars_metrics(resolution, unit='m')

Calculate metrics for GARS DGGS cells.

Parameters:

Name Type Description Default
resolution int

Resolution level (0-4)

required
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Name Type Description
tuple

(num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)

Source code in vgrid/stats/garsstats.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def gars_metrics(resolution: int, unit: str = "m"):  # length unit is km, area unit is km2
    """
    Calculate metrics for GARS DGGS cells.

    Args:
        resolution: Resolution level (0-4)
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        tuple: (num_cells, avg_edge_len_in_unit, avg_cell_area_in_unit_squared)
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # GARS grid has 43200 (180x240) cells at base level
    # Each subdivision adds 10x10 = 100 cells per parent cell
    num_cells = gars_num_cells(resolution)
    # Calculate area in km² first
    avg_cell_area = AUTHALIC_AREA / num_cells  # cell area in km²
    avg_edge_len = math.sqrt(avg_cell_area)  # edge length in km
    cls = characteristic_length_scale(avg_cell_area, unit=unit)
    # Convert to requested unit
    if unit == "km":
        avg_cell_area = avg_cell_area / (10**6)  # Convert km² to m²
        avg_edge_len = avg_edge_len / (10**3)  # Convert km to m

    return num_cells, avg_edge_len, avg_cell_area, cls

gars_norm_area(gars_gdf, crs='proj=moll')

Plot normalized area map for GARS cells.

This function creates a visualization showing how GARS cell areas vary relative to the mean area across the globe, highlighting areas of distortion.

Parameters:

Name Type Description Default
gars_gdf GeoDataFrame

GeoDataFrame from garsinspect function

required
Source code in vgrid/stats/garsstats.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def gars_norm_area(gars_gdf: gpd.GeoDataFrame, crs: str | None = "proj=moll"):
    """
    Plot normalized area map for GARS cells.

    This function creates a visualization showing how GARS cell areas vary relative
    to the mean area across the globe, highlighting areas of distortion.

    Args:
        gars_gdf: GeoDataFrame from garsinspect function
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("bottom", size="5%", pad=0.1)
    vmin, vcenter, vmax = gars_gdf["norm_area"].min(), 1.0, gars_gdf["norm_area"].max()
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)
    # gars_gdf = gars_gdf[~gars_gdf["crossed"]]  # remove cells that cross the Antimeridian
    gars_gdf.to_crs(crs).plot(
        column="norm_area",
        ax=ax,
        norm=norm,
        legend=True,
        cax=cax,
        cmap="RdYlBu_r",
        legend_kwds={"label": "cell area/mean cell area", "orientation": "horizontal"},
    )
    world_countries = gpd.read_file(
        "https://raw.githubusercontent.com/opengeoshub/vopendata/refs/heads/main/shape/world_countries.geojson"
    )
    world_countries.boundary.to_crs(crs).plot(
        color=None, edgecolor="black", linewidth=0.2, ax=ax
    )
    ax.axis("off")
    cb_ax = fig.axes[1]
    cb_ax.tick_params(labelsize=14)
    cb_ax.set_xlabel(xlabel="GARS Normalized Area", fontsize=14)
    ax.margins(0)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    plt.tight_layout()

gars_norm_area_hist(gars_gdf)

Plot histogram of normalized area for GARS cells.

This function creates a histogram visualization showing the distribution of normalized areas for GARS cells, helping to understand area variations and identify patterns in area distortion.

Parameters:

Name Type Description Default
gars_gdf GeoDataFrame

GeoDataFrame from garsinspect function

required
Source code in vgrid/stats/garsstats.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
def gars_norm_area_hist(gars_gdf: gpd.GeoDataFrame):
    """
    Plot histogram of normalized area for GARS cells.

    This function creates a histogram visualization showing the distribution
    of normalized areas for GARS cells, helping to understand area variations
    and identify patterns in area distortion.

    Args:
        gars_gdf: GeoDataFrame from garsinspect function
    """
    # Filter out cells that cross the dateline
    # gars_gdf = gars_gdf[~gars_gdf["crossed"]]

    # Create the histogram with color ramp
    fig, ax = plt.subplots(figsize=(10, 6))

    # Get histogram data
    counts, bins, patches = ax.hist(
        gars_gdf["norm_area"], bins=50, alpha=0.7, edgecolor="black"
    )

    # Create color ramp using the same normalization as the map function
    vmin, vcenter, vmax = (
        gars_gdf["norm_area"].min(),
        1.0,
        gars_gdf["norm_area"].max(),
    )
    norm = TwoSlopeNorm(vmin=vmin, vcenter=vcenter, vmax=vmax)

    # Apply colors to histogram bars using the same color mapping as the map
    for i, patch in enumerate(patches):
        # Use the center of each bin for color mapping
        bin_center = (bins[i] + bins[i + 1]) / 2
        color = plt.cm.RdYlBu_r(norm(bin_center))
        patch.set_facecolor(color)

    # Add reference line at mean area (norm_area = 1)
    ax.axvline(
        x=1, color="red", linestyle="--", linewidth=2, label="Mean Area (norm_area = 1)"
    )

    # Add statistics text box
    stats_text = f"Mean: {gars_gdf['norm_area'].mean():.3f}\nStd: {gars_gdf['norm_area'].std():.3f}\nMin: {gars_gdf['norm_area'].min():.3f}\nMax: {gars_gdf['norm_area'].max():.3f}"
    ax.text(
        0.02,
        0.98,
        stats_text,
        transform=ax.transAxes,
        fontsize=12,
        verticalalignment="top",
        bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
    )

    # Customize the plot
    ax.set_xlabel("GARS normalized area", fontsize=14)
    ax.set_ylabel("Number of cells", fontsize=14)
    ax.legend(fontsize=12)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()

garsinspect(resolution)

Generate comprehensive inspection data for GARS DGGS cells at a given resolution.

This function creates a detailed analysis of GARS cells including area variations, compactness measures, and Antimeridian crossing detection.

Parameters:

Name Type Description Default
resolution int

GARS resolution level (0-4)

required

Returns:

Type Description

geopandas.GeoDataFrame: DataFrame containing GARS cell inspection data with columns: - gars: GARS cell ID - resolution: Resolution level - geometry: Cell geometry - cell_area: Cell area in square meters - cell_perimeter: Cell perimeter in meters - crossed: Whether cell crosses the Antimeridian - norm_area: Normalized area (cell_area / mean_area) - ipq: Isoperimetric Quotient compactness - zsc: Zonal Standardized Compactness

Source code in vgrid/stats/garsstats.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def garsinspect(resolution: int):  # length unit is km, area unit is km2
    """
    Generate comprehensive inspection data for GARS DGGS cells at a given resolution.

    This function creates a detailed analysis of GARS cells including area variations,
    compactness measures, and Antimeridian crossing detection.

    Args:
        resolution: GARS resolution level (0-4)

    Returns:
        geopandas.GeoDataFrame: DataFrame containing GARS cell inspection data with columns:
            - gars: GARS cell ID
            - resolution: Resolution level
            - geometry: Cell geometry
            - cell_area: Cell area in square meters
            - cell_perimeter: Cell perimeter in meters
            - crossed: Whether cell crosses the Antimeridian
            - norm_area: Normalized area (cell_area / mean_area)
            - ipq: Isoperimetric Quotient compactness
            - zsc: Zonal Standardized Compactness
    """
    gars_gdf = garsgrid(resolution, output_format="gpd")        
    gars_gdf["crossed"] = gars_gdf["geometry"].apply(check_crossing_geom)
    mean_area = gars_gdf["cell_area"].mean()
    # Calculate normalized area
    gars_gdf["norm_area"] = gars_gdf["cell_area"] / mean_area
    # Calculate IPQ compactness using the standard formula: CI = 4πA/P²
    gars_gdf["ipq"] = (
        4 * np.pi * gars_gdf["cell_area"] / (gars_gdf["cell_perimeter"] ** 2)
    )
    # Calculate zonal standardized compactness
    gars_gdf["zsc"] = (
        np.sqrt(
            4 * np.pi * gars_gdf["cell_area"]
            - np.power(gars_gdf["cell_area"], 2) / np.power(6378137, 2)
        )
        / gars_gdf["cell_perimeter"]
    )

    convex_hull = gars_gdf["geometry"].convex_hull
    convex_hull_area = convex_hull.apply(
        lambda g: abs(geod.geometry_area_perimeter(g)[0])
    )
    # Compute CVH safely; set to NaN where convex hull area is non-positive or invalid
    gars_gdf["cvh"] = np.where(
        (convex_hull_area > 0) & np.isfinite(convex_hull_area),
        gars_gdf["cell_area"] / convex_hull_area,
        np.nan,
    )
    # Replace any accidental inf values with NaN
    gars_gdf["cvh"] = gars_gdf["cvh"].replace([np.inf, -np.inf], np.nan)

    return gars_gdf

garsinspect_cli()

Command-line interface for GARS cell inspection.

CLI options

-r, --resolution: GARS resolution level (0-4)

Source code in vgrid/stats/garsstats.py
488
489
490
491
492
493
494
495
496
497
498
499
def garsinspect_cli():
    """
    Command-line interface for GARS cell inspection.

    CLI options:
      -r, --resolution: GARS resolution level (0-4)
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-r", "--resolution", dest="resolution", type=int, default=0)
    args = parser.parse_args()  # type: ignore
    resolution = args.resolution
    print(garsinspect(resolution))

garsstats(unit='m')

Generate statistics for GARS DGGS cells.

Parameters:

Name Type Description Default
unit str

'm' or 'km' for length; area will be 'm^2' or 'km^2'

'm'

Returns:

Type Description

pandas.DataFrame: DataFrame containing GARS DGGS statistics with columns: - resolution: Resolution level (0-4) - number_of_cells: Number of cells at each resolution - avg_edge_len_{unit}: Average edge length in the given unit - avg_cell_area_{unit}2: Average cell area in the squared unit

Source code in vgrid/stats/garsstats.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def garsstats(unit: str = "m"):  # length unit is km, area unit is km2
    """
    Generate statistics for GARS DGGS cells.

    Args:
        unit: 'm' or 'km' for length; area will be 'm^2' or 'km^2'

    Returns:
        pandas.DataFrame: DataFrame containing GARS DGGS statistics with columns:
            - resolution: Resolution level (0-4)
            - number_of_cells: Number of cells at each resolution
            - avg_edge_len_{unit}: Average edge length in the given unit
            - avg_cell_area_{unit}2: Average cell area in the squared unit
    """
    # normalize and validate unit
    unit = unit.strip().lower()
    if unit not in {"m", "km"}:
        raise ValueError("unit must be one of {'m','km'}")

    # Initialize lists to store data
    resolutions = []
    num_cells_list = []
    avg_edge_lens = []
    avg_cell_areas = []
    cls_list = []
    for res in range(min_res, max_res + 1):
        num_cells, avg_edge_len, avg_cell_area, cls = gars_metrics(
            res, unit=unit
        )  # length unit is km, area unit is km2
        resolutions.append(res)
        num_cells_list.append(num_cells)
        avg_edge_lens.append(avg_edge_len)
        avg_cell_areas.append(avg_cell_area)
        cls_list.append(cls)
    # Create DataFrame
    # Build column labels with unit awareness (lower case)
    avg_edge_len = f"avg_edge_len_{unit}"
    unit_area_label = {"m": "m2", "km": "km2"}[unit]
    avg_cell_area = f"avg_cell_area_{unit_area_label}"
    cls_label = f"cls_{unit}"
    df = pd.DataFrame(
        {
            "resolution": resolutions,
            "number_of_cells": num_cells_list,
            avg_edge_len: avg_edge_lens,
            avg_cell_area: avg_cell_areas,
            cls_label: cls_list,
        }
    )

    return df

garsstats_cli()

Command-line interface for generating GARS DGGS statistics.

CLI options

-unit, --unit {m,km}

Source code in vgrid/stats/garsstats.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def garsstats_cli():
    """
    Command-line interface for generating GARS DGGS statistics.

    CLI options:
      -unit, --unit {m,km}
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "-unit", "--unit", dest="unit", choices=["m", "km"], default="m"
    )
    args = parser.parse_args()

    unit = args.unit

    # Get the DataFrame
    df = garsstats(unit=unit)

    # Display the DataFrame
    print(df)