Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
push:
branches: ["**"]
pull_request:
branches: ["**"]

jobs:
lint-and-test:
name: Lint & Tests check
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: "pip"

- name: Install dependencies
run: |
pip install --upgrade pip
pip install --upgrade build
pip install ".[dev]"

- name: Run Ruff linter
run: ruff check .

- name: Run Ruff formatter check
run: ruff format --check .

- name: Run tests
run: pytest
49 changes: 27 additions & 22 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,56 @@
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = 'pixcdust'
copyright = '2025, Zawadzki Lionel'
author = 'Zawadzki Lionel'
release = '0.1.0'
project = "pixcdust"
copyright = "2025, Zawadzki Lionel"
author = "Zawadzki Lionel"
release = "0.1.0"

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = ['sphinx.ext.viewcode','autoapi.extension','nbsphinx',"sphinxcontrib.collections"]
extensions = [
"sphinx.ext.viewcode",
"autoapi.extension",
"nbsphinx",
"sphinxcontrib.collections",
]

html_show_sourcelink = False
set_type_checking_flag = True
nbsphinx_allow_errors = True

templates_path = ['_templates']
templates_path = ["_templates"]
exclude_patterns = []
autoapi_dirs = ['../pixcdust']
source_suffix = ['.rst']
autoapi_dirs = ["../pixcdust"]
source_suffix = [".rst"]

# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = 'alabaster'
html_static_path = ['_static']
html_theme = "alabaster"
html_static_path = ["_static"]


# manual build path
collections = {
'notebooks': {
'driver': 'copy_folder',
'source': 'pixcdust/notebooks',
'target': 'notebooks/',
'ignore': ['*.py', '.sh'],
'safe': False,
"notebooks": {
"driver": "copy_folder",
"source": "pixcdust/notebooks",
"target": "notebooks/",
"ignore": ["*.py", ".sh"],
"safe": False,
}
}

# ReadTheDoc build path

collections = {
'notebooks': {
'driver': 'copy_folder',
'source': '../pixcdust/notebooks',
'target': 'notebooks/',
'ignore': ['*.py', '.sh'],
'safe': False,
"notebooks": {
"driver": "copy_folder",
"source": "../pixcdust/notebooks",
"target": "notebooks/",
"ignore": ["*.py", ".sh"],
"safe": False,
}
}
65 changes: 37 additions & 28 deletions pixcdust/converters/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@
"""Interface used by all Pixcdust Converters."""

import copy
from dataclasses import dataclass
import operator
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path

from typing import Optional, Union, Iterable

import geopandas as gpd


Expand All @@ -44,9 +43,9 @@ class Converter:
def __init__(
self,
path_in: str | Iterable[str] | Path | Iterable[Path],
variables: Optional[list[str]] = None,
area_of_interest: Optional[gpd.GeoDataFrame] = None,
conditions: Optional[dict[str, dict[str, Union[str, float]]]] = None,
variables: list[str] | None = None,
area_of_interest: gpd.GeoDataFrame | None = None,
conditions: dict[str, dict[str, str | float]] | None = None,
):
"""Basic initialisation of a pixcdust converter.

Expand Down Expand Up @@ -91,8 +90,10 @@ class ConverterWSE(Converter):
variables: Optionally only read these variables.
area_of_interest: Optionally only read points in area_of_interest.
"""
def database_from_nc(self, path_out: str | Path, mode: str = "w", compute_wse: bool = True) \
-> None:

def database_from_nc(
self, path_out: str | Path, mode: str = "w", compute_wse: bool = True
) -> None:
"""Convert the path_in files to path_out.
Args:
path_out: Output path of the convertion.
Expand All @@ -109,19 +110,21 @@ def _append_wse_vars(self):
self.variables.append(var)

def _compute_wse(self, gdf):
gdf[self._get_name_wse_var()] = \
gdf[self._get_vars_wse_computation()[0]] - \
gdf[self._get_vars_wse_computation()[1]]
gdf[self._get_name_wse_var()] = (
gdf[self._get_vars_wse_computation()[0]]
- gdf[self._get_vars_wse_computation()[1]]
)

@staticmethod
def _get_vars_wse_computation() -> list[str]:
"""Names of fields used to compute wse."""
return ['height', 'geoid']
return ["height", "geoid"]

@staticmethod
def _get_name_wse_var() -> str:
"""Output name for wse."""
return 'wse'
return "wse"


@dataclass
class GeoLayerH3Projecter:
Expand All @@ -132,10 +135,11 @@ class GeoLayerH3Projecter:
resolution: Resolution

"""

data: gpd.GeoDataFrame
resolution: int

def filter_variable(self, conditions: dict[str,dict[str, Union[str, float]]]) -> None:
def filter_variable(self, conditions: dict[str, dict[str, str | float]]) -> None:
"""filters from xarray dataset based
on operator and threshold on specific variables

Expand All @@ -154,36 +158,40 @@ def filter_variable(self, conditions: dict[str,dict[str, Union[str, float]]]) ->
AttributeError: if operator is not the function name of\
the operator module
"""
_k_operator = 'operator'
_k_to = 'threshold'
_k_operator = "operator"
_k_to = "threshold"
# Test if conditions dict meets specifications
print(conditions)
for k in conditions.keys():
if k not in self.data.columns:
raise IOError(
f'dict conditions expected existing\
for var, condition in conditions.items():
if var not in self.data.columns:
raise OSError(
f"dict conditions expected existing\
variables (in {self.data.columns}),\
received {k}'
received {var}"
)
for instructions in conditions[k].keys():
for instructions in condition:
if instructions not in [_k_operator, _k_to]:
raise ValueError(
f'dict conditions expected {_k_to} and {_k_operator}\
f"dict conditions expected {_k_to} and {_k_operator}\
keys in dict {conditions},\
received {instructions}'
received {instructions}"
)
print(f"operator.{conditions[k][_k_operator]}")
ope = getattr(operator, conditions[k][_k_operator])
op_name = condition[_k_operator]
# Typing issue, improvement would be to use TypedDict for conditions
assert isinstance(op_name, str)
print(f"operator.{op_name}")
ope = getattr(operator, op_name)
self.data = self.data[
ope(
self.data[k],
conditions[k][_k_to],
self.data[var],
condition[_k_to],
)
]

def compute_h3_layer(self) -> None:
"""Project data to h3."""
from pixcdust.dggs import h3_tools

self.data = h3_tools.gdf_to_h3_gdf(
self.data,
self.resolution,
Expand All @@ -192,6 +200,7 @@ def compute_h3_layer(self) -> None:
def compute_healpix_layer(self) -> None:
"""Project data to Healpix."""
from pixcdust.dggs import h3_tools

self.data = h3_tools.gdf_to_healpix_gdf(
self.data,
self.resolution,
Expand Down
6 changes: 2 additions & 4 deletions pixcdust/converters/geo_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@
#
"""Converters utility"""

import xarray as xr
import geopandas as gpd
import xarray as xr


def geoxarray_to_geodataframe(
ds: xr.Dataset,
*args, **kwargs) -> gpd.GeoDataFrame:
def geoxarray_to_geodataframe(ds: xr.Dataset, *args, **kwargs) -> gpd.GeoDataFrame:
"""Converts an xarray.Dataset with points coordinates into\
a geopandas.GeodataFrame with xvec

Expand Down
51 changes: 28 additions & 23 deletions pixcdust/converters/gpkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@
"""Geopackage converters."""

import os
from pathlib import Path
from typing import Optional, Union
from dataclasses import dataclass
from pathlib import Path

from tqdm import tqdm
import fiona
import geopandas as gpd
from tqdm import tqdm

from pixcdust.converters.core import ConverterWSE, GeoLayerH3Projecter
from pixcdust.readers.gpkg import GpkgReader
from pixcdust.readers.netcdf import NcSimpleReader
from pixcdust.readers.zarr import ZarrReader
from pixcdust.readers.gpkg import GpkgReader


class Nc2GpkgConverter(ConverterWSE):
Expand All @@ -45,15 +44,16 @@ class Nc2GpkgConverter(ConverterWSE):

"""

def database_from_nc(self, path_out: str | Path, mode: str = "w", compute_wse: bool = True) \
-> None:
def database_from_nc(
self, path_out: str | Path, mode: str = "w", compute_wse: bool = True
) -> None:
path_out = str(path_out)
if compute_wse:
self._append_wse_vars()
for path in tqdm(self.path_in):
ncsimple = NcSimpleReader(
path,
variables= self.variables,
variables=self.variables,
area_of_interest=self.area_of_interest,
conditions=self.conditions,
)
Expand All @@ -62,23 +62,26 @@ def database_from_nc(self, path_out: str | Path, mode: str = "w", compute_wse: b
_, dt_time_start, cycle_number, pass_number, tile_number, swath_side = (
ncsimple.extract_info_from_nc_attrs(path)
)
time_start = dt_time_start.strftime('%Y%m%d')
time_start = dt_time_start.strftime("%Y%m%d")

layer_name = f"{time_start}_{cycle_number}_\
{pass_number}_{tile_number}{swath_side}"
layer_name = (
f"{time_start}_{cycle_number}_{pass_number}_{tile_number}{swath_side}"
)

# cheking if output file and layer already exist
if os.path.exists(path_out) and mode == "w":
if layer_name in fiona.listlayers(path_out):
tqdm.write(
f"skipping layer {layer_name} \
if (
os.path.exists(path_out)
and mode == "w"
and layer_name in fiona.listlayers(path_out)
):
tqdm.write(
f"skipping layer {layer_name} \
(already in geopackage {path_out})"
)
continue
)
continue
# converting data from xarray to geodataframe
ncsimple.open_dataset()
gdf = ncsimple.to_geodataframe(
)
gdf = ncsimple.to_geodataframe()

if gdf.size == 0:
tqdm.write(
Expand Down Expand Up @@ -109,18 +112,19 @@ class GpkgDGGSProjecter:

path: str
dggs_res: int
conditions: Optional[dict[str,dict[str, Union[str, float]]]] = None
conditions: dict[str, dict[str, str | float]] | None = None
healpix: bool = False
dggs_layer_pattern: str = '_h3'
path_out: Optional[str] = None
dggs_layer_pattern: str = "_h3"
path_out: str | None = None
# database: GpkgReader

def __post_init__(self) -> None:
self.database = GpkgReader(self.path)
self.database.layers = [
layer for layer in fiona.listlayers(self.path)
layer
for layer in fiona.listlayers(self.path)
if not layer.endswith(self.dggs_layer_pattern)
]
]

if self.path_out is None:
self.path_out = self.path
Expand Down Expand Up @@ -165,6 +169,7 @@ class Zarr2GpkgConverter:
Attributes:
path: Gpkg pixelcloud to convert.
"""

path: str
data: gpd.GeoDataFrame = None

Expand Down
Loading